@haaaiawd/loom 0.1.0 → 0.7.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/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,7 +78,16 @@ 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
  }
@@ -55,6 +104,7 @@ export function writeVerification(verificationsDir, record) {
55
104
  timestamp: record.timestamp,
56
105
  summary: record.summary,
57
106
  dimensions: record.dimensions,
107
+ reproduction_command: record.reproduction_command,
58
108
  deviation_detail: record.deviation_detail,
59
109
  reset_suggested: record.reset_suggested,
60
110
  });
@@ -77,18 +127,15 @@ export function getVerificationHistory(verificationsDir, intentId) {
77
127
  if (!existsSync(filePath)) {
78
128
  return null;
79
129
  }
80
- const raw = readFileSync(filePath, 'utf-8');
81
- return JSON.parse(raw);
130
+ return readJsonFile(filePath, '验证记录');
82
131
  }
83
132
 
84
133
  /**
85
134
  * 返回所有待验证的 Intent(有实现产物但还没验证记录的)。
86
135
  * 需要传入 Intent Map 来判断哪些 Intent 是 in_progress。
87
136
  */
88
- export function getPendingVerifications(loomDir, verificationsDir) {
89
- const intentMap = JSON.parse(
90
- readFileSync(join(loomDir, '04_INTENT_MAP.json'), 'utf-8')
91
- );
137
+ export function getPendingVerifications(versionDir, verificationsDir) {
138
+ const intentMap = readJsonFile(join(versionDir, '04_INTENT_MAP.json'), 'Intent Map');
92
139
  const pending = [];
93
140
  for (const [id, intent] of Object.entries(intentMap.intents)) {
94
141
  if (intent.status === 'in_progress') {
@@ -101,11 +148,14 @@ export function getPendingVerifications(loomDir, verificationsDir) {
101
148
 
102
149
  /**
103
150
  * 列出所有验证记录文件。
151
+ * 只列出正式验证记录——文件名匹配 INT-XXX 格式且内容含 records 字段。
152
+ * 过滤掉用户写入的临时输入文件(如 INT-001.verify.json、_tmp_*.json)。
104
153
  */
105
154
  export function listVerifications(verificationsDir) {
106
155
  if (!existsSync(verificationsDir)) return [];
107
156
  return readdirSync(verificationsDir)
108
157
  .filter((f) => f.endsWith('.json'))
158
+ .filter((f) => /^INT-\d+\.json$/.test(f))
109
159
  .map((f) => f.replace('.json', ''));
110
160
  }
111
161
 
@@ -113,12 +163,12 @@ export function listVerifications(verificationsDir) {
113
163
  * 获取某 Intent 的验证契约(acceptance 字段的解析结果)。
114
164
  * 如果 acceptance 是内联定义,直接返回。
115
165
  * 如果是引用(如 "see 05_VERIFICATION.md#int-001"),解析引用并返回对应章节内容。
116
- * @param {string} loomDir — .loom/v{N}/ 目录
166
+ * @param {string} versionDir — .loom/v{N}/ 目录
117
167
  * @param {string} intentId — Intent ID
118
168
  * @returns {string} 验收契约内容
119
169
  */
120
- export function getVerificationContract(loomDir, intentId) {
121
- const intentMap = JSON.parse(readFileSync(join(loomDir, '04_INTENT_MAP.json'), 'utf-8'));
170
+ export function getVerificationContract(versionDir, intentId) {
171
+ const intentMap = readJsonFile(join(versionDir, '04_INTENT_MAP.json'), 'Intent Map');
122
172
  if (!(intentId in intentMap.intents)) {
123
173
  throw new Error(`Intent 不存在: ${intentId}`);
124
174
  }
@@ -128,72 +178,14 @@ export function getVerificationContract(loomDir, intentId) {
128
178
  const refMatch = acceptance.match(/(?:see\s+)?(\w+\.md)#([\w-]+)/i);
129
179
  if (refMatch) {
130
180
  const [, file, section] = refMatch;
131
- const filePath = join(loomDir, file);
181
+ const filePath = join(versionDir, file);
132
182
  if (!existsSync(filePath)) {
133
183
  throw new Error(`验证契约引用的文件不存在: ${filePath}`);
134
184
  }
135
185
  const content = readFileSync(filePath, 'utf-8');
136
- return extractMdSection(content, section);
186
+ return extractMdSection(content, section, '验证契约');
137
187
  }
138
188
 
139
189
  // 内联定义,直接返回
140
190
  return acceptance;
141
191
  }
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 {
@@ -55,6 +55,7 @@ Intent Map 是 loop 的地图。它不是扁平任务表,是**带依赖关系
55
55
  | 字段 | 说明 | 为什么需要 |
56
56
  |---|---|---|
57
57
  | `id` | 稳定标识(如 `INT-001`) | 全局引用,不随重命名丢失 |
58
+ | `title` | 一句话标题 | `intent next`/`status` 输出时优先展示,让 Agent 不用读完整叙事就能判断当前在做什么 |
58
59
  | `narrative_ref` | 意图叙事的引用(指向愿景文档的章节) | Keeper 验证的依据——"为什么存在" |
59
60
  | `depends_on` | 前置 Intent ID 列表 | 拓扑序计算、依赖检查 |
60
61
  | `acceptance` | 验收契约(什么算"忠实实现") | Keeper 判定通过/偏离的标准 |
@@ -66,20 +67,41 @@ Intent Map 是 loop 的地图。它不是扁平任务表,是**带依赖关系
66
67
  - `acceptance` 的具体形式(Given-When-Then / 用户故事验收 / 自定义)
67
68
  - 是否有额外字段(如 `estimated_effort`、`priority`、`sprint`)
68
69
 
70
+ **system_id 归属规则**(Architect 设计 Intent Map 时必须遵守):
71
+ - 每个 Intent 的验收契约只能要求**该 Intent 自己产出的文件**可运行/可验证
72
+ - 如果 Intent A 的契约要求"能启动",但启动入口文件归 Intent B,这是契约设计错误
73
+ - 正确做法:要么把"最小可启动骨架"划进最早执行的 Intent 的产出范围,要么把契约措辞改为"提供启动入口配置,实际启动在依赖 Intent 完成后验证"
74
+ - Forge 在实现时如果发现契约要求的文件不属于当前 Intent 的产出范围,应该自主判断:要么扩展当前 Intent 的产出(在契约允许的自由空间内),要么标记 `blocked` 报告"契约要求跨 Intent 产出"
75
+
69
76
  **acceptance 质量底线**:`acceptance` 必须具体到可验证——Keeper 读完后能明确判断"满足/不满足"。禁止模糊措辞(如"实现正确即可"、"功能正常")。如果 Keeper 验证时发现 acceptance 无法判定,必须标记 `blocked`,要求 Architect 重新定义。
70
77
 
78
+ **acceptance 承诺分层**:
79
+
80
+ acceptance 不只是"实现什么功能",是"这个 Intent 向系统承诺了什么"。分两层:
81
+ 1. **功能承诺**:这个 Intent 产出什么可观察行为(Given-When-Then)
82
+ 2. **防御承诺**:这个 Intent 不会发生什么(从 `philosophy_anchors` 引用的反模式派生)
83
+
84
+ 防御承诺不是让 Keeper 验证时自己去对照反模式,是 Architect 在设计阶段就把反模式转成可验证的契约。例:`AI_PHILOSOPHY#anti-patterns` "禁止直接 JSON.parse" → 防御契约 "LLM 返回非合法 JSON 时抛明确错误,不返回空数组假装成功"。
85
+
86
+ **Pre-Mortem 设计法**(Architect 设计时用):对每个 Intent 做一次 Pre-Mortem——假设这个 Intent 实现后失败了,最可能的失败原因是什么?把那个失败原因变成 acceptance 里的一条防御契约。这让 acceptance 从"描述成功"升级为"防御失败"。
87
+
71
88
  **acceptance 示例**:
72
89
 
73
90
  ```
74
- 具体可验证:
91
+ 具体可验证(功能承诺 + 防御承诺):
75
92
  "Given 用户已注册且邮箱已验证
76
93
  When 用户输入正确的邮箱和密码
77
94
  Then 用户成功登录并跳转到 /dashboard
78
- And 错误的密码显示'用户名或密码错误'"
95
+ And 错误的密码显示'用户名或密码错误'
96
+ And [防御承诺] 密码错误时不泄露用户是否存在(来自 ENGINEERING_CREED#anti-patterns)"
79
97
 
80
98
  ✓ 具体可验证(性能):
81
99
  "从 MySQL 抽取 100 万行数据完成时间 < 5 分钟(在标准测试环境下)"
82
100
 
101
+ ✓ 具体可验证(防御承诺):
102
+ "LLM 返回非合法 JSON 时抛明确错误,不返回空数组假装成功
103
+ (Pre-Mortem: LLM 返回乱码导致前端崩溃 → 防御契约来自 AI_PHILOSOPHY#anti-patterns)"
104
+
83
105
  ✗ 模糊不可验证:
84
106
  "实现正确即可"
85
107
  "功能正常"
@@ -143,6 +165,16 @@ Verification 是 loop 的验证环节。它不是 code review(那是看代码
143
165
  - 在人类验证完成前,Intent 的总判定为 `blocked`(不能标记 completed)
144
166
  - 如果用户长期不补充判定(默认 7 天),Keeper 将该 Intent 升级为 `blocked`,报告"人类验证超时"
145
167
 
168
+ **AUTO 模式下的 L3 规则**(关键差异):
169
+ - AUTO 模式开启时(`.loom/auto` 存在),**不需要任何 human review**
170
+ - Keeper 遇到 L3 verification_method 时,**自主判定**所有维度,不标 `pending_human`
171
+ - Keeper 用 L1 静态审查 + L2 运行时验证的能耐去覆盖 L3 维度,给出明确判定(passed/deviated)
172
+ - Intent 直接按 Keeper 判定流转,**不停下等人类**
173
+ - AUTO 模式的核心契约:**持续运行,除非出意外否则不允许私自停止**
174
+ - "出意外"定义:blocked(依赖阻塞/契约无法判定/连续 3 轮 deviated 升级)、fatal 错误(Intent Map 损坏/循环依赖)
175
+ - Agent 不允许因为"这个维度我判断不了"就停下来——要么判 passed(有证据),要么判 deviated(有偏离说明),不允许 pending_human
176
+ - **验证记录必须说明**:summary 字段里要写明"AUTO 模式下 L3 维度由 Keeper 自主判定,用 L1+L2 能耐覆盖",让后续审查者知道这是自主判定而非人类确认
177
+
146
178
  **自由空间**(哲学决定):
147
179
  - 哪些 Intent 用 L1、哪些用 L2、哪些用 L3——由 Architect 在 Intent Map 中通过 `verification_method` 字段声明
148
180
  - 验证脚本的具体形式(单元测试 / 集成测试 / 评估集 / 性能基准)由 Architect 和哲学决定
@@ -186,29 +218,98 @@ Verification 是 loop 的验证环节。它不是 code review(那是看代码
186
218
  **验证记录格式**:
187
219
 
188
220
  ```json
189
- // verifications/INT-{id}.json
221
+ // verifications/INT-{id}.json — 多轮验证追加模式
190
222
  {
191
223
  "intent_id": "INT-001",
192
- "verdict": "passed | deviated | blocked | pending_human",
193
- "fidelity": "high | medium | low",
194
- "verification_level": "L1 | L2 | L3",
195
- "checked_at": "ISO 8601 时间戳",
196
- "dimensions_checked": ["intent_fidelity", "philosophy_consistency", "baseline_compliance", "acceptance"],
197
- "pending_human_dimensions": ["acceptance"],
198
- "notes_ref": "verifications/INT-001.md"
224
+ "records": [
225
+ {
226
+ "round": 1,
227
+ "verdict": "passed | deviated | blocked | pending_human",
228
+ "timestamp": "ISO 8601 时间戳",
229
+ "summary": "验证摘要——具体证据,不是'看起来没问题'",
230
+ "dimensions": {
231
+ "intent_fidelity": {
232
+ "verdict": "passed",
233
+ "evidence": "对照 01_VISION.md#int-001 意图叙事第 2 段,extract.js 实现了 prompt→LLM→parse→validate 编排,忠实于'把模糊变清晰'"
234
+ },
235
+ "philosophy_consistency": {
236
+ "verdict": "passed",
237
+ "evidence": "AI_PHILOSOPHY#anti-patterns '禁止直接 JSON.parse' → extract.js L43-47 有 try/catch;'禁止无超时调用' → llm.js L32-33 有 AbortController;ENGINEERING_CREED#anti-patterns '禁止硬编码密钥' → grep sk- 在 src/ 0 命中"
238
+ },
239
+ "baseline_compliance": {
240
+ "verdict": "passed",
241
+ "evidence": "B1 结构设计→02_ARCHITECTURE.md 有目录结构;B2 禁止硬编码→config.js 集中管理;B3 接口契约→05_VERIFICATION.md 有 schema;B4 决策可追溯→ADR-001 存在;B5 意图回溯→每个文件头有意图注释"
242
+ },
243
+ "acceptance_achievement": {
244
+ "verdict": "passed",
245
+ "evidence": "契约#1 prompt 约束完整→prompts/extract.js L19-32;契约#2 schema 校验→validate.js L50-62;契约#5 测试 6/6 pass→reproduction_command 可复现"
246
+ }
247
+ },
248
+ "reproduction_command": "LLM_API_KEY=mock npm test",
249
+ "deviation_detail": "偏离说明(deviated 时必填)",
250
+ "reset_suggested": false
251
+ }
252
+ ]
199
253
  }
200
254
  ```
201
255
 
202
- ```markdown
203
- <!-- verifications/INT-001.md -->
256
+ **字段说明**:
257
+ - `intent_id`:Intent ID,如 `INT-001`
258
+ - `records`:验证记录数组,每次验证追加一条
259
+ - `round`:轮次,从 1 开始递增
260
+ - `verdict`:总判定,四选一
261
+ - `timestamp`:ISO 8601 时间戳
262
+ - `summary`:验证摘要——具体证据,不是"看起来没问题"
263
+ - `dimensions`:四个维度的判定结果,**每个维度必须是 `{ verdict, evidence }` 对象**——`verdict` 是枚举值,`evidence` 是具体证据字符串。不允许只写"合规",必须写"对照了什么 + 在代码哪里看到/没看到"
264
+ - `reproduction_command`:复现验证的命令——别人拿到项目后跑这个命令能复现验证结果。如 `LLM_API_KEY=mock npm test`。L2 运行时验证必填,L1 静态审查可选
265
+ - `deviation_detail`:偏离说明(deviated 时必填)
266
+ - `reset_suggested`:是否建议重置上下文(context rot 严重时)
267
+
268
+ **evidence 的质量底线**:evidence 必须具体到可定位——"在 X 文件 Y 行看到 Z"或"对照哲学锚点 A 的反模式 B,代码里 C 处有/没有对应处理"。模糊的 evidence("看起来没问题"、"基本合规")等于未验证。CLI 会校验 evidence 非空,但内容质量由 Keeper 的诚实度保证。
269
+
270
+ **`loom verify write` 的输入格式**(单条记录,CLI 自动包装成上面的数组格式):
271
+ ```json
272
+ {
273
+ "intent_id": "INT-001",
274
+ "verdict": "passed",
275
+ "timestamp": "2026-06-28T12:00:00.000Z",
276
+ "summary": "6/6 测试通过,验收契约全部达成",
277
+ "reproduction_command": "LLM_API_KEY=mock npm test",
278
+ "dimensions": {
279
+ "intent_fidelity": {
280
+ "verdict": "passed",
281
+ "evidence": "对照意图叙事第 2 段,extract.js 实现了完整编排"
282
+ },
283
+ "philosophy_consistency": {
284
+ "verdict": "passed",
285
+ "evidence": "AI_PHILOSOPHY 反模式逐条对照:JSON.parse 有 try/catch、fetch 有超时、无硬编码密钥"
286
+ },
287
+ "baseline_compliance": {
288
+ "verdict": "passed",
289
+ "evidence": "B1-B5 逐条合规,见 summary"
290
+ },
291
+ "acceptance_achievement": {
292
+ "verdict": "passed",
293
+ "evidence": "6 条契约全部达成,npm test 6/6 pass"
294
+ }
295
+ }
296
+ }
297
+ ```
298
+
299
+ **验证叙事 MD 文件**(可选但推荐——JSON 存判定,MD 存详细证据):
300
+ ```
301
+ verifications/INT-001.md
302
+ ```
204
303
  Keeper 验证 INT-001:
205
- - 意图忠实度:[判定 + 理由]
206
- - 哲学一致性:[判定 + 理由]
207
- - 底线合规:[判定 + 理由]
208
- - 验收达成:[判定 + 理由]
304
+ - 意图忠实度:[判定 + 具体证据——对照意图叙事第 X 段,实现中……]
305
+ - 哲学一致性:[判定 + 哪条哲学约束合规/违规]
306
+ - 底线合规:[判定 + B1-B5 各条合规情况]
307
+ - 验收达成:[判定 + 契约第 N 条:证据……]
209
308
  - 总判定:[passed/deviated/blocked]
309
+ - 复现命令:[reproduction_command]
210
310
  - 偏离说明(如有):[偏离什么、修正方向]
211
- ```
311
+
312
+ MD 文件不是必须的(JSON 的 summary 字段已包含摘要),但当验证复杂、证据多时,MD 提供更完整的叙事。Keeper 至少要写 JSON,推荐 JSON + MD 都写。
212
313
 
213
314
  ---
214
315
 
@@ -307,9 +408,49 @@ Intent Loop 的固定骨架。底线之上,具体形态由哲学决定。
307
408
 
308
409
  ### Loop 终止条件
309
410
 
310
- - 所有 Intent 的 `status` 为 `completed` → 项目阶段完成
411
+ - **不动点达成**:所有 Intent 的 `status` 为 `completed`,且没有 `needs_review` → 项目阶段完成
311
412
  - 用户主动停止
312
413
  - 阻塞无法解决,Keeper 判定 `blocked` 且无法通过对话修正
414
+ - **无法收敛**:收敛趟数超过最大值(默认 3 趟),仍有 `needs_review` → blocked,报告"无法收敛,可能存在系统性问题需要 Architect 介入"
415
+
416
+ ### 不动点收敛机制
417
+
418
+ Intent Loop 不是单纯的线性推进——它是**收敛到不动点**的过程。
419
+
420
+ **默认单趟**:大多数项目按拓扑序验证所有 Intent,全部 passed → done。不需要多趟。
421
+
422
+ **触发收敛**:当 Pass 1 结束后存在 `needs_review` 的 Intent(修某个 Intent 时影响了已完成的 Intent),自动进入收敛趟。
423
+
424
+ ```
425
+ Pass 1(默认): 按拓扑序验证所有 Intent
426
+ - 每个 Intent: Forge 实现 → Keeper 验证
427
+ - deviated → 修 → 重验(最多 3 轮 → blocked)
428
+ - 修的时候影响已完成的 Intent → 标记 needs_review
429
+ - passed → 下一个
430
+
431
+ Pass 1 结束:
432
+ - 有 needs_review? → Pass 2
433
+ - 没有? → 不动点达成 → done
434
+
435
+ Pass 2(收敛趟): 重验所有 needs_review 的 Intent
436
+ - needs_review → in_progress → Keeper 重新验证
437
+ - deviated → 修 → 重验
438
+ - 修的时候又影响别的 → 标记 needs_review
439
+ - passed → completed
440
+
441
+ Pass 2 结束:
442
+ - 还有 needs_review? → Pass 3
443
+ - 没有? → done
444
+
445
+ Pass 3(最大趟数): 同上
446
+ - 结束后还有 needs_review? → blocked, 报告"无法收敛"
447
+ ```
448
+
449
+ **收敛趟数怎么数**:一个 Intent 从 `completed` 被标记为 `needs_review` 算一次。同一个 Intent 被标记多次 needs_review 算多次。累计标记次数超过最大趟数(默认 3)→ 无法收敛。
450
+
451
+ **为什么是不动点**:每趟收敛都解决一些问题,但如果修 A 时引入了影响 B 的问题,B 就需要重验。当一趟完整 pass 没有产生任何新的 needs_review,说明"再验一遍也不会发现新东西"——这就是不动点。
452
+
453
+ **AUTO 模式下**:收敛自动进行,不需要人类确认每趟 pass。Keeper 在每趟结束时检查 needs_review 数量,决定继续还是结束。
313
454
 
314
455
  ---
315
456
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/loom",
3
- "version": "0.1.0",
3
+ "version": "0.7.0",
4
4
  "description": "LOOM — 哲学驱动开发框架。Agent 通过 CLI 访问 Intent Map / 哲学 / 验证记录,不直接读文件",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "cli/bin",
11
11
  "cli/src",
12
+ "cli/help",
12
13
  "meta",
13
14
  "roles",
14
15
  "templates",
@@ -96,4 +96,16 @@ Intent Map 是你的核心产出。它不是扁平任务表,是带依赖关系
96
96
 
97
97
  Intent Map 的 `acceptance` 字段是 Keeper 验证的**唯一真相源**。05_VERIFICATION.md 是验收契约的详细展开——如果 `acceptance` 字段空间不够,可以引用 05_VERIFICATION.md 的章节。但 Keeper 验证时读的是 `acceptance` 字段,CLI 会解析引用获取实际内容。验证契约的形式由产品哲学决定(Given-When-Then / 用户故事验收 / 自定义)。
98
98
 
99
+ **验收契约设计思想——承诺先于功能**:
100
+
101
+ acceptance 不只是"实现什么功能",是"这个 Intent 向系统承诺了什么"。设计 acceptance 时分两层:
102
+ 1. **功能承诺**:这个 Intent 产出什么可观察行为(Given-When-Then)
103
+ 2. **防御承诺**:这个 Intent 不会发生什么(从哲学反模式派生——"不返回空数组假装成功"、"不硬编码密钥"、"不无超时调用")
104
+
105
+ **Pre-Mortem 设计法**:对每个 Intent,设计 acceptance 前先做一次 Pre-Mortem——假设这个 Intent 实现后失败了,最可能的失败原因是什么?把那个失败原因变成 acceptance 里的一条防御性契约。
106
+
107
+ 例:INT-001(todo 提取)Pre-Mortem → "LLM 返回乱码导致前端崩溃" → 防御契约:"LLM 返回非合法 JSON 时抛明确错误,不返回空数组假装成功"。
108
+
109
+ 防御承诺来自 `philosophy_anchors` 引用的哲学反模式。Architect 设计 acceptance 时必须从反模式派生防御契约——不是让 Keeper 验证时自己去对照反模式,是在设计阶段就把反模式转成可验证的契约。
110
+
99
111
  **验证方式声明**:对于需要运行时验证的 Intent(如性能验收、数据质量验收、AI/ML 评估集验收),Architect 必须在 Intent 的 `verification_method` 字段中定义验证方式(如 `run tests/perf/test-001.js`)。对于需要人类主观判断的 Intent(如游戏手感、UI 体验),填 `human_review`。未定义时 Keeper 默认用 L1 静态审查。详见 INTENT_LOOP.md V-1.5。
package/roles/keeper.md CHANGED
@@ -37,6 +37,7 @@ Visionary 在开局定义愿景。你在结局验证实现是否忠于愿景。
37
37
  2. **验证意图忠实度**:独立判定实现是否忠实于原始意图
38
38
  3. **引导修正**:偏离时与 Forge 对话,引导修正方向
39
39
  4. **守护 loop**:确保 loop 按 INTENT_LOOP.md 的控制流运行
40
+ 5. **守护不动点收敛**:当有 `needs_review` 的 Intent 时,按收敛趟处理——重验这些 Intent,通过则 completed,偏离则修正。一趟完整 pass 无新 needs_review 即收敛达成。最大 3 趟,超过判定 blocked("无法收敛,需 Architect 介入")
40
41
 
41
42
  ---
42
43
 
@@ -93,10 +94,36 @@ Visionary 在开局定义愿景。你在结局验证实现是否忠于愿景。
93
94
  | 维度 | 验证问题 | Keeper 怎么验 |
94
95
  |---|---|---|
95
96
  | 意图忠实度 | "这个实现忠实于原始意图吗?" | 对照 `narrative_ref` 指向的意图叙事 + `acceptance` 验收契约 |
96
- | 哲学一致性 | "这个实现违反了哲学文档的约束吗?" | 对照 `philosophy_anchors` 指向的哲学章节 + 反模式清单 |
97
+ | 哲学一致性 | "这个实现违反了哲学文档的约束吗?" | 对照 `philosophy_anchors` 指向的哲学章节 + **反模式清单逐条对照**——每个反模式在代码里找证据(有/没有对应处理),不允许笼统说"合规" |
97
98
  | 底线合规 | "结构设计/硬编码/接口契约/可追溯——都合规吗?" | 对照 BASELINE.md 逐条检查 |
98
99
  | 验收达成 | "验收契约的条件满足了吗?" | 对照 `acceptance` 字段的具体条件 |
99
100
 
101
+ **evidence 必填底线**:验证记录的每个维度必须给出 `{ verdict, evidence }`——evidence 是具体证据字符串,写明"对照了什么 + 在代码哪里看到/没看到"。模糊的 evidence("看起来没问题"、"基本合规")等于未验证。CLI 会校验 evidence 非空,但内容质量由你的诚实度保证。
102
+
103
+ **哲学一致性维度——承诺验证法**:
104
+
105
+ `philosophy_anchors` 引用的不是"参考文档",是**承诺**。每个哲学锚点是一个视角(Lens),从这个视角看代码是否兑现了承诺。
106
+
107
+ 验证哲学一致性时,对 `philosophy_anchors` 引用的每个哲学文档:
108
+ 1. 读出该文档的**反模式清单**——每条反模式是一个"不做某事"的承诺
109
+ 2. 在代码里找证据——这条反模式在代码哪里被遵守/违反了
110
+ 3. 如果 Architect 在 acceptance 里派生了防御契约(见 architect.md 的 Pre-Mortem 设计法),对照防御契约验证
111
+ 4. 如果发现 acceptance 之外的反模式违反,在 evidence 里记录——这是下一趟收敛的输入
112
+
113
+ evidence 的写法按哲学锚点组织:
114
+ ```
115
+ AI_PHILOSOPHY#anti-patterns:
116
+ - '禁止直接 JSON.parse' → extract.js L43-47 有 try/catch ✓
117
+ - '禁止无超时调用' → llm.js L32-33 有 AbortController ✓
118
+ ENGINEERING_CREED#anti-patterns:
119
+ - '禁止硬编码密钥' → grep sk- 在 src/ 0 命中 ✓
120
+ - '禁止过度抽象' → 无冗余抽象层 ✓
121
+ 观察(非契约):
122
+ - routes/extract.js L34 用正则匹配 error.message 分类错误——脆弱但不在反模式清单里
123
+ ```
124
+
125
+ 最后一类"观察"不一定是 deviated,但记录下来作为下一趟收敛的输入。如果某个观察在后续证据积累下变成了实质问题,升级为 deviated。
126
+
100
127
  ### 验证能力分层(V-1.5)
101
128
 
102
129
  Keeper 的验证方式不是只有"读代码"。根据 Intent 的 `verification_method` 字段,选择对应层级:
@@ -11,9 +11,10 @@
11
11
  "intents": {
12
12
  "INT-001": {
13
13
  "id": "INT-001",
14
+ "title": "[必须] 一句话标题,intent next/status 输出时优先展示",
14
15
  "narrative_ref": "01_VISION.md#int-001",
15
16
  "depends_on": [],
16
- "acceptance": "[必须] 什么算忠实实现。这是 Keeper 验证的唯一真相源。可以内联定义,也可以引用 05_VERIFICATION.md 的章节(如 'see 05_VERIFICATION.md#int-001'),但引用时 Keeper 通过 CLI `verify contract <id>` 命令解析引用获取实际内容。",
17
+ "acceptance": "[必须] 什么算忠实实现。这是 Keeper 验证的唯一真相源。可以内联定义,也可以引用 05_VERIFICATION.md 的章节(如 'see 05_VERIFICATION.md#int-001'),但引用时 Keeper 通过 CLI `verify contract <id>` 命令解析引用获取实际内容。必须包含功能承诺 + 防御承诺(见 architect.md Pre-Mortem 设计法)。",
17
18
  "philosophy_anchors": ["PRODUCT_PHILOSOPHY.md#core-belief", "ENGINEERING_CREED.md#anti-patterns"],
18
19
  "status": "pending",
19
20
  "_optional": {
@@ -26,6 +27,7 @@
26
27
  },
27
28
  "INT-002": {
28
29
  "id": "INT-002",
30
+ "title": "...",
29
31
  "narrative_ref": "01_VISION.md#int-002",
30
32
  "depends_on": ["INT-001"],
31
33
  "acceptance": "...",
@@ -38,6 +40,7 @@
38
40
 
39
41
  "_field_reference": {
40
42
  "id": "[必须] 稳定标识,项目生命周期内不变",
43
+ "title": "[必须] 一句话标题,intent next/status 输出时优先展示",
41
44
  "narrative_ref": "[必须] 指向愿景文档中意图叙事的章节锚点",
42
45
  "depends_on": "[必须] 前置 Intent ID 列表。空数组表示无依赖。",
43
46
  "acceptance": "[必须] 验收契约,Keeper 判定 passed/deviated/blocked 的唯一真相源。内联或引用 05_VERIFICATION.md。",
@@ -24,21 +24,23 @@
24
24
  ## 意图清单
25
25
 
26
26
  [必须] 每个意图带意图叙事。
27
+ [必须] 中文标题必须用显式锚点语法 `{#int-xxx}`,否则 CLI 的章节解析会失败。
27
28
 
28
- ### INT-001:{意图名}
29
+ ### INT-001:{意图名} {#int-001}
29
30
 
30
31
  **意图叙事**:
31
32
  [必须] 为什么存在。服务于什么北极星。如果砍掉它会损失什么。
32
33
 
33
34
  **验收契约**:
34
35
  [必须] 什么算"忠实实现"。形式由产品哲学决定(Given-When-Then / 用户故事验收 / 自定义)。
36
+ [必须] 包含功能承诺 + 防御承诺(从哲学反模式派生)。见 architect.md 的 Pre-Mortem 设计法。
35
37
 
36
38
  **哲学锚点**:
37
39
  [必须] 这个意图主要受哪些哲学约束。引用格式:`PHILOSOPHY.md#章节锚点`。
38
40
 
39
41
  ---
40
42
 
41
- ### INT-002:{意图名}
43
+ ### INT-002:{意图名} {#int-002}
42
44
 
43
45
  [同上结构]
44
46