@haaaiawd/loom 0.8.0 → 0.10.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/cli/src/guide.js CHANGED
@@ -6,7 +6,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
7
  import { readCurrentPointer } from './version.js';
8
8
  import { loadIntentMap } from './intent-map.js';
9
- import { isAutoOn, writeHeartbeat, needsHumanReview } from './auto.js';
9
+ import { isAutoOn, getAutoMode, writeHeartbeat, needsHumanReview } from './auto.js';
10
10
 
11
11
  /**
12
12
  * 检测文件是否还是模板(未填充真实内容)。
@@ -36,17 +36,78 @@ 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, 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, {
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 autoMode = getAutoMode(loomRoot);
45
+ const auto = autoMode !== 'manual'; // 向后兼容 boolean
46
+ const result = diagnoseStage(cwd, loomRoot, auto);
47
+ result.auto_mode = autoMode;
48
+
49
+ // 按 stage 补充可执行信息:要读什么、要产出什么、完成后跑什么校验
50
+ const current = result.details.version || 'v1';
51
+ const stageMeta = {
52
+ not_initialized: {
53
+ inputs: [],
54
+ outputs: ['.loom/v1/'],
55
+ verify_command: 'loom guide',
56
+ },
57
+ no_version: {
58
+ inputs: [],
59
+ outputs: ['.loom/v1/'],
60
+ verify_command: 'loom guide',
61
+ },
62
+ need_philosophy: {
63
+ inputs: ['meta/PHILOSOPHY_WEAVER.md', 'meta/BASELINE.md', 'dimensions/PART_DECOMPOSITION.md', 'dimensions/SEARCH_METHODOLOGY.md'],
64
+ outputs: [`.loom/${current}/00_PHILOSOPHY/PRODUCT_PHILOSOPHY.md`, `.loom/${current}/00_PHILOSOPHY/ENGINEERING_CREED.md`, `.loom/${current}/00_PHILOSOPHY/DECISION_RUBRIC.md`],
65
+ verify_command: 'loom philosophy check',
66
+ },
67
+ need_vision: {
68
+ inputs: ['roles/visionary.md', `.loom/${current}/00_PHILOSOPHY/`],
69
+ outputs: [`.loom/${current}/01_VISION.md`],
70
+ verify_command: 'loom guide',
71
+ },
72
+ need_architecture: {
73
+ inputs: ['roles/architect.md', `.loom/${current}/01_VISION.md`],
74
+ outputs: [`.loom/${current}/02_ARCHITECTURE.md`, `.loom/${current}/04_INTENT_MAP.json`],
75
+ verify_command: 'loom doctor',
76
+ },
77
+ intent_map_broken: {
78
+ inputs: [`.loom/${current}/04_INTENT_MAP.json`],
79
+ outputs: [`.loom/${current}/04_INTENT_MAP.json`],
80
+ verify_command: 'loom intent validate',
81
+ },
82
+ in_loop: {
83
+ inputs: ['roles/forge.md', 'roles/keeper.md', `.loom/${current}/04_INTENT_MAP.json`],
84
+ outputs: ['代码文件', `.loom/${current}/verifications/INT-*.json`],
85
+ verify_command: 'loom verify pending',
86
+ },
87
+ ready_for_loop: {
88
+ inputs: ['roles/forge.md', 'roles/keeper.md', `.loom/${current}/04_INTENT_MAP.json`],
89
+ outputs: ['代码文件', `.loom/${current}/verifications/INT-*.json`],
90
+ verify_command: 'loom verify pending',
91
+ },
92
+ all_done: {
93
+ inputs: [],
94
+ outputs: [],
95
+ verify_command: 'loom doctor',
96
+ },
97
+ unknown: {
98
+ inputs: [],
99
+ outputs: [],
100
+ verify_command: 'loom doctor',
101
+ },
102
+ };
103
+ const meta = stageMeta[result.stage] || {};
104
+ result.inputs = meta.inputs || [];
105
+ result.outputs = meta.outputs || [];
106
+ result.verify_command = meta.verify_command || null;
107
+ // 统一后处理:写心跳 + 加 AUTO 提示词 + 判断是否需要人类 review
108
+ if (existsSync(loomRoot) && !options.dryRun) {
109
+ try {
110
+ writeHeartbeat(loomRoot, {
50
111
  stage: result.stage,
51
112
  stage_num: result.stage_num,
52
113
  next_command: result.next_command,
@@ -57,13 +118,19 @@ export function guideProject(projectDir, options = {}) {
57
118
  result.needs_human_review = needsHumanReview(loomRoot, result.stage_num);
58
119
  // 在 message 末尾加 AUTO 提示
59
120
  if (result.stage_num > 0 && result.stage_num < 4) {
60
- result.message += auto
61
- ? '\n\n> ⚠ 这是设计阶段(哲学/愿景/架构),即使 AUTO 模式开启也需要人类 review 后再继续。'
62
- : '\n\n> AUTO 模式关闭——每步都需要人类确认后再继续。';
63
- } else if (result.stage_num >= 4 && !auto) {
64
- result.message += '\n\n> ⚠ AUTO 模式关闭——执行 next_command 后等人类确认再继续。';
65
- } else if (result.stage_num >= 4 && auto) {
66
- result.message += '\n\n> AUTO 模式开启——直接执行 next_command,无需人类确认。';
121
+ if (autoMode === 'auto-design') {
122
+ result.message += '\n\n> AUTO 模式 auto-design——设计阶段也自动执行,无需人类确认。';
123
+ } else if (autoMode === 'auto-loop') {
124
+ result.message += '\n\n> 这是设计阶段(哲学/愿景/架构),auto-loop 模式下需要人类 review 后再继续。';
125
+ } else {
126
+ result.message += '\n\n> AUTO 模式 manual——每步都需要人类确认后再继续。';
127
+ }
128
+ } else if (result.stage_num >= 4) {
129
+ if (auto) {
130
+ result.message += '\n\n> AUTO 模式开启——直接执行 next_command,无需人类确认。';
131
+ } else {
132
+ result.message += '\n\n> ⚠ AUTO 模式关闭——执行 next_command 后等人类确认再继续。';
133
+ }
67
134
  }
68
135
  return result;
69
136
  }
@@ -145,11 +212,11 @@ function diagnoseStage(cwd, loomRoot, auto) {
145
212
  }
146
213
 
147
214
  // 状态 4-7: Intent Map 已设计,根据 Intent 状态判断
148
- let intentMap;
149
- let intents;
150
- try {
151
- intentMap = loadIntentMap(versionDir);
152
- intents = intentMap.intents;
215
+ let intentMap;
216
+ let intents;
217
+ try {
218
+ intentMap = loadIntentMap(versionDir);
219
+ intents = intentMap.intents;
153
220
  } catch (e) {
154
221
  return {
155
222
  stage: 'intent_map_broken',
@@ -220,7 +287,7 @@ function diagnoseStage(cwd, loomRoot, auto) {
220
287
  if (counts.needs_review > 0) {
221
288
  const reviewIds = allIntents.filter((i) => i.status === 'needs_review').map((i) => i.id);
222
289
  // 读 _meta.pass_count 收敛趟计数(最大 3 趟)
223
- const passCount = intentMap._meta?.pass_count || 1;
290
+ const passCount = intentMap._meta?.pass_count || 1;
224
291
  const MAX_PASSES = 3;
225
292
  const isOverLimit = passCount > MAX_PASSES;
226
293
  const passMsg = ` [Pass ${passCount}/${MAX_PASSES}]`;
@@ -62,10 +62,13 @@ const REASON_KEYWORDS = ['萃取', '理由', '为什么', '因为', '启发', '
62
62
  * 从哲学文档内容中提取"灵感来源"章节的条目。
63
63
  * @param {string} content — MD 全文
64
64
  * @returns {Array<{ raw: string, name: string, urls: string[], hasReason: boolean }>}
65
+ * 如果返回空数组,调用方需区分"没有章节"和"有章节但没条目"——
66
+ * 用 hasInspirationSection() 单独判断。
65
67
  */
