@haaaiawd/loom 0.10.0 → 1.1.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 (58) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +147 -74
  4. package/cli/bin/loom.js +428 -105
  5. package/cli/help/asset.md +36 -0
  6. package/cli/help/atelier.md +37 -0
  7. package/cli/help/capability.md +68 -0
  8. package/cli/help/concepts.md +100 -72
  9. package/cli/help/doctor.md +71 -121
  10. package/cli/help/loop.md +120 -135
  11. package/cli/help/patch.md +33 -0
  12. package/cli/help/preview.md +2 -1
  13. package/cli/help/proposals.md +21 -0
  14. package/cli/help/version.md +92 -16
  15. package/cli/help/workflow.md +101 -100
  16. package/cli/src/activate.js +349 -73
  17. package/cli/src/asset-library.js +384 -0
  18. package/cli/src/atelier.js +331 -0
  19. package/cli/src/capability-graph.js +351 -0
  20. package/cli/src/capability-proposals.js +225 -0
  21. package/cli/src/diagnostics.js +240 -44
  22. package/cli/src/guide.js +155 -40
  23. package/cli/src/init.js +58 -32
  24. package/cli/src/intent-draft.js +303 -0
  25. package/cli/src/intent-map.js +560 -54
  26. package/cli/src/patch.js +214 -0
  27. package/cli/src/philosophy.js +177 -154
  28. package/cli/src/preview-prompt.md +13 -6
  29. package/cli/src/preview.js +1 -0
  30. package/cli/src/shared/intent-ref.js +38 -0
  31. package/cli/src/shared/proof-reference.js +19 -0
  32. package/cli/src/shared/verification-method.js +32 -0
  33. package/cli/src/verify.js +202 -62
  34. package/cli/src/version.js +5 -4
  35. package/dimensions/AUTHORSHIP.md +45 -0
  36. package/dimensions/PART_DECOMPOSITION.md +42 -203
  37. package/dimensions/SEARCH_METHODOLOGY.md +101 -97
  38. package/dimensions/examples/AGENT_SYSTEM/README.md +1 -1
  39. package/dimensions/examples/CLI_TOOL/README.md +1 -1
  40. package/dimensions/universal/COLLABORATION_PHILOSOPHY.md +28 -77
  41. package/dimensions/universal/ENGINEERING_CREED.md +30 -74
  42. package/dimensions/universal/PRODUCT_PHILOSOPHY.md +32 -70
  43. package/meta/BASELINE.md +91 -276
  44. package/meta/INTENT_LOOP.md +289 -737
  45. package/meta/PHILOSOPHY_WEAVER.md +110 -343
  46. package/meta/ROLE_ACTIVATION.md +109 -267
  47. package/package.json +13 -7
  48. package/roles/architect.md +84 -111
  49. package/roles/forge.md +105 -126
  50. package/roles/keeper.md +109 -223
  51. package/roles/visionary.md +57 -86
  52. package/templates/ASSET_LIBRARY_MANIFEST_TEMPLATE.json +10 -0
  53. package/templates/ATELIER_RECORD_TEMPLATE.json +48 -0
  54. package/templates/CAPABILITY_BRIEF_TEMPLATE.md +34 -0
  55. package/templates/CAPABILITY_GRAPH_TEMPLATE.json +11 -0
  56. package/templates/INTENT_MAP_TEMPLATE.json +26 -10
  57. package/templates/PHILOSOPHY_TEMPLATE.md +44 -75
  58. package/templates/VISION_TEMPLATE.md +44 -67
@@ -3,10 +3,18 @@
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';
14
+ import { getCapabilityCoverage, getCapabilityGraphPath, loadCapabilityGraph } from './capability-graph.js';
15
+ import { listCapabilityProposals } from './capability-proposals.js';
16
+ import { getAssetManifestPath, validateAssetLibrary } from './asset-library.js';
17
+ import { validateAtelierRecord } from './atelier.js';
10
18
 
