@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
@@ -0,0 +1,214 @@
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
+ }
@@ -3,18 +3,23 @@
3
3
  // 这个库按锚点提取对应章节,不返回整个文件。
4
4
  // 另含灵感来源校验——防止 Weaver 从训练数据"背"几个名字就交差。
5
5
 
6
- import { readFileSync, existsSync, readdirSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- import { extractMdSection } from './shared/md-utils.js';
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';
9
10
 
10
11
  /**
11
12
  * 解析锚点字符串。
12
13
  * @param {string} anchor — "FILE.md#section" 或 "FILE.md"
13
14
  * @returns {{ file: string, section: string|null }}
14
15
  */
15
- export function parseAnchor(anchor) {
16
- const [file, section] = anchor.split('#');
17
- return { file: file.trim(), section: section ? section.trim() : null };
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 };
18
23
  }
19
24
 
20
25
  /**
@@ -38,25 +43,167 @@ export function getPhilosophy(philosophyDir, anchor) {
38
43
  /**
39
44
  * 列出哲学目录下所有 .md 文件名。
40
45
  */
41
- export function listPhilosophyFiles(philosophyDir) {
46
+ export function listPhilosophyFiles(philosophyDir) {
42
47
  if (!existsSync(philosophyDir)) {
43
48
  throw new Error(`哲学目录不存在: ${philosophyDir}`);
44
49
  }
45
50
  const dir = readdirSync(philosophyDir);
46
51
  return dir.filter((f) => f.endsWith('.md'));
47
- }
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
+ }
48
201
 
49
202
  // ─── 灵感来源校验 ───────────────────────────────────────
50
203
  // 防止 Weaver 从训练数据"背"几个名字就交差。
51
- // 校验规则:
52
- // 1. 至少 3 个独立源
53
- // 2. 至少 2 个非 Wikipedia 链接(Wikipedia 是常识入口,不是深度源)
54
- // 3. 每个源必须有"为什么选它"的理由(萃取/理由/为什么 等关键词)
55
- // 4. 源不能全是同一类型(如全是 Wikipedia、全是博客)
56
-
57
- const MIN_SOURCES = 3;
58
- const MIN_NON_WIKI = 2;
59
- const REASON_KEYWORDS = ['萃取', '理由', '为什么', '因为', '启发', '借鉴', '参考理由', '选取理由', '转译'];
204
+ // 校验原则:只检查证据能否追溯,以及为何影响了项目判断。
205
+ // 来源数量与类型不设固定配额;一个直接证据可以胜过十个装饰性链接。
206
+ const REASON_KEYWORDS = ['萃取', '理由', '为什么', '因为', '启发', '借鉴', '参考理由', '选取理由', '转译'];
60
207
 
61
208
  /**
62
209
  * 从哲学文档内容中提取"灵感来源"章节的条目。
@@ -68,7 +215,7 @@ const REASON_KEYWORDS = ['萃取', '理由', '为什么', '因为', '启发', '
68
215
  function parseInspirationSources(content) {
69
216
  // 匹配 "## 灵感来源" 或 "## Inspiration Sources" 等章节
70
217
  // 支持 {#anchor} 后缀和多种标题变体
71
- const sectionMatch = content.match(/^##\s+(?:灵感来源|Inspiration|参考来源|References?|参考文献|参考资料|Sources|Bibliography)/m);
218
+ const sectionMatch = content.match(/^##\s+(?:灵感来源|Inspiration|参考来源|References?|参考文献|参考资料|Sources|Bibliography)/m);
72
219
  if (!sectionMatch) return [];
73
220
 
74
221
  const startIdx = sectionMatch.index + sectionMatch[0].length;
@@ -115,16 +262,9 @@ function parseInspirationSources(content) {
115
262
  * 检查哲学文档是否有"灵感来源"章节(不管有没有条目)。
116
263
  * 用来区分"没有章节"和"有章节但没条目"两种情况。
117
264
  */
118
- function hasInspirationSection(content) {
119
- return /^##\s+(?:灵感来源|Inspiration|参考来源|References?|参考文献|参考资料|Sources|Bibliography)/m.test(content);
120
- }
121
-
122
- /**
123
- * 判断 URL 是否为 Wikipedia 链接。
124
- */
125
- function isWikipediaUrl(url) {
126
- return /wikipedia\.org/i.test(url);
127
- }
265
+ function hasInspirationSection(content) {
266
+ return /^##\s+(?:证据地图|Evidence Map|灵感来源|Inspiration|参考来源|References?|参考文献|参考资料|Sources|Bibliography)/m.test(content);
267
+ }
128
268
 
