@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
@@ -42,8 +42,10 @@
42
42
  | `00_PHILOSOPHY/*.md` | 北极星、核心信念、反模式、决策原则 |
43
43
  | `01_VISION.md` | 北极星、问题空间、意图叙事、不做什么 |
44
44
  | `02_ARCHITECTURE.md` | 关键决策、trade-offs |
45
- | `04_INTENT_MAP.json` | 依赖关系、状态分布、验收契约 |
46
- | `verifications/*.json` | 验证时间轴、通过/偏离、证据 |
45
+ | `04_INTENT_MAP.json` | 依赖关系、状态、完成/质量契约、专业能力需求 |
46
+ | `verifications/*.json` | 验证时间轴、基础维度、按需的质量维度与 Quality Proof |
47
+ | `06_CHANGELOG.json` | Patch 历史(唯一权威来源);不要从生成的 Markdown 反推数据 |
48
+ | `06_CHANGELOG.md` | JSON 的确定性只读投影,仅用于交叉检查 |
47
49
 
48
50
  文件还是模板(含 `<!-- LOOM_TEMPLATE -->` 标记)时,对应区域显示提示:"等待 {角色} 填充"。
49
51
 
@@ -149,7 +151,7 @@ SVG 折线,横轴时间,纵轴剩余数。
149
151
  - **SVG 依赖图**:依赖链和阻塞点
150
152
  - **看板列**:按状态分组的 Intent 卡片(待执行/进行中/完成/阻塞)
151
153
  - **过滤面板**:状态过滤 + 模糊搜索(Intent 多时)
152
- - **点击下钻**:点击节点或卡片 → 侧边详情面板(叙事、验收契约、哲学锚点、验证历史)
154
+ - **点击下钻**:点击节点或卡片 → 侧边详情面板(叙事、完成/质量契约、能力需求、验证历史)
153
155
 
154
156
  ### 哲学区域
155
157
 
@@ -178,13 +180,18 @@ SVG 折线,横轴时间,纵轴剩余数。
178
180
  - **关键决策对比表**:每个决策两列对比(选了什么 / 放弃了什么)
179
181
  - **trade-offs 清单**:每条一句话
180
182
 
181
- ### 验证历史区域(如果有)
183
+ ### 验证历史区域(如果有)
182
184
 
183
185
  "质量怎么样"——拆解:
184
186
 
185
187
  - **时间轴**:每次验证一个节点,通过/偏离用颜色区分,点击看证据
186
188
  - **热力图网格**(Intent 多时):Intent × 验证轮次矩阵,一眼看出哪个反复偏离
187
- - **偏离清单**:哪些 Intent 偏离了,证据是什么
189
+ - **偏离清单**:哪些 Intent 偏离了,证据是什么
190
+
191
+ ### Patch 历史区域(如果有)
192
+
193
+ - 从 `06_CHANGELOG.json` 展示 Patch 时间轴:摘要、原因、影响的 Intent、文件和已记录的验证结果
194
+ - 明确区分 Patch 与 Intent 验证;`06_CHANGELOG.md` 是生成文件,不是另一份真相
188
195
 
189
196
  ### 底部
190
197
 
@@ -201,7 +208,7 @@ SVG 折线,横轴时间,纵轴剩余数。
201
208
  - 北极星 → 全页最大字号
202
209
  - 进度数字 → 大数字 + 颜色
203
210
  - Intent ID → 中等字号,等宽字体
204
- - 验收契约正文小字,可展开才显示
211
+ - 完成契约正文小字,可展开才显示;质量契约与 Quality Proof 使用相邻但可区分的区域
205
212
  - 辅助说明 → 最小字,灰色
206
213
 
207
214
  不要所有文字一样大。人类靠大小判断重要性。
@@ -11,6 +11,7 @@ const SOURCE_FILE_NAMES = new Set([
11
11
  '02_ARCHITECTURE.md',
12
12
  '04_INTENT_MAP.json',
13
13
  '05_VERIFICATION.md',
14
+ '06_CHANGELOG.json',
14
15
  '06_CHANGELOG.md',
15
16
  ]);