11
19
  function readIntentMapRaw(versionDir) {
12
20
  const filePath = join(versionDir, '04_INTENT_MAP.json');
@@ -75,10 +83,6 @@ function intentMapDiagnostics(versionDir) {
75
83
  return { raw, valid, issues, validMap };
76
84
  }
77
85
 
78
- function getIntentVerificationMethod(intent) {
79
- return intent.verification_method || intent._optional?.verification_method || null;
80
- }
81
-
82
86
  function normalizeVerificationCommand(command) {
83
87
  return String(command || '')
84
88
  .replace(/^\s*run\s+/i, '')
@@ -143,7 +147,8 @@ const FIX_HINTS = {
143
147
  intent_map_missing: '运行 loom init 或 loom activate architect 产出 04_INTENT_MAP.json',
144
148
  intent_map_template: '运行 loom activate architect,Architect 填充真实 Intent Map 后删除 _meta._template 标记',
145
149
  intent_map_invalid: '按报错信息修正 04_INTENT_MAP.json 里对应字段(补 title / 加长 acceptance / 填必填字段)',
146
- completed_no_record: '在 .loom/v{N}/verifications/ 下补验证记录,或运行 loom verify pass {id} --summary "..."',
150
+ completed_no_record: '在 .loom/v{N}/verifications/ 下补验证记录,或运行 loom verify pass {id} --summary "..."',
151
+ completed_verification_not_passed: '最新验证不是当前 revision 的 passed;重新运行 loom verify pass {id} --summary "...",再用 loom intent done {id} 闭合。',
147
152
  in_progress_no_record: '运行 loom verify pass {id} --summary "..." 写入验证记录,或 loom intent update {id} --status pending 回退',
148
153
  orphan_philosophy_ref: '检查 04_INTENT_MAP.json 里 {id} 的 philosophy_anchors,移除或修正不存在的哲学文件引用',
149
154
  orphan_dependency: '检查 04_INTENT_MAP.json 里 {id} 的 depends_on,移除或修正不存在的 Intent ID',
@@ -152,10 +157,28 @@ const FIX_HINTS = {
152
157
  completed_depends_blocked: '检查依赖 {dep} 为什么 blocked——解决阻塞或把 {id} 回退到 in_progress',
153
158
  test_script_missing: '在 package.json 里加 test 脚本,或修正 verification_method 指向实际存在的测试命令',
154
159
  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
- };
160
+ verification_method_drift: '验证记录的 reproduction_command 要覆盖 verification_method 声明的命令(支持 npm/pnpm/bun 互相等价)',
161
+ stale_verification: '重新验证当前 Intent revision:loom verify pass {id} --summary "...",通过后再闭合 Intent',
162
+ inspiration_source: '在哲学文档的"证据地图/灵感来源"中写入实际使用的来源、选择理由和可追溯位置;数量由判断所需决定',
163
+ quality_dimension_missing: '为带 quality_contract 的 Intent 补写并通过 quality_achievement;相对提升声明在该维度中链接 Quality Proof',
164
+ quality_proof_invalid: '将 quality_proof_ref 改为项目内真实存在且含锚点的 Markdown 证据,例如 verifications/INT-001-quality-proof.md#int-001',
165
+ preservation_dimension_missing: '为 continuity_required 的 Intent 补写并通过 preservation_achievement,证据必须覆盖旧状态到新操作后的完整序列',
166
+ patch_changelog_invalid: '运行 loom patch validate 查看具体错误;修正 06_CHANGELOG.json 后重新生成 Markdown 投影',
167
+ patch_projection_drift: '不要手工编辑 06_CHANGELOG.md;重新运行 loom init 或下一次 loom patch record 生成投影',
168
+ capability_graph_missing: '这是 1.0 项目的兼容提示;迁移时运行 loom activate architect,创建 07_CAPABILITY_GRAPH.json 并路由高影响节点',
169
+ capability_graph_template: '运行 loom activate architect,先将愿景展开为 Capability Graph,再创建或修订 Intent Map',
170
+ capability_graph_invalid: '修正 07_CAPABILITY_GRAPH.json 的节点、关系或枚举值后运行 loom capability coverage',
171
+ capability_frontier_open: '为高影响节点继续展开、创建 Capability Brief、编译为 Intent,或明确延后/排除理由',
172
+ capability_plan_missing: '为高影响 capability 节点补齐可用 Brief 与获取计划,或改为明确的其他路由',
173
+ capability_route_evidence: '为该节点补齐路由所需的 Intent 回链、Brief 或延后/排除理由,避免只写一个状态标签',
174
+ capability_outcome_unexpanded: '将 outcome 连接到至少一个 concern,明确项目初衷需要被处理的问题面',
175
+ capability_outcome_unobservable: '为高影响 outcome 新建或补齐 evidence 节点,并以 validated_by 连接;evidence 必须写明观察目标、复现步骤、通过标准、证据产物,并回链承担验证的 Intent',
176
+ intent_graph_unmapped: '将该 Intent 回链到至少一个 Capability Graph 节点;不要让执行承诺失去项目初衷和能力来源',
177
+ capability_proposal_pending: '由 Architect 审核 proposal:判定已覆盖、更新 Graph、生成/修订 Intent、改变 acceptance,或升级 Minor/Major;Forge 不得静默把候选写进正式图谱。',
178
+ asset_library_invalid: '修复 08_ASSET_LIBRARY/manifest.json 的来源、许可、哈希、库内路径或 evidence 双向引用,然后运行 loom asset validate。',
179
+ atelier_record_invalid: '运行 loom atelier init {id} 创建记录,或按校验错误修正后运行 loom atelier validate {id}。',
180
+ atelier_verification_missing: '重新运行独立 Keeper 验证,让 passed 记录绑定当前 Atelier Record 与 stance_revision。',
181
+ };
159
182
 
160
183
  /**
161
184
  * 给 issue 补 fix_hint——把 {id} {dep} 等占位符替换成实际值。
@@ -190,18 +213,133 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
190
213
  return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
191
214
  }
192
215
 
193
- const { intents } = mapState.validMap;
194
-
195
- // 1. 状态一致性:in_progress/completed 但无验证记录
196
- for (const [id, intent] of Object.entries(intents)) {
197
- const hasRecord = existsSync(join(verificationsDir, `${id}.json`));
198
- if (intent.status === 'completed' && !hasRecord) {
199
- issues.push({ id, type: 'completed_no_record', severity: 'high', msg: `${id} 状态为 completed 但无验证记录` });
200
- }
201
- if (intent.status === 'in_progress' && !hasRecord) {
202
- issues.push({ id, type: 'in_progress_no_record', severity: 'medium', msg: `${id} 状态为 in_progress 但无验证记录(可能上次中断)` });
203
- }
204
- }
216
+ const { intents } = mapState.validMap;
217
+ const atelierRecords = new Map();
218
+
219
+ for (const [id, intent] of Object.entries(intents)) {
220
+ if (intent.quality_strategy !== 'atelier' || intent.status === 'pending' || intent.status === 'blocked') continue;
221
+ try {
222
+ const record = validateAtelierRecord(versionDir, id);
223
+ atelierRecords.set(id, record);
224
+ if (intent.status === 'completed' && !['selected', 'baseline_retained'].includes(record.status)) {
225
+ issues.push({ id, type: 'atelier_record_invalid', severity: 'high', msg: `${id} completed,但 Atelier Record 仍是 ${record.status}` });
226
+ }
227
+ } catch (error) {
228
+ issues.push({ id, type: 'atelier_record_invalid', severity: 'high', msg: error.message });
229
+ }
230
+ }
231
+
232
+ // 0. Incoming Graph changes are an Architect gate, not Forge implementation scope.
233
+ try {
234
+ for (const proposal of listCapabilityProposals(versionDir, { unresolvedOnly: true })) {
235
+ issues.push({ id: proposal.id, type: 'capability_proposal_pending', severity: 'high', msg: `${proposal.id} 是尚未闭合的 ${proposal.origin} 候选(${proposal.candidate_kind}: ${proposal.title});必须由 Architect 决定其影响。` });
236
+ }
237
+ } catch (error) {
238
+ issues.push({ id: 'capability_proposals', type: 'capability_proposal_pending', severity: 'high', msg: `Capability Graph proposal 无法审计: ${error.message}` });
239
+ }
240
+
241
+ // Asset Library is optional for a project, but once initialized it is a local source of truth.
242
+ if (existsSync(getAssetManifestPath(versionDir))) {
243
+ try { validateAssetLibrary(versionDir); } catch (error) {
244
+ issues.push({ id: 'asset_library', type: 'asset_library_invalid', severity: 'high', msg: error.message });
245
+ }
246
+ }
247
+
248
+ // 0. Capability Graph:旧项目允许缺失但提示迁移;图谱一旦存在则必须可读、可路由。
249
+ const capabilityGraphPath = getCapabilityGraphPath(versionDir);
250
+ if (!existsSync(capabilityGraphPath)) {
251
+ issues.push({ id: 'capability_graph', type: 'capability_graph_missing', severity: 'medium', msg: '缺少 07_CAPABILITY_GRAPH.json;现有项目仍可运行,但不会获得图谱驱动的能力拆分与覆盖检查。' });
252
+ } else {
253
+ try {
254
+ const graph = loadCapabilityGraph(versionDir);
255
+ if (graph._meta?._template === true) {
256
+ issues.push({ id: 'capability_graph', type: 'capability_graph_template', severity: 'high', msg: 'Capability Graph 仍是模板;不得在未路由项目问题与能力缺口时宣告架构完成。' });
257
+ } else {
258
+ const coverage = getCapabilityCoverage(versionDir);
259
+ for (const node of coverage.high_unrouted) {
260
+ issues.push({ id: node.id, type: 'capability_frontier_open', severity: 'high', msg: `${node.id} 是尚未路由的高影响 Capability Graph 节点: ${node.title}` });
261
+ }
262
+ for (const item of coverage.capabilities_without_plan) {
263
+ issues.push({ id: item.node_id, type: 'capability_plan_missing', severity: 'high', msg: `${item.node_id} 的能力获取计划不完整: ${item.reason}` });
264
+ }
265
+ for (const item of coverage.routing_gaps) {
266
+ issues.push({ id: item.node_id, type: 'capability_route_evidence', severity: 'high', msg: `${item.node_id} 的图谱路由缺少依据: ${item.reason}` });
267
+ }
268
+ for (const item of coverage.outcomes_without_concern) {
269
+ issues.push({ id: item.node_id, type: 'capability_outcome_unexpanded', severity: 'high', msg: `${item.node_id} 没有展开为项目问题面: ${item.reason}` });
270
+ }
271
+ for (const item of coverage.high_outcomes_without_observable_evidence) {
272
+ issues.push({ id: item.node_id, type: 'capability_outcome_unobservable', severity: 'high', msg: `${item.node_id} 缺少真实呈现或交付的验证入口: ${item.reason}` });
273
+ }
274
+ for (const item of coverage.orphan_intent_refs) {
275
+ issues.push({ id: item.node_id, type: 'intent_graph_unmapped', severity: 'high', msg: `${item.node_id} 引用了不存在的 Intent: ${item.intent_id}` });
276
+ }
277
+ for (const intentId of coverage.unmapped_intents) {
278
+ issues.push({ id: intentId, type: 'intent_graph_unmapped', severity: 'high', msg: `${intentId} 没有 Capability Graph 回链;执行承诺缺少能力与问题来源。` });
279
+ }
280
+ }
281
+ } catch (error) {
282
+ issues.push({ id: 'capability_graph', type: 'capability_graph_invalid', severity: 'high', msg: error.message });
283
+ }
284
+ }
285
+
286
+ // 1. 状态一致性:completed 必须由当前 revision 的最后一条 passed 验证支撑。
287
+ for (const [id, intent] of Object.entries(intents)) {
288
+ const history = getVerificationHistory(verificationsDir, id);
289
+ const hasRecord = Boolean(history?.records?.length);
290
+ const latest = history?.records?.[history.records.length - 1];
291
+ if (intent.status === 'completed' && !hasRecord) {
292
+ issues.push({ id, type: 'completed_no_record', severity: 'high', msg: `${id} 状态为 completed 但无验证记录` });
293
+ } else if (intent.status === 'completed' && !hasCurrentPassedVerification(intent, history)) {
294
+ issues.push({ id, type: 'completed_verification_not_passed', severity: 'high', msg: `${id} 状态为 completed,但最后一条验证不是当前 revision 的 passed` });
295
+ }
296
+ if (intent.status === 'in_progress' && !hasRecord) {
297
+ issues.push({ id, type: 'in_progress_no_record', severity: 'medium', msg: `${id} 状态为 in_progress 但无验证记录(可能上次中断)` });
298
+ }
299
+ if (intent.quality_contract && latest?.verdict === 'passed') {
300
+ const quality = latest.dimensions?.quality_achievement;
301
+ if (!quality || quality.verdict !== 'passed') {
302
+ issues.push({
303
+ id,
304
+ type: 'quality_dimension_missing',
305
+ severity: 'high',
306
+ msg: `${id} 声明了 quality_contract,但最新 passed 记录缺少通过的 quality_achievement`,
307
+ });
308
+ }
309
+ if (quality?.quality_proof_ref) {
310
+ try {
311
+ resolveQualityProofReference(versionDir, quality.quality_proof_ref);
312
+ } catch (error) {
313
+ issues.push({ id, type: 'quality_proof_invalid', severity: 'high', msg: `${id} 的 Quality Proof 无效: ${error.message}` });
314
+ }
315
+ }
316
+ }
317
+ if (intent.quality_strategy === 'atelier' && latest?.verdict === 'passed') {
318
+ const atelier = atelierRecords.get(id);
319
+ if (!atelier
320
+ || latest.atelier?.record_ref !== `09_ATELIER/${id}.json`
321
+ || latest.atelier?.stance_revision !== atelier.stance_revision
322
+ || latest.atelier?.status !== atelier.status) {
323
+ issues.push({
324
+ id,
325
+ type: 'atelier_verification_missing',
326
+ severity: 'high',
327
+ msg: `${id} 的最新 passed 未绑定当前 Atelier Record、status 与 stance_revision`,
328
+ });
329
+ }
330
+ }
331
+ if (intent.continuity_required && latest?.verdict === 'passed') {
332
+ const preservation = latest.dimensions?.preservation_achievement;
333
+ if (!preservation || preservation.verdict !== 'passed') {
334
+ issues.push({
335
+ id,
336
+ type: 'preservation_dimension_missing',
337
+ severity: 'high',
338
+ msg: `${id} 声明了 continuity_required,但最新 passed 记录缺少通过的 preservation_achievement`,
339
+ });
340
+ }
341
+ }
342
+ }
205
343
 
206
344
  // 2. 孤儿引用:哲学锚点指向不存在的文件
207
345
  for (const [id, intent] of Object.entries(intents)) {
@@ -312,12 +450,42 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
312
450
  issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 声明了 verification_method 但没有验证记录覆盖: ${method}` });
313
451
  } else if (!actual) {
314
452
  issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 最新验证记录缺少 reproduction_command,无法复现 verification_method: ${method}` });
315
- } else if (!commandCoversMethod(actual, expected)) {
453
+ } else if (!commandCoversVerificationMethod(actual, expected)) {
316
454
  issues.push({ id, type: 'verification_method_drift', severity: 'high', msg: `${id} verification_method 未被最新 reproduction_command 覆盖。method="${method}" reproduction_command="${latest.reproduction_command}"` });
317
455
  }
318
456
  }
319
457
 
320
- appendPhilosophyDiagnostics(issues, philosophyDir);
458
+ const patchJsonPath = join(versionDir, '06_CHANGELOG.json');
459
+ if (existsSync(patchJsonPath)) {
460
+ try {
461
+ validatePatches(versionDir);
462
+ } catch (error) {
463
+ const projectionDrift = String(error.message).includes('最新确定性投影') || String(error.message).includes('Markdown 投影不存在');
464
+ issues.push({
465
+ id: 'patch_changelog',
466
+ type: projectionDrift ? 'patch_projection_drift' : 'patch_changelog_invalid',
467
+ severity: projectionDrift ? 'medium' : 'high',
468
+ msg: error.message,
469
+ });
470
+ }
471
+ }
472
+
473
+ // completed/needs_review 的旧 revision 验证不能证明当前 Intent 仍然成立。
474
+ for (const [id, intent] of Object.entries(intents)) {
475
+ if (intent.status !== 'completed' && intent.status !== 'needs_review') continue;
476
+ const history = getVerificationHistory(verificationsDir, id);
477
+ const latestPassed = getLatestPassedVerification(history);
478
+ if (!latestPassed || isVerificationCurrent(intent, latestPassed)) continue;
479
+ const verifiedRevision = getVerificationIntentRevision(intent, latestPassed);
480
+ issues.push({
481
+ id,
482
+ type: 'stale_verification',
483
+ severity: 'high',
484
+ msg: `${id} 最新 passed 验证 revision ${verifiedRevision ?? 'legacy/unknown'} 早于当前 Intent revision ${intent.revision ?? 1}`,
485
+ });
486
+ }
487
+
488
+ appendPhilosophyDiagnostics(issues, philosophyDir);
321
489
  const issuesWithHints = issues.map(addFixHint);
322
490
  return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
323
491
  }
@@ -331,11 +499,6 @@ function appendPhilosophyDiagnostics(issues, philosophyDir) {
331
499
  issues.push({ id: 'philosophy', type: 'inspiration_source', severity: issue.severity, msg: issue.msg });
332
500
  }
333
501
 
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
502
  }
340
503
 
341
504
  function summarizeIssues(issues) {
@@ -403,7 +566,7 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
403
566
  const { issues } = doctor(versionDir, verificationsDir, philosophyDir);
404
567
 
405
568
  // 区分模板阶段问题(待填充)和真实损坏
406
- const templateIssues = issues.filter((i) => i.is_template || i.type === 'intent_map_template' || i.type === 'inspiration_source' || i.type === 'part_decomposition');
569
+ const templateIssues = issues.filter((i) => i.is_template || i.type === 'intent_map_template' || i.type === 'inspiration_source');
407
570
  const realIssues = issues.filter((i) => !templateIssues.includes(i));
408
571
 
409
572
  const risks = [];
@@ -416,11 +579,12 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
416
579
 
417
580
  return {
418
581
  intent_map_valid: mapState.valid === true,
419
- progress: {
582
+ progress: {
420
583
  completed: status.counts.completed,
421
584
  total: status.counts.total,
422
- rate: `${status.counts.completed}/${status.counts.total}`,
423
- },
585
+ rate: `${status.counts.completed}/${status.counts.total}`,
586
+ },
587
+ deprecated_intents: status.deprecated || [],
424
588
  next_intent: next ? next.id : null,
425
589
  pending_verifications: pending,
426
590
  inconsistent_states: issues.filter((i) => i.type === 'in_progress_no_record' || i.type === 'completed_no_record').map((i) => i.id),
@@ -440,7 +604,7 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
440
604
  * @param {string} intentId
441
605
  * @returns {object}
442
606
  */
443
- export function traceIntent(versionDir, verificationsDir, philosophyDir, intentId) {
607
+ export function traceIntent(versionDir, verificationsDir, philosophyDir, intentId) {
444
608
  const intent = getIntent(versionDir, intentId);
445
609
  if (!intent) throw new Error(`Intent 不存在: ${intentId}`);
446
610
 
@@ -482,19 +646,51 @@ export function traceIntent(versionDir, verificationsDir, philosophyDir, intentI
482
646
  walkDeps(dep, depth + 1);
483
647
  }
484
648
  }
485
- walkDeps(intentId, 0);
486
-
487
- return {
488
- intent,
649
+ walkDeps(intentId, 0);
650
+
651
+ const version = basename(versionDir);
652
+ const ref = formatIntentRef(version, intentId);
653
+ const loomRoot = dirname(versionDir);
654
+ const predecessors = (intent.lineage?.predecessors || []).map((item) => {
655
+ let revision = null;
656
+ try { revision = getIntent(join(loomRoot, item.version), item.intent_id).revision ?? 1; }
657
+ catch { /* Validation is structural; a referenced version may not be locally available. */ }
658
+ return { ...item, ref: formatIntentRef(item.version, item.intent_id), revision };
659
+ });
660
+ const successors = [];
661
+ for (const candidateVersion of readdirSync(loomRoot).filter((name) => /^v\d+$/.test(name))) {
662
+ const candidateDir = join(loomRoot, candidateVersion);
663
+ if (!statSync(candidateDir).isDirectory() || !existsSync(join(candidateDir, '04_INTENT_MAP.json'))) continue;
664
+ let candidateMap;
665
+ try { candidateMap = loadIntentMap(candidateDir); }
666
+ catch { continue; }
667
+ for (const [candidateId, candidate] of Object.entries(candidateMap.intents)) {
668
+ if (candidate.lineage?.predecessors?.some((item) => item.version === version && item.intent_id === intentId)) {
669
+ successors.push({
670
+ ref: formatIntentRef(candidateVersion, candidateId),
671
+ version: candidateVersion,
672
+ intent_id: candidateId,
673
+ revision: candidate.revision ?? 1,
674
+ });
675
+ }
676
+ }
677
+ }
678
+
679
+ return {
680
+ ref,
681
+ version,
682
+ revision: intent.revision ?? 1,
683
+ intent,
489
684
  narrative,
490
685
  narrative_error: narrativeError,
491
686
  acceptance,
492
687
  acceptance_error: acceptanceError,
493
688
  verification_history: verificationHistory,
494
- philosophy_anchors_content: philosophyContent,
495
- dependency_chain: dependencyChain,
496
- };
497
- }
689
+ philosophy_anchors_content: philosophyContent,
690
+ dependency_chain: dependencyChain,
691
+ lineage: { predecessors, successors },
692
+ };
693
+ }
498
694
 
499
695
  // ─── reverse-dep ───────────────────────────────────────
500
696
  // 反向依赖:哪些 Intent 依赖这个 Intent
package/cli/src/guide.js CHANGED
@@ -5,8 +5,11 @@
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, getAutoMode, writeHeartbeat, needsHumanReview } from './auto.js';
8
+ import { loadIntentMap } from './intent-map.js';
9
+ import { getCapabilityCoverage } from './capability-graph.js';
10
+ import { listCapabilityProposals } from './capability-proposals.js';
11
+ import { isAutoOn, getAutoMode, writeHeartbeat, needsHumanReview } from './auto.js';
12
+ import { doctor } from './diagnostics.js';
10
13
 
