@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.
@@ -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
- if (raw._meta?._template === true) {
61
- issues.push({ id: 'intent_map', type: 'intent_map_template', severity: 'high', msg: 'Intent Map 仍是模板,尚未由 Architect 产出真实意图图' });
62
- }
63
-
64
- try {
65
- validMap = loadIntentMap(versionDir);
66
- valid = true;
67
- } catch (e) {
68
- valid = false;
69
- issues.push({ id: 'intent_map', type: 'intent_map_invalid', severity: 'fatal', msg: e.message });
70
- }
71
-
72
- return { raw, valid, issues, validMap };
73
- }
74
-
75
- function getIntentVerificationMethod(intent) {
76
- return intent.verification_method || intent._optional?.verification_method || null;
77
- }
78
-
79
- function normalizeVerificationCommand(command) {
80
- return String(command || '')
81
- .replace(/^\s*run\s+/i, '')
82
- .replace(/^\s*exec\s+/i, '')
83
- .replace(/\s+/g, ' ')
84
- .trim();
85
- }
86
-
87
- function commandCoversMethod(actualCommand, expectedMethod) {
88
- const actual = normalizeVerificationCommand(actualCommand);
89
- const expected = normalizeVerificationCommand(expectedMethod);
90
- if (!actual || !expected) return false;
91
-
92
- return expected.split('&&').every((part) => {
93
- const expectedPart = normalizeVerificationCommand(part);
94
- if (!expectedPart) return true;
95
- if (actual.includes(expectedPart)) return true;
96
- // npm test is an acceptable broader reproduction for node --test based methods.
97
- if (expectedPart.startsWith('node --test') && actual.includes('npm test')) return true;
98
- return false;
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
- return { issues, summary: summarizeIssues(issues) };
119
- }
120
-
121
- const { intents } = mapState.validMap;
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
- for (const [id, intent] of Object.entries(intents)) {
194
- const vm = getIntentVerificationMethod(intent);
195
- if (!vm) continue;
196
- // 检测 npm test 引用
197
- if (vm.includes('npm test') || vm.includes('npm run test')) {
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 要求 npm test 但项目根没有 package.json` });
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 要求 npm test 但 package.json 没有 test 脚本` });
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 要求 npm test 但 test 脚本引用的 ${testTarget} 不存在` });
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
- return { issues, summary: summarizeIssues(issues) };
249
- }
250
-
251
- function appendPhilosophyDiagnostics(issues, philosophyDir) {
252
- // 哲学灵感来源校验(防止 Weaver 从训练数据"背"几个名字就交差)
253
- if (!existsSync(philosophyDir)) return;
254
-
255
- const inspirationCheck = validateInspirationSources(philosophyDir);
256
- for (const issue of inspirationCheck.issues) {
257
- issues.push({ id: 'philosophy', type: 'inspiration_source', severity: issue.severity, msg: issue.msg });
258
- }
259
-
260
- // 实现部分拆解校验(防止 Weaver 跳过拆解步骤)
261
- const decompositionCheck = validatePartDecomposition(philosophyDir);
262
- for (const issue of decompositionCheck.issues) {
263
- issues.push({ id: 'philosophy', type: 'part_decomposition', severity: issue.severity, msg: issue.msg });
264
- }
265
- }
266
-
267
- function summarizeIssues(issues) {
268
- return {
269
- total_issues: issues.length,
270
- fatal: issues.filter((i) => i.severity === 'fatal').length,
271
- high: issues.filter((i) => i.severity === 'high').length,
272
- medium: issues.filter((i) => i.severity === 'medium').length,
273
- healthy: issues.length === 0,
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 = issues.filter((i) => i.severity === 'fatal').length;
333
- const highCount = issues.filter((i) => i.severity === 'high').length;
334
- if (fatalCount > 0) risks.push(`${fatalCount} 个致命问题(Intent Map 损坏/循环依赖)`);
335
- if (highCount > 0) risks.push(`${highCount} 个高严重度问题(状态不一致/孤儿引用)`);
336
- if (status.counts.blocked > 0) risks.push(`${status.counts.blocked} 个阻塞 Intent`);
337
-
338
- return {
339
- intent_map_valid: mapState.valid === true,
340
- progress: {
341
- completed: status.counts.completed,
342
- total: status.counts.total,
343
- rate: `${status.counts.completed}/${status.counts.total}`,
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: issues.length === 0,
428
+ healthy: realIssues.length === 0,
350
429
  };
351
430
  }
352
431