@haaaiawd/loom 0.10.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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -52
  3. package/cli/bin/loom.js +285 -99
  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/diagnostics.js +138 -41
  13. package/cli/src/guide.js +41 -19
  14. package/cli/src/init.js +50 -29
  15. package/cli/src/intent-draft.js +303 -0
  16. package/cli/src/intent-map.js +540 -54
  17. package/cli/src/patch.js +214 -0
  18. package/cli/src/philosophy.js +177 -154
  19. package/cli/src/preview-prompt.md +13 -6
  20. package/cli/src/preview.js +1 -0
  21. package/cli/src/shared/intent-ref.js +38 -0
  22. package/cli/src/shared/proof-reference.js +19 -0
  23. package/cli/src/shared/verification-method.js +32 -0
  24. package/cli/src/verify.js +184 -61
  25. package/cli/src/version.js +5 -4
  26. package/dimensions/PART_DECOMPOSITION.md +42 -203
  27. package/dimensions/SEARCH_METHODOLOGY.md +101 -97
  28. package/dimensions/examples/AGENT_SYSTEM/README.md +1 -1
  29. package/dimensions/examples/CLI_TOOL/README.md +1 -1
  30. package/dimensions/universal/COLLABORATION_PHILOSOPHY.md +28 -77
  31. package/dimensions/universal/ENGINEERING_CREED.md +30 -74
  32. package/dimensions/universal/PRODUCT_PHILOSOPHY.md +32 -70
  33. package/meta/BASELINE.md +91 -276
  34. package/meta/INTENT_LOOP.md +242 -737
  35. package/meta/PHILOSOPHY_WEAVER.md +110 -343
  36. package/meta/ROLE_ACTIVATION.md +103 -267
  37. package/package.json +4 -3
  38. package/roles/architect.md +71 -111
  39. package/roles/forge.md +87 -126
  40. package/roles/keeper.md +99 -223
  41. package/roles/visionary.md +57 -86
  42. package/templates/INTENT_MAP_TEMPLATE.json +24 -10
  43. package/templates/PHILOSOPHY_TEMPLATE.md +44 -75
  44. package/templates/VISION_TEMPLATE.md +44 -67
