@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
package/cli/src/patch.js DELETED
@@ -1,214 +0,0 @@
1
- // patch — authoritative Patch ledger and deterministic Markdown projection.
2
- // Validation is structural only: verification commands are recorded, never executed.
3
-
4
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
5
- import { isAbsolute, join } from 'node:path';
6
- import { loadIntentMap } from './intent-map.js';
7
-
8
- const JSON_FILE = '06_CHANGELOG.json';
9
- const MARKDOWN_FILE = '06_CHANGELOG.md';
10
- const PATCH_ID = /^PATCH-(\d{3,})$/;
11
- const VERIFICATION_RESULTS = new Set(['passed', 'failed', 'skipped']);
12
-
13
- export function createEmptyChangelog() {
14
- return {
15
- _meta: { schema_version: '1.0', source: JSON_FILE },
16
- patches: [],
17
- };
18
- }
19
-
20
- function requireText(value, field) {
21
- if (typeof value !== 'string' || value.trim() === '') {
22
- throw new Error(`Patch ${field} 必须是非空字符串`);
23
- }
24
- }
25
-
26
- function validateSafeFile(file, index) {
27
- requireText(file, `files[${index}]`);
28
- const normalized = file.replace(/\\/g, '/');
29
- if (isAbsolute(file) || /^[A-Za-z]:/.test(normalized) || normalized === '.' || normalized.startsWith('/') || normalized.split('/').includes('..')) {
30
- throw new Error(`Patch files[${index}] 必须是安全的项目相对路径: ${file}`);
31
- }
32
- }
33
-
34
- function validateRecord(record, intents, { stored = false } = {}) {
35
- if (!record || typeof record !== 'object' || Array.isArray(record)) {
36
- throw new Error('Patch 记录必须是 JSON object');
37
- }
38
- requireText(record.summary, 'summary');
39
- requireText(record.reason, 'reason');
40
-
41
- if (!Array.isArray(record.files) || record.files.length === 0) {
42
- throw new Error('Patch files 必须是非空数组');
43
- }
44
- record.files.forEach(validateSafeFile);
45
-
46
- if (record.affects !== undefined) {
47
- if (!Array.isArray(record.affects)) throw new Error('Patch affects 必须是 Intent ID 数组');
48
- for (const id of record.affects) {
49
- requireText(id, 'affects[]');
50
- if (!intents.has(id)) throw new Error(`Patch affects 引用了不存在的 Intent: ${id}`);
51
- }
52
- }
53
-
54
- if (!Array.isArray(record.verification) || record.verification.length === 0) {
55
- throw new Error('Patch verification 必须是非空数组');
56
- }
57
- let hasPassed = false;
58
- record.verification.forEach((item, index) => {
59
- if (!item || typeof item !== 'object' || Array.isArray(item)) {
60
- throw new Error(`Patch verification[${index}] 必须是 object`);
61
- }
62
- if (item.command === undefined && item.method === undefined) {
63
- throw new Error(`Patch verification[${index}] 必须提供 command 或 method`);
64
- }
65
- if (item.command !== undefined) requireText(item.command, `verification[${index}].command`);
66
- if (item.method !== undefined) requireText(item.method, `verification[${index}].method`);
67
- if (item.evidence !== undefined) requireText(item.evidence, `verification[${index}].evidence`);
68
- if (!VERIFICATION_RESULTS.has(item.result)) {
69
- throw new Error(`Patch verification[${index}].result 必须是 passed | failed | skipped`);
70
- }
71
- if (item.result === 'passed') hasPassed = true;
72
- });
73
- if (!hasPassed) throw new Error('Patch verification 至少需要一个 passed 结果');
74
-
75
- if (stored) {
76
- if (!PATCH_ID.test(record.id)) throw new Error(`Patch id 格式无效: ${record.id ?? '(missing)'}`);
77
- if (typeof record.timestamp !== 'string' || Number.isNaN(Date.parse(record.timestamp))) {
78
- throw new Error(`Patch timestamp 无效: ${record.timestamp ?? '(missing)'}`);
79
- }
80
- } else if (record.id !== undefined || record.timestamp !== undefined) {
81
- throw new Error('Patch id 和 timestamp 由 CLI 分配,请勿在输入中提供');
82
- }
83
- }
84
-
85
- function loadIntents(versionDir) {
86
- return loadIntentMap(versionDir).intents;
87
- }
88
-
89
- function loadIntentIds(versionDir) {
90
- return new Set(Object.keys(loadIntents(versionDir)));
91
- }
92
-
93
- export function renderChangelogMarkdown(changelog) {
94
- const lines = [
95
- '<!-- GENERATED FILE. DO NOT EDIT. Source: 06_CHANGELOG.json -->',
96
- '# Patch Changelog',
97
- '',
98
- '> Generated deterministically from `06_CHANGELOG.json`. Edit the JSON through `loom patch record`.',
99
- '',
100
- ];
101
- if (changelog.patches.length === 0) {
102
- lines.push('_No patches recorded._', '');
103
- return lines.join('\n');
104
- }
105
- for (const patch of changelog.patches) {
106
- lines.push(`## ${patch.id} - ${patch.summary}`, '', `- Timestamp: ${patch.timestamp}`, `- Reason: ${patch.reason}`);
107
- lines.push(`- Affects: ${(patch.affects || []).map((id) => `\`${id}\``).join(', ') || 'none'}`);
108
- lines.push('- Files:', ...patch.files.map((file) => ` - \`${file}\``), '- Verification:');
109
- for (const item of patch.verification) {
110
- const verification = item.command ? `\`${item.command}\`` : item.method;
111
- lines.push(` - ${item.result}: ${verification}`);
112
- if (item.evidence) lines.push(` - Evidence: ${item.evidence}`);
113
- }
114
- lines.push('');
115
- }
116
- return lines.join('\n');
117
- }
118
-
119
- function loadChangelog(versionDir) {
120
- const path = join(versionDir, JSON_FILE);
121
- if (!existsSync(path)) throw new Error(`Patch changelog 不存在: ${path}`);
122
- try {
123
- return JSON.parse(readFileSync(path, 'utf-8'));
124
- } catch (error) {
125
- throw new Error(`Patch changelog JSON 解析失败: ${path}\n原因: ${error.message}`);
126
- }
127
- }
128
-
129
- function validateChangelog(versionDir, changelog) {
130
- if (!changelog || typeof changelog !== 'object' || Array.isArray(changelog)) {
131
- throw new Error('Patch changelog 必须是 JSON object');
132
- }
133
- if (changelog._meta?.schema_version !== '1.0' || changelog._meta?.source !== JSON_FILE) {
134
- throw new Error('Patch changelog _meta 无效:schema_version 必须为 1.0,source 必须为 06_CHANGELOG.json');
135
- }
136
- if (!Array.isArray(changelog.patches)) throw new Error('Patch changelog patches 必须是数组');
137
- const intents = loadIntentIds(versionDir);
138
- const ids = new Set();
139
- changelog.patches.forEach((record, index) => {
140
- validateRecord(record, intents, { stored: true });
141
- const expected = `PATCH-${String(index + 1).padStart(3, '0')}`;
142
- if (record.id !== expected) throw new Error(`Patch id 序列无效: 期望 ${expected},实际 ${record.id}`);
143
- if (ids.has(record.id)) throw new Error(`Patch id 重复: ${record.id}`);
144
- ids.add(record.id);
145
- });
146
- }
147
-
148
- function writeChangelog(versionDir, changelog) {
149
- writeFileSync(join(versionDir, JSON_FILE), `${JSON.stringify(changelog, null, 2)}\n`, 'utf-8');
150
- writeFileSync(join(versionDir, MARKDOWN_FILE), renderChangelogMarkdown(changelog), 'utf-8');
151
- }
152
-
153
- export function scaffoldChangelog(versionDir) {
154
- const jsonPath = join(versionDir, JSON_FILE);
155
- const markdownPath = join(versionDir, MARKDOWN_FILE);
156
- if (!existsSync(jsonPath) && existsSync(markdownPath)) {
157
- const legacyMarkdown = readFileSync(markdownPath, 'utf-8');
158
- if (!legacyMarkdown.startsWith('<!-- GENERATED FILE. DO NOT EDIT. Source: 06_CHANGELOG.json -->')) {
159
- throw new Error(
160
- `检测到手工维护的旧版 ${MARKDOWN_FILE},拒绝用空 Patch ledger 覆盖。\n` +
161
- `请先把历史条目迁移到 ${JSON_FILE},或备份并删除旧 Markdown 后重试。`
162
- );
163
- }
164
- }
165
- const changelog = existsSync(jsonPath) ? loadChangelog(versionDir) : createEmptyChangelog();
166
- if (!existsSync(jsonPath)) writeFileSync(jsonPath, `${JSON.stringify(changelog, null, 2)}\n`, 'utf-8');
167
- writeFileSync(markdownPath, renderChangelogMarkdown(changelog), 'utf-8');
168
- }
169
-
170
- export function recordPatch(versionDir, input, now = new Date()) {
171
- const changelog = loadChangelog(versionDir);
172
- validateChangelog(versionDir, changelog);
173
- const intents = loadIntents(versionDir);
174
- const unfinished = Object.values(intents).filter((intent) => intent.status !== 'completed');
175
- if (unfinished.length > 0) {
176
- throw new Error(`Patch 只能在全部 Intent 完成后记录;尚未完成: ${unfinished.map((intent) => intent.id).join(', ')}`);
177
- }
178
- validateRecord(input, new Set(Object.keys(intents)));
179
- const record = {
180
- id: `PATCH-${String(changelog.patches.length + 1).padStart(3, '0')}`,
181
- timestamp: now.toISOString(),
182
- summary: input.summary.trim(),
183
- reason: input.reason.trim(),
184
- affects: input.affects || [],
185
- files: input.files.map((file) => file.replace(/\\/g, '/')),
186
- verification: input.verification,
187
- };
188
- changelog.patches.push(record);
189
- writeChangelog(versionDir, changelog);
190
- return record;
191
- }
192
-
193
- export function listPatches(versionDir) {
194
- const changelog = loadChangelog(versionDir);
195
- validateChangelog(versionDir, changelog);
196
- return changelog.patches;
197
- }
198
-
199
- export function getPatch(versionDir, id) {
200
- const patch = listPatches(versionDir).find((record) => record.id === id);
201
- if (!patch) throw new Error(`Patch 不存在: ${id}`);
202
- return patch;
203
- }
204
-
205
- export function validatePatches(versionDir) {
206
- const changelog = loadChangelog(versionDir);
207
- validateChangelog(versionDir, changelog);
208
- const markdownPath = join(versionDir, MARKDOWN_FILE);
209
- if (!existsSync(markdownPath)) throw new Error(`Patch Markdown 投影不存在: ${markdownPath}`);
210
- if (readFileSync(markdownPath, 'utf-8') !== renderChangelogMarkdown(changelog)) {
211
- throw new Error('06_CHANGELOG.md 不是 06_CHANGELOG.json 的最新确定性投影');
212
- }
213
- return { valid: true, patches: changelog.patches.length };
214
- }
@@ -1,331 +0,0 @@
1
- // philosophy.js — 按锚点加载哲学文档的特定章节
2
- // 哲学文档是 MD,锚点格式: "PRODUCT_PHILOSOPHY.md#core-belief"
3
- // 这个库按锚点提取对应章节,不返回整个文件。
4
- // 另含灵感来源校验——防止 Weaver 从训练数据"背"几个名字就交差。
5
-
6
- import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
7
- import { basename, join } from 'node:path';
8
- import { extractMdSection, readJsonFile } from './shared/md-utils.js';
9
- import { applyImpactReview, dependentClosure, validateImpactPartition, validateIntentMap } from './intent-map.js';
10
-
11
- /**
12
- * 解析锚点字符串。
13
- * @param {string} anchor — "FILE.md#section" 或 "FILE.md"
14
- * @returns {{ file: string, section: string|null }}
15
- */
16
- export function parseAnchor(anchor) {
17
- const [file, section] = anchor.split('#');
18
- const normalizedFile = file.trim();
19
- if (!normalizedFile || basename(normalizedFile) !== normalizedFile || !normalizedFile.endsWith('.md')) {
20
- throw new Error(`哲学锚点文件名非法: ${normalizedFile || '(empty)'}`);
21
- }
22
- return { file: normalizedFile, section: section ? section.trim() : null };
23
- }
24
-
25
- /**
26
- * 按锚点加载哲学文档内容。
27
- * @param {string} philosophyDir — 00_PHILOSOPHY/ 目录路径
28
- * @param {string} anchor — "PRODUCT_PHILOSOPHY.md#core-belief"
29
- * @returns {string} MD 文本(章节或整个文件)
30
- */
31
- export function getPhilosophy(philosophyDir, anchor) {
32
- const { file, section } = parseAnchor(anchor);
33
- const filePath = join(philosophyDir, file);
34
-
35
- if (!existsSync(filePath)) {
36
- throw new Error(`哲学文档不存在: ${filePath}`);
37
- }
38
-
39
- const content = readFileSync(filePath, 'utf-8');
40
- return extractMdSection(content, section, '哲学');
41
- }
42
-
43
- /**
44
- * 列出哲学目录下所有 .md 文件名。
45
- */
46
- export function listPhilosophyFiles(philosophyDir) {
47
- if (!existsSync(philosophyDir)) {
48
- throw new Error(`哲学目录不存在: ${philosophyDir}`);
49
- }
50
- const dir = readdirSync(philosophyDir);
51
- return dir.filter((f) => f.endsWith('.md'));
52
- }
53
-
54
- const REVISION_CLASSIFICATIONS = ['clarification', 'minor', 'major'];
55
-
56
- function normalizeAnchor(anchor) {
57
- if (typeof anchor !== 'string' || anchor.trim() === '') throw new Error('哲学锚点必须是非空文本');
58
- const parts = anchor.trim().split('#');
59
- if (parts.length > 2 || !parts[0].trim()) throw new Error(`哲学锚点格式非法: ${anchor}`);
60
- const { file, section } = parseAnchor(anchor.trim());
61
- return section ? `${file}#${section}` : file;
62
- }
63
-
64
- function impactEntry(intent) {
65
- return {
66
- id: intent.id,
67
- title: intent.title || '',
68
- status: intent.status,
69
- revision: intent.revision ?? 1,
70
- acceptance: intent.acceptance,
71
- };
72
- }
73
-
74
- /** Resolve an anchor and report direct references plus their dependent closure without mutation. */
75
- export function assessPhilosophyImpact(versionDir, anchor) {
76
- const resolvedAnchor = normalizeAnchor(anchor);
77
- getPhilosophy(join(versionDir, '00_PHILOSOPHY'), resolvedAnchor);
78
- const data = readJsonFile(join(versionDir, '04_INTENT_MAP.json'), 'Intent Map');
79
- validateIntentMap(data);
80
- const directIds = Object.values(data.intents)
81
- .filter((intent) => (intent.philosophy_anchors || []).some((item) => normalizeAnchor(item) === resolvedAnchor))
82
- .map((intent) => intent.id);
83
- const directSet = new Set(directIds);
84
- const impacted = new Set(directIds);
85
- for (const id of directIds) {
86
- for (const dependent of dependentClosure(data.intents, id).all) impacted.add(dependent);
87
- }
88
- const ordered = (data.topo_order || Object.keys(data.intents)).filter((id) => impacted.has(id));
89
- const transitiveIds = ordered.filter((id) => !directSet.has(id));
90
- const describe = (ids) => ids.map((id) => impactEntry(data.intents[id]));
91
- return {
92
- anchor: resolvedAnchor,
93
- direct: describe(directIds),
94
- transitive: describe(transitiveIds),
95
- impacted: describe(ordered),
96
- impacted_ids: ordered,
97
- };
98
- }
99
-
100
- function nextRevisionAdr(decisionsDir) {
101
- if (!existsSync(decisionsDir)) return 'PHIL-REV-001.md';
102
- const numbers = readdirSync(decisionsDir)
103
- .map((name) => name.match(/^PHIL-REV-(\d{3,})\.md$/)?.[1])
104
- .filter(Boolean)
105
- .map(Number);
106
- return `PHIL-REV-${String((numbers.length ? Math.max(...numbers) : 0) + 1).padStart(3, '0')}.md`;
107
- }
108
-
109
- function revisionGuidance(anchor, classification, reason, impactedIds) {
110
- const escapedReason = reason.replace(/"/g, '\\"');
111
- const base = `loom philosophy revise ${anchor} --classification ${classification} --reason "${escapedReason}" --confirm`;
112
- if (classification === 'clarification') {
113
- return impactedIds.length ? `${base} --unaffected ${impactedIds.join(',')}` : base;
114
- }
115
- if (!impactedIds.length) return base;
116
- return `${base} --review <review IDs from ${impactedIds.join(',')}> --unaffected <remaining IDs from ${impactedIds.join(',')}>`;
117
- }
118
-
119
- /** Assess or confirm a philosophy revision while leaving philosophy prose to Weaver/the user. */
120
- export function revisePhilosophy(versionDir, anchor, options) {
121
- if (!REVISION_CLASSIFICATIONS.includes(options.classification)) {
122
- throw new Error(`--classification 非法: ${options.classification || '(missing)'}(合法: ${REVISION_CLASSIFICATIONS.join('|')})`);
123
- }
124
- if (typeof options.reason !== 'string' || options.reason.trim() === '') throw new Error('--reason 必须是非空文本');
125
- const reason = options.reason.trim();
126
- const impact = assessPhilosophyImpact(versionDir, anchor);
127
-
128
- if (options.classification === 'major') {
129
- return {
130
- mode: 'assessment',
131
- mutated: false,
132
- classification: 'major',
133
- reason,
134
- ...impact,
135
- follow_up: { command: 'loom version new', guidance: 'Major philosophy revisions never mutate the current version. Create a new version, then have Weaver weave its philosophy.' },
136
- };
137
- }
138
- if (!options.confirm) {
139
- return {
140
- mode: 'assessment',
141
- mutated: false,
142
- classification: options.classification,
143
- reason,
144
- ...impact,
145
- required_partition: impact.impacted_ids,
146
- follow_up: {
147
- command: revisionGuidance(impact.anchor, options.classification, reason, impact.impacted_ids),
148
- guidance: options.classification === 'clarification'
149
- ? 'Clarification requires an empty --review set; classify every impacted Intent as --unaffected because all acceptance remains valid.'
150
- : 'Classify every impacted Intent exactly once between --review and --unaffected; omit an empty group.',
151
- },
152
- };
153
- }
154
-
155
- const review = options.review || [];
156
- const unaffected = options.unaffected || [];
157
- if (options.classification === 'clarification' && review.length) throw new Error('clarification 的 --review 必须为空;所有 acceptance 必须仍然有效');
158
- validateImpactPartition(impact.impacted_ids, review, unaffected);
159
- const reviewSet = new Set(review);
160
- const unaffectedSet = new Set(unaffected);
161
- const orderedReview = impact.impacted_ids.filter((id) => reviewSet.has(id));
162
- const orderedUnaffected = impact.impacted_ids.filter((id) => unaffectedSet.has(id));
163
-
164
- const mapPath = join(versionDir, '04_INTENT_MAP.json');
165
- const data = readJsonFile(mapPath, 'Intent Map');
166
- validateIntentMap(data);
167
- const { reviewed } = applyImpactReview(data, orderedReview, { incrementPassOnce: options.classification === 'minor' });
168
- const unchanged = orderedUnaffected.map((id) => ({ id, status_before: data.intents[id].status, status_after: data.intents[id].status }));
169
- validateIntentMap(data);
170
-
171
- const decisionsDir = join(versionDir, '03_DECISIONS');
172
- mkdirSync(decisionsDir, { recursive: true });
173
- const adrName = nextRevisionAdr(decisionsDir);
174
- const adrPath = join(decisionsDir, adrName);
175
- const timestamp = new Date().toISOString();
176
- const list = (ids) => ids.length ? ids.map((id) => `- ${id}`).join('\n') : '- None';
177
- const adr = `# Philosophy Revision ${adrName.slice(9, -3)}\n\n` +
178
- `- Timestamp: ${timestamp}\n- Anchor: ${impact.anchor}\n- Classification: ${options.classification}\n- Reason: ${reason}\n\n` +
179
- `## Reviewed\n\n${list(orderedReview)}\n\n## Unaffected\n\n${list(orderedUnaffected)}\n\n` +
180
- '## Prose Ownership\n\nPhilosophy prose is edited by Weaver/user separately. This ADR is an impact audit record, not a second philosophy truth source.\n';
181
- const token = `${process.pid}-${Date.now()}`;
182
- const mapTemp = `${mapPath}.tmp-${token}`;
183
- const adrTemp = `${adrPath}.tmp-${token}`;
184
- writeFileSync(mapTemp, `${JSON.stringify(data, null, 2)}\n`, 'utf-8');
185
- writeFileSync(adrTemp, adr, 'utf-8');
186
- try {
187
- renameSync(adrTemp, adrPath);
188
- try {
189
- renameSync(mapTemp, mapPath);
190
- } catch (error) {
191
- try { unlinkSync(adrPath); } catch {}
192
- throw error;
193
- }
194
- } catch (error) {
195
- try { unlinkSync(mapTemp); } catch {}
196
- try { unlinkSync(adrTemp); } catch {}
197
- throw error;
198
- }
199
- return { mode: 'confirmed', mutated: true, classification: options.classification, reason, ...impact, reviewed, unaffected: unchanged, audit_adr: `03_DECISIONS/${adrName}`, timestamp };
200
- }
201
-
202
- // ─── 灵感来源校验 ───────────────────────────────────────
203
- // 防止 Weaver 从训练数据"背"几个名字就交差。
204
- // 校验原则:只检查证据能否追溯,以及为何影响了项目判断。
205
- // 来源数量与类型不设固定配额;一个直接证据可以胜过十个装饰性链接。
206
- const REASON_KEYWORDS = ['萃取', '理由', '为什么', '因为', '启发', '借鉴', '参考理由', '选取理由', '转译'];
207
-
208
- /**
209
- * 从哲学文档内容中提取"灵感来源"章节的条目。
210
- * @param {string} content — MD 全文
211
- * @returns {Array<{ raw: string, name: string, urls: string[], hasReason: boolean }>}
212
- * 如果返回空数组,调用方需区分"没有章节"和"有章节但没条目"——
213
- * 用 hasInspirationSection() 单独判断。
214
- */
215
- function parseInspirationSources(content) {
216
- // 匹配 "## 灵感来源" 或 "## Inspiration Sources" 等章节
217
- // 支持 {#anchor} 后缀和多种标题变体
218
- const sectionMatch = content.match(/^##\s+(?:灵感来源|Inspiration|参考来源|References?|参考文献|参考资料|Sources|Bibliography)/m);
219
- if (!sectionMatch) return [];
220
-
221
- const startIdx = sectionMatch.index + sectionMatch[0].length;
222
- // 找到下一个 ## 或文件末尾
223
- const nextSection = content.slice(startIdx).match(/\n##\s/m);
224
- const sectionText = nextSection
225
- ? content.slice(startIdx, startIdx + nextSection.index)
226
- : content.slice(startIdx);
227
-
228
- // 解析每个 list item(- / * / 1. / 2. 等开头的无序或有序列表)
229
- const items = [];
230
- const lines = sectionText.split('\n');
231
- let currentItem = null;
232
-
233
- // 匹配 - xxx / * xxx / 1. xxx / 2. xxx 等
234
- const ITEM_RE = /^\s*(?:[-*]|\d+\.)\s+/;
235
- // URL 匹配:https:// / file:// / local:./path / local:/abs/path
236
- const URL_RE = /(?:https?:|file:)[\/]+[^\s))]+|local:[^\s))]+/g;
237
-
238
- for (const line of lines) {
239
- if (ITEM_RE.test(line)) {
240
- // 新条目
241
- if (currentItem) items.push(currentItem);
242
- const raw = line.replace(ITEM_RE, '').trim();
243
- const urls = [...raw.matchAll(URL_RE)].map((m) => m[0]);
244
- const name = raw.replace(/\*\*/g, '').split(/[((——]/)[0].trim();
245
- const hasReason = REASON_KEYWORDS.some((kw) => raw.includes(kw));
246
- currentItem = { raw, name, urls, hasReason };
247
- } else if (currentItem && line.trim()) {
248
- // 多行条目的续行
249
- currentItem.raw += ' ' + line.trim();
250
- const newUrls = [...line.matchAll(URL_RE)].map((m) => m[0]);
251
- currentItem.urls.push(...newUrls);
252
- if (REASON_KEYWORDS.some((kw) => line.includes(kw))) {
253
- currentItem.hasReason = true;
254
- }
255
- }
256
- }
257
- if (currentItem) items.push(currentItem);
258
- return items;
259
- }
260
-
261
- /**
262
- * 检查哲学文档是否有"灵感来源"章节(不管有没有条目)。
263
- * 用来区分"没有章节"和"有章节但没条目"两种情况。
264
- */
265
- function hasInspirationSection(content) {
266
- return /^##\s+(?:证据地图|Evidence Map|灵感来源|Inspiration|参考来源|References?|参考文献|参考资料|Sources|Bibliography)/m.test(content);
267
- }
268
-
269
- /**
270
- * 校验哲学文档的灵感来源质量。
271
- * @param {string} philosophyDir — 00_PHILOSOPHY/ 目录路径
272
- * @returns {{ passed: boolean, issues: Array<{severity: string, msg: string}>, sources: Array }}
273
- */
274
- export function validateInspirationSources(philosophyDir) {
275
- const issues = [];
276
- const allSources = [];
277
-
278
- // 扫描目录下所有 .md 文件,找"灵感来源"章节
279
- const files = listPhilosophyFiles(philosophyDir);
280
-
281
- for (const file of files) {
282
- const content = readFileSync(join(philosophyDir, file), 'utf-8');
283
- const sources = parseInspirationSources(content);
284
- if (sources.length === 0) continue;
285
-
286
- allSources.push({ file, sources });
287
-
288
- for (const src of sources) {
289
- if (!src.hasReason) {
290
- issues.push({
291
- severity: 'medium',
292
- msg: `${file}: 灵感来源 "${src.name}" 缺乏选取理由。必须说明"为什么选这个源"——萃取/转译/启发关系。`,
293
- });
294
- }
295
- if (src.urls.length === 0) {
296
- issues.push({
297
- severity: 'medium',
298
- msg: `${file}: 灵感来源 "${src.name}" 没有 URL。必须附可验证的来源链接。`,
299
- });
300
- }
301
- }
302
- }
303
-
304
- // 如果没有任何文件提取到灵感来源条目
305
- if (allSources.length === 0) {
306
- // 区分两种情况:完全没有章节 vs 有章节但没条目
307
- const filesWithSection = [];
308
- for (const file of files) {
309
- const content = readFileSync(join(philosophyDir, file), 'utf-8');
310
- if (hasInspirationSection(content)) filesWithSection.push(file);
311
- }
312
-
313
- if (filesWithSection.length > 0) {
314
- issues.push({
315
- severity: 'high',
316
- msg: `${filesWithSection.join(', ')} 有证据章节但没有可识别的条目。请只填入实际改变了原则或取舍的来源,并写明选择理由与可追溯位置。`,
317
- });
318
- } else {
319
- issues.push({
320
- severity: 'high',
321
- msg: '所有哲学文档都没有证据地图或灵感来源。Doctrine 必须说明哪些项目事实或外部资料实际改变了原则与取舍,并提供理由和可追溯位置。',
322
- });
323
- }
324
- }
325
-
326
- return {
327
- passed: issues.length === 0,
328
- issues,
329
- sources: allSources,
330
- };
331
- }
@@ -1,38 +0,0 @@
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
- }