@haaaiawd/loom 0.9.0 → 1.0.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -52
  3. package/cli/bin/loom.js +438 -149
  4. package/cli/help/concepts.md +93 -72
  5. package/cli/help/doctor.md +71 -121
  6. package/cli/help/loop.md +120 -135
  7. package/cli/help/patch.md +33 -0
  8. package/cli/help/preview.md +2 -1
  9. package/cli/help/version.md +92 -16
  10. package/cli/help/workflow.md +89 -100
  11. package/cli/src/activate.js +302 -73
  12. package/cli/src/auto.js +41 -18
  13. package/cli/src/diagnostics.js +223 -50
  14. package/cli/src/guide.js +127 -38
  15. package/cli/src/init.js +50 -29
  16. package/cli/src/intent-draft.js +303 -0
  17. package/cli/src/intent-map.js +540 -54
  18. package/cli/src/patch.js +214 -0
  19. package/cli/src/philosophy.js +181 -156
  20. package/cli/src/preview-prompt.md +13 -6
  21. package/cli/src/preview.js +1 -0
  22. package/cli/src/shared/intent-ref.js +38 -0
  23. package/cli/src/shared/proof-reference.js +19 -0
  24. package/cli/src/shared/verification-method.js +32 -0
  25. package/cli/src/verify.js +204 -51
  26. package/cli/src/version.js +5 -4
  27. package/dimensions/PART_DECOMPOSITION.md +42 -203
  28. package/dimensions/SEARCH_METHODOLOGY.md +101 -97
  29. package/dimensions/examples/AGENT_SYSTEM/README.md +1 -1
  30. package/dimensions/examples/CLI_TOOL/README.md +1 -1
  31. package/dimensions/universal/COLLABORATION_PHILOSOPHY.md +28 -77
  32. package/dimensions/universal/ENGINEERING_CREED.md +30 -74
  33. package/dimensions/universal/PRODUCT_PHILOSOPHY.md +32 -70
  34. package/meta/BASELINE.md +91 -276
  35. package/meta/INTENT_LOOP.md +242 -737
  36. package/meta/PHILOSOPHY_WEAVER.md +110 -343
  37. package/meta/ROLE_ACTIVATION.md +103 -267
  38. package/package.json +4 -3
  39. package/roles/architect.md +71 -111
  40. package/roles/forge.md +87 -126
  41. package/roles/keeper.md +99 -223
  42. package/roles/visionary.md +57 -86
  43. package/templates/INTENT_MAP_TEMPLATE.json +24 -10
  44. package/templates/PHILOSOPHY_TEMPLATE.md +44 -75
  45. package/templates/VISION_TEMPLATE.md +44 -67
@@ -3,10 +3,14 @@
3
3
  // 全部是只读的数据聚合,不做决策、不修改文件。
4
4
 
5
5
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
6
- import { join } from 'node:path';
6
+ import { basename, dirname, join } from 'node:path';
7
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';
8
+ import { getPhilosophy, validateInspirationSources } from './philosophy.js';
9
+ import { getVerificationHistory, getPendingVerifications, getVerificationContract, getLatestPassedVerification, getVerificationIntentRevision, isVerificationCurrent, hasCurrentPassedVerification } from './verify.js';
10
+ import { validatePatches } from './patch.js';
11
+ import { formatIntentRef } from './shared/intent-ref.js';
12
+ import { commandCoversVerificationMethod, getIntentVerificationMethod } from './shared/verification-method.js';
13
+ import { resolveQualityProofReference } from './shared/proof-reference.js';
10
14
 
