-
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathgen-code-situation.mjs
More file actions
222 lines (212 loc) · 13.6 KB
/
Copy pathgen-code-situation.mjs
File metadata and controls
222 lines (212 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
#!/usr/bin/env node
/*
* gen-code-situation.mjs — 采集 axon 真实「码情」→ JSON,喂给 commander-real.html 双镜片渲染。
*
* 缘起(2026-06-01 /loop):用户说"游戏是假的(硬编码 NotesSaaS),我要真的、接 axon 自己的码情"。
* 这个脚本把 axon 工作树的真实状态拉出来——真 git(脏/未推/最近提交) + 真测试(跑 client 套件取真 pass/fail)
* + 真模块结构(文件数) + 真现实血条(从 status.md 解析)——派生每个模块的信任态:
* 🔴 broken = 该模块下有失败测试 / 🟡 claimed = 有未提交改动 / 🟢 hard = 干净。
* 这就是"接入码情":地图上烧的红、晃的黄,全是 axon 此刻真实的样子,不是编的。
*
* 输出:src/web/client/public/prototypes/code-situation.json(page 同目录 fetch ./code-situation.json)。
* 用法:node scripts/gen-code-situation.mjs [--no-tests](--no-tests 跳过慢的测试运行,只用 git+结构)。
*/
import { execSync } from 'node:child_process';
import { writeFileSync, readFileSync, existsSync, readdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const OUT = path.join(ROOT, 'src/web/client/public/prototypes/code-situation.json');
const runTests = !process.argv.includes('--no-tests');
const sh = (cmd, timeout = 30000) => {
try { return execSync(cmd, { cwd: ROOT, encoding: 'utf-8', timeout, stdio: ['pipe', 'pipe', 'pipe'] }).trim(); }
catch (e) { return (e.stdout || '').toString().trim(); }
};
// ---- 1. 真 git 码情 ----
function gatherGit() {
const branch = sh('git rev-parse --abbrev-ref HEAD') || '?';
const porcelain = sh('git status --porcelain').split('\n').filter(Boolean);
const dirty = porcelain.filter((l) => !l.startsWith('??'));
const untracked = porcelain.filter((l) => l.startsWith('??'));
let unpushed = 0;
const up = sh('git rev-list --count @{u}..HEAD'); if (/^\d+$/.test(up)) unpushed = +up;
const recent = sh('git log --oneline -6').split('\n').filter(Boolean)
.map((l) => l.replace(/^(\w+)\s/, '').slice(0, 64));
// 取脏文件路径(去掉前两列状态),用于按模块归因
const dirtyPaths = dirty.map((l) => l.slice(3).trim()).filter(Boolean);
const untrackedPaths = untracked.map((l) => l.slice(3).trim()).filter(Boolean);
return { branch, dirtyCount: dirty.length, untrackedCount: untracked.length, unpushed, recent, dirtyPaths, untrackedPaths };
}
// ---- 2. 真测试码情(跑 client 套件取真 pass/fail) ----
function gatherTests() {
if (!runTests) return { ran: false, passing: null, failing: null, failingFiles: [] };
const outFile = '/tmp/cs-vitest.json';
try { sh(`npx vitest run src/web/client --reporter=json --outputFile=${outFile} 2>/dev/null`, 120000); } catch { /* */ }
try {
const j = JSON.parse(readFileSync(outFile, 'utf-8'));
const failingFiles = (j.testResults || []).filter((r) => r.status === 'failed')
.map((r) => r.name.replace(ROOT + '/', '')).filter(Boolean);
return { ran: true, passing: j.numPassedTests ?? null, failing: j.numFailedTests ?? null,
totalFiles: (j.testResults || []).length, failingFiles };
} catch { return { ran: true, passing: null, failing: null, failingFiles: [], error: 'parse failed' }; }
}
// ---- 3. 真模块结构 + 派生信任态 ----
const MODULES = [
{ id: 'web/server', title: '后端服务', icon: '🛰️', prefix: 'src/web/server' },
{ id: 'web/client', title: '前端界面', icon: '🖥️', prefix: 'src/web/client/src' },
{ id: 'tools', title: '工具系统', icon: '🧰', prefix: 'src/tools' },
{ id: 'coding', title: '编程护城河', icon: '🏰', prefix: 'src/coding' },
{ id: 'blueprint', title: '蓝图/多智能体', icon: '🗺️', prefix: 'src/blueprint' },
{ id: 'core', title: '核心引擎', icon: '⚙️', prefix: 'src/core' },
{ id: 'cloud-sandbox', title: '云沙盒', icon: '☁️', prefix: 'src/cloud-sandbox' },
{ id: 'deploy', title: '部署托管', icon: '🚀', prefix: 'src/deploy' },
{ id: 'memory', title: '长期记忆', icon: '🧠', prefix: 'src/memory' },
{ id: 'web/shared', title: '共享协议', icon: '🔗', prefix: 'src/web/shared' },
];
// 垃圾噪音文件(用户反馈:火情里全是 logs/.pptx/Dockerfile 等非源码,要的是真代码函数级信号)。
const JUNK = /(^|\/)(logs?\d*\.txt|.*\.pptx|Dockerfile|.*\.log|.*\.lock)$|^deploy\/epay\//i;
const isSource = (p) => /\.(ts|tsx|js|jsx|mjs|css)$/.test(p) && !JUNK.test(p);
// 从改动行往上找最近的外层声明 = 真函数级(比 git 自带 xfuncname 对 TS/TSX 准)。
const SKIP_KW = new Set(['if', 'for', 'while', 'switch', 'catch', 'return', 'function', 'else', 'do', 'await', 'new']);
function enclosingDecl(lines, idx) {
for (let i = Math.min(idx, lines.length - 1); i >= 0 && i > idx - 500; i--) {
const l = lines[i];
const m =
l.match(/^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*(\w+)/) || // function foo
l.match(/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*[:=]/) || // const Foo = / : FC
l.match(/^\s*(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/) || // class Foo
l.match(/^\s*(?:export\s+)?(?:interface|type|enum)\s+(\w+)/) || // interface/type Foo
l.match(/^\s*(?:public\s+|private\s+|protected\s+|static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\([^)]*\)\s*[:{]/); // 方法 foo() {
if (m && !SKIP_KW.has(m[1])) return m[1];
}
return null;
}
// 解析 `git diff HEAD -U0` 的 hunk 新文件行号 → 定位改动落在哪个函数(函数级颗粒度,用户明确要)。
function changedFuncs(file) {
const out = sh(`git diff HEAD -U0 -- "${file}"`, 12000);
if (!out) return [];
let src = [];
try { src = readFileSync(path.join(ROOT, file), 'utf-8').split('\n'); } catch { return []; }
const funcs = new Set();
for (const line of out.split('\n')) {
const h = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,(\d+))?\s+@@/);
if (!h) continue;
const start = +h[1], count = h[2] === undefined ? 1 : +h[2];
for (let ln = start; ln < start + Math.max(count, 1); ln++) {
const name = enclosingDecl(src, ln - 1); // 行号 1-indexed → 数组 0-indexed
if (name) funcs.add(name);
}
}
return [...funcs].slice(0, 10);
}
function gatherModules(git, tests) {
const failing = tests.failingFiles || [];
const dirtyStatus = new Map(); // path -> 'M'|'A'|'D' (从 porcelain 第二列粗判)
for (const p of git.dirtyPaths) dirtyStatus.set(p, 'M');
for (const p of git.untrackedPaths || []) dirtyStatus.set(p, 'A');
return MODULES.map((m) => {
let files = 0;
const c = sh(`find ${m.prefix} -type f \\( -name '*.ts' -o -name '*.tsx' \\) 2>/dev/null | grep -v node_modules | wc -l`);
if (/^\d+$/.test(c)) files = +c;
// 该模块下「真源码」改动(滤掉垃圾噪音)→ 每个文件下钻到函数级。
const changedPaths = [...git.dirtyPaths, ...(git.untrackedPaths || [])]
.filter((p) => p.startsWith(m.prefix) && isSource(p));
const changes = changedPaths.slice(0, 12).map((p) => ({
file: p.slice(m.prefix.length).replace(/^\//, ''),
kind: dirtyStatus.get(p) === 'A' ? 'A' : 'M',
funcs: dirtyStatus.get(p) === 'A' ? [] : changedFuncs(p), // 新文件无 vs-HEAD diff,不列函数
}));
const dirty = changedPaths.length; // 源码改动数(不含垃圾,颗粒度对齐)
const brokenTests = failing.filter((p) => p.startsWith(m.prefix)).length;
const trust = brokenTests > 0 ? 'broken' : dirty > 0 ? 'claimed' : 'hard';
return { ...m, files, dirty, brokenTests, trust, changes };
}).filter((m) => m.files > 0);
}
// ---- 3b. 里程碑关卡(用户要:项目是 milestone 驱动的,面板得显示 M5→M17 关卡进度)----
// 解析 docs/spec/roadmap/M{n}-overview.md:标题 + 子任务行的 ✅/⚠ 数 → 关卡进度。
// 里程碑关卡 = axon 自蓝图 blueprint.json 驱动的**活验证状态机**(M17.n)。
// ★铁律(违则整层作废)★:blueprint.json 里的 status 只是「我声称的」(claim),绝不当 🟢。
// 🟢 verified 只能来自 verify-milestone 真跑 gate 绿 + 落盘 .milestone-verified.json + 依赖也验过(断裂检测)。
// 手敲的 "done" 一律打回 🟡 claimed——这才是「门驱动状态」,不是嘴敲(修我自己 M17.l 的违规)。
function gatherMilestones() {
const bpPath = path.join(ROOT, 'docs/spec/roadmap/blueprint.json');
if (!existsSync(bpPath)) return [];
let bp;
try { bp = JSON.parse(readFileSync(bpPath, 'utf-8')); } catch { return []; }
// 验证日志:verify-milestone 真跑 gate 绿后写的硬证据(id → {green, at, passing})。
const vlogPath = path.join(ROOT, 'docs/spec/roadmap/.milestone-verified.json');
let vlog = {};
if (existsSync(vlogPath)) { try { vlog = JSON.parse(readFileSync(vlogPath, 'utf-8')); } catch { /* */ } }
const gateGreen = (id) => !!(vlog[id] && vlog[id].green === true);
// git 出货佐证("feat(M15.a):"、"M8.c" 等引用数)——只做旁证,不当信任来源。
const log = sh('git log --oneline -600', 15000) || '';
const commitTally = {};
for (const m of log.matchAll(/\bM(\d+)(?:[.)\s:a-z]|$)/g)) { const k = 'M' + m[1]; commitTally[k] = (commitTally[k] || 0) + 1; }
return (bp.tasks || []).map((tk) => {
const subtasks = Array.isArray(tk.subtasks) ? tk.subtasks : [];
const done = subtasks.filter((s) => s.done).length;
const claim = tk.status; // 我声称的(done/wip/planned)——不当真
const deps = tk.dependsOn || [];
const green = gateGreen(tk.id);
const depsVerified = deps.every((d) => gateGreen(d)); // 依赖都 gate 验过了吗(断裂检测)
// ★信任裁决★:gate 绿+依赖验过=verified🟢;gate 绿但依赖没验=断裂(broken_seam);
// 声称 done 但没跑 gate=claimed🟡(我嘴说的);wip/planned 照声称。
let trust;
if (green && depsVerified) trust = 'verified';
else if (green && !depsVerified) trust = 'broken_seam';
else if (claim === 'done') trust = 'claimed';
else if (claim === 'wip') trust = 'wip';
else trust = 'planned';
// 下一个「可验证」= 声称 done、依赖都验过、自己还没跑 gate、且有 gate 可跑。
const actionable = claim === 'done' && !green && depsVerified && !!tk.verify;
return {
id: tk.id, num: +String(tk.id).replace(/\D/g, ''), title: tk.title,
status: claim, trust, actionable,
verifiedAt: vlog[tk.id] ? vlog[tk.id].at : null,
dependsOn: deps, depsVerified,
verify: tk.verify || null,
commits: commitTally[tk.id] || 0,
done, total: subtasks.length, subtasks,
};
}).sort((a, b) => a.num - b.num);
}
// ---- 4. 真现实血条 BOSS HP(从 status.md 解析三层 — 与「现实血条」锚点同源) ----
// 三层都从 status.md 顶部「现实血条 (BOSS HP)」真解析,不硬编码:
// 代理 BOSS = N/M 种真实形态 · 真 BOSS = 真人(非作者)交付数 · 终极 BOSS = 付费数 + MRR。
// ★诚实★:真 BOSS / 付费 = 0 就老实返 0,不粉饰(anti-gaming,铁律11)。
function gatherReality() {
let forms = 4, target = 10, realDelivery = 0, paying = 0, mrr = 0;
const sp = path.join(ROOT, 'docs/spec/status.md');
if (existsSync(sp)) {
const t = readFileSync(sp, 'utf-8');
const m = t.match(/(\d+)\s*\/\s*(\d+)\s*种真实形态/); if (m) { forms = +m[1]; target = +m[2]; }
// 真 BOSS:「真人(非作者)成功交付(当前 **0**)」
const r = t.match(/成功交付(当前\s*\*{0,2}(\d+)/); if (r) realDelivery = +r[1];
// 终极 BOSS:「付费真人(当前 **0** · MRR **¥0**)」(分隔符容差,匹配到 ¥ 数字)
const p = t.match(/付费真人(当前\s*\*{0,2}(\d+)\*{0,2}[^¥]*¥?([\d.]+)/);
if (p) { paying = +p[1]; mrr = +p[2]; }
}
return { forms, target, realDelivery, users: 0, paying, mrr };
}
// ---- 组装 ----
const git = gatherGit();
const tests = gatherTests();
const modules = gatherModules(git, tests);
const milestones = gatherMilestones();
const reality = gatherReality();
const fires = [];
if (tests.failing) fires.push({ kind: 'test', n: tests.failing, label: `${tests.failing} 个测试在红` , detail: (tests.failingFiles || []).map((f) => f.split('/').pop()).join('、') });
// 未提交火情:只算+只列「真源码」改动(滤掉 logs/.pptx 等垃圾噪音,对齐用户「要真代码函数级」反馈)。
const srcDirty = [...git.dirtyPaths, ...git.untrackedPaths].filter(isSource);
if (srcDirty.length) fires.push({ kind: 'uncommitted', n: srcDirty.length, label: `${srcDirty.length} 处源码改动没盖章(未提交)`, detail: srcDirty.slice(0, 6).map((p) => p.split('/').pop()).join('、') });
if (git.unpushed) fires.push({ kind: 'unpushed', n: git.unpushed, label: `${git.unpushed} 个提交没推上去`, detail: '本地领先远端' });
const data = {
// 注:生成时间由调用方/脚本注入避免不确定性问题——这里用 git 最近提交时间近似真实感
generatedBy: 'gen-code-situation.mjs', branch: git.branch,
git: { dirty: git.dirtyCount, untracked: git.untrackedCount, unpushed: git.unpushed, recent: git.recent },
tests, modules, milestones, reality, fires,
};
writeFileSync(OUT, JSON.stringify(data, null, 2));
console.log('✓ 真实码情已采集 →', OUT.replace(ROOT + '/', ''));
console.log(` 分支 ${git.branch} · 脏 ${git.dirtyCount} · 未推 ${git.unpushed} · 测试 ${tests.passing}✓/${tests.failing}✗ · 模块 ${modules.length}`);
console.log(' 信任态:', modules.map((m) => `${m.title}=${m.trust}`).join(' '));