129
269
  /**
130
270
  * 校验哲学文档的灵感来源质量。
@@ -145,23 +285,7 @@ export function validateInspirationSources(philosophyDir) {
145
285
 
146
286
  allSources.push({ file, sources });
147
287
 
148
- // 校验每个文件的灵感来源
149
- if (sources.length < MIN_SOURCES) {
150
- issues.push({
151
- severity: 'high',
152
- msg: `${file}: 灵感来源仅 ${sources.length} 条,要求至少 ${MIN_SOURCES} 条。可能从训练数据"背"了几个名字就交差。`,
153
- });
154
- }
155
-
156
- const nonWikiUrls = sources.filter((s) => s.urls.length > 0 && !s.urls.every(isWikipediaUrl));
157
- if (nonWikiUrls.length < MIN_NON_WIKI) {
158
- issues.push({
159
- severity: 'high',
160
- msg: `${file}: 非 Wikipedia 链接仅 ${nonWikiUrls.length} 个,要求至少 ${MIN_NON_WIKI} 个。Wikipedia 是常识入口,不是深度源——需要原著、论文、工程博客、标准文档等。`,
161
- });
162
- }
163
-
164
- for (const src of sources) {
288
+ for (const src of sources) {
165
289
  if (!src.hasReason) {
166
290
  issues.push({
167
291
  severity: 'medium',
@@ -177,18 +301,6 @@ export function validateInspirationSources(philosophyDir) {
177
301
  }
178
302
  }
179
303
 
180
- // 全局校验:所有文件的灵感来源加起来,源类型不能单一
181
- const totalUrls = allSources.flatMap((s) => s.sources).flatMap((s) => s.urls);
182
- if (totalUrls.length > 0) {
183
- const wikiCount = totalUrls.filter(isWikipediaUrl).length;
184
- if (wikiCount / totalUrls.length > 0.7) {
185
- issues.push({
186
- severity: 'medium',
187
- msg: `全部灵感来源中 Wikipedia 占比 ${Math.round((wikiCount / totalUrls.length) * 100)}%——源类型过于单一。需要原著、论文、标准文档、工程博客等多元源。`,
188
- });
189
- }
190
- }
191
-
192
304
  // 如果没有任何文件提取到灵感来源条目
193
305
  if (allSources.length === 0) {
194
306
  // 区分两种情况:完全没有章节 vs 有章节但没条目
@@ -201,108 +313,19 @@ export function validateInspirationSources(philosophyDir) {
201
313
  if (filesWithSection.length > 0) {
202
314
  issues.push({
203
315
  severity: 'high',
204
- msg: `${filesWithSection.join(', ')} 有"灵感来源"章节但没有可识别的条目。章节里可能是模板占位符。需要 Weaver 真正走搜索漏斗,填入至少 ${MIN_SOURCES} 个源(- **源名** — 理由。来源:URL 格式)。`,
316
+ msg: `${filesWithSection.join(', ')} 有证据章节但没有可识别的条目。请只填入实际改变了原则或取舍的来源,并写明选择理由与可追溯位置。`,
205
317
  });
206
318
  } else {
207
319
  issues.push({
208
320
  severity: 'high',
209
- msg: '所有哲学文档都没有"灵感来源"章节。PHILOSOPHY_WEAVER.md 要求哲学文档必须包含灵感来源(参考了哪些机构、人物、流派——附 URL 和理由)。支持的标题:灵感来源 / Inspiration / 参考来源 / References / 参考文献 / 参考资料 / Sources / Bibliography。',
321
+ msg: '所有哲学文档都没有证据地图或灵感来源。Doctrine 必须说明哪些项目事实或外部资料实际改变了原则与取舍,并提供理由和可追溯位置。',
210
322
  });
211
323
  }
212
324
  }
213
325
 
214
- return {
215
- passed: issues.length === 0,
216
- issues,
217
- sources: allSources,
218
- };
219
- }
220
-
221
- // ─── 实现部分清单校验 ───────────────────────────────────
222
- // 检查哲学文档是否包含"实现部分清单"——Weaver 是否走了拆解流程。
223
- // PHILOSOPHY_WEAVER.md Step 2 要求产出"实现部分清单"。
224
-
225
- /**
226
- * 校验哲学文档是否包含实现部分拆解清单。
227
- * Weaver 按 PART_DECOMPOSITION.md 拆解后,必须在哲学文档里显式列出拆解出的部分。
228
- * @param {string} philosophyDir — 00_PHILOSOPHY/ 目录路径
229
- * @returns {{ passed: boolean, issues: Array<{severity: string, msg: string}>, parts: string[] }}
230
- */
231
- export function validatePartDecomposition(philosophyDir) {
232
- const issues = [];
233
- const parts = [];
234
-
235
- const files = listPhilosophyFiles(philosophyDir);
236
-
237
- // 搜索"实现部分"相关章节——支持 {#anchor} 后缀和多种标题变体
238
- const PART_SECTION_PATTERNS = [
239
- /^##\s+实现部分清单/m,
240
- /^##\s+部分拆解/m,
241
- /^##\s+实现部分/m,
242
- /^##\s+Part Decomposition/m,
243
- /^##\s+Implementation Parts/m,
244
- /^##\s+拆解出的部分/m,
245
- /^##\s+Implementation Decomposition/m,
246
- /^##\s+Parts? /m,
247
- ];
248
-
249
- // 搜索部分条目——支持无序列表、有序列表、树形符号
250
- const PART_ITEM_PATTERNS = [
251
- /^\s*[-*]\s+\*\*(.+?)\*\*/gm, // - **CLI 交互设计**
252
- /^\s*\d+\.\s+\*\*(.+?)\*\*/gm, // 1. **CLI 交互设计**
253
- /^\s*\d+\.\s+(.+)/gm, // 1. CLI 交互设计
254
- /^\s*├──\s+(.+)/gm, // ├── CLI 交互设计
255
- /^\s*└──\s+(.+)/gm, // └── 产物设计
256
- ];
257
-
258
- let foundSection = false;
259
-
260
- for (const file of files) {
261
- const content = readFileSync(join(philosophyDir, file), 'utf-8');
262
-
263
- // 检查是否有实现部分章节
264
- for (const pattern of PART_SECTION_PATTERNS) {
265
- if (pattern.test(content)) {
266
- foundSection = true;
267
- // 提取该章节的部分条目
268
- const sectionMatch = content.match(pattern);
269
- if (sectionMatch) {
270
- const startIdx = sectionMatch.index + sectionMatch[0].length;
271
- const nextSection = content.slice(startIdx).match(/\n##\s/m);
272
- const sectionText = nextSection
273
- ? content.slice(startIdx, startIdx + nextSection.index)
274
- : content.slice(startIdx);
275
-
276
- for (const itemPattern of PART_ITEM_PATTERNS) {
277
- const matches = [...sectionText.matchAll(itemPattern)];
278
- for (const m of matches) {
279
- const partName = m[1].trim().replace(/[—\-–].*$/, '').trim();
280
- if (partName && !parts.includes(partName)) {
281
- parts.push(partName);
282
- }
283
- }
284
- }
285
- }
286
- break;
287
- }
288
- }
289
- }
290
-
291
- if (!foundSection) {
292
- issues.push({
293
- severity: 'high',
294
- msg: '哲学文档没有"实现部分清单"章节。PHILOSOPHY_WEAVER.md Step 2 要求按 PART_DECOMPOSITION.md 拆解实现部分,并在哲学文档中显式列出。支持的标题:实现部分清单 / 部分拆解 / 实现部分 / Part Decomposition / Implementation Parts / 拆解出的部分。',
295
- });
296
- } else if (parts.length < 2) {
297
- issues.push({
298
- severity: 'medium',
299
- msg: `找到"实现部分清单"章节但仅识别到 ${parts.length} 个部分。可能章节里是模板占位符,或条目格式不被识别(用 - **部分名** 或 ├── 部分名 格式)。PART_DECOMPOSITION.md 建议小项目 3-5 个部分,大项目 6-10 个。`,
300
- });
301
- }
302
-
303
- return {
304
- passed: issues.length === 0,
305
- issues,
306
- parts,
307
- };
308
- }
326
+ return {
327
+ passed: issues.length === 0,
328
+ issues,
329
+ sources: allSources,
330
+ };
331
+ }
@@ -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