@haaaiawd/loom 1.3.1 → 2.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 (84) hide show
  1. package/CHANGELOG.md +11 -86
  2. package/CONTRIBUTING.md +37 -0
  3. package/EVIL_EVAL.md +112 -0
  4. package/README.md +193 -445
  5. package/README.zh-CN.md +174 -0
  6. package/SECURITY.md +11 -0
  7. package/cli/bin/loom.js +171 -998
  8. package/cli/src/protocol.js +367 -0
  9. package/cli/src/store.js +626 -0
  10. package/design.md +194 -0
  11. package/docs/PROMPT_CATALOG.md +99 -0
  12. package/docs/RELEASE_CHECKLIST.md +53 -0
  13. package/docs/UX_FLOW.md +171 -0
  14. package/docs/brand/loom-mark.svg +18 -0
  15. package/docs/brand/loom-readme-header.svg +34 -0
  16. package/docs/brand/loom-readme-header.zh-CN.svg +29 -0
  17. package/docs/loom-eval-loop.drawio +21 -0
  18. package/docs/loom-eval-loop.svg +56 -0
  19. package/docs/loom-production-loop.drawio +41 -0
  20. package/docs/loom-production-loop.svg +92 -0
  21. package/package.json +43 -40
  22. package/EXTERNAL_ACQUISITION_DESIGN.md +0 -143
  23. package/cli/help/asset.md +0 -36
  24. package/cli/help/atelier.md +0 -37
  25. package/cli/help/atlas.md +0 -48
  26. package/cli/help/capability.md +0 -118
  27. package/cli/help/concepts.md +0 -105
  28. package/cli/help/doctor.md +0 -80
  29. package/cli/help/expertise.md +0 -52
  30. package/cli/help/loop.md +0 -134
  31. package/cli/help/patch.md +0 -33
  32. package/cli/help/proposals.md +0 -21
  33. package/cli/help/version.md +0 -136
  34. package/cli/help/workflow.md +0 -116
  35. package/cli/src/activate.js +0 -505
  36. package/cli/src/asset-library.js +0 -384
  37. package/cli/src/atelier.js +0 -331
  38. package/cli/src/atlas.js +0 -282
  39. package/cli/src/auto.js +0 -116
  40. package/cli/src/capability-graph.js +0 -724
  41. package/cli/src/capability-proposals.js +0 -225
  42. package/cli/src/diagnostics.js +0 -859
  43. package/cli/src/expertise-pack.js +0 -336
  44. package/cli/src/guide.js +0 -548
  45. package/cli/src/help.js +0 -41
  46. package/cli/src/init.js +0 -187
  47. package/cli/src/intent-draft.js +0 -303
  48. package/cli/src/intent-map.js +0 -747
  49. package/cli/src/patch.js +0 -214
  50. package/cli/src/philosophy.js +0 -331
  51. package/cli/src/shared/intent-ref.js +0 -38
  52. package/cli/src/shared/md-utils.js +0 -125
  53. package/cli/src/shared/paths.js +0 -73
  54. package/cli/src/shared/proof-reference.js +0 -19
  55. package/cli/src/shared/verification-method.js +0 -32
  56. package/cli/src/verify.js +0 -394
  57. package/cli/src/version.js +0 -134
  58. package/dimensions/AUTHORSHIP.md +0 -45
  59. package/dimensions/PART_DECOMPOSITION.md +0 -42
  60. package/dimensions/SEARCH_METHODOLOGY.md +0 -101
  61. package/dimensions/examples/AGENT_SYSTEM/README.md +0 -219
  62. package/dimensions/examples/CLI_TOOL/README.md +0 -163
  63. package/dimensions/universal/COLLABORATION_PHILOSOPHY.md +0 -28
  64. package/dimensions/universal/ENGINEERING_CREED.md +0 -30
  65. package/dimensions/universal/PRODUCT_PHILOSOPHY.md +0 -32
  66. package/meta/BASELINE.md +0 -91
  67. package/meta/INTENT_LOOP.md +0 -296
  68. package/meta/PHILOSOPHY_WEAVER.md +0 -110
  69. package/meta/ROLE_ACTIVATION.md +0 -114
  70. package/roles/architect.md +0 -92
  71. package/roles/forge.md +0 -110
  72. package/roles/impact-reviewer.md +0 -37
  73. package/roles/keeper.md +0 -113
  74. package/roles/visionary.md +0 -57
  75. package/templates/ASSET_LIBRARY_MANIFEST_TEMPLATE.json +0 -10
  76. package/templates/ATELIER_RECORD_TEMPLATE.json +0 -48
  77. package/templates/ATLAS_TEMPLATE.html +0 -104
  78. package/templates/CAPABILITY_BRIEF_TEMPLATE.md +0 -36
  79. package/templates/CAPABILITY_GRAPH_EXAMPLE.json +0 -188
  80. package/templates/CAPABILITY_GRAPH_TEMPLATE.json +0 -78
  81. package/templates/EXPERTISE_PACK_TEMPLATE.json +0 -22
  82. package/templates/INTENT_MAP_TEMPLATE.json +0 -85
  83. package/templates/PHILOSOPHY_TEMPLATE.md +0 -44
  84. package/templates/VISION_TEMPLATE.md +0 -44
