@haaaiawd/loom 0.1.0 → 0.8.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.
@@ -0,0 +1,125 @@
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
+ }
@@ -0,0 +1,73 @@
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
+ }
package/cli/src/verify.js CHANGED
@@ -3,10 +3,19 @@
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
7
 
7
8
  /** 合法判定结果 */
8
9
  const VALID_VERDICTS = ['passed', 'deviated', 'blocked', 'pending_human'];
9
10
 
11
+ /** 四个必须覆盖的验证维度 */
12
+ const REQUIRED_DIMENSIONS = [
13
+ 'intent_fidelity',
14
+ 'philosophy_consistency',
15
+ 'baseline_compliance',
16
+ 'acceptance_achievement',
17
+ ];
18
+
10
19
  /**
11
20
  * 写入一条验证记录(追加模式——同一 Intent 多次验证保留完整历史)。
12
21
  * 文件格式: { intent_id, records: [{ round, verdict, timestamp, ... }] }
@@ -17,6 +26,7 @@ const VALID_VERDICTS = ['passed', 'deviated', 'blocked', 'pending_human'];
17
26
  * @param {string} record.timestamp — ISO 8601
18
27
  * @param {string} record.summary — 验证摘要
19
28
  * @param {object} record.dimensions — 四个维度的验证结果
29
+ * @param {string} [record.reproduction_command] — 复现验证的命令(如 "LLM_API_KEY=mock npm test")
20
30
  * @param {string} [record.deviation_detail] — 偏离说明(deviated 时)
21
31
  * @param {boolean} [record.reset_suggested] — 是否建议重置上下文
22
32
  * @returns {{ filePath: string, round: number, deviated_count: number, should_escalate: boolean }}
@@ -29,6 +39,36 @@ export function writeVerification(verificationsDir, record) {
29
39
  }
30
40
  if (!record.timestamp) errors.push('缺少 timestamp');
31
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} 缺失(四个维度必须全覆盖)`);
48
+ } else if (typeof v === 'string') {
49
+ errors.push(`dimensions.${dim} 是旧格式(枚举值),必须改成 { verdict, evidence } 对象`);
50
+ } else if (typeof v !== 'object' || v === null) {
51
+ errors.push(`dimensions.${dim} 必须是 { verdict, evidence } 对象`);
52
+ } else {
53
+ if (!VALID_VERDICTS.includes(v.verdict)) {
54
+ errors.push(`dimensions.${dim}.verdict 非法: "${v.verdict}" (合法: ${VALID_VERDICTS.join('|')})`);
55
+ }
56
+ if (!v.evidence || typeof v.evidence !== 'string' || v.evidence.trim() === '') {
57
+ errors.push(`dimensions.${dim}.evidence 缺失——必须给出具体证据,不能只写"合规"`);
58
+ } else {
59
+ // evidence 质量校验:长度 + 废话检测
60
+ const ev = v.evidence.trim();
61
+ if (ev.length < 10) {
62
+ errors.push(`dimensions.${dim}.evidence 太短(${ev.length}字符 < 10)——必须给出具体证据,不能只写"合规"`);
63
+ }
64
+ const NONSENSE = ['合规', '通过', 'OK', 'ok', '没问题', '符合要求', '已检查', 'pass', 'passed', 'done'];
65
+ if (NONSENSE.includes(ev)) {
66
+ errors.push(`dimensions.${dim}.evidence "${ev}" 是通用评价而非具体证据——必须写"对照了什么 + 在代码哪里看到/没看到"`);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
32
72
  if (errors.length > 0) {
33
73
  throw new Error(`验证记录校验失败:\n - ${errors.join('\n - ')}`);
34
74
  }
@@ -38,15 +78,28 @@ export function writeVerification(verificationsDir, record) {
38
78
  // 读取已有记录(如果有)
39
79
  let data;
40
80
  if (existsSync(filePath)) {
41
- data = JSON.parse(readFileSync(filePath, 'utf-8'));
81
+ data = readJsonFile(filePath, '验证记录');
82
+ // 结构校验:已有文件必须是 { intent_id, records: [] } 格式
83
+ if (!data || typeof data !== 'object' || !Array.isArray(data.records)) {
84
+ throw new Error(
85
+ `已有验证记录格式错误: ${filePath}\n` +
86
+ `期望格式: { intent_id, records: [...] }\n` +
87
+ `实际格式: ${JSON.stringify(data).slice(0, 200)}\n` +
88
+ `修复: 删除或修正该文件后重试。`
89
+ );
90
+ }
42
91
  } else {
43
92
  data = { intent_id: record.intent_id, records: [] };
44
93
  }
45
94
 
46
- // 计算轮次和 deviated 计数
47
- const round = data.records.length + 1;
48
- const deviatedCount = data.records.filter(r => r.verdict === 'deviated').length
49
- + (record.verdict === 'deviated' ? 1 : 0);
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
+ }
50
103
 
51
104
  // 追加新记录
52
105
  data.records.push({
@@ -55,6 +108,7 @@ export function writeVerification(verificationsDir, record) {
55
108
  timestamp: record.timestamp,
56
109
  summary: record.summary,
57
110
  dimensions: record.dimensions,
111
+ reproduction_command: record.reproduction_command,
58
112
  deviation_detail: record.deviation_detail,
59
113
  reset_suggested: record.reset_suggested,
60
114
  });
@@ -77,18 +131,15 @@ export function getVerificationHistory(verificationsDir, intentId) {
77
131
  if (!existsSync(filePath)) {
78
132
  return null;
79
133
  }
80
- const raw = readFileSync(filePath, 'utf-8');
81
- return JSON.parse(raw);
134
+ return readJsonFile(filePath, '验证记录');
82
135
  }
83
136
 
84
137
  /**
85
138
  * 返回所有待验证的 Intent(有实现产物但还没验证记录的)。
86
139
  * 需要传入 Intent Map 来判断哪些 Intent 是 in_progress。
87
140
  */
