@haaaiawd/loom 0.7.0 → 0.9.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.
- package/README.md +31 -19
- package/cli/bin/loom.js +89 -27
- package/cli/help/doctor.md +56 -5
- package/cli/help/preview.md +59 -0
- package/cli/help/workflow.md +18 -12
- package/cli/src/diagnostics.js +156 -12
- package/cli/src/guide.js +17 -15
- package/cli/src/philosophy.js +260 -0
- package/cli/src/preview.js +67 -10
- package/cli/src/verify.js +8 -4
- package/dimensions/PART_DECOMPOSITION.md +203 -0
- package/dimensions/examples/AGENT_SYSTEM/README.md +219 -0
- package/dimensions/examples/CLI_TOOL/README.md +163 -0
- package/dimensions/universal/COLLABORATION_PHILOSOPHY.md +77 -0
- package/dimensions/universal/ENGINEERING_CREED.md +74 -0
- package/dimensions/universal/PRODUCT_PHILOSOPHY.md +70 -0
- package/meta/PHILOSOPHY_WEAVER.md +104 -50
- package/package.json +3 -3
package/cli/src/guide.js
CHANGED
|
@@ -36,17 +36,17 @@ function isTemplate(filePath) {
|
|
|
36
36
|
/**
|
|
37
37
|
* 诊断项目当前阶段。
|
|
38
38
|
* @param {string} projectDir — 项目根目录
|
|
39
|
-
* @returns {{ stage: string, stage_num: number, details: object, auto: boolean, next_action: string, next_command: string, message: string, needs_human_review: boolean }}
|
|
40
|
-
*/
|
|
41
|
-
export function guideProject(projectDir) {
|
|
42
|
-
const cwd = projectDir || process.cwd();
|
|
43
|
-
const loomRoot = join(cwd, '.loom');
|
|
44
|
-
const auto = isAutoOn(loomRoot);
|
|
45
|
-
const result = diagnoseStage(cwd, loomRoot, auto);
|
|
46
|
-
// 统一后处理:写心跳 + 加 AUTO 提示词 + 判断是否需要人类 review
|
|
47
|
-
if (existsSync(loomRoot)) {
|
|
48
|
-
try {
|
|
49
|
-
writeHeartbeat(loomRoot, {
|
|
39
|
+
* @returns {{ stage: string, stage_num: number, details: object, auto: boolean, next_action: string, next_command: string, message: string, needs_human_review: boolean }}
|
|
40
|
+
*/
|
|
41
|
+
export function guideProject(projectDir, options = {}) {
|
|
42
|
+
const cwd = projectDir || process.cwd();
|
|
43
|
+
const loomRoot = join(cwd, '.loom');
|
|
44
|
+
const auto = isAutoOn(loomRoot);
|
|
45
|
+
const result = diagnoseStage(cwd, loomRoot, auto);
|
|
46
|
+
// 统一后处理:写心跳 + 加 AUTO 提示词 + 判断是否需要人类 review
|
|
47
|
+
if (existsSync(loomRoot) && !options.dryRun) {
|
|
48
|
+
try {
|
|
49
|
+
writeHeartbeat(loomRoot, {
|
|
50
50
|
stage: result.stage,
|
|
51
51
|
stage_num: result.stage_num,
|
|
52
52
|
next_command: result.next_command,
|
|
@@ -145,9 +145,11 @@ function diagnoseStage(cwd, loomRoot, auto) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
// 状态 4-7: Intent Map 已设计,根据 Intent 状态判断
|
|
148
|
-
let
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
let intentMap;
|
|
149
|
+
let intents;
|
|
150
|
+
try {
|
|
151
|
+
intentMap = loadIntentMap(versionDir);
|
|
152
|
+
intents = intentMap.intents;
|
|
151
153
|
} catch (e) {
|
|
152
154
|
return {
|
|
153
155
|
stage: 'intent_map_broken',
|
|
@@ -218,7 +220,7 @@ function diagnoseStage(cwd, loomRoot, auto) {
|
|
|
218
220
|
if (counts.needs_review > 0) {
|
|
219
221
|
const reviewIds = allIntents.filter((i) => i.status === 'needs_review').map((i) => i.id);
|
|
220
222
|
// 读 _meta.pass_count 收敛趟计数(最大 3 趟)
|
|
221
|
-
const passCount =
|
|
223
|
+
const passCount = intentMap._meta?.pass_count || 1;
|
|
222
224
|
const MAX_PASSES = 3;
|
|
223
225
|
const isOverLimit = passCount > MAX_PASSES;
|
|
224
226
|
const passMsg = ` [Pass ${passCount}/${MAX_PASSES}]`;
|
package/cli/src/philosophy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// philosophy.js — 按锚点加载哲学文档的特定章节
|
|
2
2
|
// 哲学文档是 MD,锚点格式: "PRODUCT_PHILOSOPHY.md#core-belief"
|
|
3
3
|
// 这个库按锚点提取对应章节,不返回整个文件。
|
|
4
|
+
// 另含灵感来源校验——防止 Weaver 从训练数据"背"几个名字就交差。
|
|
4
5
|
|
|
5
6
|
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
6
7
|
import { join } from 'node:path';
|
|
@@ -44,3 +45,262 @@ export function listPhilosophyFiles(philosophyDir) {
|
|
|
44
45
|
const dir = readdirSync(philosophyDir);
|
|
45
46
|
return dir.filter((f) => f.endsWith('.md'));
|
|
46
47
|
}
|
|
48
|
+
|
|
49
|
+
// ─── 灵感来源校验 ───────────────────────────────────────
|
|
50
|
+
// 防止 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 = ['萃取', '理由', '为什么', '因为', '启发', '借鉴', '参考理由', '选取理由', '转译'];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 从哲学文档内容中提取"灵感来源"章节的条目。
|
|
63
|
+
* @param {string} content — MD 全文
|
|
64
|
+
* @returns {Array<{ raw: string, name: string, urls: string[], hasReason: boolean }>}
|
|
65
|
+
* 如果返回空数组,调用方需区分"没有章节"和"有章节但没条目"——
|
|
66
|
+
* 用 hasInspirationSection() 单独判断。
|
|
67
|
+
*/
|
|
68
|
+
function parseInspirationSources(content) {
|
|
69
|
+
// 匹配 "## 灵感来源" 或 "## Inspiration Sources" 等章节
|
|
70
|
+
// 支持 {#anchor} 后缀和多种标题变体
|
|
71
|
+
const sectionMatch = content.match(/^##\s+(?:灵感来源|Inspiration|参考来源|References?|参考文献|参考资料|Sources|Bibliography)/m);
|
|
72
|
+
if (!sectionMatch) return [];
|
|
73
|
+
|
|
74
|
+
const startIdx = sectionMatch.index + sectionMatch[0].length;
|
|
75
|
+
// 找到下一个 ## 或文件末尾
|
|
76
|
+
const nextSection = content.slice(startIdx).match(/\n##\s/m);
|
|
77
|
+
const sectionText = nextSection
|
|
78
|
+
? content.slice(startIdx, startIdx + nextSection.index)
|
|
79
|
+
: content.slice(startIdx);
|
|
80
|
+
|
|
81
|
+
// 解析每个 list item(- / * / 1. / 2. 等开头的无序或有序列表)
|
|
82
|
+
const items = [];
|
|
83
|
+
const lines = sectionText.split('\n');
|
|
84
|
+
let currentItem = null;
|
|
85
|
+
|
|
86
|
+
// 匹配 - xxx / * xxx / 1. xxx / 2. xxx 等
|
|
87
|
+
const ITEM_RE = /^\s*(?:[-*]|\d+\.)\s+/;
|
|
88
|
+
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
if (ITEM_RE.test(line)) {
|
|
91
|
+
// 新条目
|
|
92
|
+
if (currentItem) items.push(currentItem);
|
|
93
|
+
const raw = line.replace(ITEM_RE, '').trim();
|
|
94
|
+
const urls = [...raw.matchAll(/https?:\/\/[^\s))]+/g)].map((m) => m[0]);
|
|
95
|
+
const name = raw.replace(/\*\*/g, '').split(/[((——]/)[0].trim();
|
|
96
|
+
const hasReason = REASON_KEYWORDS.some((kw) => raw.includes(kw));
|
|
97
|
+
currentItem = { raw, name, urls, hasReason };
|
|
98
|
+
} else if (currentItem && line.trim()) {
|
|
99
|
+
// 多行条目的续行
|
|
100
|
+
currentItem.raw += ' ' + line.trim();
|
|
101
|
+
const newUrls = [...line.matchAll(/https?:\/\/[^\s))]+/g)].map((m) => m[0]);
|
|
102
|
+
currentItem.urls.push(...newUrls);
|
|
103
|
+
if (REASON_KEYWORDS.some((kw) => line.includes(kw))) {
|
|
104
|
+
currentItem.hasReason = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (currentItem) items.push(currentItem);
|
|
109
|
+
return items;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 检查哲学文档是否有"灵感来源"章节(不管有没有条目)。
|
|
114
|
+
* 用来区分"没有章节"和"有章节但没条目"两种情况。
|
|
115
|
+
*/
|
|
116
|
+
function hasInspirationSection(content) {
|
|
117
|
+
return /^##\s+(?:灵感来源|Inspiration|参考来源|References?|参考文献|参考资料|Sources|Bibliography)/m.test(content);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 判断 URL 是否为 Wikipedia 链接。
|
|
122
|
+
*/
|
|
123
|
+
function isWikipediaUrl(url) {
|
|
124
|
+
return /wikipedia\.org/i.test(url);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 校验哲学文档的灵感来源质量。
|
|
129
|
+
* @param {string} philosophyDir — 00_PHILOSOPHY/ 目录路径
|
|
130
|
+
* @returns {{ passed: boolean, issues: Array<{severity: string, msg: string}>, sources: Array }}
|
|
131
|
+
*/
|
|
132
|
+
export function validateInspirationSources(philosophyDir) {
|
|
133
|
+
const issues = [];
|
|
134
|
+
const allSources = [];
|
|
135
|
+
|
|
136
|
+
// 扫描目录下所有 .md 文件,找"灵感来源"章节
|
|
137
|
+
const files = listPhilosophyFiles(philosophyDir);
|
|
138
|
+
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
const content = readFileSync(join(philosophyDir, file), 'utf-8');
|
|
141
|
+
const sources = parseInspirationSources(content);
|
|
142
|
+
if (sources.length === 0) continue;
|
|
143
|
+
|
|
144
|
+
allSources.push({ file, sources });
|
|
145
|
+
|
|
146
|
+
// 校验每个文件的灵感来源
|
|
147
|
+
if (sources.length < MIN_SOURCES) {
|
|
148
|
+
issues.push({
|
|
149
|
+
severity: 'high',
|
|
150
|
+
msg: `${file}: 灵感来源仅 ${sources.length} 条,要求至少 ${MIN_SOURCES} 条。可能从训练数据"背"了几个名字就交差。`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const nonWikiUrls = sources.filter((s) => s.urls.length > 0 && !s.urls.every(isWikipediaUrl));
|
|
155
|
+
if (nonWikiUrls.length < MIN_NON_WIKI) {
|
|
156
|
+
issues.push({
|
|
157
|
+
severity: 'high',
|
|
158
|
+
msg: `${file}: 非 Wikipedia 链接仅 ${nonWikiUrls.length} 个,要求至少 ${MIN_NON_WIKI} 个。Wikipedia 是常识入口,不是深度源——需要原著、论文、工程博客、标准文档等。`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const src of sources) {
|
|
163
|
+
if (!src.hasReason) {
|
|
164
|
+
issues.push({
|
|
165
|
+
severity: 'medium',
|
|
166
|
+
msg: `${file}: 灵感来源 "${src.name}" 缺乏选取理由。必须说明"为什么选这个源"——萃取/转译/启发关系。`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (src.urls.length === 0) {
|
|
170
|
+
issues.push({
|
|
171
|
+
severity: 'medium',
|
|
172
|
+
msg: `${file}: 灵感来源 "${src.name}" 没有 URL。必须附可验证的来源链接。`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 全局校验:所有文件的灵感来源加起来,源类型不能单一
|
|
179
|
+
const totalUrls = allSources.flatMap((s) => s.sources).flatMap((s) => s.urls);
|
|
180
|
+
if (totalUrls.length > 0) {
|
|
181
|
+
const wikiCount = totalUrls.filter(isWikipediaUrl).length;
|
|
182
|
+
if (wikiCount / totalUrls.length > 0.7) {
|
|
183
|
+
issues.push({
|
|
184
|
+
severity: 'medium',
|
|
185
|
+
msg: `全部灵感来源中 Wikipedia 占比 ${Math.round((wikiCount / totalUrls.length) * 100)}%——源类型过于单一。需要原著、论文、标准文档、工程博客等多元源。`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 如果没有任何文件提取到灵感来源条目
|
|
191
|
+
if (allSources.length === 0) {
|
|
192
|
+
// 区分两种情况:完全没有章节 vs 有章节但没条目
|
|
193
|
+
const filesWithSection = [];
|
|
194
|
+
for (const file of files) {
|
|
195
|
+
const content = readFileSync(join(philosophyDir, file), 'utf-8');
|
|
196
|
+
if (hasInspirationSection(content)) filesWithSection.push(file);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (filesWithSection.length > 0) {
|
|
200
|
+
issues.push({
|
|
201
|
+
severity: 'high',
|
|
202
|
+
msg: `${filesWithSection.join(', ')} 有"灵感来源"章节但没有可识别的条目。章节里可能是模板占位符。需要 Weaver 真正走搜索漏斗,填入至少 ${MIN_SOURCES} 个源(- **源名** — 理由。来源:URL 格式)。`,
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
issues.push({
|
|
206
|
+
severity: 'high',
|
|
207
|
+
msg: '所有哲学文档都没有"灵感来源"章节。PHILOSOPHY_WEAVER.md 要求哲学文档必须包含灵感来源(参考了哪些机构、人物、流派——附 URL 和理由)。支持的标题:灵感来源 / Inspiration / 参考来源 / References / 参考文献 / 参考资料 / Sources / Bibliography。',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
passed: issues.length === 0,
|
|
214
|
+
issues,
|
|
215
|
+
sources: allSources,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─── 实现部分清单校验 ───────────────────────────────────
|
|
220
|
+
// 检查哲学文档是否包含"实现部分清单"——Weaver 是否走了拆解流程。
|
|
221
|
+
// PHILOSOPHY_WEAVER.md Step 2 要求产出"实现部分清单"。
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 校验哲学文档是否包含实现部分拆解清单。
|
|
225
|
+
* Weaver 按 PART_DECOMPOSITION.md 拆解后,必须在哲学文档里显式列出拆解出的部分。
|
|
226
|
+
* @param {string} philosophyDir — 00_PHILOSOPHY/ 目录路径
|
|
227
|
+
* @returns {{ passed: boolean, issues: Array<{severity: string, msg: string}>, parts: string[] }}
|
|
228
|
+
*/
|
|
229
|
+
export function validatePartDecomposition(philosophyDir) {
|
|
230
|
+
const issues = [];
|
|
231
|
+
const parts = [];
|
|
232
|
+
|
|
233
|
+
const files = listPhilosophyFiles(philosophyDir);
|
|
234
|
+
|
|
235
|
+
// 搜索"实现部分"相关章节——支持 {#anchor} 后缀和多种标题变体
|
|
236
|
+
const PART_SECTION_PATTERNS = [
|
|
237
|
+
/^##\s+实现部分清单/m,
|
|
238
|
+
/^##\s+部分拆解/m,
|
|
239
|
+
/^##\s+实现部分/m,
|
|
240
|
+
/^##\s+Part Decomposition/m,
|
|
241
|
+
/^##\s+Implementation Parts/m,
|
|
242
|
+
/^##\s+拆解出的部分/m,
|
|
243
|
+
/^##\s+Implementation Decomposition/m,
|
|
244
|
+
/^##\s+Parts? /m,
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
// 搜索部分条目——支持无序列表、有序列表、树形符号
|
|
248
|
+
const PART_ITEM_PATTERNS = [
|
|
249
|
+
/^\s*[-*]\s+\*\*(.+?)\*\*/gm, // - **CLI 交互设计**
|
|
250
|
+
/^\s*\d+\.\s+\*\*(.+?)\*\*/gm, // 1. **CLI 交互设计**
|
|
251
|
+
/^\s*\d+\.\s+(.+)/gm, // 1. CLI 交互设计
|
|
252
|
+
/^\s*├──\s+(.+)/gm, // ├── CLI 交互设计
|
|
253
|
+
/^\s*└──\s+(.+)/gm, // └── 产物设计
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
let foundSection = false;
|
|
257
|
+
|
|
258
|
+
for (const file of files) {
|
|
259
|
+
const content = readFileSync(join(philosophyDir, file), 'utf-8');
|
|
260
|
+
|
|
261
|
+
// 检查是否有实现部分章节
|
|
262
|
+
for (const pattern of PART_SECTION_PATTERNS) {
|
|
263
|
+
if (pattern.test(content)) {
|
|
264
|
+
foundSection = true;
|
|
265
|
+
// 提取该章节的部分条目
|
|
266
|
+
const sectionMatch = content.match(pattern);
|
|
267
|
+
if (sectionMatch) {
|
|
268
|
+
const startIdx = sectionMatch.index + sectionMatch[0].length;
|
|
269
|
+
const nextSection = content.slice(startIdx).match(/\n##\s/m);
|
|
270
|
+
const sectionText = nextSection
|
|
271
|
+
? content.slice(startIdx, startIdx + nextSection.index)
|
|
272
|
+
: content.slice(startIdx);
|
|
273
|
+
|
|
274
|
+
for (const itemPattern of PART_ITEM_PATTERNS) {
|
|
275
|
+
const matches = [...sectionText.matchAll(itemPattern)];
|
|
276
|
+
for (const m of matches) {
|
|
277
|
+
const partName = m[1].trim().replace(/[—\-–].*$/, '').trim();
|
|
278
|
+
if (partName && !parts.includes(partName)) {
|
|
279
|
+
parts.push(partName);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!foundSection) {
|
|
290
|
+
issues.push({
|
|
291
|
+
severity: 'high',
|
|
292
|
+
msg: '哲学文档没有"实现部分清单"章节。PHILOSOPHY_WEAVER.md Step 2 要求按 PART_DECOMPOSITION.md 拆解实现部分,并在哲学文档中显式列出。支持的标题:实现部分清单 / 部分拆解 / 实现部分 / Part Decomposition / Implementation Parts / 拆解出的部分。',
|
|
293
|
+
});
|
|
294
|
+
} else if (parts.length < 2) {
|
|
295
|
+
issues.push({
|
|
296
|
+
severity: 'medium',
|
|
297
|
+
msg: `找到"实现部分清单"章节但仅识别到 ${parts.length} 个部分。可能章节里是模板占位符,或条目格式不被识别(用 - **部分名** 或 ├── 部分名 格式)。PART_DECOMPOSITION.md 建议小项目 3-5 个部分,大项目 6-10 个。`,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
passed: issues.length === 0,
|
|
303
|
+
issues,
|
|
304
|
+
parts,
|
|
305
|
+
};
|
|
306
|
+
}
|
package/cli/src/preview.js
CHANGED
|
@@ -1,15 +1,72 @@
|
|
|
1
|
-
// preview — 输出提示词,让 AI 读 .loom/ 文件并生成 HTML
|
|
2
|
-
// CLI
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// preview — 输出提示词,让 AI 读 .loom/ 文件并生成 HTML,并检查投影新鲜度。
|
|
2
|
+
// CLI 不生成 HTML。AI 自己读文件、重组信息、生成 HTML。
|
|
3
|
+
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
5
|
+
import { join, relative } from 'node:path';
|
|
6
|
+
import { readCurrentPointer } from './shared/paths.js';
|
|
7
|
+
|
|
8
|
+
const PROMPT_PATH = new URL('./preview-prompt.md', import.meta.url);
|
|
9
|
+
const SOURCE_FILE_NAMES = new Set([
|
|
10
|
+
'01_VISION.md',
|
|
11
|
+
'02_ARCHITECTURE.md',
|
|
12
|
+
'04_INTENT_MAP.json',
|
|
13
|
+
'05_VERIFICATION.md',
|
|
14
|
+
'06_CHANGELOG.md',
|
|
15
|
+
]);
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* 输出 preview 提示词。
|
|
11
19
|
* @returns {string}
|
|
12
20
|
*/
|
|
13
|
-
export function generatePreviewPrompt() {
|
|
14
|
-
return readFileSync(PROMPT_PATH, 'utf-8');
|
|
15
|
-
}
|
|
21
|
+
export function generatePreviewPrompt() {
|
|
22
|
+
return readFileSync(PROMPT_PATH, 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shouldIncludeSource(filePath) {
|
|
26
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
27
|
+
const fileName = normalized.split('/').pop();
|
|
28
|
+
if (SOURCE_FILE_NAMES.has(fileName)) return true;
|
|
29
|
+
return normalized.includes('/00_PHILOSOPHY/')
|
|
30
|
+
|| normalized.includes('/03_DECISIONS/')
|
|
31
|
+
|| normalized.includes('/verifications/');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function collectSourceFiles(dir, files = []) {
|
|
35
|
+
if (!existsSync(dir)) return files;
|
|
36
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
37
|
+
const fullPath = join(dir, entry.name);
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
collectSourceFiles(fullPath, files);
|
|
40
|
+
} else if (shouldIncludeSource(fullPath)) {
|
|
41
|
+
files.push(fullPath);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return files;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getPreviewStatus(projectDir) {
|
|
48
|
+
const previewPath = join(projectDir, 'loom-preview.html');
|
|
49
|
+
const loomRoot = join(projectDir, '.loom');
|
|
50
|
+
const current = readCurrentPointer(loomRoot);
|
|
51
|
+
const versionDir = current ? join(loomRoot, current) : null;
|
|
52
|
+
const sourceFiles = versionDir ? collectSourceFiles(versionDir) : [];
|
|
53
|
+
const latestSource = sourceFiles
|
|
54
|
+
.map((filePath) => ({ filePath, mtimeMs: statSync(filePath).mtimeMs }))
|
|
55
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)[0] || null;
|
|
56
|
+
|
|
57
|
+
const exists = existsSync(previewPath);
|
|
58
|
+
const previewMtimeMs = exists ? statSync(previewPath).mtimeMs : null;
|
|
59
|
+
const sourceLatestMtimeMs = latestSource?.mtimeMs ?? null;
|
|
60
|
+
const fresh = exists && sourceLatestMtimeMs !== null && previewMtimeMs >= sourceLatestMtimeMs;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
exists,
|
|
64
|
+
fresh,
|
|
65
|
+
version: current,
|
|
66
|
+
preview_path: previewPath,
|
|
67
|
+
preview_mtime: previewMtimeMs ? new Date(previewMtimeMs).toISOString() : null,
|
|
68
|
+
source_latest_mtime: sourceLatestMtimeMs ? new Date(sourceLatestMtimeMs).toISOString() : null,
|
|
69
|
+
latest_source_file: latestSource ? relative(projectDir, latestSource.filePath).replace(/\\/g, '/') : null,
|
|
70
|
+
next_command: fresh ? 'loom preview' : 'loom preview --regen',
|
|
71
|
+
};
|
|
72
|
+
}
|
package/cli/src/verify.js
CHANGED
|
@@ -92,10 +92,14 @@ export function writeVerification(verificationsDir, record) {
|
|
|
92
92
|
data = { intent_id: record.intent_id, records: [] };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
//
|
|
96
|
-
const round = data.records.length + 1;
|
|
97
|
-
const
|
|
98
|
-
|
|
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
|
+
}
|
|
99
103
|
|
|
100
104
|
// 追加新记录
|
|
101
105
|
data.records.push({
|
|
@@ -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` 的领域形态判断走对应路径。
|