@@ -1,73 +1,302 @@
1
- // activate — 输出角色激活提示词
2
- // 把角色文件 + 哲学锚点 + 底线拼成激活上下文,供上层编排器使用。
3
-
4
- import { readFileSync, existsSync } from 'node:fs';
5
- import { join, resolve, dirname } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import { getLoomRoot } from './shared/paths.js';
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
10
-
11
- /** 合法角色名 */
12
- const VALID_ROLES = ['weaver', 'visionary', 'architect', 'forge', 'keeper'];
13
-
14
- /** 角色文件映射 */
15
- const ROLE_FILES = {
16
- weaver: 'meta/PHILOSOPHY_WEAVER.md',
17
- visionary: 'roles/visionary.md',
18
- architect: 'roles/architect.md',
19
- forge: 'roles/forge.md',
20
- keeper: 'roles/keeper.md',
21
- };
22
-
23
- /**
24
- * 输出角色激活提示词。
25
- * @param {string} role — 角色名
26
- * @param {string} versionDir — .loom/v{N} 目录(可选,weaver 不需要)
27
- * @returns {string} 激活提示词
28
- */
29
- export function activateRole(role, versionDir) {
30
- if (!VALID_ROLES.includes(role)) {
31
- throw new Error(`未知角色: ${role}\n合法角色: ${VALID_ROLES.join(', ')}`);
32
- }
33
-
34
- const loomRoot = getLoomRoot();
35
- const parts = [];
36
-
37
- // 1. 角色文件
38
- const roleFile = join(loomRoot, ROLE_FILES[role]);
39
- if (!existsSync(roleFile)) {
40
- throw new Error(`角色文件不存在: ${roleFile}`);
41
- }
42
- parts.push(readFileSync(roleFile, 'utf-8'));
43
-
44
- // 2. BASELINE 摘要(不重复全文——全文见 meta/BASELINE.md)
45
- // 5 条底线压缩成摘要,角色需要知道底线存在 + 一句话内容。
46
- // 如果角色需要底线细节(如 Weaver 织造哲学时),自行 readFileSync 全文。
47
- parts.push('\n---\n\n## 强制加载:BASELINE 摘要\n\n');
48
- parts.push('> 完整底线见 `meta/BASELINE.md`。以下是 5 条底线的摘要——\n');
49
- parts.push('> 角色激活时必须知道这些底线存在,违反任何一条必须立即停止。\n');
50
- parts.push('> Philosophy Weaver 织造哲学时必须读取完整 BASELINE.md 作为硬约束输入。\n\n');
51
- parts.push('| 编号 | 底线 | 一句话 |\n');
52
- parts.push('|------|------|--------|\n');
53
- parts.push('| B1 | 必须有结构设计 | 编码前必须有明确的目录结构 + 模块职责边界 + 显式依赖关系 |\n');
54
- parts.push('| B2 | 禁止硬编码 | 密钥/配置/环境特定值/魔法数字不进代码,用环境变量或集中配置 |\n');
55
- parts.push('| B3 | 接口契约必须显式 | API/CLI/配置/错误语义/跨系统协议必须有显式定义,变更可追溯 |\n');
56
- parts.push('| B4 | 决策必须可追溯 | 影响架构/接口/技术栈/依赖的决策必须记录(ADR 或等效格式) |\n');
57
- parts.push('| B5 | 意图必须可回溯 | 每个实现单元有意图叙事("为什么存在"),可被 Keeper 引用对照 |\n');
58
- parts.push('\n> 底线不可被哲学覆盖。如果织造的哲学与底线冲突,底线优先。\n');
59
-
60
- // 3. 项目特定底线(如果有 versionDir)
61
- if (versionDir) {
62
- const projectBaseline = join(versionDir, '00_PHILOSOPHY/PROJECT_BASELINE.md');
63
- if (existsSync(projectBaseline)) {
64
- parts.push('\n---\n\n## 强制加载:项目特定底线\n\n' + readFileSync(projectBaseline, 'utf-8'));
65
- }
66
-
67
- // 4. 角色激活协议
68
- const activationFile = join(loomRoot, 'meta/ROLE_ACTIVATION.md');
69
- parts.push('\n---\n\n## 角色激活协议\n\n' + readFileSync(activationFile, 'utf-8'));
70
- }
71
-
72
- return parts.join('\n');
73
- }
1
+ // activate — compile a role- and intent-scoped Context Pack.
2
+
3
+ import { readFileSync, existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { getLoomRoot } from './shared/paths.js';
6
+ import { getIntent, getNarrative } from './intent-map.js';
7
+ import { getIntentDraft } from './intent-draft.js';
8
+ import { getPhilosophy } from './philosophy.js';
9
+ import { getVerificationContract } from './verify.js';
10
+ import { extractMdSection } from './shared/md-utils.js';
11
+
12
+ const VALID_ROLES = ['weaver', 'visionary', 'architect', 'forge', 'keeper'];
13
+
14
+ const ROLE_FILES = {
15
+ weaver: 'meta/PHILOSOPHY_WEAVER.md',
16
+ visionary: 'roles/visionary.md',
17
+ architect: 'roles/architect.md',
18
+ forge: 'roles/forge.md',
19
+ keeper: 'roles/keeper.md',
20
+ };
21
+
22
+ const ROLE_PHILOSOPHY_FILES = {
23
+ visionary: ['PRODUCT_PHILOSOPHY.md', 'DECISION_RUBRIC.md'],
24
+ architect: ['ENGINEERING_CREED.md', 'DECISION_RUBRIC.md'],
25
+ forge: ['ENGINEERING_CREED.md'],
26
+ keeper: ['PRODUCT_PHILOSOPHY.md', 'DECISION_RUBRIC.md'],
27
+ };
28
+
29
+ const BASELINE_SUMMARY = [
30
+ '- B1:实质修改前理解真实结构,不创建平行体系。',
31
+ '- B2:秘密、环境值和可变配置不写死。',
32
+ '- B3:用户或系统可观察的行为具有显式契约。',
33
+ '- B4:重要且会影响未来的判断可追溯。',
34
+ '- B5:完成关联原始意图和当前 revision 证据;质量提升具有基线相对证据。',
35
+ ].join('\n');
36
+
37
+ function section(title, body) {
38
+ const content = String(body || '').trim();
39
+ return `## ${title}\n\n${content || '无。'}`;
40
+ }
41
+
42
+ function readRole(role) {
43
+ const filePath = join(getLoomRoot(), ROLE_FILES[role]);
44
+ if (!existsSync(filePath)) throw new Error(`角色文件不存在: ${filePath}`);
45
+ return readFileSync(filePath, 'utf-8');
46
+ }
47
+
48
+ function getVerificationMethod(intent) {
49
+ return intent?.verification_method || intent?._optional?.verification_method || null;
50
+ }
51
+
52
+ function resolveContractReference(versionDir, value, label) {
53
+ if (!value) return null;
54
+ if (typeof value !== 'string') return JSON.stringify(value, null, 2);
55
+ const match = value.trim().match(/^(?:see\s+)?([^#]+)#([\w-]+)$/i);
56
+ if (!match) return value;
57
+ const [, file, anchor] = match;
58
+ if (file.includes('/') || file.includes('\\') || file !== '05_VERIFICATION.md') {
59
+ throw new Error(`${label}引用必须位于 05_VERIFICATION.md: ${value}`);
60
+ }
61
+ const filePath = join(versionDir, file);
62
+ if (!existsSync(filePath)) throw new Error(`${label}引用的文件不存在: ${filePath}`);
63
+ return extractMdSection(readFileSync(filePath, 'utf-8'), anchor, label);
64
+ }
65
+
66
+ function compileEnvelope(role, intentId) {
67
+ const scope = intentId ? `仅 ${intentId}` : '当前角色的项目阶段';
68
+ const lines = [
69
+ `- role: ${role}`,
70
+ `- scope: ${scope}`,
71
+ '- 本 Context Pack 不会清除现有会话记忆。',
72
+ '- system、developer 与用户指令优先;项目事实冲突时报告,不静默混用。',
73
+ '- 只在当前角色权限和明确作用域内行动。',
74
+ ];
75
+ if (role === 'keeper') {
76
+ lines.push('- Keeper 必须在新的 Agent thread 中运行;同一会话切换角色不构成独立验证。');
77
+ lines.push('- 无法获得独立上下文时降低声明,关键判断使用 pending_human。');
78
+ }
79
+ return lines.join('\n');
80
+ }
81
+
82
+ function compileObjective(role, versionDir, intentId) {
83
+ if (!intentId) {
84
+ return {
85
+ body: role === 'weaver'
86
+ ? '织造当前项目的 Project Doctrine。先读取真实项目与完整 BASELINE,不预写产品或架构。'
87
+ : `履行 ${role} 的当前项目阶段职责;未指定 Intent,不得自行选择并处理实现任务。`,
88
+ intent: null,
89
+ draft: null,
90
+ narrative: null,
91
+ };
92
+ }
93
+
94
+ if (role === 'visionary' || role === 'architect') {
95
+ const draft = getIntentDraft(versionDir, intentId);
96
+ const match = draft.narrative_ref.match(/^([^#]+)#([\w-]+)$/);
97
+ if (!match || match[1] !== '01_VISION.md') throw new Error(`draft narrative_ref 非法: ${draft.narrative_ref}`);
98
+ const narrative = extractMdSection(
99
+ readFileSync(join(versionDir, match[1]), 'utf-8'),
100
+ match[2],
101
+ 'draft 意图叙事',
102
+ );
103
+ const visibleDraft = role === 'visionary'
104
+ ? {
105
+ id: draft.id,
106
+ revision: draft.revision,
107
+ title: draft.title,
108
+ narrative_ref: draft.narrative_ref,
109
+ depends_on: draft.depends_on,
110
+ }
111
+ : draft;
112
+ return {
113
+ body: [
114
+ '只处理下面这个 draft。不要修改官方 Intent Map,不要处理其他 Intent,不要自行 finalize。',
115
+ '',
116
+ '### Draft',
117
+ '',
118
+ '```json',
119
+ JSON.stringify(visibleDraft, null, 2),
120
+ '```',
121
+ '',
122
+ '### Narrative',
123
+ '',
124
+ narrative,
125
+ ].join('\n'),
126
+ intent: null,
127
+ draft,
128
+ narrative,
129
+ };
130
+ }
131
+
132
+ const intent = getIntent(versionDir, intentId);
133
+ const narrative = getNarrative(versionDir, intentId);
134
+ const objectiveView = {
135
+ id: intent.id,
136
+ revision: intent.revision,
137
+ title: intent.title,
138
+ status: intent.status,
139
+ narrative_ref: intent.narrative_ref,
140
+ depends_on: intent.depends_on,
141
+ };
142
+ return {
143
+ body: [
144
+ '只实现或验证下面这个官方 Intent。不得加载或顺便处理其他 Intent。',
145
+ '',
146
+ '### Intent',
147
+ '',
148
+ '```json',
149
+ JSON.stringify(objectiveView, null, 2),
150
+ '```',
151
+ '',
152
+ '### Narrative',
153
+ '',
154
+ narrative,
155
+ ].join('\n'),
156
+ intent,
157
+ draft: null,
158
+ narrative,
159
+ };
160
+ }
161
+
162
+ function compileInvariants(role, versionDir) {
163
+ const blocks = [];
164
+ const baselinePath = join(getLoomRoot(), 'meta/BASELINE.md');
165
+ blocks.push(role === 'weaver' ? readFileSync(baselinePath, 'utf-8') : BASELINE_SUMMARY);
166
+ if (versionDir) {
167
+ const projectBaseline = join(versionDir, '00_PHILOSOPHY', 'PROJECT_BASELINE.md');
168
+ if (existsSync(projectBaseline)) {
169
+ blocks.push(`### Project Baseline\n\n${readFileSync(projectBaseline, 'utf-8')}`);
170
+ }
171
+ }
172
+ return blocks.join('\n\n');
173
+ }
174
+
175
+ function compileContracts(role, versionDir, objective) {
176
+ const subject = objective.intent || objective.draft;
177
+ if (!subject || role === 'visionary') {
178
+ return role === 'visionary'
179
+ ? 'Visionary 只定义目标、非目标和 narrative;不要编写 acceptance 或架构。'
180
+ : '按当前角色 Output Contract 交付。';
181
+ }
182
+
183
+ const blocks = [];
184
+ if (subject.acceptance) {
185
+ const acceptance = objective.intent
186
+ ? getVerificationContract(versionDir, subject.id)
187
+ : resolveContractReference(versionDir, subject.acceptance, 'draft 完成契约');
188
+ blocks.push(`### Acceptance / Reliability Floor\n\n${acceptance}`);
189
+ }
190
+ if (subject.quality_contract) {
191
+ blocks.push(
192
+ `### Quality Contract / Distinctive Ceiling\n\n` +
193
+ resolveContractReference(versionDir, subject.quality_contract, '质量契约'),
194
+ );
195
+ }
196
+ const verificationMethod = getVerificationMethod(subject);
197
+ blocks.push(`### Verification Method\n\n${verificationMethod || '未声明:Keeper 使用适当的静态检查;需要运行时或人类判断却缺少入口时回流 Architect。'}`);
198
+ const continuity = subject.continuity_required
199
+ ? '此 Intent 已启用状态守恒门:通过前必须证明“旧状态 → 本轮操作 → 新状态”的完整序列中,未获明确授权删除或替换的既有价值、数据和可见结果仍被保留。'
200
+ : '默认不启用状态守恒门;若本 Intent 会变更既有用户数据、持久状态、迁移结果或既有工作流,Architect 必须把 continuity_required 设为 true,并在 acceptance 写出保留项与时序验证。';
201
+ blocks.push(
202
+ '### Completion Gate / Codex Goal Alignment\n\n' +
203
+ '将当前 Intent 视为本轮 Codex goal 的可闭合单元。goal 保持 active,直到结果达成、适用的状态守恒和可复现证据同时成立;goal/status 只是运行记录,不能替代验证证据。\n\n' +
204
+ continuity,
205
+ );
206
+ return blocks.join('\n\n');
207
+ }
208
+
209
+ function compileProjectJudgment(role, versionDir, objective) {
210
+ if (!versionDir || role === 'weaver') {
211
+ return role === 'weaver'
212
+ ? '从用户目标、仓库事实与决策相关证据建立 Project Doctrine。'
213
+ : '当前版本目录不可用。';
214
+ }
215
+
216
+ const philosophyDir = join(versionDir, '00_PHILOSOPHY');
217
+ const blocks = [];
218
+ const missing = [];
219
+ const anchors = (objective.intent || objective.draft)?.philosophy_anchors || [];
220
+
221
+ if (anchors.length > 0) {
222
+ for (const anchor of anchors) {
223
+ blocks.push(`### ${anchor}\n\n${getPhilosophy(philosophyDir, anchor)}`);
224
+ }
225
+ } else {
226
+ for (const file of ROLE_PHILOSOPHY_FILES[role] || []) {
227
+ const filePath = join(philosophyDir, file);
228
+ if (!existsSync(filePath)) {
229
+ missing.push(file);
230
+ continue;
231
+ }
232
+ blocks.push(`### ${file}\n\n${readFileSync(filePath, 'utf-8')}`);
233
+ }
234
+ if (missing.length) {
235
+ blocks.push(
236
+ `### Context Warning\n\n缺少: ${missing.join(', ')}。` +
237
+ '不要用通用偏好伪造项目判断;回流 Weaver 或向用户索取必要决定。',
238
+ );
239
+ }
240
+ }
241
+ return blocks.join('\n\n');
242
+ }
243
+
244
+ function compileExpertiseInputs(role, objective) {
245
+ const subject = objective.intent || objective.draft;
246
+ if (!subject || !['architect', 'forge', 'keeper'].includes(role)) {
247
+ return '当前阶段不编译任务级 Expertise Pack。';
248
+ }
249
+ const needs = Array.isArray(subject.capability_needs) ? subject.capability_needs : [];
250
+ const lines = [
251
+ `- capability_needs: ${needs.length ? needs.join(', ') : '未声明;按当前任务发现必要能力'}`,
252
+ `- creative_scope: ${subject.creative_scope || '未声明;遵循最小完整干预'}`,
253
+ '- Skill、工具和资产名称只代表可发现入口;实际检查并加载后才进入 Expertise Pack。',
254
+ ];
255
+ if (role === 'keeper') {
256
+ lines.push('- 不继承 Forge Expertise Pack;按契约独立准备验证能力。');
257
+ }
258
+ return lines.join('\n');
259
+ }
260
+
261
+ function compileWorkingFacts(versionDir, objective) {
262
+ if (!versionDir) return '检查当前仓库、用户输入和可用工具。';
263
+ const subject = objective.intent || objective.draft;
264
+ const refs = [
265
+ '- architecture: `.loom/.../02_ARCHITECTURE.md`(只读取与当前决定相关部分)',
266
+ '- artifacts: 从真实工作区检查,不从会话记忆猜测。',
267
+ ];
268
+ const systemId = subject?._optional?.system_id || subject?.system_id;
269
+ if (systemId) refs.push(`- system_id: ${systemId}`);
270
+ if (subject?.quality_contract) refs.push('- baseline: 声明相对提升时,修改前证据必须在实现前保存。');
271
+ return refs.join('\n');
272
+ }
273
+
274
+ /**
275
+ * Compile a role-scoped Context Pack.
276
+ * @param {string} role
277
+ * @param {string | null} versionDir
278
+ * @param {string | null} intentId
279
+ */
280
+ export function activateRole(role, versionDir, intentId = null) {
281
+ if (!VALID_ROLES.includes(role)) {
282
+ throw new Error(`未知角色: ${role}\n合法角色: ${VALID_ROLES.join(', ')}`);
283
+ }
284
+ if (intentId && !versionDir) throw new Error('--intent 需要当前 LOOM 版本');
285
+ if (intentId && !['visionary', 'architect', 'forge', 'keeper'].includes(role)) {
286
+ throw new Error(`角色 ${role} 不支持 --intent;draft 用 visionary/architect,官方 Intent 用 forge/keeper`);
287
+ }
288
+
289
+ const objective = compileObjective(role, versionDir, intentId);
290
+ const parts = [
291
+ '# LOOM Context Pack',
292
+ section('1. Execution Envelope', compileEnvelope(role, intentId)),
293
+ section('2. Active Objective', objective.body),
294
+ section('3. Hard Invariants', compileInvariants(role, versionDir)),
295
+ section('4. Success Contracts', compileContracts(role, versionDir, objective)),
296
+ section('5. Project Judgment', compileProjectJudgment(role, versionDir, objective)),
297
+ section('6. Expertise Inputs', compileExpertiseInputs(role, objective)),
298
+ section('7. Working Facts', compileWorkingFacts(versionDir, objective)),
299
+ section('8. Role Contract / Output / Reflow / Stop', readRole(role)),
300
+ ];
301
+ return `${parts.join('\n\n---\n\n')}\n`;
302
+ }
@@ -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');
@@ -75,10 +79,6 @@ function intentMapDiagnostics(versionDir) {
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, '')
@@ -143,7 +143,8 @@ const FIX_HINTS = {
143
143
  intent_map_missing: '运行 loom init 或 loom activate architect 产出 04_INTENT_MAP.json',
144
144
  intent_map_template: '运行 loom activate architect,Architect 填充真实 Intent Map 后删除 _meta._template 标记',
145
145
  intent_map_invalid: '按报错信息修正 04_INTENT_MAP.json 里对应字段(补 title / 加长 acceptance / 填必填字段)',
146
- completed_no_record: '在 .loom/v{N}/verifications/ 下补验证记录,或运行 loom verify pass {id} --summary "..."',
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} 闭合。',
147
148
  in_progress_no_record: '运行 loom verify pass {id} --summary "..." 写入验证记录,或 loom intent update {id} --status pending 回退',
148
149
  orphan_philosophy_ref: '检查 04_INTENT_MAP.json 里 {id} 的 philosophy_anchors,移除或修正不存在的哲学文件引用',
149
150
  orphan_dependency: '检查 04_INTENT_MAP.json 里 {id} 的 depends_on,移除或修正不存在的 Intent ID',
@@ -152,9 +153,14 @@ const FIX_HINTS = {
152
153
  completed_depends_blocked: '检查依赖 {dep} 为什么 blocked——解决阻塞或把 {id} 回退到 in_progress',
153
154
  test_script_missing: '在 package.json 里加 test 脚本,或修正 verification_method 指向实际存在的测试命令',
154
155
  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 拆解实现部分(- **部分名** 格式)',
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 生成投影',
158
164
  };
159
165
 
160
166
  /**
@@ -192,16 +198,49 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
192
198
 
193
199
  const { intents } = mapState.validMap;
194
200
 
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
- }
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
+ }
205
244
 
206
245
  // 2. 孤儿引用:哲学锚点指向不存在的文件
207
246
  for (const [id, intent] of Object.entries(intents)) {
@@ -312,12 +351,42 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
312
351
  issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 声明了 verification_method 但没有验证记录覆盖: ${method}` });
313
352
  } else if (!actual) {
314
353
  issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 最新验证记录缺少 reproduction_command,无法复现 verification_method: ${method}` });
315
- } else if (!commandCoversMethod(actual, expected)) {
354
+ } else if (!commandCoversVerificationMethod(actual, expected)) {
316
355
  issues.push({ id, type: 'verification_method_drift', severity: 'high', msg: `${id} verification_method 未被最新 reproduction_command 覆盖。method="${method}" reproduction_command="${latest.reproduction_command}"` });
317
356
  }
318
357
  }
319
358
 
320
- appendPhilosophyDiagnostics(issues, philosophyDir);
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);
321
390
  const issuesWithHints = issues.map(addFixHint);
322
391
  return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
323
392
  }
@@ -331,11 +400,6 @@ function appendPhilosophyDiagnostics(issues, philosophyDir) {
331
400
  issues.push({ id: 'philosophy', type: 'inspiration_source', severity: issue.severity, msg: issue.msg });
332
401
  }
333
402
 
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
403
  }
340
404
 
341
405
  function summarizeIssues(issues) {
@@ -403,7 +467,7 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
403
467
  const { issues } = doctor(versionDir, verificationsDir, philosophyDir);
404
468
 
405
469
  // 区分模板阶段问题(待填充)和真实损坏
406
- const templateIssues = issues.filter((i) => i.is_template || i.type === 'intent_map_template' || i.type === 'inspiration_source' || i.type === 'part_decomposition');
470
+ const templateIssues = issues.filter((i) => i.is_template || i.type === 'intent_map_template' || i.type === 'inspiration_source');
407
471
  const realIssues = issues.filter((i) => !templateIssues.includes(i));
408
472
 
409
473
  const risks = [];
@@ -416,11 +480,12 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
416
480
 
417
481
  return {
418
482
  intent_map_valid: mapState.valid === true,
419
- progress: {
483
+ progress: {
420
484
  completed: status.counts.completed,
421
485
  total: status.counts.total,
422
- rate: `${status.counts.completed}/${status.counts.total}`,
423
- },
486
+ rate: `${status.counts.completed}/${status.counts.total}`,
487
+ },
488
+ deprecated_intents: status.deprecated || [],
424
489
  next_intent: next ? next.id : null,
425
490
  pending_verifications: pending,
426
491
  inconsistent_states: issues.filter((i) => i.type === 'in_progress_no_record' || i.type === 'completed_no_record').map((i) => i.id),
@@ -440,7 +505,7 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
440
505
  * @param {string} intentId
441
506
  * @returns {object}
442
507
  */
443
- export function traceIntent(versionDir, verificationsDir, philosophyDir, intentId) {
508
+ export function traceIntent(versionDir, verificationsDir, philosophyDir, intentId) {
444
509
  const intent = getIntent(versionDir, intentId);
445
510
  if (!intent) throw new Error(`Intent 不存在: ${intentId}`);
446
511
 
@@ -482,19 +547,51 @@ export function traceIntent(versionDir, verificationsDir, philosophyDir, intentI
482
547
  walkDeps(dep, depth + 1);
483
548
  }
484
549
  }
485
- walkDeps(intentId, 0);
486
-
487
- return {
488
- 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,
489
585
  narrative,
490
586
  narrative_error: narrativeError,
491
587
  acceptance,
492
588
  acceptance_error: acceptanceError,
493
589
  verification_history: verificationHistory,
494
- philosophy_anchors_content: philosophyContent,
495
- dependency_chain: dependencyChain,
496
- };
497
- }
590
+ philosophy_anchors_content: philosophyContent,
591
+ dependency_chain: dependencyChain,
592
+ lineage: { predecessors, successors },
593
+ };
594
+ }
498
595
 
499
596
  // ─── reverse-dep ───────────────────────────────────────
500
597
  // 反向依赖:哪些 Intent 依赖这个 Intent