11
14
  /**
12
15
  * 检测文件是否还是模板(未填充真实内容)。
@@ -60,7 +63,7 @@ export function guideProject(projectDir, options = {}) {
60
63
  verify_command: 'loom guide',
61
64
  },
62
65
  need_philosophy: {
63
- inputs: ['meta/PHILOSOPHY_WEAVER.md', 'meta/BASELINE.md', 'dimensions/PART_DECOMPOSITION.md', 'dimensions/SEARCH_METHODOLOGY.md'],
66
+ inputs: ['meta/PHILOSOPHY_WEAVER.md', 'meta/BASELINE.md', 'dimensions/SEARCH_METHODOLOGY.md'],
64
67
  outputs: [`.loom/${current}/00_PHILOSOPHY/PRODUCT_PHILOSOPHY.md`, `.loom/${current}/00_PHILOSOPHY/ENGINEERING_CREED.md`, `.loom/${current}/00_PHILOSOPHY/DECISION_RUBRIC.md`],
65
68
  verify_command: 'loom philosophy check',
66
69
  },
@@ -69,11 +72,26 @@ export function guideProject(projectDir, options = {}) {
69
72
  outputs: [`.loom/${current}/01_VISION.md`],
70
73
  verify_command: 'loom guide',
71
74
  },
72
- need_architecture: {
73
- inputs: ['roles/architect.md', `.loom/${current}/01_VISION.md`],
74
- outputs: [`.loom/${current}/02_ARCHITECTURE.md`, `.loom/${current}/04_INTENT_MAP.json`],
75
- verify_command: 'loom doctor',
76
- },
75
+ need_architecture: {
76
+ inputs: ['roles/architect.md', `.loom/${current}/01_VISION.md`],
77
+ outputs: [`.loom/${current}/02_ARCHITECTURE.md`, `.loom/${current}/04_INTENT_MAP.json`],
78
+ verify_command: 'loom doctor',
79
+ },
80
+ need_capability_graph: {
81
+ inputs: ['roles/architect.md', `.loom/${current}/01_VISION.md`],
82
+ outputs: [`.loom/${current}/07_CAPABILITY_GRAPH.json`, `.loom/${current}/07_CAPABILITY_BRIEFS/`],
83
+ verify_command: 'loom capability coverage',
84
+ },
85
+ capability_graph_incomplete: {
86
+ inputs: ['roles/architect.md', `.loom/${current}/07_CAPABILITY_GRAPH.json`, `.loom/${current}/04_INTENT_MAP.json`],
87
+ outputs: [`.loom/${current}/07_CAPABILITY_GRAPH.json`, `.loom/${current}/07_CAPABILITY_BRIEFS/`],
88
+ verify_command: 'loom capability coverage',
89
+ },
90
+ capability_graph_proposals_pending: {
91
+ inputs: ['roles/architect.md', `.loom/${current}/07_GRAPH_PROPOSALS/`, `.loom/${current}/07_CAPABILITY_GRAPH.json`],
92
+ outputs: [`.loom/${current}/07_GRAPH_PROPOSALS/`, `.loom/${current}/07_CAPABILITY_GRAPH.json`, `.loom/${current}/04_INTENT_MAP.json`],
93
+ verify_command: 'loom capability proposal list',
94
+ },
77
95
  intent_map_broken: {
78
96
  inputs: [`.loom/${current}/04_INTENT_MAP.json`],
79
97
  outputs: [`.loom/${current}/04_INTENT_MAP.json`],
@@ -89,10 +107,10 @@ export function guideProject(projectDir, options = {}) {
89
107
  outputs: ['代码文件', `.loom/${current}/verifications/INT-*.json`],
90
108
  verify_command: 'loom verify pending',
91
109
  },
92
- all_done: {
93
- inputs: [],
94
- outputs: [],
95
- verify_command: 'loom doctor',
110
+ done: {
111
+ inputs: [`.loom/${current}/04_INTENT_MAP.json`, `.loom/${current}/verifications/`],
112
+ outputs: [`.loom/${current}/06_CHANGELOG.json`, `.loom/${current}/06_CHANGELOG.md`],
113
+ verify_command: 'loom doctor',
96
114
  },
97
115
  unknown: {
98
116
  inputs: [],
@@ -168,8 +186,9 @@ function diagnoseStage(cwd, loomRoot, auto) {
168
186
 
169
187
  const versionDir = join(loomRoot, current);
170
188
  const philosophyDir = join(versionDir, '00_PHILOSOPHY');
171
- const visionPath = join(versionDir, '01_VISION.md');
172
- const intentMapPath = join(versionDir, '04_INTENT_MAP.json');
189
+ const visionPath = join(versionDir, '01_VISION.md');
190
+ const intentMapPath = join(versionDir, '04_INTENT_MAP.json');
191
+ const capabilityGraphPath = join(versionDir, '07_CAPABILITY_GRAPH.json');
173
192
 
174
193
  // 状态 1: 哲学未织造
175
194
  const philosophyFile = join(philosophyDir, 'PRODUCT_PHILOSOPHY.md');
@@ -198,16 +217,77 @@ function diagnoseStage(cwd, loomRoot, auto) {
198
217
  };
199
218
  }
200
219
 
201
- // 状态 3: 愿景已定义,Intent Map 未设计
202
- if (isTemplate(intentMapPath)) {
203
- return {
204
- stage: 'need_architecture',
205
- stage_num: 3,
206
- details: { version: current },
207
- auto,
208
- next_action: '设计系统架构 + Intent Map',
209
- next_command: 'loom activate architect',
210
- message: `当前版本 ${current}:愿景已定义,Intent Map 还是模板,需要 Architect 设计。`,
220
+ // 状态 3: 愿景已定义,先展开项目问题面与能力缺口,再承诺 Intent
221
+ if (existsSync(capabilityGraphPath) && isTemplate(capabilityGraphPath)) {
222
+ return {
223
+ stage: 'need_capability_graph',
224
+ stage_num: 3,
225
+ details: { version: current },
226
+ auto,
227
+ next_action: '展开 Capability Graph,路由高影响问题与能力缺口',
228
+ next_command: 'loom activate architect',
229
+ message: `当前版本 ${current}:愿景已定义,但 Capability Graph 还是模板。Architect 必须先判断哪些体验、系统、资产、风险与能力适用,再创建正式 Intent。`,
230
+ };
231
+ }
232
+
233
+ // 图谱一旦存在,就不能把不完整路由悄悄跨过去。旧项目缺少该文件仍保留兼容路径。
234
+ if (existsSync(capabilityGraphPath)) {
235
+ try {
236
+ const pendingProposals = listCapabilityProposals(versionDir, { unresolvedOnly: true });
237
+ if (pendingProposals.length) {
238
+ return {
239
+ stage: 'capability_graph_proposals_pending',
240
+ stage_num: 3,
241
+ details: { version: current, proposals: pendingProposals.map((proposal) => proposal.id) },
242
+ auto,
243
+ next_action: '审计新信息对 Capability Graph、Intent 和契约的影响',
244
+ next_command: 'loom capability proposal list',
245
+ message: `当前版本 ${current} 有 ${pendingProposals.length} 个未闭合的 Capability Graph proposal。它们是新要求、研究或实现发现,不得由 Forge 静默变成当前 Intent 范围。`,
246
+ };
247
+ }
248
+ } catch (error) {
249
+ return {
250
+ stage: 'capability_graph_proposals_pending', stage_num: 3, details: { version: current, error: error.message }, auto,
251
+ next_action: '修复 Capability Graph proposal', next_command: 'loom capability proposal list',
252
+ message: `Capability Graph proposal 无法审计: ${error.message}`,
253
+ };
254
+ }
255
+ try {
256
+ const coverage = getCapabilityCoverage(versionDir);
257
+ if (!coverage.summary.ready) {
258
+ return {
259
+ stage: 'capability_graph_incomplete',
260
+ stage_num: 3.1,
261
+ details: { version: current, coverage: coverage.summary },
262
+ auto,
263
+ next_action: '补齐 Capability Graph 的路由、可观察验证入口与 Intent 回链',
264
+ next_command: 'loom capability coverage',
265
+ message: `当前版本 ${current}:Capability Graph 尚未闭合(高影响前沿 ${coverage.summary.high_unrouted}、路由缺口 ${coverage.summary.routing_gaps}、不可观察 outcome ${coverage.summary.high_outcomes_without_observable_evidence}、未映射 Intent ${coverage.summary.unmapped_intents})。先由 Architect 补图谱,再继续设计 Intent。`,
266
+ };
267
+ }
268
+ } catch (error) {
269
+ return {
270
+ stage: 'capability_graph_incomplete',
271
+ stage_num: 3.1,
272
+ details: { version: current, error: error.message },
273
+ auto,
274
+ next_action: '修复 Capability Graph',
275
+ next_command: 'loom capability coverage',
276
+ message: `Capability Graph 无法通过校验: ${error.message}`,
277
+ };
278
+ }
279
+ }
280
+
281
+ // 状态 3.5: 图谱已建立,Intent Map 未设计。
282
+ if (isTemplate(intentMapPath)) {
283
+ return {
284
+ stage: 'need_architecture',
285
+ stage_num: 3.5,
286
+ details: { version: current },
287
+ auto,
288
+ next_action: '从 Capability Graph 编译系统架构 + Intent Map',
289
+ next_command: 'loom activate architect',
290
+ message: `当前版本 ${current}:Capability Graph 已建立,Intent Map 还是模板。Architect 需要将已路由的问题面编译为系统架构与可闭合 Intent。`,
211
291
  };
212
292
  }
213
293
 
@@ -256,23 +336,58 @@ function diagnoseStage(cwd, loomRoot, auto) {
256
336
  };
257
337
  }
258
338
 
259
- // 状态 6: 全部 completed
260
- if (counts.completed === total && total > 0) {
261
- return {
262
- stage: 'done',
263
- stage_num: 6,
264
- details: { version: current, counts, total },
265
- auto,
266
- next_action: '项目阶段完成,考虑版本演进',
267
- next_command: 'loom version new',
268
- message: `当前版本 ${current}:全部 ${total} 个 Intent 已完成。可考虑 loom version new 开始新版本。`,
269
- };
270
- }
339
+ // 状态 6: 全部 completed
340
+ if (counts.completed === total && total > 0) {
341
+ const health = doctor(versionDir, join(versionDir, 'verifications'), philosophyDir);
342
+ const blocking = health.issues.filter((issue) => issue.severity === 'fatal' || issue.severity === 'high');
343
+ if (blocking.length) {
344
+ return {
345
+ stage: 'needs_review',
346
+ stage_num: 5.5,
347
+ details: { version: current, counts, blocking_issues: blocking.map((issue) => issue.type) },
348
+ auto,
349
+ next_action: '修复完成门或验证证据后重新运行 doctor',
350
+ next_command: 'loom doctor',
351
+ message: `当前版本 ${current} 的 Intent 均标为 completed,但健康检查仍有 ${blocking.length} 个高风险问题;不能宣告阶段完成。先运行 loom doctor。`,
352
+ };
353
+ }
354
+ const doneMessage = [
355
+ `当前版本 ${current}:全部 ${total} 个 Intent 已完成。`,
356
+ '',
357
+ '如果有新需求,先判断变更档位:',
358
+ '- Patch:不触及 Intent,只修 bug / 样式 / 实现细节;跑验证并记录 changelog。',
359
+ '- Minor:新增或修改 Intent,但不改变哲学前提、愿景北极星、架构边界;在当前版本内变更并重验受影响 Intent。',
360
+ '- Major:哲学前提、愿景北极星或架构边界变化;运行 loom version new。',
361
+ ].join('\n');
362
+ return {
363
+ stage: 'done',
364
+ stage_num: 6,
365
+ details: { version: current, counts, total },
366
+ auto,
367
+ next_action: '项目阶段完成,按 Patch / Minor / Major 判断下一步',
368
+ next_command: 'loom help version',
369
+ message: doneMessage,
370
+ };
371
+ }
271
372
 
272
- // 状态 5: 有 in_progress
273
- if (counts.in_progress > 0) {
274
- const inProgressIds = allIntents.filter((i) => i.status === 'in_progress').map((i) => i.id);
275
- return {
373
+ // 状态 5: 有 in_progress
374
+ if (counts.in_progress > 0) {
375
+ const inProgressIds = allIntents.filter((i) => i.status === 'in_progress').map((i) => i.id);
376
+ const atelierWithoutRecord = allIntents.find((intent) => intent.status === 'in_progress'
377
+ && intent.quality_strategy === 'atelier'
378
+ && !existsSync(join(versionDir, '09_ATELIER', `${intent.id}.json`)));
379
+ if (atelierWithoutRecord) {
380
+ return {
381
+ stage: 'in_loop',
382
+ stage_num: 5,
383
+ details: { version: current, counts, in_progress_ids: inProgressIds, atelier_intent: atelierWithoutRecord.id },
384
+ auto,
385
+ next_action: '创建 Atelier Record 并冻结基线',
386
+ next_command: `loom atelier init ${atelierWithoutRecord.id}`,
387
+ message: `${atelierWithoutRecord.id} 已进入 Atelier Path,但还没有唯一创作记录。先创建 Record,再按 Forge Context Pack 形成 Authorial Stance。`,
388
+ };
389
+ }
390
+ return {
276
391
  stage: 'in_loop',
277
392
  stage_num: 5,
278
393
  details: { version: current, counts, in_progress_ids: inProgressIds },