@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/intent-map.js
CHANGED
|
@@ -1,15 +1,43 @@
|
|
|
1
1
|
// intent-map.js — Intent Map 的加载、校验、查询
|
|
2
2
|
// 真相源是磁盘上的 04_INTENT_MAP.json,这个库负责按需查询,不返回整个文件。
|
|
3
3
|
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync } from 'node:fs';
|
|
5
5
|
import { resolve, dirname, join } from 'node:path';
|
|
6
|
-
import { extractMdSection, readJsonFile } from './shared/md-utils.js';
|
|
6
|
+
import { extractMdSection, readJsonFile } from './shared/md-utils.js';
|
|
7
|
+
import { formatIntentRef, INTENT_ID_PATTERN, VERSION_PATTERN } from './shared/intent-ref.js';
|
|
8
|
+
import { commandCoversVerificationMethod, getIntentVerificationMethod } from './shared/verification-method.js';
|
|
9
|
+
import { resolveQualityProofReference } from './shared/proof-reference.js';
|
|
7
10
|
|
|
8
11
|
/** 必填字段(INTENT_LOOP.md 底线) */
|
|
9
12
|
const REQUIRED_FIELDS = ['id', 'title', 'narrative_ref', 'depends_on', 'acceptance', 'philosophy_anchors', 'status'];
|
|
10
13
|
|
|
11
|
-
/** 合法 status
|
|
12
|
-
const VALID_STATUS = ['pending', 'in_progress', 'completed', 'blocked', 'needs_review'];
|
|
14
|
+
/** 合法 status 值和唯一状态转换表。guide、命令和测试都应遵循这里。 */
|
|
15
|
+
export const VALID_STATUS = ['pending', 'in_progress', 'completed', 'blocked', 'needs_review'];
|
|
16
|
+
export const VALID_TRANSITIONS = {
|
|
17
|
+
pending: ['in_progress', 'blocked'],
|
|
18
|
+
in_progress: ['completed', 'blocked'],
|
|
19
|
+
completed: ['needs_review'],
|
|
20
|
+
blocked: ['pending'],
|
|
21
|
+
needs_review: ['in_progress', 'completed', 'blocked'],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const LEGACY_REVISION = Symbol('legacyIntentRevision');
|
|
25
|
+
|
|
26
|
+
function withEffectiveRevision(intent) {
|
|
27
|
+
if (intent.revision !== undefined) return intent;
|
|
28
|
+
const effective = { ...intent, revision: 1 };
|
|
29
|
+
Object.defineProperty(effective, LEGACY_REVISION, { value: true });
|
|
30
|
+
return effective;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function hasLegacyIntentRevision(intent) {
|
|
34
|
+
return intent.revision === undefined || intent[LEGACY_REVISION] === true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A reflow can invalidate evidence without changing an Intent's semantic revision. */
|
|
38
|
+
export function getEffectiveVerificationEpoch(intent) {
|
|
39
|
+
return intent?.verification_epoch ?? 1;
|
|
40
|
+
}
|
|
13
41
|
|
|
14
42
|
/**
|
|
15
43
|
* 加载 Intent Map 文件。
|
|
@@ -27,7 +55,7 @@ export function loadIntentMap(versionDir) {
|
|
|
27
55
|
* 校验 Intent Map 结构合规性(INTENT_LOOP.md I-1, I-2 底线)。
|
|
28
56
|
* 抛出错误列表,不静默修复。
|
|
29
57
|
*/
|
|
30
|
-
export function validateIntentMap(data) {
|
|
58
|
+
export function validateIntentMap(data) {
|
|
31
59
|
const errors = [];
|
|
32
60
|
|
|
33
61
|
if (!data.intents || typeof data.intents !== 'object') {
|
|
@@ -48,9 +76,18 @@ export function validateIntentMap(data) {
|
|
|
48
76
|
errors.push(`intents["${id}"] 缺少必填字段: ${field}`);
|
|
49
77
|
}
|
|
50
78
|
}
|
|
51
|
-
if (intent.status && !VALID_STATUS.includes(intent.status)) {
|
|
52
|
-
errors.push(`intents["${id}"].status 非法: "${intent.status}" (合法: ${VALID_STATUS.join('|')})`);
|
|
53
|
-
}
|
|
79
|
+
if (intent.status && !VALID_STATUS.includes(intent.status)) {
|
|
80
|
+
errors.push(`intents["${id}"].status 非法: "${intent.status}" (合法: ${VALID_STATUS.join('|')})`);
|
|
81
|
+
}
|
|
82
|
+
if ('revision' in intent && (!Number.isInteger(intent.revision) || intent.revision < 1)) {
|
|
83
|
+
errors.push(`intents["${id}"].revision 非法: ${JSON.stringify(intent.revision)} (必须是正整数)`);
|
|
84
|
+
}
|
|
85
|
+
if ('verification_epoch' in intent && (!Number.isInteger(intent.verification_epoch) || intent.verification_epoch < 1)) {
|
|
86
|
+
errors.push(`intents["${id}"].verification_epoch 非法: ${JSON.stringify(intent.verification_epoch)} (必须是正整数)`);
|
|
87
|
+
}
|
|
88
|
+
validateLineage(data, id, intent.lineage, errors);
|
|
89
|
+
validateLifecycle(data, id, intent.lifecycle, errors);
|
|
90
|
+
validateOptionalIntentFields(id, intent, errors);
|
|
54
91
|
if (intent.depends_on) {
|
|
55
92
|
for (const dep of intent.depends_on) {
|
|
56
93
|
if (!(dep in data.intents)) {
|
|
@@ -76,19 +113,250 @@ export function validateIntentMap(data) {
|
|
|
76
113
|
}
|
|
77
114
|
|
|
78
115
|
// topo_order 必须覆盖所有 Intent
|
|
79
|
-
if (Array.isArray(data.topo_order)) {
|
|
80
|
-
const topoSet = new Set(data.topo_order);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
if (Array.isArray(data.topo_order)) {
|
|
117
|
+
const topoSet = new Set(data.topo_order);
|
|
118
|
+
if (topoSet.size !== data.topo_order.length) errors.push('topo_order 含重复 Intent');
|
|
119
|
+
for (const id of data.topo_order) {
|
|
120
|
+
if (!(id in data.intents)) errors.push(`topo_order 含不存在的 Intent: ${id}`);
|
|
121
|
+
}
|
|
122
|
+
for (const id of Object.keys(data.intents)) {
|
|
123
|
+
if (!topoSet.has(id)) {
|
|
124
|
+
errors.push(`topo_order 缺少 Intent: ${id}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const positions = new Map(data.topo_order.map((id, index) => [id, index]));
|
|
128
|
+
for (const [id, intent] of Object.entries(data.intents)) {
|
|
129
|
+
for (const dependency of intent.depends_on || []) {
|
|
130
|
+
if (positions.has(dependency) && positions.has(id) && positions.get(dependency) >= positions.get(id)) {
|
|
131
|
+
errors.push(`topo_order 顺序非法: ${dependency} 必须位于 ${id} 之前`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
computeTopoOrder(data.intents, data.topo_order);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
errors.push(error.message);
|
|
139
|
+
}
|
|
86
140
|
}
|
|
87
141
|
|
|
88
142
|
if (errors.length > 0) {
|
|
89
143
|
throw new Error(`Intent Map 校验失败:\n - ${errors.join('\n - ')}`);
|
|
90
144
|
}
|
|
91
|
-
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validateLifecycle(data, id, lifecycle, errors) {
|
|
148
|
+
if (lifecycle === undefined) return;
|
|
149
|
+
const prefix = `intents["${id}"].lifecycle`;
|
|
150
|
+
if (!lifecycle || typeof lifecycle !== 'object' || Array.isArray(lifecycle)) {
|
|
151
|
+
errors.push(`${prefix} 必须是对象`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (lifecycle.deprecation === undefined) return;
|
|
155
|
+
const deprecation = lifecycle.deprecation;
|
|
156
|
+
const deprecationPrefix = `${prefix}.deprecation`;
|
|
157
|
+
if (!deprecation || typeof deprecation !== 'object' || Array.isArray(deprecation)) {
|
|
158
|
+
errors.push(`${deprecationPrefix} 必须是对象`);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (typeof deprecation.deprecated_at !== 'string' || Number.isNaN(Date.parse(deprecation.deprecated_at))) {
|
|
162
|
+
errors.push(`${deprecationPrefix}.deprecated_at 必须是 ISO 8601 时间戳`);
|
|
163
|
+
}
|
|
164
|
+
if (typeof deprecation.reason !== 'string' || deprecation.reason.trim() === '') {
|
|
165
|
+
errors.push(`${deprecationPrefix}.reason 必须是非空字符串`);
|
|
166
|
+
}
|
|
167
|
+
if (!('replacement' in deprecation) || (deprecation.replacement !== null && typeof deprecation.replacement !== 'string')) {
|
|
168
|
+
errors.push(`${deprecationPrefix}.replacement 必须是 Intent ID 或 null`);
|
|
169
|
+
} else if (typeof deprecation.replacement === 'string') {
|
|
170
|
+
if (deprecation.replacement === id) errors.push(`${deprecationPrefix}.replacement 不能引用自身`);
|
|
171
|
+
else if (!(deprecation.replacement in data.intents)) errors.push(`${deprecationPrefix}.replacement 引用了不存在的当前 Intent: ${deprecation.replacement}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateOptionalIntentFields(id, intent, errors) {
|
|
176
|
+
const prefix = `intents["${id}"]`;
|
|
177
|
+
|
|
178
|
+
if ('continuity_required' in intent && typeof intent.continuity_required !== 'boolean') {
|
|
179
|
+
errors.push(`${prefix}.continuity_required 必须是布尔值;仅在本 Intent 会变更既有用户或系统状态且必须证明未误伤旧状态时设为 true`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if ('quality_contract' in intent) {
|
|
183
|
+
if (typeof intent.quality_contract !== 'string' || intent.quality_contract.trim().length < 10) {
|
|
184
|
+
errors.push(`${prefix}.quality_contract 必须是非空质量契约或 05_VERIFICATION.md 章节引用`);
|
|
185
|
+
} else if (/^(?:\.{3}|…|todo|tbd|待填)$/i.test(intent.quality_contract.trim())) {
|
|
186
|
+
errors.push(`${prefix}.quality_contract 不能是占位符`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if ('capability_needs' in intent) {
|
|
191
|
+
if (!Array.isArray(intent.capability_needs)) {
|
|
192
|
+
errors.push(`${prefix}.capability_needs 必须是字符串数组`);
|
|
193
|
+
} else {
|
|
194
|
+
const normalized = [];
|
|
195
|
+
for (const [index, need] of intent.capability_needs.entries()) {
|
|
196
|
+
if (typeof need !== 'string' || need.trim().length < 2) {
|
|
197
|
+
errors.push(`${prefix}.capability_needs[${index}] 必须是非空专业领域`);
|
|
198
|
+
} else {
|
|
199
|
+
normalized.push(need.trim().toLowerCase());
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (new Set(normalized).size !== normalized.length) {
|
|
203
|
+
errors.push(`${prefix}.capability_needs 含重复专业领域`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if ('creative_scope' in intent) {
|
|
209
|
+
if (typeof intent.creative_scope !== 'string' || intent.creative_scope.trim().length < 10) {
|
|
210
|
+
errors.push(`${prefix}.creative_scope 必须说明可以改变什么、必须保持什么`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function validateLineage(data, id, lineage, errors) {
|
|
216
|
+
if (lineage === undefined) return;
|
|
217
|
+
const prefix = `intents["${id}"].lineage`;
|
|
218
|
+
if (!lineage || typeof lineage !== 'object' || Array.isArray(lineage)) {
|
|
219
|
+
errors.push(`${prefix} 必须是对象`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!Array.isArray(lineage.predecessors)) {
|
|
223
|
+
errors.push(`${prefix}.predecessors 必须是数组`);
|
|
224
|
+
} else {
|
|
225
|
+
const seen = new Set();
|
|
226
|
+
for (const [index, predecessor] of lineage.predecessors.entries()) {
|
|
227
|
+
const refPrefix = `${prefix}.predecessors[${index}]`;
|
|
228
|
+
if (!predecessor || typeof predecessor !== 'object' || Array.isArray(predecessor)) {
|
|
229
|
+
errors.push(`${refPrefix} 必须是 { version, intent_id } 对象`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (!VERSION_PATTERN.test(predecessor.version || '')) errors.push(`${refPrefix}.version 格式非法`);
|
|
233
|
+
if (!INTENT_ID_PATTERN.test(predecessor.intent_id || '')) errors.push(`${refPrefix}.intent_id 格式非法`);
|
|
234
|
+
const ref = `${predecessor.version}:${predecessor.intent_id}`;
|
|
235
|
+
if (seen.has(ref)) errors.push(`${prefix}.predecessors 含重复引用: ${ref}`);
|
|
236
|
+
seen.add(ref);
|
|
237
|
+
if (predecessor.version === data._meta?._loom_version && predecessor.intent_id === id) {
|
|
238
|
+
errors.push(`${prefix} 不能自引用: ${ref}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (typeof lineage.change_summary !== 'string' || lineage.change_summary.trim() === '') {
|
|
243
|
+
errors.push(`${prefix}.change_summary 必须是非空字符串`);
|
|
244
|
+
}
|
|
245
|
+
if ('change_ref' in lineage && (typeof lineage.change_ref !== 'string' || lineage.change_ref.trim() === '')) {
|
|
246
|
+
errors.push(`${prefix}.change_ref 必须是非空字符串`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const NON_SEMANTIC_FIELDS = new Set(['id', 'revision', 'status', 'lineage', '_runtime']);
|
|
251
|
+
|
|
252
|
+
function semanticIntent(intent) {
|
|
253
|
+
return Object.fromEntries(Object.entries(intent).filter(([key]) => !NON_SEMANTIC_FIELDS.has(key)));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function changedSemanticFields(before, after) {
|
|
257
|
+
const a = semanticIntent(before);
|
|
258
|
+
const b = semanticIntent(after);
|
|
259
|
+
return [...new Set([...Object.keys(a), ...Object.keys(b)])]
|
|
260
|
+
.filter((key) => JSON.stringify(a[key]) !== JSON.stringify(b[key]))
|
|
261
|
+
.sort();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Compare Intent semantics using only explicit target-version predecessor references. */
|
|
265
|
+
export function diffIntentVersions(loomRoot, fromVersion, toVersion) {
|
|
266
|
+
const from = fromVersion.startsWith('v') ? fromVersion : `v${fromVersion}`;
|
|
267
|
+
const to = toVersion.startsWith('v') ? toVersion : `v${toVersion}`;
|
|
268
|
+
if (!VERSION_PATTERN.test(from) || !VERSION_PATTERN.test(to)) throw new Error('版本格式应为 v1、v2 等');
|
|
269
|
+
const fromMap = loadIntentMap(join(loomRoot, from));
|
|
270
|
+
const toMap = loadIntentMap(join(loomRoot, to));
|
|
271
|
+
const sourceToTargets = new Map(Object.keys(fromMap.intents).map((id) => [id, []]));
|
|
272
|
+
const targetMappings = new Map();
|
|
273
|
+
const targetLineages = new Map();
|
|
274
|
+
const warnings = [];
|
|
275
|
+
|
|
276
|
+
for (const [targetId, target] of Object.entries(toMap.intents)) {
|
|
277
|
+
const refs = target.lineage?.predecessors || [];
|
|
278
|
+
const mapped = refs.filter((ref) => ref.version === from);
|
|
279
|
+
targetLineages.set(targetId, refs);
|
|
280
|
+
targetMappings.set(targetId, mapped);
|
|
281
|
+
for (const ref of mapped) {
|
|
282
|
+
if (fromMap.intents[ref.intent_id]) sourceToTargets.get(ref.intent_id).push(targetId);
|
|
283
|
+
else warnings.push(`${formatIntentRef(to, targetId)} 引用了不存在的 predecessor ${formatIntentRef(from, ref.intent_id)}`);
|
|
284
|
+
}
|
|
285
|
+
if (fromMap.intents[targetId] && !mapped.some((ref) => ref.intent_id === targetId)) {
|
|
286
|
+
warnings.push(`${formatIntentRef(from, targetId)} 与 ${formatIntentRef(to, targetId)} ID 相同但没有显式 lineage,不作映射`);
|
|
287
|
+
}
|
|
288
|
+
if (refs.length > 0 && mapped.length === 0) {
|
|
289
|
+
warnings.push(`${formatIntentRef(to, targetId)} 没有指向比较源版本 ${from} 的 predecessor`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const split = [...sourceToTargets.entries()]
|
|
294
|
+
.filter(([, targets]) => targets.length > 1)
|
|
295
|
+
.map(([sourceId, targets]) => ({ from: formatIntentRef(from, sourceId), to: targets.map((id) => formatIntentRef(to, id)) }));
|
|
296
|
+
const merged = [...targetMappings.entries()]
|
|
297
|
+
.filter(([, refs]) => refs.filter((ref) => fromMap.intents[ref.intent_id]).length > 1)
|
|
298
|
+
.map(([targetId, refs]) => ({ from: refs.filter((ref) => fromMap.intents[ref.intent_id]).map((ref) => formatIntentRef(from, ref.intent_id)), to: formatIntentRef(to, targetId) }));
|
|
299
|
+
const revised = [];
|
|
300
|
+
const unchanged = [];
|
|
301
|
+
for (const [targetId, refs] of targetMappings) {
|
|
302
|
+
const valid = refs.filter((ref) => fromMap.intents[ref.intent_id]);
|
|
303
|
+
if (valid.length !== 1 || sourceToTargets.get(valid[0].intent_id).length !== 1) continue;
|
|
304
|
+
const fields = changedSemanticFields(fromMap.intents[valid[0].intent_id], toMap.intents[targetId]);
|
|
305
|
+
const item = { from: formatIntentRef(from, valid[0].intent_id), to: formatIntentRef(to, targetId), changed_fields: fields };
|
|
306
|
+
(fields.length ? revised : unchanged).push(item);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const unmappedFrom = [...sourceToTargets.entries()].filter(([, targets]) => targets.length === 0).map(([id]) => formatIntentRef(from, id));
|
|
310
|
+
const unmappedTo = [...targetLineages.entries()]
|
|
311
|
+
.filter(([id, refs]) => refs.length > 0 && !targetMappings.get(id).some((ref) => fromMap.intents[ref.intent_id]))
|
|
312
|
+
.map(([id]) => formatIntentRef(to, id));
|
|
313
|
+
return {
|
|
314
|
+
from,
|
|
315
|
+
to,
|
|
316
|
+
new: Object.entries(toMap.intents)
|
|
317
|
+
.filter(([, intent]) => !intent.lineage?.predecessors?.length)
|
|
318
|
+
.map(([id]) => formatIntentRef(to, id)),
|
|
319
|
+
revised,
|
|
320
|
+
split,
|
|
321
|
+
merged,
|
|
322
|
+
unmapped: [...unmappedFrom, ...unmappedTo],
|
|
323
|
+
unmapped_from: unmappedFrom,
|
|
324
|
+
unmapped_to: unmappedTo,
|
|
325
|
+
unchanged,
|
|
326
|
+
warnings,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Compute a stable topological order, preferring the previous order where possible. */
|
|
331
|
+
export function computeTopoOrder(intents, previousOrder = []) {
|
|
332
|
+
const ids = Object.keys(intents);
|
|
333
|
+
const rank = new Map(previousOrder.map((id, index) => [id, index]));
|
|
334
|
+
const compare = (a, b) => (rank.get(a) ?? Number.MAX_SAFE_INTEGER) - (rank.get(b) ?? Number.MAX_SAFE_INTEGER) || a.localeCompare(b);
|
|
335
|
+
const indegree = new Map(ids.map((id) => [id, 0]));
|
|
336
|
+
const dependents = new Map(ids.map((id) => [id, []]));
|
|
337
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
338
|
+
for (const dependency of intent.depends_on || []) {
|
|
339
|
+
if (!indegree.has(dependency)) throw new Error(`${id} 引用了不存在的依赖: ${dependency}`);
|
|
340
|
+
indegree.set(id, indegree.get(id) + 1);
|
|
341
|
+
dependents.get(dependency).push(id);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const ready = ids.filter((id) => indegree.get(id) === 0).sort(compare);
|
|
345
|
+
const order = [];
|
|
346
|
+
while (ready.length) {
|
|
347
|
+
const id = ready.shift();
|
|
348
|
+
order.push(id);
|
|
349
|
+
for (const dependent of dependents.get(id).sort(compare)) {
|
|
350
|
+
indegree.set(dependent, indegree.get(dependent) - 1);
|
|
351
|
+
if (indegree.get(dependent) === 0) {
|
|
352
|
+
ready.push(dependent);
|
|
353
|
+
ready.sort(compare);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (order.length !== ids.length) throw new Error('Intent 依赖图存在循环');
|
|
358
|
+
return order;
|
|
359
|
+
}
|
|
92
360
|
|
|
93
361
|
/**
|
|
94
362
|
* 返回下一个可执行 Intent:
|
|
@@ -97,13 +365,14 @@ export function validateIntentMap(data) {
|
|
|
97
365
|
*/
|
|
98
366
|
export function getNextIntent(versionDir) {
|
|
99
367
|
const { intents, topo_order } = loadIntentMap(versionDir);
|
|
100
|
-
for (const id of topo_order) {
|
|
101
|
-
const intent = intents[id];
|
|
102
|
-
if (intent.
|
|
368
|
+
for (const id of topo_order) {
|
|
369
|
+
const intent = intents[id];
|
|
370
|
+
if (intent.lifecycle?.deprecation) continue;
|
|
371
|
+
if (intent.status !== 'pending') continue;
|
|
103
372
|
const depsReady = intent.depends_on.every(
|
|
104
373
|
(dep) => intents[dep]?.status === 'completed'
|
|
105
374
|
);
|
|
106
|
-
if (depsReady) return intent;
|
|
375
|
+
if (depsReady) return withEffectiveRevision(intent);
|
|
107
376
|
}
|
|
108
377
|
return null;
|
|
109
378
|
}
|
|
@@ -113,25 +382,174 @@ export function getNextIntent(versionDir) {
|
|
|
113
382
|
*/
|
|
114
383
|
export function getStatus(versionDir) {
|
|
115
384
|
const { intents } = loadIntentMap(versionDir);
|
|
116
|
-
const summary = { pending: [], in_progress: [], completed: [], blocked: [] };
|
|
117
|
-
const titles = {};
|
|
385
|
+
const summary = { pending: [], in_progress: [], completed: [], blocked: [], needs_review: [] };
|
|
386
|
+
const titles = {};
|
|
387
|
+
const deprecated = [];
|
|
118
388
|
for (const [id, intent] of Object.entries(intents)) {
|
|
119
389
|
const s = intent.status;
|
|
120
|
-
if (summary[s]) summary[s].push(id);
|
|
121
|
-
titles[id] = intent.title || '';
|
|
390
|
+
if (summary[s]) summary[s].push(id);
|
|
391
|
+
titles[id] = intent.title || '';
|
|
392
|
+
if (intent.lifecycle?.deprecation) deprecated.push(id);
|
|
122
393
|
}
|
|
123
394
|
return {
|
|
124
395
|
counts: {
|
|
125
396
|
pending: summary.pending.length,
|
|
126
397
|
in_progress: summary.in_progress.length,
|
|
127
|
-
completed: summary.completed.length,
|
|
128
|
-
blocked: summary.blocked.length,
|
|
129
|
-
|
|
398
|
+
completed: summary.completed.length,
|
|
399
|
+
blocked: summary.blocked.length,
|
|
400
|
+
needs_review: summary.needs_review.length,
|
|
401
|
+
total: Object.keys(intents).length,
|
|
402
|
+
deprecated: deprecated.length,
|
|
130
403
|
},
|
|
131
404
|
ids: summary,
|
|
132
|
-
titles,
|
|
133
|
-
|
|
134
|
-
}
|
|
405
|
+
titles,
|
|
406
|
+
deprecated,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function dependentClosure(intents, targetId) {
|
|
411
|
+
const direct = Object.values(intents)
|
|
412
|
+
.filter((intent) => intent.depends_on?.includes(targetId))
|
|
413
|
+
.map((intent) => intent.id);
|
|
414
|
+
const seen = new Set(direct);
|
|
415
|
+
const queue = [...direct];
|
|
416
|
+
while (queue.length) {
|
|
417
|
+
const current = queue.shift();
|
|
418
|
+
for (const intent of Object.values(intents)) {
|
|
419
|
+
if (intent.depends_on?.includes(current) && !seen.has(intent.id)) {
|
|
420
|
+
seen.add(intent.id);
|
|
421
|
+
queue.push(intent.id);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { direct, transitive: [...seen], all: [...seen] };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Validate an exact, one-time partition of an impact set. */
|
|
429
|
+
export function validateImpactPartition(impactedIds, review = [], unaffected = []) {
|
|
430
|
+
const duplicate = (ids) => ids.find((id, index) => ids.indexOf(id) !== index);
|
|
431
|
+
if (duplicate(review)) throw new Error(`--review 含重复 ID: ${duplicate(review)}`);
|
|
432
|
+
if (duplicate(unaffected)) throw new Error(`--unaffected 含重复 ID: ${duplicate(unaffected)}`);
|
|
433
|
+
const overlap = review.filter((id) => unaffected.includes(id));
|
|
434
|
+
if (overlap.length) throw new Error(`review 与 unaffected 重叠: ${overlap.join(', ')}`);
|
|
435
|
+
const related = new Set(impactedIds);
|
|
436
|
+
const unrelated = [...review, ...unaffected].filter((id) => !related.has(id));
|
|
437
|
+
if (unrelated.length) throw new Error(`分区包含无关 Intent: ${[...new Set(unrelated)].join(', ')}`);
|
|
438
|
+
const classified = new Set([...review, ...unaffected]);
|
|
439
|
+
const missing = impactedIds.filter((id) => !classified.has(id));
|
|
440
|
+
if (missing.length) throw new Error(`依赖分区不完整,缺少: ${missing.join(', ')}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Apply the canonical completed -> needs_review transition and report every reviewed state. */
|
|
444
|
+
export function applyImpactReview(data, review, { incrementPassOnce = false } = {}) {
|
|
445
|
+
const completedIds = [];
|
|
446
|
+
const reviewed = review.map((id) => {
|
|
447
|
+
const intent = data.intents[id];
|
|
448
|
+
const before = intent.status;
|
|
449
|
+
if (before !== 'pending') intent.verification_epoch = getEffectiveVerificationEpoch(intent) + 1;
|
|
450
|
+
if (before === 'completed') {
|
|
451
|
+
if (!VALID_TRANSITIONS.completed.includes('needs_review')) throw new Error('completed 不能进入 needs_review');
|
|
452
|
+
intent.status = 'needs_review';
|
|
453
|
+
completedIds.push(id);
|
|
454
|
+
}
|
|
455
|
+
return { id, status_before: before, status_after: intent.status };
|
|
456
|
+
});
|
|
457
|
+
if (completedIds.length) registerReviewCycle(data, completedIds, incrementPassOnce);
|
|
458
|
+
return { reviewed, completedReviewed: completedIds.length > 0 };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Track one convergence event while retaining all Intents still being reworked. */
|
|
462
|
+
export function registerReviewCycle(data, intentIds, incrementPass = true) {
|
|
463
|
+
if (!intentIds.length) return;
|
|
464
|
+
data._meta ??= {};
|
|
465
|
+
const reviewing = new Set(Array.isArray(data._meta.reviewing_ids) ? data._meta.reviewing_ids : []);
|
|
466
|
+
for (const id of intentIds) reviewing.add(id);
|
|
467
|
+
data._meta.reviewing_ids = [...reviewing];
|
|
468
|
+
if (incrementPass) data._meta.pass_count = (data._meta.pass_count || 0) + 1;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function finishReviewCycleIntent(data, intentId) {
|
|
472
|
+
const reviewing = new Set(Array.isArray(data._meta?.reviewing_ids) ? data._meta.reviewing_ids : []);
|
|
473
|
+
if (!reviewing.delete(intentId)) return;
|
|
474
|
+
data._meta.reviewing_ids = [...reviewing];
|
|
475
|
+
if (reviewing.size === 0) {
|
|
476
|
+
data._meta.pass_count = 0;
|
|
477
|
+
delete data._meta.reviewing_ids;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function deprecationEntry(intent) {
|
|
482
|
+
return { id: intent.id, title: intent.title || '', status: intent.status };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function validateDeprecationTarget(data, intentId, reason) {
|
|
486
|
+
if (typeof reason !== 'string' || reason.trim() === '') throw new Error('--reason 必须是非空文本');
|
|
487
|
+
const target = data.intents[intentId];
|
|
488
|
+
if (!target) throw new Error(`Intent 不存在: ${intentId}`);
|
|
489
|
+
if (target.lifecycle?.deprecation) throw new Error(`Intent ${intentId} 已弃用,不能重复确认`);
|
|
490
|
+
if (target.status !== 'completed') throw new Error(`Intent ${intentId} 必须是 completed 才能弃用(当前: ${target.status})`);
|
|
491
|
+
return target;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Assess or atomically confirm deprecation of a completed current-version Intent. */
|
|
495
|
+
export function deprecateIntent(versionDir, intentId, options) {
|
|
496
|
+
const filePath = join(versionDir, '04_INTENT_MAP.json');
|
|
497
|
+
const data = readJsonFile(filePath, 'Intent Map');
|
|
498
|
+
validateIntentMap(data);
|
|
499
|
+
const target = validateDeprecationTarget(data, intentId, options.reason);
|
|
500
|
+
const dependents = dependentClosure(data.intents, intentId);
|
|
501
|
+
const describe = (ids) => ids.map((id) => deprecationEntry(data.intents[id]));
|
|
502
|
+
const impact = {
|
|
503
|
+
target: deprecationEntry(target),
|
|
504
|
+
dependents: { direct: describe(dependents.direct), transitive: describe(dependents.transitive) },
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
if (!options.confirm) {
|
|
508
|
+
const ids = dependents.all.join(',');
|
|
509
|
+
const escapedReason = options.reason.trim().replace(/"/g, '\\"');
|
|
510
|
+
const command = dependents.all.length
|
|
511
|
+
? `loom intent deprecate ${intentId} --reason "${escapedReason}" --confirm --review <IDs from ${ids}> --unaffected <remaining IDs from ${ids}>`
|
|
512
|
+
: `loom intent deprecate ${intentId} --reason "${escapedReason}" --confirm`;
|
|
513
|
+
return {
|
|
514
|
+
mode: 'assessment',
|
|
515
|
+
mutated: false,
|
|
516
|
+
...impact,
|
|
517
|
+
required_partition: dependents.all,
|
|
518
|
+
follow_up: {
|
|
519
|
+
command,
|
|
520
|
+
guidance: dependents.all.length
|
|
521
|
+
? 'Classify every listed dependent exactly once between --review and --unaffected; omit an empty group.'
|
|
522
|
+
: 'This is a leaf Intent; confirm without --review or --unaffected.',
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const replacement = options.replacement || null;
|
|
528
|
+
if (replacement !== null) {
|
|
529
|
+
if (replacement === intentId) throw new Error('--replacement 必须是另一个当前 Intent');
|
|
530
|
+
if (!(replacement in data.intents)) throw new Error(`replacement 不是当前 Intent: ${replacement}`);
|
|
531
|
+
}
|
|
532
|
+
const review = options.review || [];
|
|
533
|
+
const unaffected = options.unaffected || [];
|
|
534
|
+
validateImpactPartition(dependents.all, review, unaffected);
|
|
535
|
+
|
|
536
|
+
const { reviewed } = applyImpactReview(data, review, { incrementPassOnce: true });
|
|
537
|
+
const unchanged = unaffected.map((id) => ({ id, status_before: data.intents[id].status, status_after: data.intents[id].status }));
|
|
538
|
+
target.lifecycle = {
|
|
539
|
+
...(target.lifecycle || {}),
|
|
540
|
+
deprecation: { deprecated_at: new Date().toISOString(), reason: options.reason.trim(), replacement },
|
|
541
|
+
};
|
|
542
|
+
validateIntentMap(data);
|
|
543
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
544
|
+
writeFileSync(tempPath, `${JSON.stringify(data, null, 2)}\n`, 'utf-8');
|
|
545
|
+
try {
|
|
546
|
+
renameSync(tempPath, filePath);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
try { unlinkSync(tempPath); } catch {}
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
return { mode: 'confirmed', mutated: true, ...impact, deprecation: target.lifecycle.deprecation, reviewed, unaffected: unchanged };
|
|
552
|
+
}
|
|
135
553
|
|
|
136
554
|
/**
|
|
137
555
|
* 输出 Mermaid 依赖图。
|
|
@@ -164,7 +582,7 @@ export function getIntent(versionDir, intentId) {
|
|
|
164
582
|
if (!(intentId in intents)) {
|
|
165
583
|
throw new Error(`Intent 不存在: ${intentId}`);
|
|
166
584
|
}
|
|
167
|
-
return intents[intentId];
|
|
585
|
+
return withEffectiveRevision(intents[intentId]);
|
|
168
586
|
}
|
|
169
587
|
|
|
170
588
|
/**
|
|
@@ -188,29 +606,97 @@ export function updateIntentStatus(versionDir, intentId, newStatus) {
|
|
|
188
606
|
}
|
|
189
607
|
|
|
190
608
|
const oldStatus = data.intents[intentId].status;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
609
|
+
if (!VALID_TRANSITIONS[oldStatus]?.includes(newStatus)) {
|
|
610
|
+
throw new Error(
|
|
611
|
+
`非法状态转换: ${oldStatus} → ${newStatus}` +
|
|
612
|
+
`\n合法转换: ${oldStatus} → [${VALID_TRANSITIONS[oldStatus]?.join(', ') || '无(终态)'}]`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (oldStatus === 'pending' && newStatus === 'in_progress') {
|
|
617
|
+
const incompleteDeps = data.intents[intentId].depends_on.filter(
|
|
618
|
+
(dep) => data.intents[dep]?.status !== 'completed'
|
|
619
|
+
);
|
|
620
|
+
if (incompleteDeps.length > 0) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
`Intent ${intentId} 的依赖尚未完成: ${incompleteDeps.join(', ')}` +
|
|
623
|
+
'\n先完成依赖,或运行 loom intent next 获取当前可执行 Intent。'
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// completed 是事实声明,不是自由状态标签。无当前 revision 的最后一条 passed
|
|
629
|
+
// 验证时,必须通过 `loom intent done` 的受保护闭合路径,而不是直接 update。
|
|
630
|
+
if (newStatus === 'completed') {
|
|
631
|
+
const verificationPath = join(versionDir, 'verifications', `${intentId}.json`);
|
|
632
|
+
if (!existsSync(verificationPath)) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
`Intent ${intentId} 没有验证记录,不能标记 completed。` +
|
|
635
|
+
`\n先运行 loom verify pass ${intentId} --summary "...",再运行 loom intent done ${intentId}。`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
const history = readJsonFile(verificationPath, '验证记录');
|
|
639
|
+
const latest = history?.records?.[history.records.length - 1];
|
|
640
|
+
const expectedRevision = data.intents[intentId].revision ?? 1;
|
|
641
|
+
const recordRevision = Number.isInteger(latest?.intent_revision)
|
|
642
|
+
? latest.intent_revision
|
|
643
|
+
: (data.intents[intentId].revision === undefined ? 1 : null);
|
|
644
|
+
if (latest?.verdict !== 'passed' || recordRevision !== expectedRevision) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
`Intent ${intentId} 缺少当前 revision ${expectedRevision} 的最后一条 passed 验证,不能标记 completed。` +
|
|
647
|
+
`\n先重新验证,再运行 loom intent done ${intentId}。`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (data.intents[intentId].quality_contract) {
|
|
651
|
+
const quality = latest.dimensions?.quality_achievement;
|
|
652
|
+
if (quality?.verdict !== 'passed') {
|
|
653
|
+
throw new Error(
|
|
654
|
+
`Intent ${intentId} 含 quality_contract,但最新验证缺少通过的 quality_achievement,不能标记 completed。`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
resolveQualityProofReference(versionDir, quality.quality_proof_ref);
|
|
659
|
+
} catch (error) {
|
|
660
|
+
throw new Error(`Intent ${intentId} 的 Quality Proof 无效,不能标记 completed:${error.message}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (data.intents[intentId].continuity_required) {
|
|
664
|
+
const preservation = latest.dimensions?.preservation_achievement;
|
|
665
|
+
if (preservation?.verdict !== 'passed') {
|
|
666
|
+
throw new Error(
|
|
667
|
+
`Intent ${intentId} 声明了 continuity_required,但最新验证缺少通过的 preservation_achievement,不能标记 completed。`
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const expectedEpoch = getEffectiveVerificationEpoch(data.intents[intentId]);
|
|
672
|
+
const recordEpoch = Number.isInteger(latest?.verification_epoch)
|
|
673
|
+
? latest.verification_epoch
|
|
674
|
+
: (data.intents[intentId].verification_epoch === undefined ? 1 : null);
|
|
675
|
+
if (recordEpoch !== expectedEpoch) {
|
|
676
|
+
throw new Error(
|
|
677
|
+
`Intent ${intentId} 缺少验证代次 ${expectedEpoch} 的最新 passed 记录,不能标记 completed。` +
|
|
678
|
+
'\n回流后必须重新验证,旧证据不能闭合当前 Intent。'
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
const method = getIntentVerificationMethod(data.intents[intentId]);
|
|
682
|
+
if (method && !latest?.reproduction_command) {
|
|
683
|
+
throw new Error(`Intent ${intentId} 声明了 verification_method,但最新验证缺少 reproduction_command,不能标记 completed。`);
|
|
684
|
+
}
|
|
685
|
+
if (method && !commandCoversVerificationMethod(latest.reproduction_command, method)) {
|
|
686
|
+
throw new Error(`Intent ${intentId} 的 reproduction_command 未覆盖 verification_method,不能标记 completed。`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// completed → needs_review 开启一轮回流;同轮跟踪到该 Intent 再次闭合。
|
|
691
|
+
if (oldStatus === 'completed' && newStatus === 'needs_review') {
|
|
692
|
+
data.intents[intentId].verification_epoch = getEffectiveVerificationEpoch(data.intents[intentId]) + 1;
|
|
693
|
+
registerReviewCycle(data, [intentId]);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
data.intents[intentId].status = newStatus;
|
|
697
|
+
if ((newStatus === 'completed' || newStatus === 'blocked') && oldStatus !== 'completed') {
|
|
698
|
+
finishReviewCycleIntent(data, intentId);
|
|
699
|
+
}
|
|
214
700
|
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
215
701
|
return data.intents[intentId];
|
|
216
702
|
}
|