88
- export function getPendingVerifications(loomDir, verificationsDir) {
89
- const intentMap = JSON.parse(
90
- readFileSync(join(loomDir, '04_INTENT_MAP.json'), 'utf-8')
91
- );
141
+ export function getPendingVerifications(versionDir, verificationsDir) {
142
+ const intentMap = readJsonFile(join(versionDir, '04_INTENT_MAP.json'), 'Intent Map');
92
143
  const pending = [];
93
144
  for (const [id, intent] of Object.entries(intentMap.intents)) {
94
145
  if (intent.status === 'in_progress') {
@@ -101,11 +152,14 @@ export function getPendingVerifications(loomDir, verificationsDir) {
101
152
 
102
153
  /**
103
154
  * 列出所有验证记录文件。
155
+ * 只列出正式验证记录——文件名匹配 INT-XXX 格式且内容含 records 字段。
156
+ * 过滤掉用户写入的临时输入文件(如 INT-001.verify.json、_tmp_*.json)。
104
157
  */
105
158
  export function listVerifications(verificationsDir) {
106
159
  if (!existsSync(verificationsDir)) return [];
107
160
  return readdirSync(verificationsDir)
108
161
  .filter((f) => f.endsWith('.json'))
162
+ .filter((f) => /^INT-\d+\.json$/.test(f))
109
163
  .map((f) => f.replace('.json', ''));
110
164
  }
111
165
 
@@ -113,12 +167,12 @@ export function listVerifications(verificationsDir) {
113
167
  * 获取某 Intent 的验证契约(acceptance 字段的解析结果)。
114
168
  * 如果 acceptance 是内联定义,直接返回。
115
169
  * 如果是引用(如 "see 05_VERIFICATION.md#int-001"),解析引用并返回对应章节内容。
116
- * @param {string} loomDir — .loom/v{N}/ 目录
170
+ * @param {string} versionDir — .loom/v{N}/ 目录
117
171
  * @param {string} intentId — Intent ID
118
172
  * @returns {string} 验收契约内容
119
173
  */
120
- export function getVerificationContract(loomDir, intentId) {
121
- const intentMap = JSON.parse(readFileSync(join(loomDir, '04_INTENT_MAP.json'), 'utf-8'));
174
+ export function getVerificationContract(versionDir, intentId) {
175
+ const intentMap = readJsonFile(join(versionDir, '04_INTENT_MAP.json'), 'Intent Map');
122
176
  if (!(intentId in intentMap.intents)) {
123
177
  throw new Error(`Intent 不存在: ${intentId}`);
124
178
  }
@@ -128,72 +182,14 @@ export function getVerificationContract(loomDir, intentId) {
128
182
  const refMatch = acceptance.match(/(?:see\s+)?(\w+\.md)#([\w-]+)/i);
129
183
  if (refMatch) {
130
184
  const [, file, section] = refMatch;
131
- const filePath = join(loomDir, file);
185
+ const filePath = join(versionDir, file);
132
186
  if (!existsSync(filePath)) {
133
187
  throw new Error(`验证契约引用的文件不存在: ${filePath}`);
134
188
  }
135
189
  const content = readFileSync(filePath, 'utf-8');
136
- return extractMdSection(content, section);
190
+ return extractMdSection(content, section, '验证契约');
137
191
  }
138
192
 
139
193
  // 内联定义,直接返回
140
194
  return acceptance;
141
195
  }
142
-
143
- /**
144
- * 从 heading 文本中提取显式锚点。
145
- * 支持 Pandoc/MDX 风格语法: "## INT-003 {#int-003}"
146
- */
147
- function extractExplicitAnchor(headingText) {
148
- const match = headingText.match(/\{#([\w-]+)\}\s*$/);
149
- return match ? match[1] : null;
150
- }
151
-
152
- /**
153
- * slugify — 和 philosophy.js 保持一致的逻辑。
154
- */
155
- function slugify(text) {
156
- return text
157
- .replace(/\r/g, '')
158
- .replace(/\{#[\w-]+\}\s*$/, '')
159
- .toLowerCase()
160
- .replace(/[^\w\s-]/g, '')
161
- .replace(/\s+/g, '-')
162
- .replace(/-+/g, '-')
163
- .replace(/^-|-$/g, '')
164
- .trim();
165
- }
166
-
167
- /**
168
- * 从 MD 内容中按 heading slug 提取章节。
169
- * 支持显式锚点 {#slug} 和自动 slugify。
170
- */
171
- function extractMdSection(content, sectionSlug) {
172
- const lines = content.split('\n');
173
- let capturing = false;
174
- let targetLevel = 0;
175
- const captured = [];
176
-
177
- for (const line of lines) {
178
- const cleanLine = line.replace(/\r$/, '');
179
- const headingMatch = cleanLine.match(/^(#{1,6})\s+(.+)$/);
180
- if (headingMatch) {
181
- const level = headingMatch[1].length;
182
- const headingText = headingMatch[2];
183
- const slug = extractExplicitAnchor(headingText) || slugify(headingText);
184
- if (capturing && level <= targetLevel) break;
185
- if (slug === sectionSlug) {
186
- capturing = true;
187
- targetLevel = level;
188
- captured.push(cleanLine);
189
- continue;
190
- }
191
- }
192
- if (capturing) captured.push(cleanLine);
193
- }
194
-
195
- if (captured.length === 0) {
196
- throw new Error(`验证契约章节未找到: #${sectionSlug}`);
197
- }
198
- return captured.join('\n').trim();
199
- }
@@ -122,7 +122,7 @@ function listFilesRelative(dir, base = '') {
122
122
  if (!existsSync(dir)) return result;
123
123
  for (const entry of readdirSync(dir)) {
124
124
  const full = join(dir, entry);
125
- const rel = base ? `${base}/${entry}` : entry;
125
+ const rel = base ? join(base, entry) : entry;
126
126
  if (statSync(full).isDirectory()) {
127
127
  result.push(...listFilesRelative(full, rel));
128
128
  } else {
@@ -0,0 +1,203 @@
1
+ # 实现部分拆解方法论
2
+
3
+ > 这份文档给 Agent 一套拆解方法,让它自己识别"这个项目由哪些实现部分组成",
4
+ > 然后对每个部分走搜索漏斗,织造该部分的哲学约束。
5
+ >
6
+ > 项目千变万化,预填维度文件覆盖不了所有情况。方法不会过时,清单会。
7
+
8
+ ---
9
+
10
+ ## 核心思路
11
+
12
+ 传统维度库靠预填——CLI 工具该有哪些维度、Agent 系统该有哪些维度、Web 前端该有哪些维度,全部写死在文件里。这样有三个问题:
13
+
14
+ 1. 项目类型太多,预填永远追不上
15
+ 2. 新项目类型出现时,维度库来不及更新
16
+ 3. 预设的"UX 哲学"对 CLI 工具没意义,预设的"CLI 美学"对 Agent 系统也没意义——错配
17
+
18
+ LOOM 换了个方向:预设"怎么拆部分",不预设"有哪些部分"。Agent 拿到项目特征后,自己拆解出实现部分,每个部分独立织造哲学。
19
+
20
+ ---
21
+
22
+ ## 拆解流程
23
+
24
+ ### Step 1:识别项目类型
25
+
26
+ 先判断项目属于哪个大类。判断结果用来确定拆解的起点,不用来查预设清单。
27
+
28
+ 常见类型(非穷举——Agent 自行判断):
29
+
30
+ | 类型 | 特征 | 用户接触面 |
31
+ |---|---|---|
32
+ | CLI 工具 | 命令行交互,输入→输出 | 终端输出、参数、退出码 |
33
+ | Agent 系统 | 自主决策,工具调用 | 对话、工具调用结果、状态反馈 |
34
+ | Web 前端 | 浏览器渲染,用户交互 | 页面、交互、视觉 |
35
+ | 后端服务 | API 驱动,多客户端 | API 响应、错误码、文档 |
36
+ | 游戏引擎 | 实时渲染,物理模拟 | 画面、操作反馈、性能 |
37
+ | 嵌入式系统 | 资源受限,硬件交互 | 设备行为、指示灯、串口 |
38
+ | 数据管道 | ETL/流处理,数据变换 | 数据质量、吞吐量、延迟 |
39
+ | 混合型 | 以上多种组合 | 按子系统拆分,各走各的类型 |
40
+
41
+ **判断方法**:看项目的用户接触面和核心交互方式。如果项目跨多个类型(如 Agent 系统 + Web 前端),按子系统分别判断。
42
+
43
+ **用户提到的特殊类型**:上面这张表只列了常见类型。用户可能提出表里没有的系统——编译器、数据库、操作系统内核、游戏引擎、实时渲染管线、分布式共识系统、密码学库、嵌入式固件、区块链协议、消息队列、时序数据库……遇到表里没有的类型,按 Step 2 的三个问题自己拆,不要硬套。`examples/` 目录下有参考案例的才几个,没参考案例的类型更要认真走搜索漏斗——这类系统的实践知识往往在论文、标准文档、源码注释里,不在博客里。
44
+
45
+ ### Step 2:拆解实现部分
46
+
47
+ 对项目类型,问三个问题,每个答案是一个"实现部分":
48
+
49
+ **问题 A:用户接触面是什么?**
50
+ - CLI 工具 → 终端输出、参数解析、帮助信息、错误呈现
51
+ - Agent 系统 → 对话格式、工具调用展示、状态反馈、进度提示
52
+ - Web 前端 → 页面布局、交互反馈、视觉风格、动效
53
+
54
+ **问题 B:内部由哪些子系统组成?**
55
+ - CLI 工具 → 转换引擎、文件 IO、配置管理(如果有)
56
+ - Agent 系统 → 编排器、工具调度、上下文管理、提示词构造、验证器
57
+ - Web 前端 → 路由、状态管理、组件层、数据获取、样式系统
58
+
59
+ **问题 C:每个子系统的职责边界在哪?**
60
+ - 问的是"每个模块该怎么做、什么标准"——"有哪些模块"是架构的事,不是哲学的事
61
+ - 职责边界 = 这个部分"做什么"和"不做什么"的划分
62
+
63
+ **拆解原则**:
64
+ 1. **按职责拆,不按文件拆**——"CLI 输出美学"是一个部分,"cli.js 这个文件"不是
65
+ 2. **粒度适中**——太粗("整个 CLI")没有约束力,太细("每个函数")变成架构了
66
+ 3. **每个部分能独立回答"该怎么做"**——如果一个部分的"怎么做"完全依赖另一个部分,合并它们
67
+ 4. **用户接触面优先**——用户能看到、能感知的部分,哲学约束最重要
68
+
69
+ ### Step 3:对每个部分走搜索漏斗
70
+
71
+ 每个识别出的实现部分,独立走"搜索 → 萃取 → 转译 → 落地"漏斗(见 PHILOSOPHY_WEAVER.md "织造漏斗"章节)。
72
+
73
+ 搜索时问的问题要具体:**"这个部分该怎么做、什么标准、有什么好实践"**。别问"这个领域的哲学是什么"——实践领域的知识很少叫"哲学"。
74
+
75
+ 例如对 CLI 工具的"帮助信息"部分:
76
+ - 搜 "CLI help text design best practices"
77
+ - 搜 "ripgrep --help output design"
78
+ - 搜 "clap help formatting conventions"
79
+ - 搜 "POSIX utility argument syntax conventions"
80
+ - 从结果中萃取:帮助信息的结构、示例的放法、链接的放法、退出码的说明
81
+
82
+ ### Step 4:产出部分哲学文档
83
+
84
+ 每个实现部分产出一个哲学文档(或融入通用层文档的对应章节)。文档必须包含:
85
+
86
+ 1. **部分北极星**:这个部分的判断基准——"遇到冲突时,拿这句话量一下"
87
+ 2. **该做什么**:可执行原则,不是口号
88
+ 3. **不该做什么**:反模式清单,每条有"为什么"
89
+ 4. **参考实践**:至少 2 个真实工具/系统是怎么做这个部分的(要一手实践——源码、文档、工程博客,不要 Wikipedia)
90
+ 5. **灵感来源**:满足 LOOM 的源多样性校验
91
+
92
+ ---
93
+
94
+ ## 拆解示例
95
+
96
+ ### 示例 1:CLI 工具(md2html)
97
+
98
+ ```
99
+ 项目类型:CLI 工具
100
+ 用户接触面:终端
101
+
102
+ 拆解出的实现部分:
103
+ ├── CLI 交互设计 — 参数解析、--help、--version、用法提示
104
+ ├── CLI 输出美学 — 成功反馈格式、颜色策略、Rule of Silence 的正确理解
105
+ ├── CLI 错误呈现 — 错误结构、修复建议、退出码语义
106
+ ├── 转换引擎 — 纯函数、子集策略、透传 vs 报错
107
+ └── 产物设计 — HTML 结构、CSS 内联、可预测性
108
+ ```
109
+
110
+ 每个部分独立搜索:
111
+ - CLI 交互设计 → 搜 POSIX 参数约定、clap/cobra 设计、ripgrep/fd 的 --help
112
+ - CLI 输出美学 → 搜 "CLI output design color"、bat/exa 的输出风格、Unix Rule of Silence 原文
113
+ - CLI 错误呈现 → 搜 "CLI error message design"、Rust 的 error message 传统、Go 的 error-as-value
114
+ - 转换引擎 → 搜 Markdown 解析策略、纯函数设计、子集 vs 全集
115
+ - 产物设计 → 搜 "self-contained HTML"、CSS 内联策略、可预测输出
116
+
117
+ ### 示例 2:Agent 系统
118
+
119
+ ```
120
+ 项目类型:Agent 系统
121
+ 用户接触面:对话 + 工具调用结果
122
+
123
+ 拆解出的实现部分:
124
+ ├── 系统架构 — 编排 vs 控制、进程边界、IPC 机制
125
+ ├── 工具调用哲学 — 委托边界、失控收回、工具描述怎么写
126
+ ├── 上下文压缩 — 什么时候压缩、压缩什么、保留什么
127
+ ├── 提示词工程 — 角色激活、约束注入、上下文窗口管理
128
+ ├── 验证哲学 — 怎么信、怎么验、自动化 vs 人类
129
+ └── 失败与恢复 — 崩溃恢复、状态一致性、回滚策略
130
+ ```
131
+
132
+ 每个部分独立搜索:
133
+ - 系统架构 → 搜 "agent orchestration architecture"、LangChain/AutoGPT/CrewAI 架构设计
134
+ - 工具调用 → 搜 "tool calling philosophy"、OpenAI function calling 设计、MCP 协议
135
+ - 上下文压缩 → 搜 "LLM context window management"、conversation summarization 策略
136
+ - 提示词工程 → 搜 "prompt engineering philosophy"、system prompt 设计、role activation
137
+ - 验证哲学 → 搜 "AI agent verification"、human-in-the-loop 设计、automated verification
138
+ - 失败与恢复 → 搜 "agent failure recovery"、state management、checkpoint 设计
139
+
140
+ ### 示例 3:Web 前端
141
+
142
+ ```
143
+ 项目类型:Web 前端
144
+ 用户接触面:浏览器
145
+
146
+ 拆解出的实现部分:
147
+ ├── 视觉设计哲学 — 排版、色彩、留白、层次
148
+ ├── 交互反馈哲学 — 加载状态、错误提示、成功反馈、动效
149
+ ├── 数据获取哲学 — 缓存策略、乐观更新、错误重试、loading 边界
150
+ ├── 组件设计哲学 — 组件粒度、状态边界、复用策略
151
+ └── 性能哲学 — 首屏速度、包体积、渲染策略
152
+ ```
153
+
154
+ ---
155
+
156
+ ## 拆解的元原则
157
+
158
+ 1. **不预设结果**——拆解出的部分由 Agent 根据项目特征判断,不是查表
159
+ 2. **用户接触面优先**——用户能看到的部分,哲学约束最重要
160
+ 3. **每个部分独立可搜索**——"CLI 输出美学"能独立搜到好实践,"整个 CLI 的哲学"太泛搜不到有用的
161
+ 4. **部分之间可以有依赖**——"CLI 错误呈现"依赖"CLI 交互设计"的参数约定,这是正常的
162
+ 5. **部分数量适中**——小项目 3-5 个部分,大项目 6-10 个,超过 10 个考虑合并
163
+ 6. **拆解结果要记录**——在哲学文档里显式列出"本项目拆解出哪些实现部分",供 Architect 和 Forge 引用
164
+
165
+ ---
166
+
167
+ ## 与通用层的关系
168
+
169
+ 通用层(产品哲学 / 工程哲学 / 协作哲学)回答"**为什么**"——产品为什么存在、代码怎么写、团队怎么协作。
170
+
171
+ 实现部分层回答"**怎么做**"——CLI 的帮助信息怎么做、Agent 的工具调用怎么做、前端的交互反馈怎么做。
172
+
173
+ 两层正交,缺一不可:
174
+ - 通用层是地基。没有产品哲学,实现部分的哲学就没有判断基准
175
+ - 实现部分层是落地。没有部分哲学,通用层就飘在空中,Forge 实现时不知道该对照什么
176
+
177
+ ---
178
+
179
+ ## 与 Architect 的接口
180
+
181
+ Weaver 拆解出的实现部分,是 Architect 设计架构的输入:
182
+
183
+ 1. Weaver 产出"实现部分清单"(在哲学文档里显式列出)
184
+ 2. Architect 读这个清单,为每个部分设计对应的模块/子系统
185
+ 3. 每个模块的接口设计,对照该部分的哲学约束
186
+ 4. Forge 实现某个模块时,引用该部分的哲学文档作为约束
187
+
188
+ 从哲学到架构到实现,每个环节都有约束传递链。
189
+
190
+ ---
191
+
192
+ ## 搜索时的关键提醒
193
+
194
+ 对每个实现部分搜索时,别只搜"哲学"——实践领域的知识很少叫"哲学",但就是哲学:
195
+
196
+ - 搜 "best practices"
197
+ - 搜 "design conventions"
198
+ - 搜 具体工具名 + "design"(如 "ripgrep output design")
199
+ - 搜 具体库的文档(如 clap 的 README、cobra 的 design doc)
200
+ - 搜 标准文档(如 POSIX、IEEE)
201
+ - 搜 工程博客(如 Stripe engineering blog、Cloudflare blog)
202
+
203
+ 实践驱动的领域,知识在工具和标准里,在论文里的反而少。按 `SEARCH_METHODOLOGY.md` 的领域形态判断走对应路径。