@@ -1,125 +0,0 @@
1
- // shared/md-utils.js — MD 章节解析的公共工具
2
- // 提取自 philosophy.js / intent-map.js / verify.js 三处重复实现。
3
- // 统一 slugify + extractMdSection + 显式锚点逻辑,修中文标题 bug。
4
-
5
- import { readFileSync, existsSync } from 'node:fs';
6
-
7
- /**
8
- * 从 heading 文本中提取显式锚点。
9
- * 支持 Pandoc/MDX 风格语法: "## 核心信念 {#core-belief}"
10
- * @param {string} headingText — heading 文本(不含 # 前缀)
11
- * @returns {string|null} 显式锚点 slug,或 null(无显式锚点时)
12
- */
13
- export function extractExplicitAnchor(headingText) {
14
- const match = headingText.match(/\{#([\w-]+)\}\s*$/);
15
- return match ? match[1] : null;
16
- }
17
-
18
- /**
19
- * 从 heading 文本生成 slug(fallback,无显式锚点时用)。
20
- * 规则:小写、去除 \r、空格转连字符、去除非 [a-z0-9_-] 字符。
21
- *
22
- * 中文标题处理:\w 不匹配中文,所以纯中文标题 slugify 后是空字符串。
23
- * 这是设计约束——中文标题必须用显式锚点 {#anchor} 标注。
24
- * slugify 返回空字符串时,调用方应给出明确错误提示。
25
- *
26
- * @param {string} text — heading 文本
27
- * @returns {string} slug(可能为空字符串——纯中文标题无显式锚点时)
28
- */
29
- export function slugify(text) {
30
- return text
31
- .replace(/\r/g, '') // strip CRLF 的 \r
32
- .replace(/\{#[\w-]+\}\s*$/, '') // 去掉显式锚点标记
33
- .toLowerCase()
34
- .replace(/[^\w\s-]/g, '') // \w = [a-zA-Z0-9_]
35
- .replace(/\s+/g, '-')
36
- .replace(/-+/g, '-')
37
- .replace(/^-|-$/g, '')
38
- .trim();
39
- }
40
-
41
- /**
42
- * 从 MD 内容中提取指定 section 的内容(到下一个同级或更高级 heading 为止)。
43
- * 如果 sectionSlug 为 null/空,返回整个文件。
44
- * 支持显式锚点 {#slug} 和自动 slugify 两种方式匹配。
45
- *
46
- * @param {string} content — MD 文件内容
47
- * @param {string} sectionSlug — 目标 section 的 slug
48
- * @param {string} contextLabel — 错误信息里的上下文标签(如 "意图叙事"、"验证契约")
49
- * @returns {string} 提取的章节内容
50
- * @throws {Error} 章节未找到时抛错,错误信息包含 contextLabel 和 slug
51
- */
52
- export function extractMdSection(content, sectionSlug, contextLabel = '章节') {
53
- if (!sectionSlug) return content;
54
-
55
- const lines = content.split('\n');
56
- let capturing = false;
57
- let targetLevel = 0;
58
- const captured = [];
59
-
60
- for (const line of lines) {
61
- // strip \r 以兼容 Windows CRLF
62
- const cleanLine = line.replace(/\r$/, '');
63
- const headingMatch = cleanLine.match(/^(#{1,6})\s+(.+)$/);
64
- if (headingMatch) {
65
- const level = headingMatch[1].length;
66
- const headingText = headingMatch[2];
67
- // 优先用显式锚点,没有再 fallback 到 slugify
68
- const slug = extractExplicitAnchor(headingText) || slugify(headingText);
69
-
70
- if (capturing && level <= targetLevel) {
71
- break;
72
- }
73
- if (slug === sectionSlug) {
74
- capturing = true;
75
- targetLevel = level;
76
- captured.push(cleanLine);
77
- continue;
78
- }
79
- }
80
- if (capturing) {
81
- captured.push(cleanLine);
82
- }
83
- }
84
-
85
- if (captured.length === 0) {
86
- throw new Error(
87
- `${contextLabel}章节未找到: #${sectionSlug}\n` +
88
- `可能原因:\n` +
89
- ` 1. 章节标题用了中文但没加显式锚点 {#anchor}\n` +
90
- ` 2. 锚点 slug 拼写错误\n` +
91
- ` 3. 引用的文件不存在该章节`
92
- );
93
- }
94
- return captured.join('\n').trim();
95
- }
96
-
97
- /**
98
- * 安全读取并解析 JSON 文件。
99
- * 统一错误处理——文件不存在、JSON 解析失败时给出明确错误信息。
100
- *
101
- * @param {string} filePath — JSON 文件绝对路径
102
- * @param {string} contextLabel — 错误信息里的上下文标签(如 "Intent Map"、"验证记录")
103
- * @returns {object} 解析后的 JSON 对象
104
- * @throws {Error} 文件不存在或 JSON 解析失败时抛错,错误信息包含文件路径和原因
105
- */
106
- export function readJsonFile(filePath, contextLabel = 'JSON 文件') {
107
- if (!existsSync(filePath)) {
108
- throw new Error(`${contextLabel}文件不存在: ${filePath}`);
109
- }
110
- let raw;
111
- try {
112
- raw = readFileSync(filePath, 'utf-8');
113
- } catch (e) {
114
- throw new Error(`${contextLabel}文件读取失败: ${filePath}\n原因: ${e.message}`);
115
- }
116
- try {
117
- return JSON.parse(raw);
118
- } catch (e) {
119
- throw new Error(
120
- `${contextLabel}文件 JSON 解析失败: ${filePath}\n` +
121
- `原因: ${e.message}\n` +
122
- `请检查文件内容是否为合法 JSON(多余逗号、缺少引号等)。`
123
- );
124
- }
125
- }
@@ -1,73 +0,0 @@
1
- // shared/paths.js — LOOM 路径解析的公共工具
2
- // 统一 getLoomRoot / findLoomRoot / findVersionDir 的命名和实现。
3
-
4
- import { resolve, join, dirname } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
7
- import { argv, cwd } from 'node:process';
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
10
-
11
- /**
12
- * 获取 LOOM 框架根目录(cli/src/shared 的上上上级)。
13
- * cli/src/shared/paths.js -> cli/src/shared -> cli/src -> cli -> LOOM root
14
- * @returns {string} LOOM 框架根目录绝对路径
15
- */
16
- export function getLoomRoot() {
17
- return resolve(__dirname, '..', '..', '..');
18
- }
19
-
20
- /**
21
- * 从命令行参数或 cwd 推断 .loom 目录路径。
22
- * 优先级:--loom-dir 参数(指向版本目录,反推 .loom root)> cwd/.loom
23
- * @returns {string} .loom 目录绝对路径
24
- */
25
- export function findLoomRoot() {
26
- const flagIdx = argv.indexOf('--loom-dir');
27
- if (flagIdx !== -1 && argv[flagIdx + 1]) {
28
- // --loom-dir 直接指向版本目录,反推 .loom root
29
- const dir = resolve(argv[flagIdx + 1]);
30
- return resolve(dir, '..');
31
- }
32
- return join(cwd(), '.loom');
33
- }
34
-
35
- /**
36
- * 从命令行参数或 .loom/current 指针推断当前版本目录。
37
- * @returns {string} .loom/v{N} 目录绝对路径
38
- * @throws {Error} .loom 不存在或没有版本目录时抛错
39
- */
40
- export function findVersionDir() {
41
- const flagIdx = argv.indexOf('--loom-dir');
42
- if (flagIdx !== -1 && argv[flagIdx + 1]) {
43
- return resolve(argv[flagIdx + 1]);
44
- }
45
- const loomRoot = join(cwd(), '.loom');
46
- if (!existsSync(loomRoot)) {
47
- throw new Error(`找不到 .loom 目录: ${loomRoot}`);
48
- }
49
- const current = readCurrentPointer(loomRoot);
50
- if (!current) {
51
- throw new Error(`.loom 下没有版本目录 (v1, v2, ...)`);
52
- }
53
- return join(loomRoot, current);
54
- }
55
-
56
- /**
57
- * 读取当前版本指针。
58
- * 优先读 .loom/current 文件;不存在则回退到自动探测最新版本。
59
- * @param {string} loomRoot — .loom 目录路径
60
- * @returns {string|null} 版本号如 'v1',或 null
61
- */
62
- export function readCurrentPointer(loomRoot) {
63
- const pointerPath = join(loomRoot, 'current');
64
- if (existsSync(pointerPath)) {
65
- const v = readFileSync(pointerPath, 'utf-8').trim();
66
- if (/^v\d+$/.test(v) && existsSync(join(loomRoot, v))) return v;
67
- }
68
- if (!existsSync(loomRoot)) return null;
69
- const versions = readdirSync(loomRoot)
70
- .filter((d) => /^v\d+$/.test(d) && statSync(join(loomRoot, d)).isDirectory())
71
- .sort((a, b) => parseInt(b.slice(1)) - parseInt(a.slice(1)));
72
- return versions[0] ?? null;
73
- }
@@ -1,19 +0,0 @@
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
- }
@@ -1,32 +0,0 @@
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 DELETED
@@ -1,394 +0,0 @@
1
- // verify.js — 验证记录的读写和查询
2
- // 验证记录存放在 .loom/v{N}/verifications/ 下,每个 Intent 一份 JSON + 一份 MD。
3
-
4
- import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
5
- import { join } from 'node:path';
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';
10
- import { validateAtelierRecord } from './atelier.js';
11
- import { assertExpertiseReady } from './expertise-pack.js';
12
-
13
- /** 合法判定结果 */
14
- const VALID_VERDICTS = ['passed', 'deviated', 'blocked', 'pending_human'];
15
- const VALID_VERIFICATION_CONTEXTS = ['independent_thread', 'human_review'];
16
-
17
- /** 每个 Intent 都必须覆盖的基础验证维度。 */
18
- const BASE_DIMENSIONS = [
19
- 'intent_fidelity',
20
- 'philosophy_consistency',
21
- 'baseline_compliance',
22
- 'acceptance_achievement',
23
- ];
24
-
25
- function getRequiredDimensions(intent) {
26
- const dimensions = [...BASE_DIMENSIONS];
27
- if (intent?.continuity_required) dimensions.push('preservation_achievement');
28
- if (intent?.quality_contract) dimensions.push('quality_achievement');
29
- return dimensions;
30
- }
31
-
32
- /**
33
- * 写入一条验证记录(追加模式——同一 Intent 多次验证保留完整历史)。
34
- * 文件格式: { intent_id, records: [{ round, verdict, timestamp, ... }] }
35
- * @param {string} versionDir — 当前 .loom/v{N}/ 目录,用于可信读取 Intent revision
36
- * @param {string} verificationsDir — verifications/ 目录路径
37
- * @param {object} record — 验证记录
38
- * @param {string} record.intent_id — 如 "INT-001"
39
- * @param {string} record.verdict — passed | deviated | blocked
40
- * @param {string} record.timestamp — ISO 8601
41
- * @param {string} record.summary — 验证摘要
42
- * @param {object} record.dimensions — 基础维度,以及质量契约存在时的 quality_achievement
43
- * @param {string} [record.reproduction_command] — 复现验证的命令(如 "LLM_API_KEY=mock npm test")
44
- * @param {string} [record.deviation_detail] — 偏离说明(deviated 时)
45
- * @param {boolean} [record.reset_suggested] — 是否建议重置上下文
46
- * @returns {{ filePath: string, round: number, deviated_count: number, should_escalate: boolean }}
47
- */
48
- export function writeVerification(versionDir, verificationsDir, record) {
49
- const errors = [];
50
- let atelierEvidence = null;
51
- let expertiseEvidence = null;
52
- if (!record.intent_id) errors.push('缺少 intent_id');
53
- if (!record.verdict || !VALID_VERDICTS.includes(record.verdict)) {
54
- errors.push(`verdict 非法: "${record.verdict}" (合法: ${VALID_VERDICTS.join('|')})`);
55
- }
56
- if (!record.timestamp) errors.push('缺少 timestamp');
57
- if (!record.dimensions) errors.push('缺少 dimensions(适用验证维度结果)');
58
- const intent = record.intent_id ? getIntent(versionDir, record.intent_id) : null;
59
- if (intent && !['in_progress', 'needs_review'].includes(intent.status)) {
60
- errors.push(`Intent ${record.intent_id} 当前状态为 ${intent.status};只能为 in_progress 或 needs_review 的 Intent 写入验证记录`);
61
- }
62
- const requiredDimensions = getRequiredDimensions(intent);
63
- if (record.verdict === 'passed') {
64
- const provenance = record.verification_provenance;
65
- if (!provenance || typeof provenance !== 'object') {
66
- errors.push('passed 必须声明 verification_provenance(verified_by + context);实现者自检不能单独闭合 Intent');
67
- } else {
68
- if (typeof provenance.verified_by !== 'string' || !provenance.verified_by.trim()) errors.push('verification_provenance.verified_by 必须是非空验证者标识');
69
- if (!VALID_VERIFICATION_CONTEXTS.includes(provenance.context)) errors.push(`verification_provenance.context 非法: ${provenance.context}(合法: ${VALID_VERIFICATION_CONTEXTS.join('|')})`);
70
- }
71
- }
72
- // dimensions 结构校验:每个维度必须是 { verdict, evidence } 对象
73
- if (record.dimensions) {
74
- for (const dim of requiredDimensions) {
75
- const v = record.dimensions[dim];
76
- if (v === undefined) {
77
- errors.push(`dimensions.${dim} 缺失(当前 Intent 的适用维度必须全覆盖)`);
78
- } else if (typeof v === 'string') {
79
- errors.push(`dimensions.${dim} 是旧格式(枚举值),必须改成 { verdict, evidence } 对象`);
80
- } else if (typeof v !== 'object' || v === null) {
81
- errors.push(`dimensions.${dim} 必须是 { verdict, evidence } 对象`);
82
- } else {
83
- if (!VALID_VERDICTS.includes(v.verdict)) {
84
- errors.push(`dimensions.${dim}.verdict 非法: "${v.verdict}" (合法: ${VALID_VERDICTS.join('|')})`);
85
- }
86
- if (record.verdict === 'passed' && v.verdict !== 'passed') {
87
- errors.push(`整体 verdict 为 passed 时,dimensions.${dim}.verdict 也必须是 passed`);
88
- }
89
- if (!v.evidence || typeof v.evidence !== 'string' || v.evidence.trim() === '') {
90
- errors.push(`dimensions.${dim}.evidence 缺失——必须给出具体证据,不能只写"合规"`);
91
- } else {
92
- // evidence 质量校验:长度 + 废话检测
93
- const ev = v.evidence.trim();
94
- if (ev.length < 10) {
95
- errors.push(`dimensions.${dim}.evidence 太短(${ev.length}字符 < 10)——必须给出具体证据,不能只写"合规"`);
96
- }
97
- const NONSENSE = ['合规', '通过', 'OK', 'ok', '没问题', '符合要求', '已检查', 'pass', 'passed', 'done'];
98
- if (NONSENSE.includes(ev)) {
99
- errors.push(`dimensions.${dim}.evidence "${ev}" 是通用评价而非具体证据——必须写"对照了什么 + 在代码哪里看到/没看到"`);
100
- }
101
- }
102
- }
103
- }
104
- }
105
- const qualityProofRef = record.dimensions?.quality_achievement?.quality_proof_ref;
106
- if (intent?.quality_contract && record.verdict === 'passed' && !qualityProofRef) {
107
- errors.push('声明 quality_contract 的 Intent 通过时必须提供 dimensions.quality_achievement.quality_proof_ref');
108
- }
109
- if (qualityProofRef !== undefined
110
- && (typeof qualityProofRef !== 'string' || qualityProofRef.trim() === '')) {
111
- errors.push('dimensions.quality_achievement.quality_proof_ref 必须是非空字符串');
112
- } else if (qualityProofRef !== undefined) {
113
- try {
114
- resolveQualityProofReference(versionDir, qualityProofRef);
115
- } catch (error) {
116
- errors.push(error.message);
117
- }
118
- }
119
- if (record.verdict === 'passed' && intent) {
120
- try {
121
- expertiseEvidence = assertExpertiseReady(versionDir, record.intent_id);
122
- } catch (error) {
123
- errors.push(`passed 前必须闭合外部能力获取强门: ${error.message}`);
124
- }
125
- }
126
- if (intent?.quality_strategy === 'atelier' && record.verdict === 'passed') {
127
- try {
128
- atelierEvidence = validateAtelierRecord(versionDir, record.intent_id);
129
- if (!['selected', 'baseline_retained'].includes(atelierEvidence.status)) {
130
- errors.push(`quality_strategy=atelier 通过前,Atelier Record 必须是 selected 或 baseline_retained(当前: ${atelierEvidence.status})`);
131
- }
132
- } catch (error) {
133
- errors.push(`quality_strategy=atelier 通过前必须有当前且合法的 Atelier Record: ${error.message}`);
134
- }
135
- }
136
- if (errors.length > 0) {
137
- throw new Error(`验证记录校验失败:\n - ${errors.join('\n - ')}`);
138
- }
139
-
140
- const intentRevision = getEffectiveIntentRevision(intent);
141
-
142
- const filePath = join(verificationsDir, `${record.intent_id}.json`);
143
-
144
- // 读取已有记录(如果有)
145
- let data;
146
- if (existsSync(filePath)) {
147
- data = readJsonFile(filePath, '验证记录');
148
- // 结构校验:已有文件必须是 { intent_id, records: [] } 格式
149
- if (!data || typeof data !== 'object' || !Array.isArray(data.records)) {
150
- throw new Error(
151
- `已有验证记录格式错误: ${filePath}\n` +
152
- `期望格式: { intent_id, records: [...] }\n` +
153
- `实际格式: ${JSON.stringify(data).slice(0, 200)}\n` +
154
- `修复: 删除或修正该文件后重试。`
155
- );
156
- }
157
- } else {
158
- data = { intent_id: record.intent_id, records: [] };
159
- }
160
-
161
- // 计算轮次和连续 deviated 计数。规范要求中间出现 passed/blocked 后重置。
162
- const round = data.records.length + 1;
163
- const recordsWithCurrent = [...data.records, record];
164
- let deviatedCount = 0;
165
- for (let i = recordsWithCurrent.length - 1; i >= 0; i--) {
166
- if (recordsWithCurrent[i].verdict !== 'deviated') break;
167
- deviatedCount++;
168
- }
169
-
170
- // 追加新记录
171
- data.records.push({
172
- round,
173
- intent_revision: intentRevision,
174
- verification_epoch: getEffectiveVerificationEpoch(intent),
175
- verdict: record.verdict,
176
- timestamp: record.timestamp,
177
- summary: record.summary,
178
- verification_provenance: record.verification_provenance,
179
- dimensions: record.dimensions,
180
- atelier: atelierEvidence ? {
181
- record_ref: `09_ATELIER/${record.intent_id}.json`,
182
- stance_revision: atelierEvidence.stance_revision,
183
- status: atelierEvidence.status,
184
- } : undefined,
185
- expertise: expertiseEvidence ? {
186
- record_ref: `10_EXPERTISE_PACKS/${record.intent_id}.json`,
187
- intent_revision: expertiseEvidence.intent_revision,
188
- required_node_ids: expertiseEvidence.required_node_ids,
189
- source_count: expertiseEvidence.source_count,
190
- capsule_count: expertiseEvidence.capsule_count,
191
- pack_digest: expertiseEvidence.pack_digest,
192
- } : undefined,
193
- reproduction_command: record.reproduction_command,
194
- deviation_detail: record.deviation_detail,
195
- reset_suggested: record.reset_suggested,
196
- });
197
-
198
- writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
199
-
200
- // 检查是否应该升级 blocked(连续 3 轮 deviated,默认值)
201
- const DEVIATED_LIMIT = 3;
202
- const shouldEscalate = record.verdict === 'deviated' && deviatedCount >= DEVIATED_LIMIT;
203
-
204
- return { filePath, round, deviated_count: deviatedCount, should_escalate: shouldEscalate };
205
- }
206
-
207
- /**
208
- * 读取某 Intent 的验证历史。
209
- * @returns {{ intent_id: string, records: array } | null}
210
- */
211
- export function getVerificationHistory(verificationsDir, intentId) {
212
- const filePath = join(verificationsDir, `${intentId}.json`);
213
- if (!existsSync(filePath)) {
214
- return null;
215
- }
216
- return readJsonFile(filePath, '验证记录');
217
- }
218
-
219
- /** Read each owning version's local records along the explicit predecessor graph. */
220
- export function getAcrossVersionVerificationHistory(currentVersionDir, inputRef) {
221
- const root = resolveIntentRef(currentVersionDir, inputRef);
222
- const histories = [];
223
- const visited = new Set();
224
- const active = new Set();
225
-
226
- function walk(resolved) {
227
- if (active.has(resolved.ref)) throw new Error(`Intent lineage 存在循环: ${[...active, resolved.ref].join(' -> ')}`);
228
- if (visited.has(resolved.ref)) return;
229
- active.add(resolved.ref);
230
- const intent = getIntent(resolved.versionDir, resolved.intentId);
231
- const local = getVerificationHistory(join(resolved.versionDir, 'verifications'), resolved.intentId);
232
- histories.push({
233
- ref: resolved.ref,
234
- source_version: resolved.version,
235
- source_intent: resolved.intentId,
236
- source_intent_id: resolved.intentId,
237
- records: (local?.records || []).map((record) => ({
238
- ...record,
239
- source_version: resolved.version,
240
- source_intent: resolved.intentId,
241
- source_intent_id: resolved.intentId,
242
- })),
243
- });
244
- for (const predecessor of intent.lineage?.predecessors || []) {
245
- walk(resolveIntentRef(currentVersionDir, formatIntentRef(predecessor.version, predecessor.intent_id)));
246
- }
247
- active.delete(resolved.ref);
248
- visited.add(resolved.ref);
249
- }
250
-
251
- walk(root);
252
- return { intent_ref: root.ref, across_versions: true, histories };
253
- }
254
-
255
- /**
256
- * 快捷创建验证记录——Agent 不用手动构造完整 JSON。
257
- * 快捷记录只能用于低风险的结构化核验;passed 仍必须显式声明独立验证来源。
258
- * @param {string} versionDir — 当前 .loom/v{N}/ 目录
259
- * @param {string} verificationsDir — verifications/ 目录路径
260
- * @param {string} intentId — 如 "INT-001"
261
- * @param {string} verdict — 'passed' | 'deviated' | 'blocked'
262
- * @param {string} summary — 验证摘要(也会作为所有适用维度的 evidence)
263
- * @param {object} [extras]
264
- * @param {string} [extras.reproduction_command] — 复现命令
265
- * @param {string} [extras.quality_proof_ref] — Quality Proof 证据引用
266
- * @param {string} [extras.preservation_evidence] — 对既有状态守恒的独立证据
267
- * @param {string} [extras.deviation_detail] — 偏离说明(deviated 时)
268
- * @returns {{ filePath: string, round: number, deviated_count: number, should_escalate: boolean }}
269
- */
270
- export function createQuickVerification(versionDir, verificationsDir, intentId, verdict, summary, extras = {}) {
271
- const timestamp = new Date().toISOString();
272
- const intent = getIntent(versionDir, intentId);
273
- // 用 summary 填充适用维度的 evidence——快捷命令不要求 Agent 逐维度写
274
- const dimensions = {};
275
- for (const dim of getRequiredDimensions(intent)) {
276
- dimensions[dim] = {
277
- verdict,
278
- evidence: dim === 'preservation_achievement'
279
- ? (extras.preservation_evidence || summary)
280
- : summary,
281
- };
282
- }
283
- if (extras.quality_proof_ref && dimensions.quality_achievement) {
284
- dimensions.quality_achievement.quality_proof_ref = extras.quality_proof_ref;
285
- }
286
- return writeVerification(versionDir, verificationsDir, {
287
- intent_id: intentId,
288
- verdict,
289
- timestamp,
290
- summary,
291
- dimensions,
292
- reproduction_command: extras.reproduction_command || null,
293
- verification_provenance: extras.verification_provenance || null,
294
- deviation_detail: extras.deviation_detail || null,
295
- });
296
- }
297
-
298
- /**
299
- * 返回所有待验证的 Intent(有实现产物但还没验证记录的)。
300
- * 需要传入 Intent Map 来判断哪些 Intent 是 in_progress。
301
- */
302
- export function getPendingVerifications(versionDir, verificationsDir) {
303
- const intentMap = readJsonFile(join(versionDir, '04_INTENT_MAP.json'), 'Intent Map');
304
- const pending = [];
305
- for (const [id, intent] of Object.entries(intentMap.intents)) {
306
- if (intent.status === 'in_progress' || intent.status === 'needs_review') {
307
- const history = getVerificationHistory(verificationsDir, id);
308
- if (!hasCurrentPassedVerification(intent, history)) pending.push(id);
309
- }
310
- }
311
- return pending;
312
- }
313
-
314
- /** Missing Intent revisions are revision 1 without mutating the map. */
315
- export function getEffectiveIntentRevision(intent) {
316
- return intent.revision ?? 1;
317
- }
318
-
319
- /**
320
- * Legacy records count as revision 1 only while the Intent itself is legacy.
321
- * Once revision is explicit, an untagged record cannot prove freshness.
322
- */
323
- export function getVerificationIntentRevision(intent, record) {
324
- if (Number.isInteger(record?.intent_revision) && record.intent_revision >= 1) {
325
- return record.intent_revision;
326
- }
327
- return hasLegacyIntentRevision(intent) ? 1 : null;
328
- }
329
-
330
- export function isVerificationCurrent(intent, record) {
331
- return getVerificationIntentRevision(intent, record) === getEffectiveIntentRevision(intent)
332
- && getVerificationEpoch(intent, record) === getEffectiveVerificationEpoch(intent);
333
- }
334
-
335
- export function getVerificationEpoch(intent, record) {
336
- if (Number.isInteger(record?.verification_epoch) && record.verification_epoch >= 1) {
337
- return record.verification_epoch;
338
- }
339
- return intent?.verification_epoch === undefined ? 1 : null;
340
- }
341
-
342
- export function getLatestPassedVerification(history) {
343
- if (!Array.isArray(history?.records)) return null;
344
- return [...history.records].reverse().find((record) => record.verdict === 'passed') ?? null;
345
- }
346
-
347
- export function hasCurrentPassedVerification(intent, history) {
348
- const latest = history?.records?.[history.records.length - 1];
349
- return latest?.verdict === 'passed' && isVerificationCurrent(intent, latest);
350
- }
351
-
352
- /**
353
- * 列出所有验证记录文件。
354
- * 只列出正式验证记录——文件名匹配 INT-XXX 格式且内容含 records 字段。
355
- * 过滤掉用户写入的临时输入文件(如 INT-001.verify.json、_tmp_*.json)。
356
- */
357
- export function listVerifications(verificationsDir) {
358
- if (!existsSync(verificationsDir)) return [];
359
- return readdirSync(verificationsDir)
360
- .filter((f) => f.endsWith('.json'))
361
- .filter((f) => /^INT-\d+\.json$/.test(f))
362
- .map((f) => f.replace('.json', ''));
363
- }
364
-
365
- /**
366
- * 获取某 Intent 的验证契约(acceptance 字段的解析结果)。
367
- * 如果 acceptance 是内联定义,直接返回。
368
- * 如果是引用(如 "see 05_VERIFICATION.md#int-001"),解析引用并返回对应章节内容。
369
- * @param {string} versionDir — .loom/v{N}/ 目录
370
- * @param {string} intentId — Intent ID
371
- * @returns {string} 验收契约内容
372
- */
373
- export function getVerificationContract(versionDir, intentId) {
374
- const intentMap = readJsonFile(join(versionDir, '04_INTENT_MAP.json'), 'Intent Map');
375
- if (!(intentId in intentMap.intents)) {
376
- throw new Error(`Intent 不存在: ${intentId}`);
377
- }
378
- const acceptance = intentMap.intents[intentId].acceptance;
379
-
380
- // 检测是否是引用格式: "see 05_VERIFICATION.md#section" 或 "05_VERIFICATION.md#section"
381
- const refMatch = acceptance.match(/(?:see\s+)?(\w+\.md)#([\w-]+)/i);
382
- if (refMatch) {
383
- const [, file, section] = refMatch;
384
- const filePath = join(versionDir, file);
385
- if (!existsSync(filePath)) {
386
- throw new Error(`验证契约引用的文件不存在: ${filePath}`);
387
- }
388
- const content = readFileSync(filePath, 'utf-8');
389
- return extractMdSection(content, section, '验证契约');
390
- }
391
-
392
- // 内联定义,直接返回
393
- return acceptance;
394
- }