11
15
  function readIntentMapRaw(versionDir) {
12
16
  const filePath = join(versionDir, '04_INTENT_MAP.json');
@@ -59,7 +63,7 @@ function intentMapDiagnostics(versionDir) {
59
63
 
60
64
  const isTemplate = raw._meta?._template === true;
61
65
  if (isTemplate) {
62
- issues.push({ id: 'intent_map', type: 'intent_map_template', severity: 'high', msg: 'Intent Map 仍是模板,尚未由 Architect 产出真实意图图' });
66
+ issues.push({ id: 'intent_map', type: 'intent_map_template', severity: 'high', msg: 'Intent Map 仍是模板,尚未由 Architect 产出真实意图图', is_template: true });
63
67
  }
64
68
 
65
69
  try {
@@ -69,16 +73,12 @@ function intentMapDiagnostics(versionDir) {
69
73
  valid = false;
70
74
  // 模板状态下字段缺失是预期的,降级为 high 而非 fatal
71
75
  // 非模板状态下字段缺失是真正的损坏,保持 fatal
72
- issues.push({ id: 'intent_map', type: 'intent_map_invalid', severity: isTemplate ? 'high' : 'fatal', msg: e.message });
76
+ issues.push({ id: 'intent_map', type: 'intent_map_invalid', severity: isTemplate ? 'high' : 'fatal', msg: e.message, is_template: isTemplate });
73
77
  }
74
78
 
75
79
  return { raw, valid, issues, validMap };
76
80
  }
77
81
 
78
- function getIntentVerificationMethod(intent) {
79
- return intent.verification_method || intent._optional?.verification_method || null;
80
- }
81
-
82
82
  function normalizeVerificationCommand(command) {
83
83
  return String(command || '')
84
84
  .replace(/^\s*run\s+/i, '')
@@ -87,6 +87,34 @@ function normalizeVerificationCommand(command) {
87
87
  .trim();
88
88
  }
89
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
+
90
118
  function commandCoversMethod(actualCommand, expectedMethod) {
91
119
  const actual = normalizeVerificationCommand(actualCommand);
92
120
  const expected = normalizeVerificationCommand(expectedMethod);
@@ -96,8 +124,12 @@ function commandCoversMethod(actualCommand, expectedMethod) {
96
124
  const expectedPart = normalizeVerificationCommand(part);
97
125
  if (!expectedPart) return true;
98
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;
99
131
  // npm test is an acceptable broader reproduction for node --test based methods.
100
- if (expectedPart.startsWith('node --test') && actual.includes('npm test')) return true;
132
+ if (expectedPart.startsWith('node --test') && actualNorm.includes('<PM> test')) return true;
101
133
  return false;
102
134
  });
103
135
  }
@@ -105,6 +137,48 @@ function commandCoversMethod(actualCommand, expectedMethod) {
105
137
  // ─── doctor ────────────────────────────────────────────
106
138
  // 全面健康检查:一致性 + 孤儿引用 + 循环依赖 + 僵尸 Intent
107
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
+ completed_verification_not_passed: '最新验证不是当前 revision 的 passed;重新运行 loom verify pass {id} --summary "...",再用 loom intent done {id} 闭合。',
148
+ in_progress_no_record: '运行 loom verify pass {id} --summary "..." 写入验证记录,或 loom intent update {id} --status pending 回退',
149
+ orphan_philosophy_ref: '检查 04_INTENT_MAP.json 里 {id} 的 philosophy_anchors,移除或修正不存在的哲学文件引用',
150
+ orphan_dependency: '检查 04_INTENT_MAP.json 里 {id} 的 depends_on,移除或修正不存在的 Intent ID',
151
+ cycle: '打破循环:把循环链中某个 Intent 的 depends_on 里去掉前驱,或拆成更小的 Intent',
152
+ zombie: '检查 {id} 是否还需要——不需要就 loom intent update {id} --status completed 或 blocked',
153
+ completed_depends_blocked: '检查依赖 {dep} 为什么 blocked——解决阻塞或把 {id} 回退到 in_progress',
154
+ test_script_missing: '在 package.json 里加 test 脚本,或修正 verification_method 指向实际存在的测试命令',
155
+ verification_method_unverified: '运行 loom verify pass {id} --summary "..." --reproduction-command "..." 覆盖声明的验证方式',
156
+ verification_method_drift: '验证记录的 reproduction_command 要覆盖 verification_method 声明的命令(支持 npm/pnpm/bun 互相等价)',
157
+ stale_verification: '重新验证当前 Intent revision:loom verify pass {id} --summary "...",通过后再闭合 Intent',
158
+ inspiration_source: '在哲学文档的"证据地图/灵感来源"中写入实际使用的来源、选择理由和可追溯位置;数量由判断所需决定',
159
+ quality_dimension_missing: '为带 quality_contract 的 Intent 补写并通过 quality_achievement;相对提升声明在该维度中链接 Quality Proof',
160
+ quality_proof_invalid: '将 quality_proof_ref 改为项目内真实存在且含锚点的 Markdown 证据,例如 verifications/INT-001-quality-proof.md#int-001',
161
+ preservation_dimension_missing: '为 continuity_required 的 Intent 补写并通过 preservation_achievement,证据必须覆盖旧状态到新操作后的完整序列',
162
+ patch_changelog_invalid: '运行 loom patch validate 查看具体错误;修正 06_CHANGELOG.json 后重新生成 Markdown 投影',
163
+ patch_projection_drift: '不要手工编辑 06_CHANGELOG.md;重新运行 loom init 或下一次 loom patch record 生成投影',
164
+ };
165
+
166
+ /**
167
+ * 给 issue 补 fix_hint——把 {id} {dep} 等占位符替换成实际值。
168
+ */
169
+ function addFixHint(issue) {
170
+ const template = FIX_HINTS[issue.type];
171
+ if (!template) return issue;
172
+ let hint = template;
173
+ // 提取 id 里的实际 Intent ID(issue.id 可能是 "INT-001" 或 "INT-002→INT-001" 等)
174
+ const idMatch = String(issue.id).match(/(INT-\d+)/);
175
+ if (idMatch) hint = hint.replace(/\{id\}/g, idMatch[1]);
176
+ // 提取 dep(从 msg 里找 depends_on 后的 Intent ID)
177
+ const depMatch = issue.msg && issue.msg.match(/依赖.*?(INT-\d+)/);
178
+ if (depMatch) hint = hint.replace(/\{dep\}/g, depMatch[1]);
179
+ return { ...issue, fix_hint: hint };
180
+ }
181
+
108
182
  /**
109
183
  * 项目健康检查。
110
184
  * @param {string} versionDir — 当前版本目录
@@ -118,21 +192,55 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
118
192
 
119
193
  if (!mapState.validMap) {
120
194
  appendPhilosophyDiagnostics(issues, philosophyDir);
121
- return { issues, summary: summarizeIssues(issues) };
195
+ const issuesWithHints = issues.map(addFixHint);
196
+ return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
122
197
  }
123
198
 
124
199
  const { intents } = mapState.validMap;
125
200
 
126
- // 1. 状态一致性:in_progress/completed 但无验证记录
127
- for (const [id, intent] of Object.entries(intents)) {
128
- const hasRecord = existsSync(join(verificationsDir, `${id}.json`));
129
- if (intent.status === 'completed' && !hasRecord) {
130
- issues.push({ id, type: 'completed_no_record', severity: 'high', msg: `${id} 状态为 completed 但无验证记录` });
131
- }
132
- if (intent.status === 'in_progress' && !hasRecord) {
133
- issues.push({ id, type: 'in_progress_no_record', severity: 'medium', msg: `${id} 状态为 in_progress 但无验证记录(可能上次中断)` });
134
- }
135
- }
201
+ // 1. 状态一致性:completed 必须由当前 revision 的最后一条 passed 验证支撑。
202
+ for (const [id, intent] of Object.entries(intents)) {
203
+ const history = getVerificationHistory(verificationsDir, id);
204
+ const hasRecord = Boolean(history?.records?.length);
205
+ const latest = history?.records?.[history.records.length - 1];
206
+ if (intent.status === 'completed' && !hasRecord) {
207
+ issues.push({ id, type: 'completed_no_record', severity: 'high', msg: `${id} 状态为 completed 但无验证记录` });
208
+ } else if (intent.status === 'completed' && !hasCurrentPassedVerification(intent, history)) {
209
+ issues.push({ id, type: 'completed_verification_not_passed', severity: 'high', msg: `${id} 状态为 completed,但最后一条验证不是当前 revision 的 passed` });
210
+ }
211
+ if (intent.status === 'in_progress' && !hasRecord) {
212
+ issues.push({ id, type: 'in_progress_no_record', severity: 'medium', msg: `${id} 状态为 in_progress 但无验证记录(可能上次中断)` });
213
+ }
214
+ if (intent.quality_contract && latest?.verdict === 'passed') {
215
+ const quality = latest.dimensions?.quality_achievement;
216
+ if (!quality || quality.verdict !== 'passed') {
217
+ issues.push({
218
+ id,
219
+ type: 'quality_dimension_missing',
220
+ severity: 'high',
221
+ msg: `${id} 声明了 quality_contract,但最新 passed 记录缺少通过的 quality_achievement`,
222
+ });
223
+ }
224
+ if (quality?.quality_proof_ref) {
225
+ try {
226
+ resolveQualityProofReference(versionDir, quality.quality_proof_ref);
227
+ } catch (error) {
228
+ issues.push({ id, type: 'quality_proof_invalid', severity: 'high', msg: `${id} 的 Quality Proof 无效: ${error.message}` });
229
+ }
230
+ }
231
+ }
232
+ if (intent.continuity_required && latest?.verdict === 'passed') {
233
+ const preservation = latest.dimensions?.preservation_achievement;
234
+ if (!preservation || preservation.verdict !== 'passed') {
235
+ issues.push({
236
+ id,
237
+ type: 'preservation_dimension_missing',
238
+ severity: 'high',
239
+ msg: `${id} 声明了 continuity_required,但最新 passed 记录缺少通过的 preservation_achievement`,
240
+ });
241
+ }
242
+ }
243
+ }
136
244
 
137
245
  // 2. 孤儿引用:哲学锚点指向不存在的文件
138
246
  for (const [id, intent] of Object.entries(intents)) {
@@ -193,28 +301,29 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
193
301
 
194
302
  // 7. 验证脚本可执行性:检查 verification_method 引用的脚本/目录是否存在
195
303
  const projectDir = join(versionDir, '..', '..');
304
+ const pm = detectPackageManager(projectDir);
196
305
  for (const [id, intent] of Object.entries(intents)) {
197
306
  const vm = getIntentVerificationMethod(intent);
198
307
  if (!vm) continue;
199
- // 检测 npm test 引用
200
- if (vm.includes('npm test') || vm.includes('npm run test')) {
308
+ // 检测任意包管理器的 test 引用(npm/pnpm/bun/yarn)
309
+ const pmTestRe = new RegExp(`(?:${PM_ALIASES.join('|')})\\s+(?:run\\s+)?test`);
310
+ if (pmTestRe.test(vm)) {
201
311
  const pkgPath = join(projectDir, 'package.json');
202
312
  if (!existsSync(pkgPath)) {
203
- issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 npm test 但项目根没有 package.json` });
313
+ issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但项目根没有 package.json` });
204
314
  } else {
205
315
  try {
206
316
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
207
317
  const testScript = pkg.scripts && pkg.scripts.test;
208
318
  if (!testScript) {
209
- issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 npm test 但 package.json 没有 test 脚本` });
319
+ issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但 package.json 没有 test 脚本` });
210
320
  } else {
211
321
  // 检查 test 脚本引用的目录/文件是否存在
212
- // 常见模式: "node --test test/" / "mocha test/" / "jest" 等
213
322
  const testDirMatch = testScript.match(/(?:--test|test)\s+(\S+)/);
214
323
  if (testDirMatch) {
215
324
  const testTarget = testDirMatch[1].replace(/['"]/g, '');
216
325
  if (!existsSync(join(projectDir, testTarget))) {
217
- issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 npm test 但 test 脚本引用的 ${testTarget} 不存在` });
326
+ issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但 test 脚本引用的 ${testTarget} 不存在` });
218
327
  }
219
328
  }
220
329
  }
@@ -242,13 +351,44 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
242
351
  issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 声明了 verification_method 但没有验证记录覆盖: ${method}` });
243
352
  } else if (!actual) {
244
353
  issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 最新验证记录缺少 reproduction_command,无法复现 verification_method: ${method}` });
245
- } else if (!commandCoversMethod(actual, expected)) {
354
+ } else if (!commandCoversVerificationMethod(actual, expected)) {
246
355
  issues.push({ id, type: 'verification_method_drift', severity: 'high', msg: `${id} verification_method 未被最新 reproduction_command 覆盖。method="${method}" reproduction_command="${latest.reproduction_command}"` });
247
356
  }
248
357
  }
249
358
 
250
- appendPhilosophyDiagnostics(issues, philosophyDir);
251
- return { issues, summary: summarizeIssues(issues) };
359
+ const patchJsonPath = join(versionDir, '06_CHANGELOG.json');
360
+ if (existsSync(patchJsonPath)) {
361
+ try {
362
+ validatePatches(versionDir);
363
+ } catch (error) {
364
+ const projectionDrift = String(error.message).includes('最新确定性投影') || String(error.message).includes('Markdown 投影不存在');
365
+ issues.push({
366
+ id: 'patch_changelog',
367
+ type: projectionDrift ? 'patch_projection_drift' : 'patch_changelog_invalid',
368
+ severity: projectionDrift ? 'medium' : 'high',
369
+ msg: error.message,
370
+ });
371
+ }
372
+ }
373
+
374
+ // completed/needs_review 的旧 revision 验证不能证明当前 Intent 仍然成立。
375
+ for (const [id, intent] of Object.entries(intents)) {
376
+ if (intent.status !== 'completed' && intent.status !== 'needs_review') continue;
377
+ const history = getVerificationHistory(verificationsDir, id);
378
+ const latestPassed = getLatestPassedVerification(history);
379
+ if (!latestPassed || isVerificationCurrent(intent, latestPassed)) continue;
380
+ const verifiedRevision = getVerificationIntentRevision(intent, latestPassed);
381
+ issues.push({
382
+ id,
383
+ type: 'stale_verification',
384
+ severity: 'high',
385
+ msg: `${id} 最新 passed 验证 revision ${verifiedRevision ?? 'legacy/unknown'} 早于当前 Intent revision ${intent.revision ?? 1}`,
386
+ });
387
+ }
388
+
389
+ appendPhilosophyDiagnostics(issues, philosophyDir);
390
+ const issuesWithHints = issues.map(addFixHint);
391
+ return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
252
392
  }
253
393
 
254
394
  function appendPhilosophyDiagnostics(issues, philosophyDir) {
@@ -260,11 +400,6 @@ function appendPhilosophyDiagnostics(issues, philosophyDir) {
260
400
  issues.push({ id: 'philosophy', type: 'inspiration_source', severity: issue.severity, msg: issue.msg });
261
401
  }
262
402
 
263
- // 实现部分拆解校验(防止 Weaver 跳过拆解步骤)
264
- const decompositionCheck = validatePartDecomposition(philosophyDir);
265
- for (const issue of decompositionCheck.issues) {
266
- issues.push({ id: 'philosophy', type: 'part_decomposition', severity: issue.severity, msg: issue.msg });
267
- }
268
403
  }
269
404
 
270
405
  function summarizeIssues(issues) {
@@ -331,25 +466,31 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
331
466
  const pending = mapState.valid ? getPendingVerifications(versionDir, verificationsDir) : [];
332
467
  const { issues } = doctor(versionDir, verificationsDir, philosophyDir);
333
468
 
469
+ // 区分模板阶段问题(待填充)和真实损坏
470
+ const templateIssues = issues.filter((i) => i.is_template || i.type === 'intent_map_template' || i.type === 'inspiration_source');
471
+ const realIssues = issues.filter((i) => !templateIssues.includes(i));
472
+
334
473
  const risks = [];
335
- const fatalCount = issues.filter((i) => i.severity === 'fatal').length;
336
- const highCount = issues.filter((i) => i.severity === 'high').length;
474
+ const fatalCount = realIssues.filter((i) => i.severity === 'fatal').length;
475
+ const highCount = realIssues.filter((i) => i.severity === 'high').length;
337
476
  if (fatalCount > 0) risks.push(`${fatalCount} 个致命问题(Intent Map 损坏/循环依赖)`);
338
477
  if (highCount > 0) risks.push(`${highCount} 个高严重度问题(状态不一致/孤儿引用)`);
478
+ if (templateIssues.length > 0) risks.push(`${templateIssues.length} 个待填充(模板未产出,需 Weaver/Architect 填充)`);
339
479
  if (status.counts.blocked > 0) risks.push(`${status.counts.blocked} 个阻塞 Intent`);
340
480
 
341
481
  return {
342
482
  intent_map_valid: mapState.valid === true,
343
- progress: {
483
+ progress: {
344
484
  completed: status.counts.completed,
345
485
  total: status.counts.total,
346
- rate: `${status.counts.completed}/${status.counts.total}`,
347
- },
486
+ rate: `${status.counts.completed}/${status.counts.total}`,
487
+ },
488
+ deprecated_intents: status.deprecated || [],
348
489
  next_intent: next ? next.id : null,
349
490
  pending_verifications: pending,
350
491
  inconsistent_states: issues.filter((i) => i.type === 'in_progress_no_record' || i.type === 'completed_no_record').map((i) => i.id),
351
492
  risks,
352
- healthy: issues.length === 0,
493
+ healthy: realIssues.length === 0,
353
494
  };
354
495
  }
355
496
 
@@ -364,7 +505,7 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
364
505
  * @param {string} intentId
365
506
  * @returns {object}
366
507
  */
367
- export function traceIntent(versionDir, verificationsDir, philosophyDir, intentId) {
508
+ export function traceIntent(versionDir, verificationsDir, philosophyDir, intentId) {
368
509
  const intent = getIntent(versionDir, intentId);
369
510
  if (!intent) throw new Error(`Intent 不存在: ${intentId}`);
370
511
 
@@ -406,19 +547,51 @@ export function traceIntent(versionDir, verificationsDir, philosophyDir, intentI
406
547
  walkDeps(dep, depth + 1);
407
548
  }
408
549
  }
409
- walkDeps(intentId, 0);
410
-
411
- return {
412
- intent,
550
+ walkDeps(intentId, 0);
551
+
552
+ const version = basename(versionDir);
553
+ const ref = formatIntentRef(version, intentId);
554
+ const loomRoot = dirname(versionDir);
555
+ const predecessors = (intent.lineage?.predecessors || []).map((item) => {
556
+ let revision = null;
557
+ try { revision = getIntent(join(loomRoot, item.version), item.intent_id).revision ?? 1; }
558
+ catch { /* Validation is structural; a referenced version may not be locally available. */ }
559
+ return { ...item, ref: formatIntentRef(item.version, item.intent_id), revision };
560
+ });
561
+ const successors = [];
562
+ for (const candidateVersion of readdirSync(loomRoot).filter((name) => /^v\d+$/.test(name))) {
563
+ const candidateDir = join(loomRoot, candidateVersion);
564
+ if (!statSync(candidateDir).isDirectory() || !existsSync(join(candidateDir, '04_INTENT_MAP.json'))) continue;
565
+ let candidateMap;
566
+ try { candidateMap = loadIntentMap(candidateDir); }
567
+ catch { continue; }
568
+ for (const [candidateId, candidate] of Object.entries(candidateMap.intents)) {
569
+ if (candidate.lineage?.predecessors?.some((item) => item.version === version && item.intent_id === intentId)) {
570
+ successors.push({
571
+ ref: formatIntentRef(candidateVersion, candidateId),
572
+ version: candidateVersion,
573
+ intent_id: candidateId,
574
+ revision: candidate.revision ?? 1,
575
+ });
576
+ }
577
+ }
578
+ }
579
+
580
+ return {
581
+ ref,
582
+ version,
583
+ revision: intent.revision ?? 1,
584
+ intent,
413
585
  narrative,
414
586
  narrative_error: narrativeError,
415
587
  acceptance,
416
588
  acceptance_error: acceptanceError,
417
589
  verification_history: verificationHistory,
418
- philosophy_anchors_content: philosophyContent,
419
- dependency_chain: dependencyChain,
420
- };
421
- }
590
+ philosophy_anchors_content: philosophyContent,
591
+ dependency_chain: dependencyChain,
592
+ lineage: { predecessors, successors },
593
+ };
594
+ }
422
595
 
423
596
  // ─── reverse-dep ───────────────────────────────────────
424
597
  // 反向依赖:哪些 Intent 依赖这个 Intent
package/cli/src/guide.js CHANGED
@@ -5,8 +5,9 @@
5
5
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
7
  import { readCurrentPointer } from './version.js';
8
- import { loadIntentMap } from './intent-map.js';
9
- import { isAutoOn, writeHeartbeat, needsHumanReview } from './auto.js';
8
+ import { loadIntentMap } from './intent-map.js';
9
+ import { isAutoOn, getAutoMode, writeHeartbeat, needsHumanReview } from './auto.js';
10
+ import { doctor } from './diagnostics.js';
10
11
 
11
12
  /**
12
13
  * 检测文件是否还是模板(未填充真实内容)。
@@ -36,17 +37,78 @@ function isTemplate(filePath) {
36
37
  /**
37
38
  * 诊断项目当前阶段。
38
39
  * @param {string} projectDir — 项目根目录
39
- * @returns {{ stage: string, stage_num: number, details: object, auto: boolean, next_action: string, next_command: string, message: string, needs_human_review: boolean }}
40
- */
41
- export function guideProject(projectDir, options = {}) {
42
- const cwd = projectDir || process.cwd();
43
- const loomRoot = join(cwd, '.loom');
44
- const auto = isAutoOn(loomRoot);
45
- const result = diagnoseStage(cwd, loomRoot, auto);
46
- // 统一后处理:写心跳 + AUTO 提示词 + 判断是否需要人类 review
47
- if (existsSync(loomRoot) && !options.dryRun) {
48
- try {
49
- writeHeartbeat(loomRoot, {
40
+ * @returns {{ stage: string, stage_num: number, details: object, auto: boolean, next_action: string, next_command: string, message: string, needs_human_review: boolean }}
41
+ */
42
+ export function guideProject(projectDir, options = {}) {
43
+ const cwd = projectDir || process.cwd();
44
+ const loomRoot = join(cwd, '.loom');
45
+ const autoMode = getAutoMode(loomRoot);
46
+ const auto = autoMode !== 'manual'; // 向后兼容 boolean
47
+ const result = diagnoseStage(cwd, loomRoot, auto);
48
+ result.auto_mode = autoMode;
49
+
50
+ // 按 stage 补充可执行信息:要读什么、要产出什么、完成后跑什么校验
51
+ const current = result.details.version || 'v1';
52
+ const stageMeta = {
53
+ not_initialized: {
54
+ inputs: [],
55
+ outputs: ['.loom/v1/'],
56
+ verify_command: 'loom guide',
57
+ },
58
+ no_version: {
59
+ inputs: [],
60
+ outputs: ['.loom/v1/'],
61
+ verify_command: 'loom guide',
62
+ },
63
+ need_philosophy: {
64
+ inputs: ['meta/PHILOSOPHY_WEAVER.md', 'meta/BASELINE.md', 'dimensions/SEARCH_METHODOLOGY.md'],
65
+ outputs: [`.loom/${current}/00_PHILOSOPHY/PRODUCT_PHILOSOPHY.md`, `.loom/${current}/00_PHILOSOPHY/ENGINEERING_CREED.md`, `.loom/${current}/00_PHILOSOPHY/DECISION_RUBRIC.md`],
66
+ verify_command: 'loom philosophy check',
67
+ },
68
+ need_vision: {
69
+ inputs: ['roles/visionary.md', `.loom/${current}/00_PHILOSOPHY/`],
70
+ outputs: [`.loom/${current}/01_VISION.md`],
71
+ verify_command: 'loom guide',
72
+ },
73
+ need_architecture: {
74
+ inputs: ['roles/architect.md', `.loom/${current}/01_VISION.md`],
75
+ outputs: [`.loom/${current}/02_ARCHITECTURE.md`, `.loom/${current}/04_INTENT_MAP.json`],
76
+ verify_command: 'loom doctor',
77
+ },
78
+ intent_map_broken: {
79
+ inputs: [`.loom/${current}/04_INTENT_MAP.json`],
80
+ outputs: [`.loom/${current}/04_INTENT_MAP.json`],
81
+ verify_command: 'loom intent validate',
82
+ },
83
+ in_loop: {
84
+ inputs: ['roles/forge.md', 'roles/keeper.md', `.loom/${current}/04_INTENT_MAP.json`],
85
+ outputs: ['代码文件', `.loom/${current}/verifications/INT-*.json`],
86
+ verify_command: 'loom verify pending',
87
+ },
88
+ ready_for_loop: {
89
+ inputs: ['roles/forge.md', 'roles/keeper.md', `.loom/${current}/04_INTENT_MAP.json`],
90
+ outputs: ['代码文件', `.loom/${current}/verifications/INT-*.json`],
91
+ verify_command: 'loom verify pending',
92
+ },
93
+ done: {
94
+ inputs: [`.loom/${current}/04_INTENT_MAP.json`, `.loom/${current}/verifications/`],
95
+ outputs: [`.loom/${current}/06_CHANGELOG.json`, `.loom/${current}/06_CHANGELOG.md`],
96
+ verify_command: 'loom doctor',
97
+ },
98
+ unknown: {
99
+ inputs: [],
100
+ outputs: [],
101
+ verify_command: 'loom doctor',
102
+ },
103
+ };
104
+ const meta = stageMeta[result.stage] || {};
105
+ result.inputs = meta.inputs || [];
106
+ result.outputs = meta.outputs || [];
107
+ result.verify_command = meta.verify_command || null;
108
+ // 统一后处理:写心跳 + 加 AUTO 提示词 + 判断是否需要人类 review
109
+ if (existsSync(loomRoot) && !options.dryRun) {
110
+ try {
111
+ writeHeartbeat(loomRoot, {
50
112
  stage: result.stage,
51
113
  stage_num: result.stage_num,
52
114
  next_command: result.next_command,
@@ -57,13 +119,19 @@ export function guideProject(projectDir, options = {}) {
57
119
  result.needs_human_review = needsHumanReview(loomRoot, result.stage_num);
58
120
  // 在 message 末尾加 AUTO 提示
59
121
  if (result.stage_num > 0 && result.stage_num < 4) {
60
- result.message += auto
61
- ? '\n\n> ⚠ 这是设计阶段(哲学/愿景/架构),即使 AUTO 模式开启也需要人类 review 后再继续。'
62
- : '\n\n> AUTO 模式关闭——每步都需要人类确认后再继续。';
63
- } else if (result.stage_num >= 4 && !auto) {
64
- result.message += '\n\n> ⚠ AUTO 模式关闭——执行 next_command 后等人类确认再继续。';
65
- } else if (result.stage_num >= 4 && auto) {
66
- result.message += '\n\n> AUTO 模式开启——直接执行 next_command,无需人类确认。';
122
+ if (autoMode === 'auto-design') {
123
+ result.message += '\n\n> AUTO 模式 auto-design——设计阶段也自动执行,无需人类确认。';
124
+ } else if (autoMode === 'auto-loop') {
125
+ result.message += '\n\n> 这是设计阶段(哲学/愿景/架构),auto-loop 模式下需要人类 review 后再继续。';
126
+ } else {
127
+ result.message += '\n\n> AUTO 模式 manual——每步都需要人类确认后再继续。';
128
+ }
129
+ } else if (result.stage_num >= 4) {
130
+ if (auto) {
131
+ result.message += '\n\n> AUTO 模式开启——直接执行 next_command,无需人类确认。';
132
+ } else {
133
+ result.message += '\n\n> ⚠ AUTO 模式关闭——执行 next_command 后等人类确认再继续。';
134
+ }
67
135
  }
68
136
  return result;
69
137
  }
@@ -145,11 +213,11 @@ function diagnoseStage(cwd, loomRoot, auto) {
145
213
  }
146
214
 
147
215
  // 状态 4-7: Intent Map 已设计,根据 Intent 状态判断
148
- let intentMap;
149
- let intents;
150
- try {
151
- intentMap = loadIntentMap(versionDir);
152
- intents = intentMap.intents;
216
+ let intentMap;
217
+ let intents;
218
+ try {
219
+ intentMap = loadIntentMap(versionDir);
220
+ intents = intentMap.intents;
153
221
  } catch (e) {
154
222
  return {
155
223
  stage: 'intent_map_broken',
@@ -189,18 +257,39 @@ function diagnoseStage(cwd, loomRoot, auto) {
189
257
  };
190
258
  }
191
259
 
192
- // 状态 6: 全部 completed
193
- if (counts.completed === total && total > 0) {
194
- return {
195
- stage: 'done',
196
- stage_num: 6,
197
- details: { version: current, counts, total },
198
- auto,
199
- next_action: '项目阶段完成,考虑版本演进',
200
- next_command: 'loom version new',
201
- message: `当前版本 ${current}:全部 ${total} 个 Intent 已完成。可考虑 loom version new 开始新版本。`,
202
- };
203
- }
260
+ // 状态 6: 全部 completed
261
+ if (counts.completed === total && total > 0) {
262
+ const health = doctor(versionDir, join(versionDir, 'verifications'), philosophyDir);
263
+ const blocking = health.issues.filter((issue) => issue.severity === 'fatal' || issue.severity === 'high');
264
+ if (blocking.length) {
265
+ return {
266
+ stage: 'needs_review',
267
+ stage_num: 5.5,
268
+ details: { version: current, counts, blocking_issues: blocking.map((issue) => issue.type) },
269
+ auto,
270
+ next_action: '修复完成门或验证证据后重新运行 doctor',
271
+ next_command: 'loom doctor',
272
+ message: `当前版本 ${current} 的 Intent 均标为 completed,但健康检查仍有 ${blocking.length} 个高风险问题;不能宣告阶段完成。先运行 loom doctor。`,
273
+ };
274
+ }
275
+ const doneMessage = [
276
+ `当前版本 ${current}:全部 ${total} 个 Intent 已完成。`,
277
+ '',
278
+ '如果有新需求,先判断变更档位:',
279
+ '- Patch:不触及 Intent,只修 bug / 样式 / 实现细节;跑验证并记录 changelog。',
280
+ '- Minor:新增或修改 Intent,但不改变哲学前提、愿景北极星、架构边界;在当前版本内变更并重验受影响 Intent。',
281
+ '- Major:哲学前提、愿景北极星或架构边界变化;运行 loom version new。',
282
+ ].join('\n');
283
+ return {
284
+ stage: 'done',
285
+ stage_num: 6,
286
+ details: { version: current, counts, total },
287
+ auto,
288
+ next_action: '项目阶段完成,按 Patch / Minor / Major 判断下一步',
289
+ next_command: 'loom help version',
290
+ message: doneMessage,
291
+ };
292
+ }
204
293
 
205
294
  // 状态 5: 有 in_progress
206
295
  if (counts.in_progress > 0) {
@@ -220,7 +309,7 @@ function diagnoseStage(cwd, loomRoot, auto) {
220
309
  if (counts.needs_review > 0) {
221
310
  const reviewIds = allIntents.filter((i) => i.status === 'needs_review').map((i) => i.id);
222
311
  // 读 _meta.pass_count 收敛趟计数(最大 3 趟)
223
- const passCount = intentMap._meta?.pass_count || 1;
312
+ const passCount = intentMap._meta?.pass_count || 1;
224
313
  const MAX_PASSES = 3;
225
314
  const isOverLimit = passCount > MAX_PASSES;
226
315
  const passMsg = ` [Pass ${passCount}/${MAX_PASSES}]`;