@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.
- package/LICENSE +21 -0
- package/README.md +87 -52
- package/cli/bin/loom.js +285 -99
- package/cli/help/concepts.md +93 -72
- package/cli/help/doctor.md +71 -121
- package/cli/help/loop.md +120 -135
- package/cli/help/patch.md +33 -0
- package/cli/help/preview.md +2 -1
- package/cli/help/version.md +92 -16
- package/cli/help/workflow.md +89 -100
- package/cli/src/activate.js +302 -73
- package/cli/src/diagnostics.js +138 -41
- package/cli/src/guide.js +41 -19
- package/cli/src/init.js +50 -29
- package/cli/src/intent-draft.js +303 -0
- package/cli/src/intent-map.js +540 -54
- package/cli/src/patch.js +214 -0
- package/cli/src/philosophy.js +177 -154
- package/cli/src/preview-prompt.md +13 -6
- package/cli/src/preview.js +1 -0
- package/cli/src/shared/intent-ref.js +38 -0
- package/cli/src/shared/proof-reference.js +19 -0
- package/cli/src/shared/verification-method.js +32 -0
- package/cli/src/verify.js +184 -61
- package/cli/src/version.js +5 -4
- package/dimensions/PART_DECOMPOSITION.md +42 -203
- package/dimensions/SEARCH_METHODOLOGY.md +101 -97
- package/dimensions/examples/AGENT_SYSTEM/README.md +1 -1
- package/dimensions/examples/CLI_TOOL/README.md +1 -1
- package/dimensions/universal/COLLABORATION_PHILOSOPHY.md +28 -77
- package/dimensions/universal/ENGINEERING_CREED.md +30 -74
- package/dimensions/universal/PRODUCT_PHILOSOPHY.md +32 -70
- package/meta/BASELINE.md +91 -276
- package/meta/INTENT_LOOP.md +242 -737
- package/meta/PHILOSOPHY_WEAVER.md +110 -343
- package/meta/ROLE_ACTIVATION.md +103 -267
- package/package.json +4 -3
- package/roles/architect.md +71 -111
- package/roles/forge.md +87 -126
- package/roles/keeper.md +99 -223
- package/roles/visionary.md +57 -86
- package/templates/INTENT_MAP_TEMPLATE.json +24 -10
- package/templates/PHILOSOPHY_TEMPLATE.md +44 -75
- package/templates/VISION_TEMPLATE.md +44 -67
package/cli/src/activate.js
CHANGED
|
@@ -1,73 +1,302 @@
|
|
|
1
|
-
// activate —
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const VALID_ROLES = ['weaver', 'visionary', 'architect', 'forge', 'keeper'];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
1
|
+
// activate — compile a role- and intent-scoped Context Pack.
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { getLoomRoot } from './shared/paths.js';
|
|
6
|
+
import { getIntent, getNarrative } from './intent-map.js';
|
|
7
|
+
import { getIntentDraft } from './intent-draft.js';
|
|
8
|
+
import { getPhilosophy } from './philosophy.js';
|
|
9
|
+
import { getVerificationContract } from './verify.js';
|
|
10
|
+
import { extractMdSection } from './shared/md-utils.js';
|
|
11
|
+
|
|
12
|
+
const VALID_ROLES = ['weaver', 'visionary', 'architect', 'forge', 'keeper'];
|
|
13
|
+
|
|
14
|
+
const ROLE_FILES = {
|
|
15
|
+
weaver: 'meta/PHILOSOPHY_WEAVER.md',
|
|
16
|
+
visionary: 'roles/visionary.md',
|
|
17
|
+
architect: 'roles/architect.md',
|
|
18
|
+
forge: 'roles/forge.md',
|
|
19
|
+
keeper: 'roles/keeper.md',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ROLE_PHILOSOPHY_FILES = {
|
|
23
|
+
visionary: ['PRODUCT_PHILOSOPHY.md', 'DECISION_RUBRIC.md'],
|
|
24
|
+
architect: ['ENGINEERING_CREED.md', 'DECISION_RUBRIC.md'],
|
|
25
|
+
forge: ['ENGINEERING_CREED.md'],
|
|
26
|
+
keeper: ['PRODUCT_PHILOSOPHY.md', 'DECISION_RUBRIC.md'],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const BASELINE_SUMMARY = [
|
|
30
|
+
'- B1:实质修改前理解真实结构,不创建平行体系。',
|
|
31
|
+
'- B2:秘密、环境值和可变配置不写死。',
|
|
32
|
+
'- B3:用户或系统可观察的行为具有显式契约。',
|
|
33
|
+
'- B4:重要且会影响未来的判断可追溯。',
|
|
34
|
+
'- B5:完成关联原始意图和当前 revision 证据;质量提升具有基线相对证据。',
|
|
35
|
+
].join('\n');
|
|
36
|
+
|
|
37
|
+
function section(title, body) {
|
|
38
|
+
const content = String(body || '').trim();
|
|
39
|
+
return `## ${title}\n\n${content || '无。'}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readRole(role) {
|
|
43
|
+
const filePath = join(getLoomRoot(), ROLE_FILES[role]);
|
|
44
|
+
if (!existsSync(filePath)) throw new Error(`角色文件不存在: ${filePath}`);
|
|
45
|
+
return readFileSync(filePath, 'utf-8');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getVerificationMethod(intent) {
|
|
49
|
+
return intent?.verification_method || intent?._optional?.verification_method || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveContractReference(versionDir, value, label) {
|
|
53
|
+
if (!value) return null;
|
|
54
|
+
if (typeof value !== 'string') return JSON.stringify(value, null, 2);
|
|
55
|
+
const match = value.trim().match(/^(?:see\s+)?([^#]+)#([\w-]+)$/i);
|
|
56
|
+
if (!match) return value;
|
|
57
|
+
const [, file, anchor] = match;
|
|
58
|
+
if (file.includes('/') || file.includes('\\') || file !== '05_VERIFICATION.md') {
|
|
59
|
+
throw new Error(`${label}引用必须位于 05_VERIFICATION.md: ${value}`);
|
|
60
|
+
}
|
|
61
|
+
const filePath = join(versionDir, file);
|
|
62
|
+
if (!existsSync(filePath)) throw new Error(`${label}引用的文件不存在: ${filePath}`);
|
|
63
|
+
return extractMdSection(readFileSync(filePath, 'utf-8'), anchor, label);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function compileEnvelope(role, intentId) {
|
|
67
|
+
const scope = intentId ? `仅 ${intentId}` : '当前角色的项目阶段';
|
|
68
|
+
const lines = [
|
|
69
|
+
`- role: ${role}`,
|
|
70
|
+
`- scope: ${scope}`,
|
|
71
|
+
'- 本 Context Pack 不会清除现有会话记忆。',
|
|
72
|
+
'- system、developer 与用户指令优先;项目事实冲突时报告,不静默混用。',
|
|
73
|
+
'- 只在当前角色权限和明确作用域内行动。',
|
|
74
|
+
];
|
|
75
|
+
if (role === 'keeper') {
|
|
76
|
+
lines.push('- Keeper 必须在新的 Agent thread 中运行;同一会话切换角色不构成独立验证。');
|
|
77
|
+
lines.push('- 无法获得独立上下文时降低声明,关键判断使用 pending_human。');
|
|
78
|
+
}
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function compileObjective(role, versionDir, intentId) {
|
|
83
|
+
if (!intentId) {
|
|
84
|
+
return {
|
|
85
|
+
body: role === 'weaver'
|
|
86
|
+
? '织造当前项目的 Project Doctrine。先读取真实项目与完整 BASELINE,不预写产品或架构。'
|
|
87
|
+
: `履行 ${role} 的当前项目阶段职责;未指定 Intent,不得自行选择并处理实现任务。`,
|
|
88
|
+
intent: null,
|
|
89
|
+
draft: null,
|
|
90
|
+
narrative: null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (role === 'visionary' || role === 'architect') {
|
|
95
|
+
const draft = getIntentDraft(versionDir, intentId);
|
|
96
|
+
const match = draft.narrative_ref.match(/^([^#]+)#([\w-]+)$/);
|
|
97
|
+
if (!match || match[1] !== '01_VISION.md') throw new Error(`draft narrative_ref 非法: ${draft.narrative_ref}`);
|
|
98
|
+
const narrative = extractMdSection(
|
|
99
|
+
readFileSync(join(versionDir, match[1]), 'utf-8'),
|
|
100
|
+
match[2],
|
|
101
|
+
'draft 意图叙事',
|
|
102
|
+
);
|
|
103
|
+
const visibleDraft = role === 'visionary'
|
|
104
|
+
? {
|
|
105
|
+
id: draft.id,
|
|
106
|
+
revision: draft.revision,
|
|
107
|
+
title: draft.title,
|
|
108
|
+
narrative_ref: draft.narrative_ref,
|
|
109
|
+
depends_on: draft.depends_on,
|
|
110
|
+
}
|
|
111
|
+
: draft;
|
|
112
|
+
return {
|
|
113
|
+
body: [
|
|
114
|
+
'只处理下面这个 draft。不要修改官方 Intent Map,不要处理其他 Intent,不要自行 finalize。',
|
|
115
|
+
'',
|
|
116
|
+
'### Draft',
|
|
117
|
+
'',
|
|
118
|
+
'```json',
|
|
119
|
+
JSON.stringify(visibleDraft, null, 2),
|
|
120
|
+
'```',
|
|
121
|
+
'',
|
|
122
|
+
'### Narrative',
|
|
123
|
+
'',
|
|
124
|
+
narrative,
|
|
125
|
+
].join('\n'),
|
|
126
|
+
intent: null,
|
|
127
|
+
draft,
|
|
128
|
+
narrative,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const intent = getIntent(versionDir, intentId);
|
|
133
|
+
const narrative = getNarrative(versionDir, intentId);
|
|
134
|
+
const objectiveView = {
|
|
135
|
+
id: intent.id,
|
|
136
|
+
revision: intent.revision,
|
|
137
|
+
title: intent.title,
|
|
138
|
+
status: intent.status,
|
|
139
|
+
narrative_ref: intent.narrative_ref,
|
|
140
|
+
depends_on: intent.depends_on,
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
body: [
|
|
144
|
+
'只实现或验证下面这个官方 Intent。不得加载或顺便处理其他 Intent。',
|
|
145
|
+
'',
|
|
146
|
+
'### Intent',
|
|
147
|
+
'',
|
|
148
|
+
'```json',
|
|
149
|
+
JSON.stringify(objectiveView, null, 2),
|
|
150
|
+
'```',
|
|
151
|
+
'',
|
|
152
|
+
'### Narrative',
|
|
153
|
+
'',
|
|
154
|
+
narrative,
|
|
155
|
+
].join('\n'),
|
|
156
|
+
intent,
|
|
157
|
+
draft: null,
|
|
158
|
+
narrative,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function compileInvariants(role, versionDir) {
|
|
163
|
+
const blocks = [];
|
|
164
|
+
const baselinePath = join(getLoomRoot(), 'meta/BASELINE.md');
|
|
165
|
+
blocks.push(role === 'weaver' ? readFileSync(baselinePath, 'utf-8') : BASELINE_SUMMARY);
|
|
166
|
+
if (versionDir) {
|
|
167
|
+
const projectBaseline = join(versionDir, '00_PHILOSOPHY', 'PROJECT_BASELINE.md');
|
|
168
|
+
if (existsSync(projectBaseline)) {
|
|
169
|
+
blocks.push(`### Project Baseline\n\n${readFileSync(projectBaseline, 'utf-8')}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return blocks.join('\n\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function compileContracts(role, versionDir, objective) {
|
|
176
|
+
const subject = objective.intent || objective.draft;
|
|
177
|
+
if (!subject || role === 'visionary') {
|
|
178
|
+
return role === 'visionary'
|
|
179
|
+
? 'Visionary 只定义目标、非目标和 narrative;不要编写 acceptance 或架构。'
|
|
180
|
+
: '按当前角色 Output Contract 交付。';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const blocks = [];
|
|
184
|
+
if (subject.acceptance) {
|
|
185
|
+
const acceptance = objective.intent
|
|
186
|
+
? getVerificationContract(versionDir, subject.id)
|
|
187
|
+
: resolveContractReference(versionDir, subject.acceptance, 'draft 完成契约');
|
|
188
|
+
blocks.push(`### Acceptance / Reliability Floor\n\n${acceptance}`);
|
|
189
|
+
}
|
|
190
|
+
if (subject.quality_contract) {
|
|
191
|
+
blocks.push(
|
|
192
|
+
`### Quality Contract / Distinctive Ceiling\n\n` +
|
|
193
|
+
resolveContractReference(versionDir, subject.quality_contract, '质量契约'),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
const verificationMethod = getVerificationMethod(subject);
|
|
197
|
+
blocks.push(`### Verification Method\n\n${verificationMethod || '未声明:Keeper 使用适当的静态检查;需要运行时或人类判断却缺少入口时回流 Architect。'}`);
|
|
198
|
+
const continuity = subject.continuity_required
|
|
199
|
+
? '此 Intent 已启用状态守恒门:通过前必须证明“旧状态 → 本轮操作 → 新状态”的完整序列中,未获明确授权删除或替换的既有价值、数据和可见结果仍被保留。'
|
|
200
|
+
: '默认不启用状态守恒门;若本 Intent 会变更既有用户数据、持久状态、迁移结果或既有工作流,Architect 必须把 continuity_required 设为 true,并在 acceptance 写出保留项与时序验证。';
|
|
201
|
+
blocks.push(
|
|
202
|
+
'### Completion Gate / Codex Goal Alignment\n\n' +
|
|
203
|
+
'将当前 Intent 视为本轮 Codex goal 的可闭合单元。goal 保持 active,直到结果达成、适用的状态守恒和可复现证据同时成立;goal/status 只是运行记录,不能替代验证证据。\n\n' +
|
|
204
|
+
continuity,
|
|
205
|
+
);
|
|
206
|
+
return blocks.join('\n\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function compileProjectJudgment(role, versionDir, objective) {
|
|
210
|
+
if (!versionDir || role === 'weaver') {
|
|
211
|
+
return role === 'weaver'
|
|
212
|
+
? '从用户目标、仓库事实与决策相关证据建立 Project Doctrine。'
|
|
213
|
+
: '当前版本目录不可用。';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const philosophyDir = join(versionDir, '00_PHILOSOPHY');
|
|
217
|
+
const blocks = [];
|
|
218
|
+
const missing = [];
|
|
219
|
+
const anchors = (objective.intent || objective.draft)?.philosophy_anchors || [];
|
|
220
|
+
|
|
221
|
+
if (anchors.length > 0) {
|
|
222
|
+
for (const anchor of anchors) {
|
|
223
|
+
blocks.push(`### ${anchor}\n\n${getPhilosophy(philosophyDir, anchor)}`);
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
for (const file of ROLE_PHILOSOPHY_FILES[role] || []) {
|
|
227
|
+
const filePath = join(philosophyDir, file);
|
|
228
|
+
if (!existsSync(filePath)) {
|
|
229
|
+
missing.push(file);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
blocks.push(`### ${file}\n\n${readFileSync(filePath, 'utf-8')}`);
|
|
233
|
+
}
|
|
234
|
+
if (missing.length) {
|
|
235
|
+
blocks.push(
|
|
236
|
+
`### Context Warning\n\n缺少: ${missing.join(', ')}。` +
|
|
237
|
+
'不要用通用偏好伪造项目判断;回流 Weaver 或向用户索取必要决定。',
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return blocks.join('\n\n');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function compileExpertiseInputs(role, objective) {
|
|
245
|
+
const subject = objective.intent || objective.draft;
|
|
246
|
+
if (!subject || !['architect', 'forge', 'keeper'].includes(role)) {
|
|
247
|
+
return '当前阶段不编译任务级 Expertise Pack。';
|
|
248
|
+
}
|
|
249
|
+
const needs = Array.isArray(subject.capability_needs) ? subject.capability_needs : [];
|
|
250
|
+
const lines = [
|
|
251
|
+
`- capability_needs: ${needs.length ? needs.join(', ') : '未声明;按当前任务发现必要能力'}`,
|
|
252
|
+
`- creative_scope: ${subject.creative_scope || '未声明;遵循最小完整干预'}`,
|
|
253
|
+
'- Skill、工具和资产名称只代表可发现入口;实际检查并加载后才进入 Expertise Pack。',
|
|
254
|
+
];
|
|
255
|
+
if (role === 'keeper') {
|
|
256
|
+
lines.push('- 不继承 Forge Expertise Pack;按契约独立准备验证能力。');
|
|
257
|
+
}
|
|
258
|
+
return lines.join('\n');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function compileWorkingFacts(versionDir, objective) {
|
|
262
|
+
if (!versionDir) return '检查当前仓库、用户输入和可用工具。';
|
|
263
|
+
const subject = objective.intent || objective.draft;
|
|
264
|
+
const refs = [
|
|
265
|
+
'- architecture: `.loom/.../02_ARCHITECTURE.md`(只读取与当前决定相关部分)',
|
|
266
|
+
'- artifacts: 从真实工作区检查,不从会话记忆猜测。',
|
|
267
|
+
];
|
|
268
|
+
const systemId = subject?._optional?.system_id || subject?.system_id;
|
|
269
|
+
if (systemId) refs.push(`- system_id: ${systemId}`);
|
|
270
|
+
if (subject?.quality_contract) refs.push('- baseline: 声明相对提升时,修改前证据必须在实现前保存。');
|
|
271
|
+
return refs.join('\n');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Compile a role-scoped Context Pack.
|
|
276
|
+
* @param {string} role
|
|
277
|
+
* @param {string | null} versionDir
|
|
278
|
+
* @param {string | null} intentId
|
|
279
|
+
*/
|
|
280
|
+
export function activateRole(role, versionDir, intentId = null) {
|
|
281
|
+
if (!VALID_ROLES.includes(role)) {
|
|
282
|
+
throw new Error(`未知角色: ${role}\n合法角色: ${VALID_ROLES.join(', ')}`);
|
|
283
|
+
}
|
|
284
|
+
if (intentId && !versionDir) throw new Error('--intent 需要当前 LOOM 版本');
|
|
285
|
+
if (intentId && !['visionary', 'architect', 'forge', 'keeper'].includes(role)) {
|
|
286
|
+
throw new Error(`角色 ${role} 不支持 --intent;draft 用 visionary/architect,官方 Intent 用 forge/keeper`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const objective = compileObjective(role, versionDir, intentId);
|
|
290
|
+
const parts = [
|
|
291
|
+
'# LOOM Context Pack',
|
|
292
|
+
section('1. Execution Envelope', compileEnvelope(role, intentId)),
|
|
293
|
+
section('2. Active Objective', objective.body),
|
|
294
|
+
section('3. Hard Invariants', compileInvariants(role, versionDir)),
|
|
295
|
+
section('4. Success Contracts', compileContracts(role, versionDir, objective)),
|
|
296
|
+
section('5. Project Judgment', compileProjectJudgment(role, versionDir, objective)),
|
|
297
|
+
section('6. Expertise Inputs', compileExpertiseInputs(role, objective)),
|
|
298
|
+
section('7. Working Facts', compileWorkingFacts(versionDir, objective)),
|
|
299
|
+
section('8. Role Contract / Output / Reflow / Stop', readRole(role)),
|
|
300
|
+
];
|
|
301
|
+
return `${parts.join('\n\n---\n\n')}\n`;
|
|
302
|
+
}
|
package/cli/src/diagnostics.js
CHANGED
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
// 全部是只读的数据聚合,不做决策、不修改文件。
|
|
4
4
|
|
|
5
5
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
-
import { join } from 'node:path';
|
|
6
|
+
import { basename, dirname, join } from 'node:path';
|
|
7
7
|
import { loadIntentMap, getStatus, getNextIntent, getNarrative, getIntent } from './intent-map.js';
|
|
8
|
-
import { getPhilosophy, validateInspirationSources
|
|
9
|
-
import { getVerificationHistory, getPendingVerifications, getVerificationContract } from './verify.js';
|
|
8
|
+
import { getPhilosophy, validateInspirationSources } from './philosophy.js';
|
|
9
|
+
import { getVerificationHistory, getPendingVerifications, getVerificationContract, getLatestPassedVerification, getVerificationIntentRevision, isVerificationCurrent, hasCurrentPassedVerification } from './verify.js';
|
|
10
|
+
import { validatePatches } from './patch.js';
|
|
11
|
+
import { formatIntentRef } from './shared/intent-ref.js';
|
|
12
|
+
import { commandCoversVerificationMethod, getIntentVerificationMethod } from './shared/verification-method.js';
|
|
13
|
+
import { resolveQualityProofReference } from './shared/proof-reference.js';
|
|
10
14
|
|
|
11
15
|
function readIntentMapRaw(versionDir) {
|
|
12
16
|
const filePath = join(versionDir, '04_INTENT_MAP.json');
|
|
@@ -75,10 +79,6 @@ function intentMapDiagnostics(versionDir) {
|
|
|
75
79
|
return { raw, valid, issues, validMap };
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
function getIntentVerificationMethod(intent) {
|
|
79
|
-
return intent.verification_method || intent._optional?.verification_method || null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
82
|
function normalizeVerificationCommand(command) {
|
|
83
83
|
return String(command || '')
|
|
84
84
|
.replace(/^\s*run\s+/i, '')
|
|
@@ -143,7 +143,8 @@ const FIX_HINTS = {
|
|
|
143
143
|
intent_map_missing: '运行 loom init 或 loom activate architect 产出 04_INTENT_MAP.json',
|
|
144
144
|
intent_map_template: '运行 loom activate architect,Architect 填充真实 Intent Map 后删除 _meta._template 标记',
|
|
145
145
|
intent_map_invalid: '按报错信息修正 04_INTENT_MAP.json 里对应字段(补 title / 加长 acceptance / 填必填字段)',
|
|
146
|
-
completed_no_record: '在 .loom/v{N}/verifications/ 下补验证记录,或运行 loom verify pass {id} --summary "..."',
|
|
146
|
+
completed_no_record: '在 .loom/v{N}/verifications/ 下补验证记录,或运行 loom verify pass {id} --summary "..."',
|
|
147
|
+
completed_verification_not_passed: '最新验证不是当前 revision 的 passed;重新运行 loom verify pass {id} --summary "...",再用 loom intent done {id} 闭合。',
|
|
147
148
|
in_progress_no_record: '运行 loom verify pass {id} --summary "..." 写入验证记录,或 loom intent update {id} --status pending 回退',
|
|
148
149
|
orphan_philosophy_ref: '检查 04_INTENT_MAP.json 里 {id} 的 philosophy_anchors,移除或修正不存在的哲学文件引用',
|
|
149
150
|
orphan_dependency: '检查 04_INTENT_MAP.json 里 {id} 的 depends_on,移除或修正不存在的 Intent ID',
|
|
@@ -152,9 +153,14 @@ const FIX_HINTS = {
|
|
|
152
153
|
completed_depends_blocked: '检查依赖 {dep} 为什么 blocked——解决阻塞或把 {id} 回退到 in_progress',
|
|
153
154
|
test_script_missing: '在 package.json 里加 test 脚本,或修正 verification_method 指向实际存在的测试命令',
|
|
154
155
|
verification_method_unverified: '运行 loom verify pass {id} --summary "..." --reproduction-command "..." 覆盖声明的验证方式',
|
|
155
|
-
verification_method_drift: '验证记录的 reproduction_command 要覆盖 verification_method 声明的命令(支持 npm/pnpm/bun 互相等价)',
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
verification_method_drift: '验证记录的 reproduction_command 要覆盖 verification_method 声明的命令(支持 npm/pnpm/bun 互相等价)',
|
|
157
|
+
stale_verification: '重新验证当前 Intent revision:loom verify pass {id} --summary "...",通过后再闭合 Intent',
|
|
158
|
+
inspiration_source: '在哲学文档的"证据地图/灵感来源"中写入实际使用的来源、选择理由和可追溯位置;数量由判断所需决定',
|
|
159
|
+
quality_dimension_missing: '为带 quality_contract 的 Intent 补写并通过 quality_achievement;相对提升声明在该维度中链接 Quality Proof',
|
|
160
|
+
quality_proof_invalid: '将 quality_proof_ref 改为项目内真实存在且含锚点的 Markdown 证据,例如 verifications/INT-001-quality-proof.md#int-001',
|
|
161
|
+
preservation_dimension_missing: '为 continuity_required 的 Intent 补写并通过 preservation_achievement,证据必须覆盖旧状态到新操作后的完整序列',
|
|
162
|
+
patch_changelog_invalid: '运行 loom patch validate 查看具体错误;修正 06_CHANGELOG.json 后重新生成 Markdown 投影',
|
|
163
|
+
patch_projection_drift: '不要手工编辑 06_CHANGELOG.md;重新运行 loom init 或下一次 loom patch record 生成投影',
|
|
158
164
|
};
|
|
159
165
|
|
|
160
166
|
/**
|
|
@@ -192,16 +198,49 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
192
198
|
|
|
193
199
|
const { intents } = mapState.validMap;
|
|
194
200
|
|
|
195
|
-
// 1. 状态一致性:
|
|
196
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
// 1. 状态一致性:completed 必须由当前 revision 的最后一条 passed 验证支撑。
|
|
202
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
203
|
+
const history = getVerificationHistory(verificationsDir, id);
|
|
204
|
+
const hasRecord = Boolean(history?.records?.length);
|
|
205
|
+
const latest = history?.records?.[history.records.length - 1];
|
|
206
|
+
if (intent.status === 'completed' && !hasRecord) {
|
|
207
|
+
issues.push({ id, type: 'completed_no_record', severity: 'high', msg: `${id} 状态为 completed 但无验证记录` });
|
|
208
|
+
} else if (intent.status === 'completed' && !hasCurrentPassedVerification(intent, history)) {
|
|
209
|
+
issues.push({ id, type: 'completed_verification_not_passed', severity: 'high', msg: `${id} 状态为 completed,但最后一条验证不是当前 revision 的 passed` });
|
|
210
|
+
}
|
|
211
|
+
if (intent.status === 'in_progress' && !hasRecord) {
|
|
212
|
+
issues.push({ id, type: 'in_progress_no_record', severity: 'medium', msg: `${id} 状态为 in_progress 但无验证记录(可能上次中断)` });
|
|
213
|
+
}
|
|
214
|
+
if (intent.quality_contract && latest?.verdict === 'passed') {
|
|
215
|
+
const quality = latest.dimensions?.quality_achievement;
|
|
216
|
+
if (!quality || quality.verdict !== 'passed') {
|
|
217
|
+
issues.push({
|
|
218
|
+
id,
|
|
219
|
+
type: 'quality_dimension_missing',
|
|
220
|
+
severity: 'high',
|
|
221
|
+
msg: `${id} 声明了 quality_contract,但最新 passed 记录缺少通过的 quality_achievement`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (quality?.quality_proof_ref) {
|
|
225
|
+
try {
|
|
226
|
+
resolveQualityProofReference(versionDir, quality.quality_proof_ref);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
issues.push({ id, type: 'quality_proof_invalid', severity: 'high', msg: `${id} 的 Quality Proof 无效: ${error.message}` });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (intent.continuity_required && latest?.verdict === 'passed') {
|
|
233
|
+
const preservation = latest.dimensions?.preservation_achievement;
|
|
234
|
+
if (!preservation || preservation.verdict !== 'passed') {
|
|
235
|
+
issues.push({
|
|
236
|
+
id,
|
|
237
|
+
type: 'preservation_dimension_missing',
|
|
238
|
+
severity: 'high',
|
|
239
|
+
msg: `${id} 声明了 continuity_required,但最新 passed 记录缺少通过的 preservation_achievement`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
205
244
|
|
|
206
245
|
// 2. 孤儿引用:哲学锚点指向不存在的文件
|
|
207
246
|
for (const [id, intent] of Object.entries(intents)) {
|
|
@@ -312,12 +351,42 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
312
351
|
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 声明了 verification_method 但没有验证记录覆盖: ${method}` });
|
|
313
352
|
} else if (!actual) {
|
|
314
353
|
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 最新验证记录缺少 reproduction_command,无法复现 verification_method: ${method}` });
|
|
315
|
-
} else if (!
|
|
354
|
+
} else if (!commandCoversVerificationMethod(actual, expected)) {
|
|
316
355
|
issues.push({ id, type: 'verification_method_drift', severity: 'high', msg: `${id} verification_method 未被最新 reproduction_command 覆盖。method="${method}" reproduction_command="${latest.reproduction_command}"` });
|
|
317
356
|
}
|
|
318
357
|
}
|
|
319
358
|
|
|
320
|
-
|
|
359
|
+
const patchJsonPath = join(versionDir, '06_CHANGELOG.json');
|
|
360
|
+
if (existsSync(patchJsonPath)) {
|
|
361
|
+
try {
|
|
362
|
+
validatePatches(versionDir);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
const projectionDrift = String(error.message).includes('最新确定性投影') || String(error.message).includes('Markdown 投影不存在');
|
|
365
|
+
issues.push({
|
|
366
|
+
id: 'patch_changelog',
|
|
367
|
+
type: projectionDrift ? 'patch_projection_drift' : 'patch_changelog_invalid',
|
|
368
|
+
severity: projectionDrift ? 'medium' : 'high',
|
|
369
|
+
msg: error.message,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// completed/needs_review 的旧 revision 验证不能证明当前 Intent 仍然成立。
|
|
375
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
376
|
+
if (intent.status !== 'completed' && intent.status !== 'needs_review') continue;
|
|
377
|
+
const history = getVerificationHistory(verificationsDir, id);
|
|
378
|
+
const latestPassed = getLatestPassedVerification(history);
|
|
379
|
+
if (!latestPassed || isVerificationCurrent(intent, latestPassed)) continue;
|
|
380
|
+
const verifiedRevision = getVerificationIntentRevision(intent, latestPassed);
|
|
381
|
+
issues.push({
|
|
382
|
+
id,
|
|
383
|
+
type: 'stale_verification',
|
|
384
|
+
severity: 'high',
|
|
385
|
+
msg: `${id} 最新 passed 验证 revision ${verifiedRevision ?? 'legacy/unknown'} 早于当前 Intent revision ${intent.revision ?? 1}`,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
321
390
|
const issuesWithHints = issues.map(addFixHint);
|
|
322
391
|
return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
|
|
323
392
|
}
|
|
@@ -331,11 +400,6 @@ function appendPhilosophyDiagnostics(issues, philosophyDir) {
|
|
|
331
400
|
issues.push({ id: 'philosophy', type: 'inspiration_source', severity: issue.severity, msg: issue.msg });
|
|
332
401
|
}
|
|
333
402
|
|
|
334
|
-
// 实现部分拆解校验(防止 Weaver 跳过拆解步骤)
|
|
335
|
-
const decompositionCheck = validatePartDecomposition(philosophyDir);
|
|
336
|
-
for (const issue of decompositionCheck.issues) {
|
|
337
|
-
issues.push({ id: 'philosophy', type: 'part_decomposition', severity: issue.severity, msg: issue.msg });
|
|
338
|
-
}
|
|
339
403
|
}
|
|
340
404
|
|
|
341
405
|
function summarizeIssues(issues) {
|
|
@@ -403,7 +467,7 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
|
|
|
403
467
|
const { issues } = doctor(versionDir, verificationsDir, philosophyDir);
|
|
404
468
|
|
|
405
469
|
// 区分模板阶段问题(待填充)和真实损坏
|
|
406
|
-
const templateIssues = issues.filter((i) => i.is_template || i.type === 'intent_map_template' || i.type === 'inspiration_source'
|
|
470
|
+
const templateIssues = issues.filter((i) => i.is_template || i.type === 'intent_map_template' || i.type === 'inspiration_source');
|
|
407
471
|
const realIssues = issues.filter((i) => !templateIssues.includes(i));
|
|
408
472
|
|
|
409
473
|
const risks = [];
|
|
@@ -416,11 +480,12 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
|
|
|
416
480
|
|
|
417
481
|
return {
|
|
418
482
|
intent_map_valid: mapState.valid === true,
|
|
419
|
-
progress: {
|
|
483
|
+
progress: {
|
|
420
484
|
completed: status.counts.completed,
|
|
421
485
|
total: status.counts.total,
|
|
422
|
-
rate: `${status.counts.completed}/${status.counts.total}`,
|
|
423
|
-
},
|
|
486
|
+
rate: `${status.counts.completed}/${status.counts.total}`,
|
|
487
|
+
},
|
|
488
|
+
deprecated_intents: status.deprecated || [],
|
|
424
489
|
next_intent: next ? next.id : null,
|
|
425
490
|
pending_verifications: pending,
|
|
426
491
|
inconsistent_states: issues.filter((i) => i.type === 'in_progress_no_record' || i.type === 'completed_no_record').map((i) => i.id),
|
|
@@ -440,7 +505,7 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
|
|
|
440
505
|
* @param {string} intentId
|
|
441
506
|
* @returns {object}
|
|
442
507
|
*/
|
|
443
|
-
export function traceIntent(versionDir, verificationsDir, philosophyDir, intentId) {
|
|
508
|
+
export function traceIntent(versionDir, verificationsDir, philosophyDir, intentId) {
|
|
444
509
|
const intent = getIntent(versionDir, intentId);
|
|
445
510
|
if (!intent) throw new Error(`Intent 不存在: ${intentId}`);
|
|
446
511
|
|
|
@@ -482,19 +547,51 @@ export function traceIntent(versionDir, verificationsDir, philosophyDir, intentI
|
|
|
482
547
|
walkDeps(dep, depth + 1);
|
|
483
548
|
}
|
|
484
549
|
}
|
|
485
|
-
walkDeps(intentId, 0);
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
550
|
+
walkDeps(intentId, 0);
|
|
551
|
+
|
|
552
|
+
const version = basename(versionDir);
|
|
553
|
+
const ref = formatIntentRef(version, intentId);
|
|
554
|
+
const loomRoot = dirname(versionDir);
|
|
555
|
+
const predecessors = (intent.lineage?.predecessors || []).map((item) => {
|
|
556
|
+
let revision = null;
|
|
557
|
+
try { revision = getIntent(join(loomRoot, item.version), item.intent_id).revision ?? 1; }
|
|
558
|
+
catch { /* Validation is structural; a referenced version may not be locally available. */ }
|
|
559
|
+
return { ...item, ref: formatIntentRef(item.version, item.intent_id), revision };
|
|
560
|
+
});
|
|
561
|
+
const successors = [];
|
|
562
|
+
for (const candidateVersion of readdirSync(loomRoot).filter((name) => /^v\d+$/.test(name))) {
|
|
563
|
+
const candidateDir = join(loomRoot, candidateVersion);
|
|
564
|
+
if (!statSync(candidateDir).isDirectory() || !existsSync(join(candidateDir, '04_INTENT_MAP.json'))) continue;
|
|
565
|
+
let candidateMap;
|
|
566
|
+
try { candidateMap = loadIntentMap(candidateDir); }
|
|
567
|
+
catch { continue; }
|
|
568
|
+
for (const [candidateId, candidate] of Object.entries(candidateMap.intents)) {
|
|
569
|
+
if (candidate.lineage?.predecessors?.some((item) => item.version === version && item.intent_id === intentId)) {
|
|
570
|
+
successors.push({
|
|
571
|
+
ref: formatIntentRef(candidateVersion, candidateId),
|
|
572
|
+
version: candidateVersion,
|
|
573
|
+
intent_id: candidateId,
|
|
574
|
+
revision: candidate.revision ?? 1,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
ref,
|
|
582
|
+
version,
|
|
583
|
+
revision: intent.revision ?? 1,
|
|
584
|
+
intent,
|
|
489
585
|
narrative,
|
|
490
586
|
narrative_error: narrativeError,
|
|
491
587
|
acceptance,
|
|
492
588
|
acceptance_error: acceptanceError,
|
|
493
589
|
verification_history: verificationHistory,
|
|
494
|
-
philosophy_anchors_content: philosophyContent,
|
|
495
|
-
dependency_chain: dependencyChain,
|
|
496
|
-
|
|
497
|
-
}
|
|
590
|
+
philosophy_anchors_content: philosophyContent,
|
|
591
|
+
dependency_chain: dependencyChain,
|
|
592
|
+
lineage: { predecessors, successors },
|
|
593
|
+
};
|
|
594
|
+
}
|
|
498
595
|
|
|
499
596
|
// ─── reverse-dep ───────────────────────────────────────
|
|
500
597
|
// 反向依赖:哪些 Intent 依赖这个 Intent
|