@haaaiawd/loom 0.8.0 → 0.10.0
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/cli/bin/loom.js +190 -87
- package/cli/src/auto.js +41 -18
- package/cli/src/diagnostics.js +270 -191
- package/cli/src/guide.js +92 -25
- package/cli/src/philosophy.js +51 -17
- package/cli/src/verify.js +38 -8
- package/package.json +4 -4
package/cli/src/diagnostics.js
CHANGED
|
@@ -2,106 +2,177 @@
|
|
|
2
2
|
// 提供 doctor / context / trace / reverse-dep / reverse-ref 五个聚合命令。
|
|
3
3
|
// 全部是只读的数据聚合,不做决策、不修改文件。
|
|
4
4
|
|
|
5
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { loadIntentMap, getStatus, getNextIntent, getNarrative, getIntent } from './intent-map.js';
|
|
8
|
-
import { getPhilosophy, validateInspirationSources, validatePartDecomposition } from './philosophy.js';
|
|
9
|
-
import { getVerificationHistory, getPendingVerifications, getVerificationContract } from './verify.js';
|
|
10
|
-
|
|
11
|
-
function readIntentMapRaw(versionDir) {
|
|
12
|
-
const filePath = join(versionDir, '04_INTENT_MAP.json');
|
|
13
|
-
if (!existsSync(filePath)) return null;
|
|
14
|
-
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function summarizeRawIntentMap(data) {
|
|
18
|
-
const intents = data?.intents && typeof data.intents === 'object' ? data.intents : {};
|
|
19
|
-
const ids = { pending: [], in_progress: [], completed: [], blocked: [], needs_review: [] };
|
|
20
|
-
const titles = {};
|
|
21
|
-
|
|
22
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
23
|
-
const status = intent?.status;
|
|
24
|
-
if (ids[status]) ids[status].push(id);
|
|
25
|
-
titles[id] = intent?.title || '';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
counts: {
|
|
30
|
-
pending: ids.pending.length,
|
|
31
|
-
in_progress: ids.in_progress.length,
|
|
32
|
-
completed: ids.completed.length,
|
|
33
|
-
blocked: ids.blocked.length,
|
|
34
|
-
needs_review: ids.needs_review.length,
|
|
35
|
-
total: Object.keys(intents).length,
|
|
36
|
-
},
|
|
37
|
-
ids,
|
|
38
|
-
titles,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function intentMapDiagnostics(versionDir) {
|
|
43
|
-
const issues = [];
|
|
44
|
-
let raw = null;
|
|
45
|
-
let valid = null;
|
|
46
|
-
let validMap = null;
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
raw = readIntentMapRaw(versionDir);
|
|
50
|
-
} catch (e) {
|
|
51
|
-
issues.push({ id: 'intent_map', type: 'intent_map_unreadable', severity: 'fatal', msg: `Intent Map 无法读取或 JSON 损坏: ${e.message}` });
|
|
52
|
-
return { raw, valid, issues, validMap: null };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!raw) {
|
|
56
|
-
issues.push({ id: 'intent_map', type: 'intent_map_missing', severity: 'fatal', msg: '缺少 04_INTENT_MAP.json' });
|
|
57
|
-
return { raw, valid, issues, validMap: null };
|
|
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
|
-
}
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { loadIntentMap, getStatus, getNextIntent, getNarrative, getIntent } from './intent-map.js';
|
|
8
|
+
import { getPhilosophy, validateInspirationSources, validatePartDecomposition } from './philosophy.js';
|
|
9
|
+
import { getVerificationHistory, getPendingVerifications, getVerificationContract } from './verify.js';
|
|
10
|
+
|
|
11
|
+
function readIntentMapRaw(versionDir) {
|
|
12
|
+
const filePath = join(versionDir, '04_INTENT_MAP.json');
|
|
13
|
+
if (!existsSync(filePath)) return null;
|
|
14
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function summarizeRawIntentMap(data) {
|
|
18
|
+
const intents = data?.intents && typeof data.intents === 'object' ? data.intents : {};
|
|
19
|
+
const ids = { pending: [], in_progress: [], completed: [], blocked: [], needs_review: [] };
|
|
20
|
+
const titles = {};
|
|
21
|
+
|
|
22
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
23
|
+
const status = intent?.status;
|
|
24
|
+
if (ids[status]) ids[status].push(id);
|
|
25
|
+
titles[id] = intent?.title || '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
counts: {
|
|
30
|
+
pending: ids.pending.length,
|
|
31
|
+
in_progress: ids.in_progress.length,
|
|
32
|
+
completed: ids.completed.length,
|
|
33
|
+
blocked: ids.blocked.length,
|
|
34
|
+
needs_review: ids.needs_review.length,
|
|
35
|
+
total: Object.keys(intents).length,
|
|
36
|
+
},
|
|
37
|
+
ids,
|
|
38
|
+
titles,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function intentMapDiagnostics(versionDir) {
|
|
43
|
+
const issues = [];
|
|
44
|
+
let raw = null;
|
|
45
|
+
let valid = null;
|
|
46
|
+
let validMap = null;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
raw = readIntentMapRaw(versionDir);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
issues.push({ id: 'intent_map', type: 'intent_map_unreadable', severity: 'fatal', msg: `Intent Map 无法读取或 JSON 损坏: ${e.message}` });
|
|
52
|
+
return { raw, valid, issues, validMap: null };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!raw) {
|
|
56
|
+
issues.push({ id: 'intent_map', type: 'intent_map_missing', severity: 'fatal', msg: '缺少 04_INTENT_MAP.json' });
|
|
57
|
+
return { raw, valid, issues, validMap: null };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isTemplate = raw._meta?._template === true;
|
|
61
|
+
if (isTemplate) {
|
|
62
|
+
issues.push({ id: 'intent_map', type: 'intent_map_template', severity: 'high', msg: 'Intent Map 仍是模板,尚未由 Architect 产出真实意图图', is_template: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
validMap = loadIntentMap(versionDir);
|
|
67
|
+
valid = true;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
valid = false;
|
|
70
|
+
// 模板状态下字段缺失是预期的,降级为 high 而非 fatal
|
|
71
|
+
// 非模板状态下字段缺失是真正的损坏,保持 fatal
|
|
72
|
+
issues.push({ id: 'intent_map', type: 'intent_map_invalid', severity: isTemplate ? 'high' : 'fatal', msg: e.message, is_template: isTemplate });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { raw, valid, issues, validMap };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getIntentVerificationMethod(intent) {
|
|
79
|
+
return intent.verification_method || intent._optional?.verification_method || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeVerificationCommand(command) {
|
|
83
|
+
return String(command || '')
|
|
84
|
+
.replace(/^\s*run\s+/i, '')
|
|
85
|
+
.replace(/^\s*exec\s+/i, '')
|
|
86
|
+
.replace(/\s+/g, ' ')
|
|
87
|
+
.trim();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 包管理器别名——npm test / pnpm test / bun test / yarn test 互相等价
|
|
91
|
+
const PM_ALIASES = ['npm', 'pnpm', 'bun', 'yarn'];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 把命令里的包管理器名归一化成 token,方便比较。
|
|
95
|
+
* "pnpm test" → "<PM> test","npm run build" → "<PM> run build"
|
|
96
|
+
*/
|
|
97
|
+
function normalizePackageManager(command) {
|
|
98
|
+
let result = command;
|
|
99
|
+
for (const pm of PM_ALIASES) {
|
|
100
|
+
result = result.replace(new RegExp(`\\b${pm}\\b`, 'g'), '<PM>');
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 检测项目使用的包管理器。
|
|
107
|
+
* 优先级:锁文件存在性
|
|
108
|
+
* @param {string} projectDir — 项目根目录
|
|
109
|
+
* @returns {string} 'pnpm' | 'bun' | 'yarn' | 'npm'
|
|
110
|
+
*/
|
|
111
|
+
export function detectPackageManager(projectDir) {
|
|
112
|
+
if (existsSync(join(projectDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
113
|
+
if (existsSync(join(projectDir, 'bun.lockb'))) return 'bun';
|
|
114
|
+
if (existsSync(join(projectDir, 'yarn.lock'))) return 'yarn';
|
|
115
|
+
return 'npm';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function commandCoversMethod(actualCommand, expectedMethod) {
|
|
119
|
+
const actual = normalizeVerificationCommand(actualCommand);
|
|
120
|
+
const expected = normalizeVerificationCommand(expectedMethod);
|
|
121
|
+
if (!actual || !expected) return false;
|
|
122
|
+
|
|
123
|
+
return expected.split('&&').every((part) => {
|
|
124
|
+
const expectedPart = normalizeVerificationCommand(part);
|
|
125
|
+
if (!expectedPart) return true;
|
|
126
|
+
if (actual.includes(expectedPart)) return true;
|
|
127
|
+
// 包管理器别名归一化:pnpm test = npm test = bun test = yarn test
|
|
128
|
+
const actualNorm = normalizePackageManager(actual);
|
|
129
|
+
const expectedNorm = normalizePackageManager(expectedPart);
|
|
130
|
+
if (actualNorm.includes(expectedNorm)) return true;
|
|
131
|
+
// npm test is an acceptable broader reproduction for node --test based methods.
|
|
132
|
+
if (expectedPart.startsWith('node --test') && actualNorm.includes('<PM> test')) return true;
|
|
133
|
+
return false;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
101
136
|
|
|
102
137
|
// ─── doctor ────────────────────────────────────────────
|
|
103
138
|
// 全面健康检查:一致性 + 孤儿引用 + 循环依赖 + 僵尸 Intent
|
|
104
139
|
|
|
140
|
+
// 每种 issue 类型的修复提示——给 Agent 行动化建议
|
|
141
|
+
const FIX_HINTS = {
|
|
142
|
+
intent_map_unreadable: '检查 .loom/v{N}/04_INTENT_MAP.json 是否合法 JSON(jsonlint.com 或 node -e "JSON.parse(require(\'fs\').readFileSync(\'04_INTENT_MAP.json\'))")',
|
|
143
|
+
intent_map_missing: '运行 loom init 或 loom activate architect 产出 04_INTENT_MAP.json',
|
|
144
|
+
intent_map_template: '运行 loom activate architect,Architect 填充真实 Intent Map 后删除 _meta._template 标记',
|
|
145
|
+
intent_map_invalid: '按报错信息修正 04_INTENT_MAP.json 里对应字段(补 title / 加长 acceptance / 填必填字段)',
|
|
146
|
+
completed_no_record: '在 .loom/v{N}/verifications/ 下补验证记录,或运行 loom verify pass {id} --summary "..."',
|
|
147
|
+
in_progress_no_record: '运行 loom verify pass {id} --summary "..." 写入验证记录,或 loom intent update {id} --status pending 回退',
|
|
148
|
+
orphan_philosophy_ref: '检查 04_INTENT_MAP.json 里 {id} 的 philosophy_anchors,移除或修正不存在的哲学文件引用',
|
|
149
|
+
orphan_dependency: '检查 04_INTENT_MAP.json 里 {id} 的 depends_on,移除或修正不存在的 Intent ID',
|
|
150
|
+
cycle: '打破循环:把循环链中某个 Intent 的 depends_on 里去掉前驱,或拆成更小的 Intent',
|
|
151
|
+
zombie: '检查 {id} 是否还需要——不需要就 loom intent update {id} --status completed 或 blocked',
|
|
152
|
+
completed_depends_blocked: '检查依赖 {dep} 为什么 blocked——解决阻塞或把 {id} 回退到 in_progress',
|
|
153
|
+
test_script_missing: '在 package.json 里加 test 脚本,或修正 verification_method 指向实际存在的测试命令',
|
|
154
|
+
verification_method_unverified: '运行 loom verify pass {id} --summary "..." --reproduction-command "..." 覆盖声明的验证方式',
|
|
155
|
+
verification_method_drift: '验证记录的 reproduction_command 要覆盖 verification_method 声明的命令(支持 npm/pnpm/bun 互相等价)',
|
|
156
|
+
inspiration_source: '在哲学文档的"灵感来源"章节填入至少 3 个源(- **源名** — 理由。来源:URL 或 file:// 或 local:./path)',
|
|
157
|
+
part_decomposition: '在哲学文档加"实现部分清单"章节,按 PART_DECOMPOSITION.md 拆解实现部分(- **部分名** 格式)',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 给 issue 补 fix_hint——把 {id} {dep} 等占位符替换成实际值。
|
|
162
|
+
*/
|
|
163
|
+
function addFixHint(issue) {
|
|
164
|
+
const template = FIX_HINTS[issue.type];
|
|
165
|
+
if (!template) return issue;
|
|
166
|
+
let hint = template;
|
|
167
|
+
// 提取 id 里的实际 Intent ID(issue.id 可能是 "INT-001" 或 "INT-002→INT-001" 等)
|
|
168
|
+
const idMatch = String(issue.id).match(/(INT-\d+)/);
|
|
169
|
+
if (idMatch) hint = hint.replace(/\{id\}/g, idMatch[1]);
|
|
170
|
+
// 提取 dep(从 msg 里找 depends_on 后的 Intent ID)
|
|
171
|
+
const depMatch = issue.msg && issue.msg.match(/依赖.*?(INT-\d+)/);
|
|
172
|
+
if (depMatch) hint = hint.replace(/\{dep\}/g, depMatch[1]);
|
|
173
|
+
return { ...issue, fix_hint: hint };
|
|
174
|
+
}
|
|
175
|
+
|
|
105
176
|
/**
|
|
106
177
|
* 项目健康检查。
|
|
107
178
|
* @param {string} versionDir — 当前版本目录
|
|
@@ -109,16 +180,17 @@ function commandCoversMethod(actualCommand, expectedMethod) {
|
|
|
109
180
|
* @param {string} philosophyDir — 哲学目录
|
|
110
181
|
* @returns {{ issues: object[], summary: object }}
|
|
111
182
|
*/
|
|
112
|
-
export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
113
|
-
const mapState = intentMapDiagnostics(versionDir);
|
|
114
|
-
const issues = [...mapState.issues];
|
|
115
|
-
|
|
116
|
-
if (!mapState.validMap) {
|
|
117
|
-
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
183
|
+
export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
184
|
+
const mapState = intentMapDiagnostics(versionDir);
|
|
185
|
+
const issues = [...mapState.issues];
|
|
186
|
+
|
|
187
|
+
if (!mapState.validMap) {
|
|
188
|
+
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
189
|
+
const issuesWithHints = issues.map(addFixHint);
|
|
190
|
+
return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const { intents } = mapState.validMap;
|
|
122
194
|
|
|
123
195
|
// 1. 状态一致性:in_progress/completed 但无验证记录
|
|
124
196
|
for (const [id, intent] of Object.entries(intents)) {
|
|
@@ -188,91 +260,93 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
188
260
|
}
|
|
189
261
|
}
|
|
190
262
|
|
|
191
|
-
// 7. 验证脚本可执行性:检查 verification_method 引用的脚本/目录是否存在
|
|
192
|
-
const projectDir = join(versionDir, '..', '..');
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
263
|
+
// 7. 验证脚本可执行性:检查 verification_method 引用的脚本/目录是否存在
|
|
264
|
+
const projectDir = join(versionDir, '..', '..');
|
|
265
|
+
const pm = detectPackageManager(projectDir);
|
|
266
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
267
|
+
const vm = getIntentVerificationMethod(intent);
|
|
268
|
+
if (!vm) continue;
|
|
269
|
+
// 检测任意包管理器的 test 引用(npm/pnpm/bun/yarn)
|
|
270
|
+
const pmTestRe = new RegExp(`(?:${PM_ALIASES.join('|')})\\s+(?:run\\s+)?test`);
|
|
271
|
+
if (pmTestRe.test(vm)) {
|
|
198
272
|
const pkgPath = join(projectDir, 'package.json');
|
|
199
273
|
if (!existsSync(pkgPath)) {
|
|
200
|
-
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求
|
|
274
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但项目根没有 package.json` });
|
|
201
275
|
} else {
|
|
202
276
|
try {
|
|
203
277
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
204
278
|
const testScript = pkg.scripts && pkg.scripts.test;
|
|
205
279
|
if (!testScript) {
|
|
206
|
-
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求
|
|
280
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但 package.json 没有 test 脚本` });
|
|
207
281
|
} else {
|
|
208
282
|
// 检查 test 脚本引用的目录/文件是否存在
|
|
209
|
-
// 常见模式: "node --test test/" / "mocha test/" / "jest" 等
|
|
210
283
|
const testDirMatch = testScript.match(/(?:--test|test)\s+(\S+)/);
|
|
211
284
|
if (testDirMatch) {
|
|
212
285
|
const testTarget = testDirMatch[1].replace(/['"]/g, '');
|
|
213
286
|
if (!existsSync(join(projectDir, testTarget))) {
|
|
214
|
-
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求
|
|
287
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但 test 脚本引用的 ${testTarget} 不存在` });
|
|
215
288
|
}
|
|
216
289
|
}
|
|
217
290
|
}
|
|
218
291
|
} catch {
|
|
219
292
|
// package.json 解析失败,不报——不是 doctor 的职责
|
|
220
293
|
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// 8. completed Intent 的 verification_method 必须被最新验证记录覆盖,防止契约命令漂移。
|
|
226
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
227
|
-
if (intent.status !== 'completed') continue;
|
|
228
|
-
const method = getIntentVerificationMethod(intent);
|
|
229
|
-
if (!method) continue;
|
|
230
|
-
|
|
231
|
-
const expected = normalizeVerificationCommand(method);
|
|
232
|
-
if (!expected || expected === 'human_review') continue;
|
|
233
|
-
|
|
234
|
-
const history = getVerificationHistory(verificationsDir, id);
|
|
235
|
-
const latest = history?.records?.[history.records.length - 1];
|
|
236
|
-
const actual = normalizeVerificationCommand(latest?.reproduction_command);
|
|
237
|
-
|
|
238
|
-
if (!latest) {
|
|
239
|
-
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 声明了 verification_method 但没有验证记录覆盖: ${method}` });
|
|
240
|
-
} else if (!actual) {
|
|
241
|
-
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 最新验证记录缺少 reproduction_command,无法复现 verification_method: ${method}` });
|
|
242
|
-
} else if (!commandCoversMethod(actual, expected)) {
|
|
243
|
-
issues.push({ id, type: 'verification_method_drift', severity: 'high', msg: `${id} verification_method 未被最新 reproduction_command 覆盖。method="${method}" reproduction_command="${latest.reproduction_command}"` });
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 8. completed Intent 的 verification_method 必须被最新验证记录覆盖,防止契约命令漂移。
|
|
299
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
300
|
+
if (intent.status !== 'completed') continue;
|
|
301
|
+
const method = getIntentVerificationMethod(intent);
|
|
302
|
+
if (!method) continue;
|
|
303
|
+
|
|
304
|
+
const expected = normalizeVerificationCommand(method);
|
|
305
|
+
if (!expected || expected === 'human_review') continue;
|
|
306
|
+
|
|
307
|
+
const history = getVerificationHistory(verificationsDir, id);
|
|
308
|
+
const latest = history?.records?.[history.records.length - 1];
|
|
309
|
+
const actual = normalizeVerificationCommand(latest?.reproduction_command);
|
|
310
|
+
|
|
311
|
+
if (!latest) {
|
|
312
|
+
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 声明了 verification_method 但没有验证记录覆盖: ${method}` });
|
|
313
|
+
} else if (!actual) {
|
|
314
|
+
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 最新验证记录缺少 reproduction_command,无法复现 verification_method: ${method}` });
|
|
315
|
+
} else if (!commandCoversMethod(actual, expected)) {
|
|
316
|
+
issues.push({ id, type: 'verification_method_drift', severity: 'high', msg: `${id} verification_method 未被最新 reproduction_command 覆盖。method="${method}" reproduction_command="${latest.reproduction_command}"` });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
321
|
+
const issuesWithHints = issues.map(addFixHint);
|
|
322
|
+
return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function appendPhilosophyDiagnostics(issues, philosophyDir) {
|
|
326
|
+
// 哲学灵感来源校验(防止 Weaver 从训练数据"背"几个名字就交差)
|
|
327
|
+
if (!existsSync(philosophyDir)) return;
|
|
328
|
+
|
|
329
|
+
const inspirationCheck = validateInspirationSources(philosophyDir);
|
|
330
|
+
for (const issue of inspirationCheck.issues) {
|
|
331
|
+
issues.push({ id: 'philosophy', type: 'inspiration_source', severity: issue.severity, msg: issue.msg });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 实现部分拆解校验(防止 Weaver 跳过拆解步骤)
|
|
335
|
+
const decompositionCheck = validatePartDecomposition(philosophyDir);
|
|
336
|
+
for (const issue of decompositionCheck.issues) {
|
|
337
|
+
issues.push({ id: 'philosophy', type: 'part_decomposition', severity: issue.severity, msg: issue.msg });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function summarizeIssues(issues) {
|
|
342
|
+
return {
|
|
343
|
+
total_issues: issues.length,
|
|
344
|
+
fatal: issues.filter((i) => i.severity === 'fatal').length,
|
|
345
|
+
high: issues.filter((i) => i.severity === 'high').length,
|
|
346
|
+
medium: issues.filter((i) => i.severity === 'medium').length,
|
|
347
|
+
healthy: issues.length === 0,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
276
350
|
|
|
277
351
|
/**
|
|
278
352
|
* DFS 检测循环依赖。
|
|
@@ -321,32 +395,37 @@ function detectCycles(intents) {
|
|
|
321
395
|
* @param {string} philosophyDir
|
|
322
396
|
* @returns {object}
|
|
323
397
|
*/
|
|
324
|
-
export function contextSummary(versionDir, verificationsDir, philosophyDir) {
|
|
325
|
-
const mapState = intentMapDiagnostics(versionDir);
|
|
326
|
-
const status = mapState.valid ? getStatus(versionDir) : summarizeRawIntentMap(mapState.raw);
|
|
327
|
-
const next = mapState.valid ? getNextIntent(versionDir) : null;
|
|
328
|
-
const pending = mapState.valid ? getPendingVerifications(versionDir, verificationsDir) : [];
|
|
329
|
-
const { issues } = doctor(versionDir, verificationsDir, philosophyDir);
|
|
398
|
+
export function contextSummary(versionDir, verificationsDir, philosophyDir) {
|
|
399
|
+
const mapState = intentMapDiagnostics(versionDir);
|
|
400
|
+
const status = mapState.valid ? getStatus(versionDir) : summarizeRawIntentMap(mapState.raw);
|
|
401
|
+
const next = mapState.valid ? getNextIntent(versionDir) : null;
|
|
402
|
+
const pending = mapState.valid ? getPendingVerifications(versionDir, verificationsDir) : [];
|
|
403
|
+
const { issues } = doctor(versionDir, verificationsDir, philosophyDir);
|
|
404
|
+
|
|
405
|
+
// 区分模板阶段问题(待填充)和真实损坏
|
|
406
|
+
const templateIssues = issues.filter((i) => i.is_template || i.type === 'intent_map_template' || i.type === 'inspiration_source' || i.type === 'part_decomposition');
|
|
407
|
+
const realIssues = issues.filter((i) => !templateIssues.includes(i));
|
|
330
408
|
|
|
331
409
|
const risks = [];
|
|
332
|
-
const fatalCount =
|
|
333
|
-
const highCount =
|
|
334
|
-
if (fatalCount > 0) risks.push(`${fatalCount} 个致命问题(Intent Map 损坏/循环依赖)`);
|
|
335
|
-
if (highCount > 0) risks.push(`${highCount} 个高严重度问题(状态不一致/孤儿引用)`);
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
410
|
+
const fatalCount = realIssues.filter((i) => i.severity === 'fatal').length;
|
|
411
|
+
const highCount = realIssues.filter((i) => i.severity === 'high').length;
|
|
412
|
+
if (fatalCount > 0) risks.push(`${fatalCount} 个致命问题(Intent Map 损坏/循环依赖)`);
|
|
413
|
+
if (highCount > 0) risks.push(`${highCount} 个高严重度问题(状态不一致/孤儿引用)`);
|
|
414
|
+
if (templateIssues.length > 0) risks.push(`${templateIssues.length} 个待填充(模板未产出,需 Weaver/Architect 填充)`);
|
|
415
|
+
if (status.counts.blocked > 0) risks.push(`${status.counts.blocked} 个阻塞 Intent`);
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
intent_map_valid: mapState.valid === true,
|
|
419
|
+
progress: {
|
|
420
|
+
completed: status.counts.completed,
|
|
421
|
+
total: status.counts.total,
|
|
422
|
+
rate: `${status.counts.completed}/${status.counts.total}`,
|
|
344
423
|
},
|
|
345
424
|
next_intent: next ? next.id : null,
|
|
346
425
|
pending_verifications: pending,
|
|
347
426
|
inconsistent_states: issues.filter((i) => i.type === 'in_progress_no_record' || i.type === 'completed_no_record').map((i) => i.id),
|
|
348
427
|
risks,
|
|
349
|
-
healthy:
|
|
428
|
+
healthy: realIssues.length === 0,
|
|
350
429
|
};
|
|
351
430
|
}
|
|
352
431
|
|