16
17
 
@@ -0,0 +1,38 @@
1
+ // intent-ref.js — Resolve current and version-qualified Intent read references.
2
+
3
+ import { existsSync } from 'node:fs';
4
+ import { basename, dirname, join } from 'node:path';
5
+
6
+ export const VERSION_PATTERN = /^v\d+$/;
7
+ export const INTENT_ID_PATTERN = /^INT-\d+$/;
8
+
9
+ export function formatIntentRef(version, intentId) {
10
+ return `${version}:${intentId}`;
11
+ }
12
+
13
+ /** Resolve INT-003 against the current version or v1:INT-003 against .loom/v1. */
14
+ export function resolveIntentRef(currentVersionDir, input) {
15
+ const currentVersion = basename(currentVersionDir);
16
+ if (!VERSION_PATTERN.test(currentVersion)) {
17
+ throw new Error(`无法从目录确定当前 LOOM 版本: ${currentVersionDir}`);
18
+ }
19
+
20
+ const parts = String(input || '').split(':');
21
+ if (parts.length > 2) throw new Error(`Intent 引用格式非法: ${input}`);
22
+ const qualified = parts.length === 2;
23
+ const version = qualified ? parts[0] : currentVersion;
24
+ const intentId = qualified ? parts[1] : parts[0];
25
+ if (!VERSION_PATTERN.test(version) || !INTENT_ID_PATTERN.test(intentId)) {
26
+ throw new Error(`Intent 引用格式非法: ${input}(应为 INT-003 或 v1:INT-003)`);
27
+ }
28
+
29
+ const versionDir = qualified ? join(dirname(currentVersionDir), version) : currentVersionDir;
30
+ if (!existsSync(versionDir)) throw new Error(`版本不存在: ${version}`);
31
+ return {
32
+ ref: formatIntentRef(version, intentId),
33
+ version,
34
+ intentId,
35
+ versionDir,
36
+ historical: version !== currentVersion,
37
+ };
38
+ }
@@ -0,0 +1,19 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { isAbsolute, relative, resolve } from 'node:path';
3
+ import { extractMdSection } from './md-utils.js';
4
+
5
+ /** Resolve a root-relative Markdown proof reference without allowing path escape. */
6
+ export function resolveQualityProofReference(versionDir, ref) {
7
+ if (typeof ref !== 'string' || !ref.trim()) throw new Error('quality_proof_ref 必须是非空字符串');
8
+ const match = ref.trim().match(/^([^#]+)#([\w-]+)$/);
9
+ if (!match) throw new Error('quality_proof_ref 必须是项目相对路径加 Markdown 锚点,例如 verifications/INT-001-quality-proof.md#int-001');
10
+ const [, file, anchor] = match;
11
+ if (isAbsolute(file)) throw new Error('quality_proof_ref 不得使用绝对路径');
12
+ const projectDir = resolve(versionDir, '..', '..');
13
+ const filePath = resolve(projectDir, file);
14
+ const relation = relative(projectDir, filePath);
15
+ if (relation.startsWith('..') || isAbsolute(relation)) throw new Error('quality_proof_ref 不得越出项目目录');
16
+ if (!existsSync(filePath)) throw new Error(`quality_proof_ref 指向的文件不存在: ${file}`);
17
+ extractMdSection(readFileSync(filePath, 'utf-8'), anchor, 'Quality Proof');
18
+ return { filePath, anchor, ref: ref.trim() };
19
+ }
@@ -0,0 +1,32 @@
1
+ export function getIntentVerificationMethod(intent) {
2
+ return intent?.verification_method || intent?._optional?.verification_method || null;
3
+ }
4
+
5
+ function normalize(command) {
6
+ return String(command || '')
7
+ .replace(/^\s*(?:run|exec)\s+/i, '')
8
+ .replace(/\s+/g, ' ')
9
+ .trim();
10
+ }
11
+
12
+ function normalizePackageManager(command) {
13
+ return ['npm', 'pnpm', 'bun', 'yarn'].reduce(
14
+ (result, manager) => result.replace(new RegExp(`\\b${manager}\\b`, 'g'), '<PM>'),
15
+ command,
16
+ );
17
+ }
18
+
19
+ /** Whether a recorded reproduction command covers the Architect-declared method. */
20
+ export function commandCoversVerificationMethod(actualCommand, expectedMethod) {
21
+ const actual = normalize(actualCommand);
22
+ const expected = normalize(expectedMethod);
23
+ if (!actual || !expected) return false;
24
+ return expected.split('&&').every((part) => {
25
+ const expectedPart = normalize(part);
26
+ if (!expectedPart || actual.includes(expectedPart)) return true;
27
+ const actualNorm = normalizePackageManager(actual);
28
+ const expectedNorm = normalizePackageManager(expectedPart);
29
+ if (actualNorm.includes(expectedNorm)) return true;
30
+ return expectedPart.startsWith('node --test') && actualNorm.includes('<PM> test');
31
+ });
32
+ }
package/cli/src/verify.js CHANGED
@@ -3,56 +3,75 @@
3
3
 
4
4
  import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
- import { extractMdSection, readJsonFile } from './shared/md-utils.js';
6
+ import { extractMdSection, readJsonFile } from './shared/md-utils.js';
7
+ import { getIntent, getEffectiveVerificationEpoch, hasLegacyIntentRevision } from './intent-map.js';
8
+ import { formatIntentRef, resolveIntentRef } from './shared/intent-ref.js';
9
+ import { resolveQualityProofReference } from './shared/proof-reference.js';
7
10
 
8
11
  /** 合法判定结果 */
9
12
  const VALID_VERDICTS = ['passed', 'deviated', 'blocked', 'pending_human'];
10
13
 
11
- /** 四个必须覆盖的验证维度 */
12
- const REQUIRED_DIMENSIONS = [
13
- 'intent_fidelity',
14
- 'philosophy_consistency',
15
- 'baseline_compliance',
16
- 'acceptance_achievement',
17
- ];
14
+ /** 每个 Intent 都必须覆盖的基础验证维度。 */
15
+ const BASE_DIMENSIONS = [
16
+ 'intent_fidelity',
17
+ 'philosophy_consistency',
18
+ 'baseline_compliance',
19
+ 'acceptance_achievement',
20
+ ];
21
+
22
+ function getRequiredDimensions(intent) {
23
+ const dimensions = [...BASE_DIMENSIONS];
24
+ if (intent?.continuity_required) dimensions.push('preservation_achievement');
25
+ if (intent?.quality_contract) dimensions.push('quality_achievement');
26
+ return dimensions;
27
+ }
18
28
 
19
29
  /**
20
30
  * 写入一条验证记录(追加模式——同一 Intent 多次验证保留完整历史)。
21
31
  * 文件格式: { intent_id, records: [{ round, verdict, timestamp, ... }] }
22
- * @param {string} verificationsDirverifications/ 目录路径
32
+ * @param {string} versionDir当前 .loom/v{N}/ 目录,用于可信读取 Intent revision
33
+ * @param {string} verificationsDir — verifications/ 目录路径
23
34
  * @param {object} record — 验证记录
24
35
  * @param {string} record.intent_id — 如 "INT-001"
25
36
  * @param {string} record.verdict — passed | deviated | blocked
26
37
  * @param {string} record.timestamp — ISO 8601
27
38
  * @param {string} record.summary — 验证摘要
28
- * @param {object} record.dimensions — 四个维度的验证结果
39
+ * @param {object} record.dimensions — 基础维度,以及质量契约存在时的 quality_achievement
29
40
  * @param {string} [record.reproduction_command] — 复现验证的命令(如 "LLM_API_KEY=mock npm test")
30
41
  * @param {string} [record.deviation_detail] — 偏离说明(deviated 时)
31
42
  * @param {boolean} [record.reset_suggested] — 是否建议重置上下文
32
43
  * @returns {{ filePath: string, round: number, deviated_count: number, should_escalate: boolean }}
33
44
  */
34
- export function writeVerification(verificationsDir, record) {
35
- const errors = [];
36
- if (!record.intent_id) errors.push('缺少 intent_id');
45
+ export function writeVerification(versionDir, verificationsDir, record) {
46
+ const errors = [];
47
+ if (!record.intent_id) errors.push('缺少 intent_id');
37
48
  if (!record.verdict || !VALID_VERDICTS.includes(record.verdict)) {
38
49
  errors.push(`verdict 非法: "${record.verdict}" (合法: ${VALID_VERDICTS.join('|')})`);
39
50
  }
40
- if (!record.timestamp) errors.push('缺少 timestamp');
41
- if (!record.dimensions) errors.push('缺少 dimensions(四个维度结果)');
42
- // dimensions 结构校验:每个维度必须是 { verdict, evidence } 对象
43
- if (record.dimensions) {
44
- for (const dim of REQUIRED_DIMENSIONS) {
45
- const v = record.dimensions[dim];
46
- if (v === undefined) {
47
- errors.push(`dimensions.${dim} 缺失(四个维度必须全覆盖)`);
51
+ if (!record.timestamp) errors.push('缺少 timestamp');
52
+ if (!record.dimensions) errors.push('缺少 dimensions(适用验证维度结果)');
53
+ const intent = record.intent_id ? getIntent(versionDir, record.intent_id) : null;
54
+ if (intent && !['in_progress', 'needs_review'].includes(intent.status)) {
55
+ errors.push(`Intent ${record.intent_id} 当前状态为 ${intent.status};只能为 in_progress 或 needs_review 的 Intent 写入验证记录`);
56
+ }
57
+ const requiredDimensions = getRequiredDimensions(intent);
58
+ // dimensions 结构校验:每个维度必须是 { verdict, evidence } 对象
59
+ if (record.dimensions) {
60
+ for (const dim of requiredDimensions) {
61
+ const v = record.dimensions[dim];
62
+ if (v === undefined) {
63
+ errors.push(`dimensions.${dim} 缺失(当前 Intent 的适用维度必须全覆盖)`);
48
64
  } else if (typeof v === 'string') {
49
65
  errors.push(`dimensions.${dim} 是旧格式(枚举值),必须改成 { verdict, evidence } 对象`);
50
66
  } else if (typeof v !== 'object' || v === null) {
51
67
  errors.push(`dimensions.${dim} 必须是 { verdict, evidence } 对象`);
52
68
  } else {
53
- if (!VALID_VERDICTS.includes(v.verdict)) {
54
- errors.push(`dimensions.${dim}.verdict 非法: "${v.verdict}" (合法: ${VALID_VERDICTS.join('|')})`);
55
- }
69
+ if (!VALID_VERDICTS.includes(v.verdict)) {
70
+ errors.push(`dimensions.${dim}.verdict 非法: "${v.verdict}" (合法: ${VALID_VERDICTS.join('|')})`);
71
+ }
72
+ if (record.verdict === 'passed' && v.verdict !== 'passed') {
73
+ errors.push(`整体 verdict 为 passed 时,dimensions.${dim}.verdict 也必须是 passed`);
74
+ }
56
75
  if (!v.evidence || typeof v.evidence !== 'string' || v.evidence.trim() === '') {
57
76
  errors.push(`dimensions.${dim}.evidence 缺失——必须给出具体证据,不能只写"合规"`);
58
77
  } else {
@@ -66,12 +85,28 @@ export function writeVerification(verificationsDir, record) {
66
85
  errors.push(`dimensions.${dim}.evidence "${ev}" 是通用评价而非具体证据——必须写"对照了什么 + 在代码哪里看到/没看到"`);
67
86
  }
68
87
  }
69
- }
70
- }
71
- }
72
- if (errors.length > 0) {
73
- throw new Error(`验证记录校验失败:\n - ${errors.join('\n - ')}`);
74
- }
88
+ }
89
+ }
90
+ }
91
+ const qualityProofRef = record.dimensions?.quality_achievement?.quality_proof_ref;
92
+ if (intent?.quality_contract && record.verdict === 'passed' && !qualityProofRef) {
93
+ errors.push('声明 quality_contract 的 Intent 通过时必须提供 dimensions.quality_achievement.quality_proof_ref');
94
+ }
95
+ if (qualityProofRef !== undefined
96
+ && (typeof qualityProofRef !== 'string' || qualityProofRef.trim() === '')) {
97
+ errors.push('dimensions.quality_achievement.quality_proof_ref 必须是非空字符串');
98
+ } else if (qualityProofRef !== undefined) {
99
+ try {
100
+ resolveQualityProofReference(versionDir, qualityProofRef);
101
+ } catch (error) {
102
+ errors.push(error.message);
103
+ }
104
+ }
105
+ if (errors.length > 0) {
106
+ throw new Error(`验证记录校验失败:\n - ${errors.join('\n - ')}`);
107
+ }
108
+
109
+ const intentRevision = getEffectiveIntentRevision(intent);
75
110
 
76
111
  const filePath = join(verificationsDir, `${record.intent_id}.json`);
77
112
 
@@ -92,24 +127,26 @@ export function writeVerification(verificationsDir, record) {
92
127
  data = { intent_id: record.intent_id, records: [] };
93
128
  }
94
129
 
95
- // 计算轮次和连续 deviated 计数。规范要求中间出现 passed/blocked 后重置。
96
- const round = data.records.length + 1;
97
- const recordsWithCurrent = [...data.records, record];
98
- let deviatedCount = 0;
99
- for (let i = recordsWithCurrent.length - 1; i >= 0; i--) {
100
- if (recordsWithCurrent[i].verdict !== 'deviated') break;
101
- deviatedCount++;
102
- }
130
+ // 计算轮次和连续 deviated 计数。规范要求中间出现 passed/blocked 后重置。
131
+ const round = data.records.length + 1;
132
+ const recordsWithCurrent = [...data.records, record];
133
+ let deviatedCount = 0;
134
+ for (let i = recordsWithCurrent.length - 1; i >= 0; i--) {
135
+ if (recordsWithCurrent[i].verdict !== 'deviated') break;
136
+ deviatedCount++;
137
+ }
103
138
 
104
139
  // 追加新记录
105
- data.records.push({
106
- round,
140
+ data.records.push({
141
+ round,
142
+ intent_revision: intentRevision,
143
+ verification_epoch: getEffectiveVerificationEpoch(intent),
107
144
  verdict: record.verdict,
108
145
  timestamp: record.timestamp,
109
146
  summary: record.summary,
110
147
  dimensions: record.dimensions,
111
- reproduction_command: record.reproduction_command,
112
- deviation_detail: record.deviation_detail,
148
+ reproduction_command: record.reproduction_command,
149
+ deviation_detail: record.deviation_detail,
113
150
  reset_suggested: record.reset_suggested,
114
151
  });
115
152
 
@@ -126,29 +163,145 @@ export function writeVerification(verificationsDir, record) {
126
163
  * 读取某 Intent 的验证历史。
127
164
  * @returns {{ intent_id: string, records: array } | null}
128
165
  */
129
- export function getVerificationHistory(verificationsDir, intentId) {
166
+ export function getVerificationHistory(verificationsDir, intentId) {
130
167
  const filePath = join(verificationsDir, `${intentId}.json`);
131
168
  if (!existsSync(filePath)) {
132
169
  return null;
133
170
  }
134
- return readJsonFile(filePath, '验证记录');
135
- }
171
+ return readJsonFile(filePath, '验证记录');
172
+ }
173
+
174
+ /** Read each owning version's local records along the explicit predecessor graph. */
175
+ export function getAcrossVersionVerificationHistory(currentVersionDir, inputRef) {
176
+ const root = resolveIntentRef(currentVersionDir, inputRef);
177
+ const histories = [];
178
+ const visited = new Set();
179
+ const active = new Set();
180
+
181
+ function walk(resolved) {
182
+ if (active.has(resolved.ref)) throw new Error(`Intent lineage 存在循环: ${[...active, resolved.ref].join(' -> ')}`);
183
+ if (visited.has(resolved.ref)) return;
184
+ active.add(resolved.ref);
185
+ const intent = getIntent(resolved.versionDir, resolved.intentId);
186
+ const local = getVerificationHistory(join(resolved.versionDir, 'verifications'), resolved.intentId);
187
+ histories.push({
188
+ ref: resolved.ref,
189
+ source_version: resolved.version,
190
+ source_intent: resolved.intentId,
191
+ source_intent_id: resolved.intentId,
192
+ records: (local?.records || []).map((record) => ({
193
+ ...record,
194
+ source_version: resolved.version,
195
+ source_intent: resolved.intentId,
196
+ source_intent_id: resolved.intentId,
197
+ })),
198
+ });
199
+ for (const predecessor of intent.lineage?.predecessors || []) {
200
+ walk(resolveIntentRef(currentVersionDir, formatIntentRef(predecessor.version, predecessor.intent_id)));
201
+ }
202
+ active.delete(resolved.ref);
203
+ visited.add(resolved.ref);
204
+ }
205
+
206
+ walk(root);
207
+ return { intent_ref: root.ref, across_versions: true, histories };
208
+ }
209
+
210
+ /**
211
+ * 快捷创建验证记录——Agent 不用手动构造完整 JSON。
212
+ * 内部用 summary 填充适用维度的 evidence,生成标准记录格式。
213
+ * @param {string} versionDir — 当前 .loom/v{N}/ 目录
214
+ * @param {string} verificationsDir — verifications/ 目录路径
215
+ * @param {string} intentId — 如 "INT-001"
216
+ * @param {string} verdict — 'passed' | 'deviated' | 'blocked'
217
+ * @param {string} summary — 验证摘要(也会作为所有适用维度的 evidence)
218
+ * @param {object} [extras]
219
+ * @param {string} [extras.reproduction_command] — 复现命令
220
+ * @param {string} [extras.quality_proof_ref] — Quality Proof 证据引用
221
+ * @param {string} [extras.preservation_evidence] — 对既有状态守恒的独立证据
222
+ * @param {string} [extras.deviation_detail] — 偏离说明(deviated 时)
223
+ * @returns {{ filePath: string, round: number, deviated_count: number, should_escalate: boolean }}
224
+ */
225
+ export function createQuickVerification(versionDir, verificationsDir, intentId, verdict, summary, extras = {}) {
226
+ const timestamp = new Date().toISOString();
227
+ const intent = getIntent(versionDir, intentId);
228
+ // 用 summary 填充适用维度的 evidence——快捷命令不要求 Agent 逐维度写
229
+ const dimensions = {};
230
+ for (const dim of getRequiredDimensions(intent)) {
231
+ dimensions[dim] = {
232
+ verdict,
233
+ evidence: dim === 'preservation_achievement'
234
+ ? (extras.preservation_evidence || summary)
235
+ : summary,
236
+ };
237
+ }
238
+ if (extras.quality_proof_ref && dimensions.quality_achievement) {
239
+ dimensions.quality_achievement.quality_proof_ref = extras.quality_proof_ref;
240
+ }
241
+ return writeVerification(versionDir, verificationsDir, {
242
+ intent_id: intentId,
243
+ verdict,
244
+ timestamp,
245
+ summary,
246
+ dimensions,
247
+ reproduction_command: extras.reproduction_command || null,
248
+ deviation_detail: extras.deviation_detail || null,
249
+ });
250
+ }
136
251
 
137
252
  /**
138
253
  * 返回所有待验证的 Intent(有实现产物但还没验证记录的)。
139
254
  * 需要传入 Intent Map 来判断哪些 Intent 是 in_progress。
140
255
  */
141
- export function getPendingVerifications(versionDir, verificationsDir) {
256
+ export function getPendingVerifications(versionDir, verificationsDir) {
142
257
  const intentMap = readJsonFile(join(versionDir, '04_INTENT_MAP.json'), 'Intent Map');
143
258
  const pending = [];
144
259
  for (const [id, intent] of Object.entries(intentMap.intents)) {
145
- if (intent.status === 'in_progress') {
146
- const hasRecord = existsSync(join(verificationsDir, `${id}.json`));
147
- if (!hasRecord) pending.push(id);
148
- }
260
+ if (intent.status === 'in_progress' || intent.status === 'needs_review') {
261
+ const history = getVerificationHistory(verificationsDir, id);
262
+ if (!hasCurrentPassedVerification(intent, history)) pending.push(id);
263
+ }
149
264
  }
150
265
  return pending;
151
- }
266
+ }
267
+
268
+ /** Missing Intent revisions are revision 1 without mutating the map. */
269
+ export function getEffectiveIntentRevision(intent) {
270
+ return intent.revision ?? 1;
271
+ }
272
+
273
+ /**
274
+ * Legacy records count as revision 1 only while the Intent itself is legacy.
275
+ * Once revision is explicit, an untagged record cannot prove freshness.
276
+ */
277
+ export function getVerificationIntentRevision(intent, record) {
278
+ if (Number.isInteger(record?.intent_revision) && record.intent_revision >= 1) {
279
+ return record.intent_revision;
280
+ }
281
+ return hasLegacyIntentRevision(intent) ? 1 : null;
282
+ }
283
+
284
+ export function isVerificationCurrent(intent, record) {
285
+ return getVerificationIntentRevision(intent, record) === getEffectiveIntentRevision(intent)
286
+ && getVerificationEpoch(intent, record) === getEffectiveVerificationEpoch(intent);
287
+ }
288
+
289
+ export function getVerificationEpoch(intent, record) {
290
+ if (Number.isInteger(record?.verification_epoch) && record.verification_epoch >= 1) {
291
+ return record.verification_epoch;
292
+ }
293
+ return intent?.verification_epoch === undefined ? 1 : null;
294
+ }
295
+
296
+ export function getLatestPassedVerification(history) {
297
+ if (!Array.isArray(history?.records)) return null;
298
+ return [...history.records].reverse().find((record) => record.verdict === 'passed') ?? null;
299
+ }
300
+
301
+ export function hasCurrentPassedVerification(intent, history) {
302
+ const latest = history?.records?.[history.records.length - 1];
303
+ return latest?.verdict === 'passed' && isVerificationCurrent(intent, latest);
304
+ }
152
305
 
153
306
  /**
154
307
  * 列出所有验证记录文件。
@@ -48,14 +48,15 @@ export function readCurrentPointer(loomRoot) {
48
48
  * @param {string} projectDir — 项目根目录
49
49
  * @returns {{ version: string, created: string[], skipped: string[] }}
50
50
  */
51
- export function newVersion(projectDir) {
52
- const loomRoot = join(projectDir, '.loom');
53
- const { versions } = listVersions(loomRoot);
51
+ export function newVersion(projectDir) {
52
+ const loomRoot = join(projectDir, '.loom');
53
+ const { versions } = listVersions(loomRoot);
54
+ const parentVersion = readCurrentPointer(loomRoot);
54
55
  const nextNum = versions.length === 0
55
56
  ? 1
56
57
  : parseInt(versions[versions.length - 1].slice(1)) + 1;
57
58
  const nextV = `v${nextNum}`;
58
- const result = createVersionStructure(projectDir, nextV);
59
+ const result = createVersionStructure(projectDir, nextV, parentVersion);
59
60
  // 自动切换为当前版本
60
61
  writeFileSync(join(loomRoot, 'current'), nextV, 'utf-8');
61
62
  result.created.push('.loom/current');