sillyspec 3.29.2 → 3.29.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/analyze-transcript.cjs +38 -0
- package/asset-audit-2026-09-20/README.md +42 -0
- package/asset-audit-2026-09-20/asset-audit.cjs +253 -0
- package/asset-audit-2026-09-20/extract-candidates.cjs +37 -0
- package/asset-audit-2026-09-20/platform-archive.csv +294 -0
- package/asset-audit-2026-09-20/platform-fr.csv +12 -0
- package/asset-audit-2026-09-20/platform-gotchas.csv +6 -0
- package/asset-audit-2026-09-20/platform-knownissues.csv +23 -0
- package/asset-audit-2026-09-20/platform-modules.csv +222 -0
- package/asset-audit-2026-09-20/platform-quicklog.csv +1076 -0
- package/asset-audit-2026-09-20/platform-troubleshooting-candidates.csv +88 -0
- package/asset-audit-2026-09-20/platform-troubleshooting.csv +1 -0
- package/asset-audit-2026-09-20/sillyspec-archive.csv +108 -0
- package/asset-audit-2026-09-20/sillyspec-fr.csv +51 -0
- package/asset-audit-2026-09-20/sillyspec-gotchas.csv +4 -0
- package/asset-audit-2026-09-20/sillyspec-knownissues.csv +34 -0
- package/asset-audit-2026-09-20/sillyspec-modules.csv +30 -0
- package/asset-audit-2026-09-20/sillyspec-quicklog.csv +460 -0
- package/asset-audit-2026-09-20/sillyspec-troubleshooting.csv +69 -0
- package/asset-audit-2026-09-20/summary.json +241 -0
- package/package.json +1 -1
- package/src/ceremony-config.js +64 -0
- package/src/ceremony-tier.js +66 -13
- package/src/config-schema.js +16 -3
- package/src/cross-repo-reconcile.js +232 -46
- package/src/fr-index.js +80 -2
- package/src/index.js +2 -2
- package/src/review-tier.js +5 -2
- package/src/run/bg-sync.js +231 -0
- package/src/run/complete-handlers.js +208 -5
- package/src/run/gates.js +10 -5
- package/src/run/prompt.js +1 -1
- package/src/run/shared.js +32 -0
- package/src/run/verify-quality-scan.js +131 -3
- package/src/scope-audit.js +303 -20
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/execute.js +1 -0
- package/src/stages/plan-postcheck.js +66 -0
- package/src/stages/plan.js +1 -0
- package/src/stages/quick.js +2 -0
- package/src/sync.js +12 -0
- package/src/verify-postcheck.js +92 -8
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// 转录命令级分析 v2:response.toolCalls 提取
|
|
2
|
+
// 用法: node analyze-transcript.cjs <model-io.jsonl>
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const file = process.argv[2];
|
|
5
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
6
|
+
const calls = [];
|
|
7
|
+
const slowReqs = [];
|
|
8
|
+
for (const line of lines) {
|
|
9
|
+
let rec;
|
|
10
|
+
try { rec = JSON.parse(line); } catch { continue; }
|
|
11
|
+
const dur = rec.durationMs || 0;
|
|
12
|
+
const ts = rec.startedAt ? new Date(rec.startedAt).toTimeString().slice(0, 8) : '?';
|
|
13
|
+
if (dur > 30000) slowReqs.push({ ts, dur: Math.round(dur / 1000), n: (rec.response && rec.response.toolCalls || []).length });
|
|
14
|
+
for (const tc of (rec.response && rec.response.toolCalls) || []) {
|
|
15
|
+
const inp = tc.input || {};
|
|
16
|
+
let arg = '';
|
|
17
|
+
if (tc.name === 'Bash') arg = String(inp.command || '').replace(/\s+/g, ' ').slice(0, 130);
|
|
18
|
+
else if (tc.name === 'Read') arg = String(inp.file_path || '').split(/[\\/]/).slice(-2).join('/');
|
|
19
|
+
else if (tc.name === 'Edit' || tc.name === 'Write') arg = String(inp.file_path || '').split(/[\\/]/).slice(-2).join('/');
|
|
20
|
+
else if (tc.name === 'Grep') arg = String(inp.pattern || '').slice(0, 40) + ' @' + String(inp.path || '').split(/[\\/]/).slice(-1);
|
|
21
|
+
else if (tc.name === 'Agent') arg = 'dispatch: ' + String(inp.description || '').slice(0, 50);
|
|
22
|
+
else arg = JSON.stringify(inp).slice(0, 80);
|
|
23
|
+
calls.push({ ts, tool: tc.name, arg });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const byTool = {};
|
|
27
|
+
const byArg = {};
|
|
28
|
+
for (const c of calls) {
|
|
29
|
+
byTool[c.tool] = (byTool[c.tool] || 0) + 1;
|
|
30
|
+
byArg[c.tool + ' :: ' + c.arg] = (byArg[c.tool + ' :: ' + c.arg] || 0) + 1;
|
|
31
|
+
}
|
|
32
|
+
console.log('请求行:', lines.length, '| 工具调用:', calls.length, '| 分布:', JSON.stringify(byTool));
|
|
33
|
+
console.log('\n== 重复 ≥2 次调用 top20 ==');
|
|
34
|
+
for (const [k, n] of Object.entries(byArg).filter(([, n]) => n >= 2).sort((a, b) => b[1] - a[1]).slice(0, 20)) console.log(n + 'x |', k.slice(0, 160));
|
|
35
|
+
console.log('\n== 慢请求 >30s ==');
|
|
36
|
+
for (const s of slowReqs.sort((a, b) => b.dur - a.dur)) console.log(s.ts, '|', s.dur + 's', '| 本轮工具数:', s.n);
|
|
37
|
+
console.log('\n== 全部 Bash 命令时间线 ==');
|
|
38
|
+
for (const c of calls.filter((c) => c.tool === 'Bash')) console.log(c.ts, '|', c.arg);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# 资产审计数据(2026-09-20)
|
|
2
|
+
|
|
3
|
+
机器生成,非手抄。每个数字都可复算:`node asset-audit.cjs` 重跑即得(脚本即方法)。
|
|
4
|
+
|
|
5
|
+
覆盖两仓:
|
|
6
|
+
- `sillyspec` = C:/Users/qinyi/IdeaProjects/sillyspec(本仓)
|
|
7
|
+
- `platform` = C:/Users/qinyi/IdeaProjects/multi-agent-platform(SillyHub 平台侧)
|
|
8
|
+
|
|
9
|
+
## 文件清单
|
|
10
|
+
|
|
11
|
+
| 文件 | 内容 |
|
|
12
|
+
|---|---|
|
|
13
|
+
| summary.json | 两仓全部汇总数字(唯一事实源) |
|
|
14
|
+
| sillyspec-archive.csv / platform-archive.csv | 归档变更逐条:名称、六件套文档齐缺 |
|
|
15
|
+
| sillyspec-quicklog.csv / platform-quicklog.csv | quicklog 逐条:ql-ID、时间、状态、标题 |
|
|
16
|
+
| sillyspec-fr.csv / platform-fr.csv | FR 需求索引逐条:来源变更、状态、最近确认 commit、commit 是否在 git 中存在 |
|
|
17
|
+
| sillyspec-modules.csv / platform-modules.csv | 模块文档逐文件行数 |
|
|
18
|
+
| sillyspec-gotchas.csv / platform-gotchas.csv | knowledge/ 下坑目/模式账本逐文件(gotchas 族+patterns/conventions/uncategorized)——**平台仓的真实坑目基础设施**(此前误按文件名找 troubleshooting.md 断言 0,已纠正) |
|
|
19
|
+
| sillyspec-troubleshooting.csv / platform-troubleshooting.csv | 排坑条目逐条标题(仅按 troubleshooting.md 文件名采集;平台侧真身在 gotchas 族) |
|
|
20
|
+
| sillyspec-knownissues.csv / platform-knownissues.csv | 已知问题账本逐条 |
|
|
21
|
+
| asset-audit.cjs | 生成上述一切的脚本(只读仓库,不改动任何文件) |
|
|
22
|
+
| extract-candidates.cjs | 从平台仓 quicklog 提取事故候选(根因含实证/坑/假红等信号) |
|
|
23
|
+
| platform-troubleshooting-candidates.csv | 87 条事故候选 backlog(倒序,分诊冷启动用);mentions_test 为弱指示器 |
|
|
24
|
+
|
|
25
|
+
## 口径说明(防夸大)
|
|
26
|
+
|
|
27
|
+
- 「回归钉指标」= test 文件内容含 `回归钉|回归锁定|回归锁` 的文件数,是**指示器**不是精确计数(commit message 里的回归钉不在此列)。
|
|
28
|
+
- 测试文件数 = 全仓 `*.test.* / *.spec.*`,剔除 node_modules、构建产物、`.sillyspec`(运行态)、`.worktrees`(worktree 存储副本)、`.claude`(测试夹具仓)——副本不计入。
|
|
29
|
+
- FR「确认有效」= `最近确认` 的 commit 哈希经 `git cat-file -t` 验证真实存在;不验证场景语义本身。
|
|
30
|
+
- quicklog 状态按条目内 `状态:` 行原样统计。
|
|
31
|
+
- 归档「六件套」= proposal/requirements/design/tasks/verify-result/verify-facts.json 六文件齐备(平台仓多数走轻量档,缺属常态而非数据错误)。
|
|
32
|
+
- troubleshooting「编号坑目」= 标题形如 `## N.` 的条目;另有少量非编号小节。
|
|
33
|
+
- 本目录为分析产物,未纳入 git 跟踪,可整目录删除。
|
|
34
|
+
|
|
35
|
+
## 历史口径修正记录(防再犯)
|
|
36
|
+
|
|
37
|
+
1. v1:平台测试计 0 —— 采集面只扫根 `test/`,实际 monorepo 测试在 frontend/sillyhub-daemon 下。
|
|
38
|
+
2. v1:quicklog 460→459、模块文档 37→29(changelog 误计)、troubleshooting 67→66(非编号小节)。
|
|
39
|
+
3. v2:测试数混入 `.worktrees` 存储副本(2746 个)与 `.claude` 夹具仓(302 个),已加排除规则。
|
|
40
|
+
4. v3:「平台归档只 16/293 全档」为误读——212/293 五件套齐,缺的 verify-facts.json 是 2026-09-07 后新版才产的工件(版本时差非缺陷)。
|
|
41
|
+
5. v3:「平台无坑目账本」为误诊——真身在 `knowledge/`(sillyspec-gotchas 10 + testing-gotchas 6 + patterns 10 + uncategorized 39 = 65 条,另有 INDEX 关键词路由 74 行),按 troubleshooting.md 文件名采集致漏。
|
|
42
|
+
6. v4:「事故闭环薄、需补沉淀纪律」为误诊——①uncategorized.md 头部自述为设计内暂存箱(成熟后迁出分类文件+加 INDEX,INDEX 不索引本文件),39 条是流水线在制品非债务;②平台 .runtime 有 knowledge-hit-report.json / knowledge-hits.jsonl / friction-tally 与名为 knowledge-precipitation 的变更——工具的知识沉淀回路在平台仓已在跑;③候选抽样 7~24 多为一次性产品 bug(正确归宿=quicklog+测试+commit),机制坑(sillyspec CLI 坑)平台当日即登记。「蒸馏率低」是产品仓与工具仓的经济差异,非缺陷。
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 资产审计:对 sillyspec 本仓与 SillyHub 平台仓做只读盘点,产出 CSV/JSON 数据文件。
|
|
3
|
+
// 方法即证据:本脚本只读不写仓库内容,输出全部落在本目录。重跑:node asset-audit.cjs
|
|
4
|
+
'use strict';
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const OUT = __dirname;
|
|
10
|
+
const REPOS = [
|
|
11
|
+
{ key: 'sillyspec', root: 'C:/Users/qinyi/IdeaProjects/sillyspec' },
|
|
12
|
+
{ key: 'platform', root: 'C:/Users/qinyi/IdeaProjects/multi-agent-platform' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// ---------- 工具 ----------
|
|
16
|
+
const readText = (p) => { try { return fs.readFileSync(p, 'utf8'); } catch { return null; } };
|
|
17
|
+
const walk = (dir, pred, acc = [], exclude = []) => {
|
|
18
|
+
let ents; try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch { return acc; }
|
|
19
|
+
for (const e of ents) {
|
|
20
|
+
if (e.isDirectory() && exclude.includes(e.name)) continue;
|
|
21
|
+
const p = path.join(dir, e.name);
|
|
22
|
+
if (e.isDirectory()) walk(p, pred, acc, exclude);
|
|
23
|
+
else if (pred(p)) acc.push(p);
|
|
24
|
+
}
|
|
25
|
+
return acc;
|
|
26
|
+
};
|
|
27
|
+
const csvEscape = (s) => `"${String(s == null ? '' : s).replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`;
|
|
28
|
+
const writeCsv = (name, header, rows) => {
|
|
29
|
+
const body = [header, ...rows].map((r) => r.map(csvEscape).join(',')).join('\r\n');
|
|
30
|
+
fs.writeFileSync(path.join(OUT, name), '\ufeff' + body, 'utf8'); // BOM 便于 Excel 直接开
|
|
31
|
+
};
|
|
32
|
+
const lineCount = (p) => { const t = readText(p); return t ? t.split(/\r?\n/).length : 0; };
|
|
33
|
+
|
|
34
|
+
// ---------- 采集器 ----------
|
|
35
|
+
function auditArchive(repo) {
|
|
36
|
+
const dir = path.join(repo.root, '.sillyspec/changes/archive');
|
|
37
|
+
const names = fs.existsSync(dir) ? fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name) : [];
|
|
38
|
+
const DOCS = ['proposal.md', 'requirements.md', 'design.md', 'tasks.md', 'verify-result.md', 'verify-facts.json'];
|
|
39
|
+
const rows = []; const complete = []; const dated = [];
|
|
40
|
+
for (const n of names) {
|
|
41
|
+
const flags = DOCS.map((d) => (fs.existsSync(path.join(dir, n, d)) ? 1 : 0));
|
|
42
|
+
rows.push([n, ...flags, flags.reduce((a, b) => a + b, 0)]);
|
|
43
|
+
if (flags.every((f) => f === 1)) complete.push(n);
|
|
44
|
+
const m = n.match(/^(\d{4}-\d{2}-\d{2})/); if (m) dated.push(m[1]);
|
|
45
|
+
}
|
|
46
|
+
dated.sort();
|
|
47
|
+
const active = (() => {
|
|
48
|
+
const cd = path.join(repo.root, '.sillyspec/changes');
|
|
49
|
+
try { return fs.readdirSync(cd, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name !== 'archive').map((e) => e.name); } catch { return []; }
|
|
50
|
+
})();
|
|
51
|
+
writeCsv(`${repo.key}-archive.csv`, ['change', ...DOCS, 'doc_count'], rows);
|
|
52
|
+
return {
|
|
53
|
+
total: names.length,
|
|
54
|
+
fullSixDocs: complete.length,
|
|
55
|
+
dateRange: dated.length ? `${dated[0]} ~ ${dated[dated.length - 1]}` : '(无日期前缀)',
|
|
56
|
+
datedNamed: dated.length,
|
|
57
|
+
activeChanges: active,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function auditQuicklog(repo) {
|
|
62
|
+
const dir = path.join(repo.root, '.sillyspec/quicklog');
|
|
63
|
+
const files = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => /^QUICKLOG-.*\.md$/.test(f)).sort() : [];
|
|
64
|
+
const rows = []; const status = {}; const byMonth = {}; const sessions = new Set();
|
|
65
|
+
let firstTs = null, lastTs = null;
|
|
66
|
+
for (const f of files) {
|
|
67
|
+
const mUser = f.match(/^QUICKLOG-(.+?)(-\d{4}-\d{2}-\d{2})?\.md$/);
|
|
68
|
+
if (mUser) sessions.add(f.replace(/-\d{4}-\d{2}-\d{2}\.md$/, '').replace(/^QUICKLOG-/, ''));
|
|
69
|
+
const lines = (readText(path.join(dir, f)) || '').split(/\r?\n/);
|
|
70
|
+
for (let i = 0; i < lines.length; i++) {
|
|
71
|
+
const h = lines[i].match(/^## (ql-[A-Za-z0-9-]+) \| ([^|]+) \| (.*)$/);
|
|
72
|
+
if (!h) continue;
|
|
73
|
+
const [, id, ts, title] = h;
|
|
74
|
+
let st = '(未标)';
|
|
75
|
+
for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
|
|
76
|
+
const s = lines[j].match(/^状态:(.+)$/); if (s) { st = s[1].trim(); break; }
|
|
77
|
+
}
|
|
78
|
+
rows.push([id, ts.trim(), st, title.trim()]);
|
|
79
|
+
status[st] = (status[st] || 0) + 1;
|
|
80
|
+
const mo = (ts.trim().match(/^(\d{4}-\d{2})/) || [])[1]; if (mo) byMonth[mo] = (byMonth[mo] || 0) + 1;
|
|
81
|
+
if (!firstTs || ts.trim() < firstTs) firstTs = ts.trim();
|
|
82
|
+
if (!lastTs || ts.trim() > lastTs) lastTs = ts.trim();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
writeCsv(`${repo.key}-quicklog.csv`, ['ql_id', 'timestamp', 'status', 'title'], rows);
|
|
86
|
+
return { files: files.length, entries: rows.length, status, byMonth, firstTs, lastTs };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function collectDocFiles(repo) {
|
|
90
|
+
// 模块文档与 troubleshooting 只认 docs/ 与 .sillyspec/docs/ 两个根,避开 .runtime/worktrees 副本
|
|
91
|
+
const roots = [path.join(repo.root, 'docs'), path.join(repo.root, '.sillyspec/docs')];
|
|
92
|
+
const moduleFiles = [], tsFiles = [];
|
|
93
|
+
for (const r of roots) {
|
|
94
|
+
walk(r, (p) => { if (/\.md$/.test(p) && path.dirname(p).split(path.sep).pop() === 'modules' && !/\.changelog\.md$/.test(p)) moduleFiles.push(p); });
|
|
95
|
+
walk(r, (p) => { if (path.basename(p) === 'troubleshooting.md') tsFiles.push(p); });
|
|
96
|
+
}
|
|
97
|
+
return { moduleFiles, tsFiles };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function auditModules(repo, files) {
|
|
101
|
+
const rows = files.map((p) => [path.relative(repo.root, p).replace(/\\/g, '/'), lineCount(p)]).sort((a, b) => a[0].localeCompare(b[0]));
|
|
102
|
+
writeCsv(`${repo.key}-modules.csv`, ['file', 'lines'], rows);
|
|
103
|
+
const byDir = {};
|
|
104
|
+
for (const r of rows) { const d = r[0].split('/').slice(0, -1).join('/'); byDir[d] = (byDir[d] || 0) + 1; }
|
|
105
|
+
return { count: rows.length, totalLines: rows.reduce((a, b) => a + b[1], 0), byDir };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function auditTroubleshooting(repo, files) {
|
|
109
|
+
const rows = []; let total = 0; let numbered = 0;
|
|
110
|
+
for (const p of files) {
|
|
111
|
+
const lines = (readText(p) || '').split(/\r?\n/);
|
|
112
|
+
for (const ln of lines) {
|
|
113
|
+
const h = ln.match(/^##\s+(.+)$/);
|
|
114
|
+
if (h && !/^##\s*#/.test(ln) && !/^## (参考|附录|索引)/.test(h[1])) {
|
|
115
|
+
rows.push([path.relative(repo.root, p).replace(/\\/g, '/'), ++total, h[1].trim()]);
|
|
116
|
+
if (/^\d+[.、]/.test(h[1].trim())) numbered++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
writeCsv(`${repo.key}-troubleshooting.csv`, ['file', 'seq', 'title'], rows);
|
|
121
|
+
return { files: files.map((p) => path.relative(repo.root, p).replace(/\\/g, '/')), entries: total, numberedEntries: numbered };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function auditKnownIssues(repo) {
|
|
125
|
+
const p = path.join(repo.root, '.sillyspec/knowledge/known-issues.md');
|
|
126
|
+
const t = readText(p);
|
|
127
|
+
if (!t) return { exists: false };
|
|
128
|
+
const rows = []; const tally = {};
|
|
129
|
+
let cur = null;
|
|
130
|
+
for (const ln of t.split(/\r?\n/)) {
|
|
131
|
+
const h = ln.match(/^##\s+(.+)$/);
|
|
132
|
+
if (h) { if (cur) rows.push(cur); cur = [h[1].trim(), '(未标)']; continue; }
|
|
133
|
+
const s = ln.match(/^状态[::]\s*(.+)$/);
|
|
134
|
+
if (s && cur) { cur[1] = s[1].trim(); tally[cur[1]] = (tally[cur[1]] || 0) + 1; }
|
|
135
|
+
}
|
|
136
|
+
if (cur) rows.push(cur);
|
|
137
|
+
writeCsv(`${repo.key}-knownissues.csv`, ['title', 'status'], rows);
|
|
138
|
+
return { exists: true, entries: rows.length, statusTally: tally, lines: t.split(/\r?\n/).length };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function auditFr(repo) {
|
|
142
|
+
const dir = path.join(repo.root, '.sillyspec/knowledge/fr');
|
|
143
|
+
const files = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => f.endsWith('.md')) : [];
|
|
144
|
+
const rows = []; const uniqHash = new Set();
|
|
145
|
+
for (const f of files) {
|
|
146
|
+
const t = readText(path.join(dir, f)) || '';
|
|
147
|
+
let cur = null;
|
|
148
|
+
for (const ln of t.split(/\r?\n/)) {
|
|
149
|
+
const h = ln.match(/^##\s+(FR-.+)$/);
|
|
150
|
+
if (h) { if (cur) rows.push(cur); cur = [h[1].trim(), '', '', '', '']; continue; }
|
|
151
|
+
if (!cur) continue;
|
|
152
|
+
let m = ln.match(/^变更[::]\s*(.+)$/); if (m) cur[1] = m[1].trim();
|
|
153
|
+
m = ln.match(/^状态[::]\s*(.+)$/); if (m) cur[2] = m[1].trim();
|
|
154
|
+
m = ln.match(/^最近确认[::]\s*([0-9a-f]{7,40})/); if (m) { cur[3] = m[1]; uniqHash.add(m[1]); }
|
|
155
|
+
}
|
|
156
|
+
if (cur) rows.push(cur);
|
|
157
|
+
}
|
|
158
|
+
const hashOk = {};
|
|
159
|
+
for (const h of uniqHash) {
|
|
160
|
+
try { execSync(`git cat-file -t ${h}`, { cwd: repo.root, stdio: ['ignore', 'ignore', 'ignore'] }); hashOk[h] = 1; }
|
|
161
|
+
catch { hashOk[h] = 0; }
|
|
162
|
+
}
|
|
163
|
+
for (const r of rows) r[4] = r[3] ? String(hashOk[r[3]] === 1) : '(无确认哈希)';
|
|
164
|
+
writeCsv(`${repo.key}-fr.csv`, ['fr_id', 'source_change', 'status', 'last_confirm_commit', 'commit_exists_in_git'], rows);
|
|
165
|
+
return {
|
|
166
|
+
entries: rows.length,
|
|
167
|
+
withConfirm: rows.filter((r) => r[3]).length,
|
|
168
|
+
confirmVerified: rows.filter((r) => r[4] === 'true').length,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function auditTests(repo) {
|
|
173
|
+
// monorepo:全仓扫(避开 node_modules/.git/构建产物/.sillyspec 运行态/.worktrees 存储副本/.claude 夹具仓),匹配 *.test.* / *.spec.*
|
|
174
|
+
const EXCLUDE = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.sillyspec', '.next', 'target', 'out', '.worktrees', '.claude']);
|
|
175
|
+
const files = walk(repo.root, (p) => /\.(test|spec)\.[cm]?[jt]s$/.test(p), [], [...EXCLUDE]);
|
|
176
|
+
const pin = files.filter((p) => { const t = readText(p); return t && /回归钉|回归锁定|回归锁/.test(t); }).length;
|
|
177
|
+
const byTop = {};
|
|
178
|
+
for (const p of files) {
|
|
179
|
+
const rel = path.relative(repo.root, p).replace(/\\/g, '/');
|
|
180
|
+
const seg = rel.split('/')[0]; byTop[seg] = (byTop[seg] || 0) + 1;
|
|
181
|
+
}
|
|
182
|
+
return { testFiles: files.length, testLines: files.reduce((a, p) => a + lineCount(p), 0), regressionPinIndicator: pin, byTopLevelDir: byTop };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function auditGit(repo) {
|
|
186
|
+
try {
|
|
187
|
+
const count = execSync('git rev-list --count HEAD', { cwd: repo.root }).toString().trim();
|
|
188
|
+
const head = execSync('git rev-parse --short HEAD', { cwd: repo.root }).toString().trim();
|
|
189
|
+
const lastDate = execSync('git log -1 --format=%cs', { cwd: repo.root }).toString().trim();
|
|
190
|
+
return { commits: Number(count), head, lastCommitDate: lastDate };
|
|
191
|
+
} catch (e) { return { error: String(e.message) }; }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function auditGotchas(repo) {
|
|
195
|
+
// knowledge/ 下的坑目/模式账本(gotchas 族):平台仓的真实坑目基础设施,条目带 frontmatter + INDEX 关键词路由
|
|
196
|
+
const dir = path.join(repo.root, '.sillyspec/knowledge');
|
|
197
|
+
const files = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => /gotchas|patterns|uncategorized|conventions/.test(f) && f.endsWith('.md')) : [];
|
|
198
|
+
const rows = []; const tally = {};
|
|
199
|
+
for (const f of files) {
|
|
200
|
+
const t = readText(path.join(dir, f)) || '';
|
|
201
|
+
const entries = (t.match(/^##\s+(?![#])/gm) || []).length;
|
|
202
|
+
rows.push([`knowledge/${f}`, t.split(/\r?\n/).length, entries]);
|
|
203
|
+
tally[f] = entries;
|
|
204
|
+
}
|
|
205
|
+
const index = readText(path.join(dir, 'INDEX.md')) || '';
|
|
206
|
+
const routes = (index.match(/^\s*-\s+.+→/gm) || []).length;
|
|
207
|
+
writeCsv(`${repo.key}-gotchas.csv`, ['file', 'lines', 'entries'], rows);
|
|
208
|
+
return { files: rows.length, entries: rows.reduce((a, b) => a + b[2], 0), byFile: tally, indexRouteLines: routes };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function auditMisc(repo) {
|
|
212
|
+
const roadmap = path.join(repo.root, '.sillyspec/ROADMAP.md');
|
|
213
|
+
const rt = readText(roadmap);
|
|
214
|
+
const index = path.join(repo.root, '.sillyspec/knowledge/INDEX.md');
|
|
215
|
+
const it = readText(index);
|
|
216
|
+
return {
|
|
217
|
+
roadmap: rt ? { exists: true, lines: rt.split(/\r?\n/).length, items: (rt.match(/^###\s+/gm) || []).length } : { exists: false },
|
|
218
|
+
knowledgeIndex: it ? { exists: true, lines: it.split(/\r?\n/).length } : { exists: false },
|
|
219
|
+
redlinesYaml: fs.existsSync(path.join(repo.root, '.sillyspec/redlines.yaml')),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------- 汇总 ----------
|
|
224
|
+
const summary = { generatedAt: new Date().toISOString(), repos: {} };
|
|
225
|
+
for (const repo of REPOS) {
|
|
226
|
+
const docFiles = collectDocFiles(repo);
|
|
227
|
+
summary.repos[repo.key] = {
|
|
228
|
+
root: repo.root,
|
|
229
|
+
archive: auditArchive(repo),
|
|
230
|
+
quicklog: auditQuicklog(repo),
|
|
231
|
+
moduleDocs: auditModules(repo, docFiles.moduleFiles),
|
|
232
|
+
troubleshooting: auditTroubleshooting(repo, docFiles.tsFiles),
|
|
233
|
+
knownIssues: auditKnownIssues(repo),
|
|
234
|
+
frIndex: auditFr(repo),
|
|
235
|
+
gotchas: auditGotchas(repo),
|
|
236
|
+
tests: auditTests(repo),
|
|
237
|
+
git: auditGit(repo),
|
|
238
|
+
misc: auditMisc(repo),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
fs.writeFileSync(path.join(OUT, 'summary.json'), JSON.stringify(summary, null, 2), 'utf8');
|
|
242
|
+
|
|
243
|
+
// 控制台速览
|
|
244
|
+
for (const [k, v] of Object.entries(summary.repos)) {
|
|
245
|
+
console.log(`== ${k} (${v.root})`);
|
|
246
|
+
console.log(` 归档变更 ${v.archive.total}(六件套齐全 ${v.archive.fullSixDocs},${v.archive.dateRange})|活跃 ${v.archive.activeChanges.length}`);
|
|
247
|
+
console.log(` quicklog ${v.quicklog.entries} 条 / ${v.quicklog.files} 文件(${v.quicklog.firstTs} ~ ${v.quicklog.lastTs})状态分布 ${JSON.stringify(v.quicklog.status)}`);
|
|
248
|
+
console.log(` 模块文档 ${v.moduleDocs.count} 个 / ${v.moduleDocs.totalLines} 行|troubleshooting ${v.troubleshooting.entries} 条`);
|
|
249
|
+
console.log(` known-issues ${v.knownIssues.entries ?? 0} 条|FR 索引 ${v.frIndex.entries} 条(带确认 ${v.frIndex.withConfirm},哈希验真 ${v.frIndex.confirmVerified})`);
|
|
250
|
+
console.log(` gotchas 族 ${v.gotchas.entries} 条(${JSON.stringify(v.gotchas.byFile)},INDEX 路由行 ${v.gotchas.indexRouteLines})`);
|
|
251
|
+
console.log(` 测试文件 ${v.tests.testFiles} 个 / ${v.tests.testLines} 行(回归钉指示器 ${v.tests.regressionPinIndicator})|git 提交 ${v.git.commits}(HEAD ${v.git.head} @ ${v.git.lastCommitDate})`);
|
|
252
|
+
}
|
|
253
|
+
console.log('输出目录:', OUT);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 从平台仓 quicklog 提取「事故候选」:根因段含实证/坑/假红/误拦等信号的条目,
|
|
3
|
+
// 作为 troubleshooting.md 冷启动的分诊 backlog。只读,输出 CSV。
|
|
4
|
+
// 口径:mentions_test = 条目块内出现 test/spec/pytest 字样(事故是否钉了测试的弱指示器)。
|
|
5
|
+
'use strict';
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const OUT = __dirname;
|
|
9
|
+
const DIR = 'C:/Users/qinyi/IdeaProjects/multi-agent-platform/.sillyspec/quicklog';
|
|
10
|
+
|
|
11
|
+
const MARKER = /实证|踩坑|两坑|三坑|四坑|五坑|假红|假败|误拦|误报|误伤|事故|意外|教训|摩擦|坑位|翻车|卡死|挂死/;
|
|
12
|
+
const csvEscape = (s) => `"${String(s == null ? '' : s).replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`;
|
|
13
|
+
|
|
14
|
+
const files = fs.readdirSync(DIR).filter((f) => /^QUICKLOG-.*\.md$/.test(f)).sort();
|
|
15
|
+
const rows = []; let total = 0;
|
|
16
|
+
for (const f of files) {
|
|
17
|
+
const text = fs.readFileSync(path.join(DIR, f), 'utf8');
|
|
18
|
+
const blocks = text.split(/^(?=## ql-)/m);
|
|
19
|
+
for (const b of blocks) {
|
|
20
|
+
const h = b.match(/^## (ql-[A-Za-z0-9-]+) \| ([^|]+) \| ([^\r\n]*)/);
|
|
21
|
+
if (!h) continue;
|
|
22
|
+
total++;
|
|
23
|
+
const rootCause = ((b.match(/^根因[::]\s*(.+)$/m) || [])[1] || '').trim();
|
|
24
|
+
if (!MARKER.test(rootCause) && !MARKER.test(h[3])) continue;
|
|
25
|
+
const mentionsTest = /test|spec|pytest/i.test(b);
|
|
26
|
+
rows.push([h[1], h[2].trim(), h[3].trim().slice(0, 90), rootCause.slice(0, 160), String(mentionsTest)]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
rows.sort((a, b) => (a[1] < b[1] ? 1 : -1)); // 新的在前,分诊从最近的开始
|
|
30
|
+
fs.writeFileSync(
|
|
31
|
+
path.join(OUT, 'platform-troubleshooting-candidates.csv'),
|
|
32
|
+
'\ufeff' + [['ql_id', 'timestamp', 'title', 'root_cause_excerpt', 'mentions_test'], ...rows].map((r) => r.map(csvEscape).join(',')).join('\r\n'),
|
|
33
|
+
'utf8'
|
|
34
|
+
);
|
|
35
|
+
const withTest = rows.filter((r) => r[4] === 'true').length;
|
|
36
|
+
console.log(`平台 quicklog 总条目 ${total},命中事故信号 ${rows.length} 条(其中带 test 字样 ${withTest} 条,链接率 ${(rows.length ? ((withTest / rows.length) * 100).toFixed(0) : 0)}%)`);
|
|
37
|
+
console.log('输出: platform-troubleshooting-candidates.csv(按时间倒序,分诊从最近的做起)');
|