66
68
  function parseInspirationSources(content) {
67
- // 匹配 "## 灵感来源" 或 "## Inspiration" 章节
68
- const sectionMatch = content.match(/^##\s+(?:灵感来源|Inspiration|参考来源|References)/m);
69
+ // 匹配 "## 灵感来源" 或 "## Inspiration Sources" 等章节
70
+ // 支持 {#anchor} 后缀和多种标题变体
71
+ const sectionMatch = content.match(/^##\s+(?:灵感来源|Inspiration|参考来源|References?|参考文献|参考资料|Sources|Bibliography)/m);
69
72
  if (!sectionMatch) return [];
70
73
 
71
74
  const startIdx = sectionMatch.index + sectionMatch[0].length;
@@ -75,24 +78,29 @@ function parseInspirationSources(content) {
75
78
  ? content.slice(startIdx, startIdx + nextSection.index)
76
79
  : content.slice(startIdx);
77
80
 
78
- // 解析每个 list item(- * 开头)
81
+ // 解析每个 list item(- / * / 1. / 2. 等开头的无序或有序列表)
79
82
  const items = [];
80
83
  const lines = sectionText.split('\n');
81
84
  let currentItem = null;
82
85
 
86
+ // 匹配 - xxx / * xxx / 1. xxx / 2. xxx 等
87
+ const ITEM_RE = /^\s*(?:[-*]|\d+\.)\s+/;
88
+ // URL 匹配:https:// / file:// / local:./path / local:/abs/path
89
+ const URL_RE = /(?:https?:|file:)[\/]+[^\s))]+|local:[^\s))]+/g;
90
+
83
91
  for (const line of lines) {
84
- if (/^\s*[-*]\s/.test(line)) {
92
+ if (ITEM_RE.test(line)) {
85
93
  // 新条目
86
94
  if (currentItem) items.push(currentItem);
87
- const raw = line.replace(/^\s*[-*]\s/, '').trim();
88
- const urls = [...raw.matchAll(/https?:\/\/[^\s))]+/g)].map((m) => m[0]);
95
+ const raw = line.replace(ITEM_RE, '').trim();
96
+ const urls = [...raw.matchAll(URL_RE)].map((m) => m[0]);
89
97
  const name = raw.replace(/\*\*/g, '').split(/[((——]/)[0].trim();
90
98
  const hasReason = REASON_KEYWORDS.some((kw) => raw.includes(kw));
91
99
  currentItem = { raw, name, urls, hasReason };
92
100
  } else if (currentItem && line.trim()) {
93
101
  // 多行条目的续行
94
102
  currentItem.raw += ' ' + line.trim();
95
- const newUrls = [...line.matchAll(/https?:\/\/[^\s))]+/g)].map((m) => m[0]);
103
+ const newUrls = [...line.matchAll(URL_RE)].map((m) => m[0]);
96
104
  currentItem.urls.push(...newUrls);
97
105
  if (REASON_KEYWORDS.some((kw) => line.includes(kw))) {
98
106
  currentItem.hasReason = true;
@@ -103,6 +111,14 @@ function parseInspirationSources(content) {
103
111
  return items;
104
112
  }
105
113
 
114
+ /**
115
+ * 检查哲学文档是否有"灵感来源"章节(不管有没有条目)。
116
+ * 用来区分"没有章节"和"有章节但没条目"两种情况。
117
+ */
118
+ function hasInspirationSection(content) {
119
+ return /^##\s+(?:灵感来源|Inspiration|参考来源|References?|参考文献|参考资料|Sources|Bibliography)/m.test(content);
120
+ }
121
+
106
122
  /**
107
123
  * 判断 URL 是否为 Wikipedia 链接。
108
124
  */
@@ -173,12 +189,26 @@ export function validateInspirationSources(philosophyDir) {
173
189
  }
174
190
  }
175
191
 
176
- // 如果没有任何文件包含灵感来源章节
192
+ // 如果没有任何文件提取到灵感来源条目
177
193
  if (allSources.length === 0) {
178
- issues.push({
179
- severity: 'high',
180
- msg: '所有哲学文档都没有"灵感来源"章节。PHILOSOPHY_WEAVER.md 要求哲学文档必须包含灵感来源(参考了哪些机构、人物、流派——附 URL 和理由)。',
181
- });
194
+ // 区分两种情况:完全没有章节 vs 有章节但没条目
195
+ const filesWithSection = [];
196
+ for (const file of files) {
197
+ const content = readFileSync(join(philosophyDir, file), 'utf-8');
198
+ if (hasInspirationSection(content)) filesWithSection.push(file);
199
+ }
200
+
201
+ if (filesWithSection.length > 0) {
202
+ issues.push({
203
+ severity: 'high',
204
+ msg: `${filesWithSection.join(', ')} 有"灵感来源"章节但没有可识别的条目。章节里可能是模板占位符。需要 Weaver 真正走搜索漏斗,填入至少 ${MIN_SOURCES} 个源(- **源名** — 理由。来源:URL 格式)。`,
205
+ });
206
+ } else {
207
+ issues.push({
208
+ severity: 'high',
209
+ msg: '所有哲学文档都没有"灵感来源"章节。PHILOSOPHY_WEAVER.md 要求哲学文档必须包含灵感来源(参考了哪些机构、人物、流派——附 URL 和理由)。支持的标题:灵感来源 / Inspiration / 参考来源 / References / 参考文献 / 参考资料 / Sources / Bibliography。',
210
+ });
211
+ }
182
212
  }
183
213
 
184
214
  return {
@@ -204,7 +234,7 @@ export function validatePartDecomposition(philosophyDir) {
204
234
 
205
235
  const files = listPhilosophyFiles(philosophyDir);
206
236
 
207
- // 搜索"实现部分"相关章节——可能叫"实现部分清单""部分拆解""Part Decomposition"等
237
+ // 搜索"实现部分"相关章节——支持 {#anchor} 后缀和多种标题变体
208
238
  const PART_SECTION_PATTERNS = [
209
239
  /^##\s+实现部分清单/m,
210
240
  /^##\s+部分拆解/m,
@@ -212,11 +242,15 @@ export function validatePartDecomposition(philosophyDir) {
212
242
  /^##\s+Part Decomposition/m,
213
243
  /^##\s+Implementation Parts/m,
214
244
  /^##\s+拆解出的部分/m,
245
+ /^##\s+Implementation Decomposition/m,
246
+ /^##\s+Parts? /m,
215
247
  ];
216
248
 
217
- // 搜索部分条目——通常是 "- **部分名**" 或 "├── 部分名" 或 "| 部分名 |"
249
+ // 搜索部分条目——支持无序列表、有序列表、树形符号
218
250
  const PART_ITEM_PATTERNS = [
219
- /^\s*[-*]\s+\*\*(.+?)\*\*/gm, // - **CLI 交互设计**
251
+ /^\s*[-*]\s+\*\*(.+?)\*\*/gm, // - **CLI 交互设计**
252
+ /^\s*\d+\.\s+\*\*(.+?)\*\*/gm, // 1. **CLI 交互设计**
253
+ /^\s*\d+\.\s+(.+)/gm, // 1. CLI 交互设计
220
254
  /^\s*├──\s+(.+)/gm, // ├── CLI 交互设计
221
255
  /^\s*└──\s+(.+)/gm, // └── 产物设计
222
256
  ];
@@ -257,12 +291,12 @@ export function validatePartDecomposition(philosophyDir) {
257
291
  if (!foundSection) {
258
292
  issues.push({
259
293
  severity: 'high',
260
- msg: '哲学文档没有"实现部分清单"章节。PHILOSOPHY_WEAVER.md Step 2 要求按 PART_DECOMPOSITION.md 拆解实现部分,并在哲学文档中显式列出。可能 Weaver 跳过了拆解步骤。',
294
+ msg: '哲学文档没有"实现部分清单"章节。PHILOSOPHY_WEAVER.md Step 2 要求按 PART_DECOMPOSITION.md 拆解实现部分,并在哲学文档中显式列出。支持的标题:实现部分清单 / 部分拆解 / 实现部分 / Part Decomposition / Implementation Parts / 拆解出的部分。',
261
295
  });
262
296
  } else if (parts.length < 2) {
263
297
  issues.push({
264
298
  severity: 'medium',
265
- msg: `实现部分清单仅识别到 ${parts.length} 个部分。PART_DECOMPOSITION.md 建议小项目 3-5 个部分,大项目 6-10 个。可能拆解不充分。`,
299
+ msg: `找到"实现部分清单"章节但仅识别到 ${parts.length} 个部分。可能章节里是模板占位符,或条目格式不被识别(用 - **部分名** 或 ├── 部分名 格式)。PART_DECOMPOSITION.md 建议小项目 3-5 个部分,大项目 6-10 个。`,
266
300
  });
267
301
  }
268
302
 
package/cli/src/verify.js CHANGED
@@ -92,14 +92,14 @@ export function writeVerification(verificationsDir, record) {
92
92
  data = { intent_id: record.intent_id, records: [] };
93
93
  }
94
94
 
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
- }
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
+ }
103
103
 
104
104
  // 追加新记录
105
105
  data.records.push({
@@ -134,6 +134,36 @@ export function getVerificationHistory(verificationsDir, intentId) {
134
134
  return readJsonFile(filePath, '验证记录');
135
135
  }
136
136
 
137
+ /**
138
+ * 快捷创建验证记录——Agent 不用手动构造完整 JSON。
139
+ * 内部用 summary 填充四个维度的 evidence,生成标准记录格式。
140
+ * @param {string} verificationsDir — verifications/ 目录路径
141
+ * @param {string} intentId — 如 "INT-001"
142
+ * @param {string} verdict — 'passed' | 'deviated' | 'blocked'
143
+ * @param {string} summary — 验证摘要(也会作为四个维度的 evidence)
144
+ * @param {object} [extras]
145
+ * @param {string} [extras.reproduction_command] — 复现命令
146
+ * @param {string} [extras.deviation_detail] — 偏离说明(deviated 时)
147
+ * @returns {{ filePath: string, round: number, deviated_count: number, should_escalate: boolean }}
148
+ */
149
+ export function createQuickVerification(verificationsDir, intentId, verdict, summary, extras = {}) {
150
+ const timestamp = new Date().toISOString();
151
+ // 用 summary 填充四个维度的 evidence——快捷命令不要求 Agent 逐维度写
152
+ const dimensions = {};
153
+ for (const dim of REQUIRED_DIMENSIONS) {
154
+ dimensions[dim] = { verdict, evidence: summary };
155
+ }
156
+ return writeVerification(verificationsDir, {
157
+ intent_id: intentId,
158
+ verdict,
159
+ timestamp,
160
+ summary,
161
+ dimensions,
162
+ reproduction_command: extras.reproduction_command || null,
163
+ deviation_detail: extras.deviation_detail || null,
164
+ });
165
+ }
166
+
137
167
  /**
138
168
  * 返回所有待验证的 Intent(有实现产物但还没验证记录的)。
139
169
  * 需要传入 Intent Map 来判断哪些 Intent 是 in_progress。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/loom",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "LOOM — 哲学驱动开发框架。Agent 通过 CLI 访问 Intent Map / 哲学 / 验证记录,不直接读文件",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,8 +35,8 @@
35
35
  "license": "MIT",
36
36
  "repository": {
37
37
  "type": "git",
38
- "url": "git+https://github.com/Haaaiawd/loom.git"
39
- },
40
- "homepage": "https://github.com/Haaaiawd/loom#readme",
38
+ "url": "git+https://github.com/Haaaiawd/loom.git"
39
+ },
40
+ "homepage": "https://github.com/Haaaiawd/loom#readme",
41
41
  "author": "haaaiawd"
42
42
  }