@haaaiawd/loom 1.3.1 → 2.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/CHANGELOG.md +11 -86
- package/CONTRIBUTING.md +37 -0
- package/EVIL_EVAL.md +112 -0
- package/README.md +193 -445
- package/README.zh-CN.md +174 -0
- package/SECURITY.md +11 -0
- package/cli/bin/loom.js +171 -998
- package/cli/src/protocol.js +367 -0
- package/cli/src/store.js +626 -0
- package/design.md +194 -0
- package/docs/PROMPT_CATALOG.md +99 -0
- package/docs/RELEASE_CHECKLIST.md +53 -0
- package/docs/UX_FLOW.md +171 -0
- package/docs/brand/loom-mark.svg +18 -0
- package/docs/brand/loom-readme-header.svg +34 -0
- package/docs/brand/loom-readme-header.zh-CN.svg +29 -0
- package/docs/loom-eval-loop.drawio +21 -0
- package/docs/loom-eval-loop.svg +56 -0
- package/docs/loom-production-loop.drawio +41 -0
- package/docs/loom-production-loop.svg +92 -0
- package/package.json +43 -40
- package/EXTERNAL_ACQUISITION_DESIGN.md +0 -143
- package/cli/help/asset.md +0 -36
- package/cli/help/atelier.md +0 -37
- package/cli/help/atlas.md +0 -48
- package/cli/help/capability.md +0 -118
- package/cli/help/concepts.md +0 -105
- package/cli/help/doctor.md +0 -80
- package/cli/help/expertise.md +0 -52
- package/cli/help/loop.md +0 -134
- package/cli/help/patch.md +0 -33
- package/cli/help/proposals.md +0 -21
- package/cli/help/version.md +0 -136
- package/cli/help/workflow.md +0 -116
- package/cli/src/activate.js +0 -505
- package/cli/src/asset-library.js +0 -384
- package/cli/src/atelier.js +0 -331
- package/cli/src/atlas.js +0 -282
- package/cli/src/auto.js +0 -116
- package/cli/src/capability-graph.js +0 -724
- package/cli/src/capability-proposals.js +0 -225
- package/cli/src/diagnostics.js +0 -859
- package/cli/src/expertise-pack.js +0 -336
- package/cli/src/guide.js +0 -548
- package/cli/src/help.js +0 -41
- package/cli/src/init.js +0 -187
- package/cli/src/intent-draft.js +0 -303
- package/cli/src/intent-map.js +0 -747
- package/cli/src/patch.js +0 -214
- package/cli/src/philosophy.js +0 -331
- package/cli/src/shared/intent-ref.js +0 -38
- package/cli/src/shared/md-utils.js +0 -125
- package/cli/src/shared/paths.js +0 -73
- package/cli/src/shared/proof-reference.js +0 -19
- package/cli/src/shared/verification-method.js +0 -32
- package/cli/src/verify.js +0 -394
- package/cli/src/version.js +0 -134
- package/dimensions/AUTHORSHIP.md +0 -45
- package/dimensions/PART_DECOMPOSITION.md +0 -42
- package/dimensions/SEARCH_METHODOLOGY.md +0 -101
- package/dimensions/examples/AGENT_SYSTEM/README.md +0 -219
- package/dimensions/examples/CLI_TOOL/README.md +0 -163
- package/dimensions/universal/COLLABORATION_PHILOSOPHY.md +0 -28
- package/dimensions/universal/ENGINEERING_CREED.md +0 -30
- package/dimensions/universal/PRODUCT_PHILOSOPHY.md +0 -32
- package/meta/BASELINE.md +0 -91
- package/meta/INTENT_LOOP.md +0 -296
- package/meta/PHILOSOPHY_WEAVER.md +0 -110
- package/meta/ROLE_ACTIVATION.md +0 -114
- package/roles/architect.md +0 -92
- package/roles/forge.md +0 -110
- package/roles/impact-reviewer.md +0 -37
- package/roles/keeper.md +0 -113
- package/roles/visionary.md +0 -57
- package/templates/ASSET_LIBRARY_MANIFEST_TEMPLATE.json +0 -10
- package/templates/ATELIER_RECORD_TEMPLATE.json +0 -48
- package/templates/ATLAS_TEMPLATE.html +0 -104
- package/templates/CAPABILITY_BRIEF_TEMPLATE.md +0 -36
- package/templates/CAPABILITY_GRAPH_EXAMPLE.json +0 -188
- package/templates/CAPABILITY_GRAPH_TEMPLATE.json +0 -78
- package/templates/EXPERTISE_PACK_TEMPLATE.json +0 -22
- package/templates/INTENT_MAP_TEMPLATE.json +0 -85
- package/templates/PHILOSOPHY_TEMPLATE.md +0 -44
- package/templates/VISION_TEMPLATE.md +0 -44
package/cli/src/intent-map.js
DELETED
|
@@ -1,747 +0,0 @@
|
|
|
1
|
-
// intent-map.js — Intent Map 的加载、校验、查询
|
|
2
|
-
// 真相源是磁盘上的 04_INTENT_MAP.json,这个库负责按需查询,不返回整个文件。
|
|
3
|
-
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync } from 'node:fs';
|
|
5
|
-
import { resolve, dirname, join } from 'node:path';
|
|
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';
|
|
10
|
-
|
|
11
|
-
/** 必填字段(INTENT_LOOP.md 底线) */
|
|
12
|
-
const REQUIRED_FIELDS = ['id', 'title', 'narrative_ref', 'depends_on', 'acceptance', 'philosophy_anchors', 'status'];
|
|
13
|
-
|
|
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
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 加载 Intent Map 文件。
|
|
44
|
-
* @param {string} versionDir — .loom/v{N}/ 目录的绝对路径
|
|
45
|
-
* @returns {{ _meta: object, intents: Record<string, object>, topo_order: string[] }}
|
|
46
|
-
*/
|
|
47
|
-
export function loadIntentMap(versionDir) {
|
|
48
|
-
const filePath = join(versionDir, '04_INTENT_MAP.json');
|
|
49
|
-
const data = readJsonFile(filePath, 'Intent Map');
|
|
50
|
-
validateIntentMap(data);
|
|
51
|
-
return data;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 校验 Intent Map 结构合规性(INTENT_LOOP.md I-1, I-2 底线)。
|
|
56
|
-
* 抛出错误列表,不静默修复。
|
|
57
|
-
*/
|
|
58
|
-
export function validateIntentMap(data) {
|
|
59
|
-
const errors = [];
|
|
60
|
-
|
|
61
|
-
if (!data.intents || typeof data.intents !== 'object') {
|
|
62
|
-
errors.push('缺少 intents 对象');
|
|
63
|
-
throw new Error(`Intent Map 校验失败:\n - ${errors.join('\n - ')}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!Array.isArray(data.topo_order)) {
|
|
67
|
-
errors.push('缺少 topo_order 数组');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
for (const [id, intent] of Object.entries(data.intents)) {
|
|
71
|
-
if (intent.id !== id) {
|
|
72
|
-
errors.push(`intents["${id}"].id 与 key 不一致 (实际: "${intent.id}")`);
|
|
73
|
-
}
|
|
74
|
-
for (const field of REQUIRED_FIELDS) {
|
|
75
|
-
if (!(field in intent)) {
|
|
76
|
-
errors.push(`intents["${id}"] 缺少必填字段: ${field}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
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);
|
|
91
|
-
if (intent.depends_on) {
|
|
92
|
-
for (const dep of intent.depends_on) {
|
|
93
|
-
if (!(dep in data.intents)) {
|
|
94
|
-
errors.push(`intents["${id}"].depends_on 引用了不存在的 Intent: ${dep}`);
|
|
95
|
-
}
|
|
96
|
-
// 依赖状态一致性:completed 的 Intent 不能依赖 blocked 的 Intent
|
|
97
|
-
const depIntent = data.intents[dep];
|
|
98
|
-
if (depIntent && intent.status === 'completed' && depIntent.status === 'blocked') {
|
|
99
|
-
errors.push(`intents["${id}"] 状态为 completed 但依赖 blocked 的 ${dep}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// acceptance 质量底线(IM-2):必须具体到可验证,不能是占位符
|
|
104
|
-
if (intent.acceptance && typeof intent.acceptance === 'string') {
|
|
105
|
-
const acc = intent.acceptance.trim();
|
|
106
|
-
if (acc === '...' || acc === '' || acc.length < 20) {
|
|
107
|
-
errors.push(
|
|
108
|
-
`intents["${id}"].acceptance 太短(${acc.length}字符)——必须是具体可验证的契约,不能是占位符。\n` +
|
|
109
|
-
` acceptance 应包含功能承诺 + 防御承诺(见 INTENT_LOOP.md IM-2 "acceptance 承诺分层")。`
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// topo_order 必须覆盖所有 Intent
|
|
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
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (errors.length > 0) {
|
|
143
|
-
throw new Error(`Intent Map 校验失败:\n - ${errors.join('\n - ')}`);
|
|
144
|
-
}
|
|
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
|
-
const qualityStrategy = intent.quality_strategy ?? 'adaptive';
|
|
178
|
-
|
|
179
|
-
if (!['adaptive', 'atelier'].includes(qualityStrategy)) {
|
|
180
|
-
errors.push(`${prefix}.quality_strategy 非法: ${JSON.stringify(qualityStrategy)} (合法: adaptive|atelier)`);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if ('proposal_refs' in intent) {
|
|
184
|
-
if (!Array.isArray(intent.proposal_refs) || intent.proposal_refs.some((ref) => typeof ref !== 'string' || !ref.trim())) {
|
|
185
|
-
errors.push(`${prefix}.proposal_refs 必须是非空 proposal ID 字符串数组`);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if ('continuity_required' in intent && typeof intent.continuity_required !== 'boolean') {
|
|
190
|
-
errors.push(`${prefix}.continuity_required 必须是布尔值;仅在本 Intent 会变更既有用户或系统状态且必须证明未误伤旧状态时设为 true`);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if ('quality_contract' in intent) {
|
|
194
|
-
if (typeof intent.quality_contract !== 'string' || intent.quality_contract.trim().length < 10) {
|
|
195
|
-
errors.push(`${prefix}.quality_contract 必须是非空质量契约或 05_VERIFICATION.md 章节引用`);
|
|
196
|
-
} else if (/^(?:\.{3}|…|todo|tbd|待填)$/i.test(intent.quality_contract.trim())) {
|
|
197
|
-
errors.push(`${prefix}.quality_contract 不能是占位符`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if ('capability_needs' in intent) {
|
|
202
|
-
if (!Array.isArray(intent.capability_needs)) {
|
|
203
|
-
errors.push(`${prefix}.capability_needs 必须是字符串数组`);
|
|
204
|
-
} else {
|
|
205
|
-
const normalized = [];
|
|
206
|
-
for (const [index, need] of intent.capability_needs.entries()) {
|
|
207
|
-
if (typeof need !== 'string' || need.trim().length < 2) {
|
|
208
|
-
errors.push(`${prefix}.capability_needs[${index}] 必须是非空专业领域`);
|
|
209
|
-
} else {
|
|
210
|
-
normalized.push(need.trim().toLowerCase());
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (new Set(normalized).size !== normalized.length) {
|
|
214
|
-
errors.push(`${prefix}.capability_needs 含重复专业领域`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if ('creative_scope' in intent) {
|
|
220
|
-
if (typeof intent.creative_scope !== 'string' || intent.creative_scope.trim().length < 10) {
|
|
221
|
-
errors.push(`${prefix}.creative_scope 必须说明可以改变什么、必须保持什么`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (qualityStrategy === 'atelier') {
|
|
226
|
-
if (typeof intent.quality_contract !== 'string' || intent.quality_contract.trim().length < 10) {
|
|
227
|
-
errors.push(`${prefix}.quality_strategy=atelier 必须声明有效 quality_contract`);
|
|
228
|
-
}
|
|
229
|
-
if (typeof intent.creative_scope !== 'string' || intent.creative_scope.trim().length < 10) {
|
|
230
|
-
errors.push(`${prefix}.quality_strategy=atelier 必须声明有效 creative_scope`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function validateLineage(data, id, lineage, errors) {
|
|
236
|
-
if (lineage === undefined) return;
|
|
237
|
-
const prefix = `intents["${id}"].lineage`;
|
|
238
|
-
if (!lineage || typeof lineage !== 'object' || Array.isArray(lineage)) {
|
|
239
|
-
errors.push(`${prefix} 必须是对象`);
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
if (!Array.isArray(lineage.predecessors)) {
|
|
243
|
-
errors.push(`${prefix}.predecessors 必须是数组`);
|
|
244
|
-
} else {
|
|
245
|
-
const seen = new Set();
|
|
246
|
-
for (const [index, predecessor] of lineage.predecessors.entries()) {
|
|
247
|
-
const refPrefix = `${prefix}.predecessors[${index}]`;
|
|
248
|
-
if (!predecessor || typeof predecessor !== 'object' || Array.isArray(predecessor)) {
|
|
249
|
-
errors.push(`${refPrefix} 必须是 { version, intent_id } 对象`);
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
if (!VERSION_PATTERN.test(predecessor.version || '')) errors.push(`${refPrefix}.version 格式非法`);
|
|
253
|
-
if (!INTENT_ID_PATTERN.test(predecessor.intent_id || '')) errors.push(`${refPrefix}.intent_id 格式非法`);
|
|
254
|
-
const ref = `${predecessor.version}:${predecessor.intent_id}`;
|
|
255
|
-
if (seen.has(ref)) errors.push(`${prefix}.predecessors 含重复引用: ${ref}`);
|
|
256
|
-
seen.add(ref);
|
|
257
|
-
if (predecessor.version === data._meta?._loom_version && predecessor.intent_id === id) {
|
|
258
|
-
errors.push(`${prefix} 不能自引用: ${ref}`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (typeof lineage.change_summary !== 'string' || lineage.change_summary.trim() === '') {
|
|
263
|
-
errors.push(`${prefix}.change_summary 必须是非空字符串`);
|
|
264
|
-
}
|
|
265
|
-
if ('change_ref' in lineage && (typeof lineage.change_ref !== 'string' || lineage.change_ref.trim() === '')) {
|
|
266
|
-
errors.push(`${prefix}.change_ref 必须是非空字符串`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const NON_SEMANTIC_FIELDS = new Set(['id', 'revision', 'status', 'lineage', '_runtime']);
|
|
271
|
-
|
|
272
|
-
function semanticIntent(intent) {
|
|
273
|
-
return Object.fromEntries(Object.entries(intent).filter(([key]) => !NON_SEMANTIC_FIELDS.has(key)));
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function changedSemanticFields(before, after) {
|
|
277
|
-
const a = semanticIntent(before);
|
|
278
|
-
const b = semanticIntent(after);
|
|
279
|
-
return [...new Set([...Object.keys(a), ...Object.keys(b)])]
|
|
280
|
-
.filter((key) => JSON.stringify(a[key]) !== JSON.stringify(b[key]))
|
|
281
|
-
.sort();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** Compare Intent semantics using only explicit target-version predecessor references. */
|
|
285
|
-
export function diffIntentVersions(loomRoot, fromVersion, toVersion) {
|
|
286
|
-
const from = fromVersion.startsWith('v') ? fromVersion : `v${fromVersion}`;
|
|
287
|
-
const to = toVersion.startsWith('v') ? toVersion : `v${toVersion}`;
|
|
288
|
-
if (!VERSION_PATTERN.test(from) || !VERSION_PATTERN.test(to)) throw new Error('版本格式应为 v1、v2 等');
|
|
289
|
-
const fromMap = loadIntentMap(join(loomRoot, from));
|
|
290
|
-
const toMap = loadIntentMap(join(loomRoot, to));
|
|
291
|
-
const sourceToTargets = new Map(Object.keys(fromMap.intents).map((id) => [id, []]));
|
|
292
|
-
const targetMappings = new Map();
|
|
293
|
-
const targetLineages = new Map();
|
|
294
|
-
const warnings = [];
|
|
295
|
-
|
|
296
|
-
for (const [targetId, target] of Object.entries(toMap.intents)) {
|
|
297
|
-
const refs = target.lineage?.predecessors || [];
|
|
298
|
-
const mapped = refs.filter((ref) => ref.version === from);
|
|
299
|
-
targetLineages.set(targetId, refs);
|
|
300
|
-
targetMappings.set(targetId, mapped);
|
|
301
|
-
for (const ref of mapped) {
|
|
302
|
-
if (fromMap.intents[ref.intent_id]) sourceToTargets.get(ref.intent_id).push(targetId);
|
|
303
|
-
else warnings.push(`${formatIntentRef(to, targetId)} 引用了不存在的 predecessor ${formatIntentRef(from, ref.intent_id)}`);
|
|
304
|
-
}
|
|
305
|
-
if (fromMap.intents[targetId] && !mapped.some((ref) => ref.intent_id === targetId)) {
|
|
306
|
-
warnings.push(`${formatIntentRef(from, targetId)} 与 ${formatIntentRef(to, targetId)} ID 相同但没有显式 lineage,不作映射`);
|
|
307
|
-
}
|
|
308
|
-
if (refs.length > 0 && mapped.length === 0) {
|
|
309
|
-
warnings.push(`${formatIntentRef(to, targetId)} 没有指向比较源版本 ${from} 的 predecessor`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const split = [...sourceToTargets.entries()]
|
|
314
|
-
.filter(([, targets]) => targets.length > 1)
|
|
315
|
-
.map(([sourceId, targets]) => ({ from: formatIntentRef(from, sourceId), to: targets.map((id) => formatIntentRef(to, id)) }));
|
|
316
|
-
const merged = [...targetMappings.entries()]
|
|
317
|
-
.filter(([, refs]) => refs.filter((ref) => fromMap.intents[ref.intent_id]).length > 1)
|
|
318
|
-
.map(([targetId, refs]) => ({ from: refs.filter((ref) => fromMap.intents[ref.intent_id]).map((ref) => formatIntentRef(from, ref.intent_id)), to: formatIntentRef(to, targetId) }));
|
|
319
|
-
const revised = [];
|
|
320
|
-
const unchanged = [];
|
|
321
|
-
for (const [targetId, refs] of targetMappings) {
|
|
322
|
-
const valid = refs.filter((ref) => fromMap.intents[ref.intent_id]);
|
|
323
|
-
if (valid.length !== 1 || sourceToTargets.get(valid[0].intent_id).length !== 1) continue;
|
|
324
|
-
const fields = changedSemanticFields(fromMap.intents[valid[0].intent_id], toMap.intents[targetId]);
|
|
325
|
-
const item = { from: formatIntentRef(from, valid[0].intent_id), to: formatIntentRef(to, targetId), changed_fields: fields };
|
|
326
|
-
(fields.length ? revised : unchanged).push(item);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const unmappedFrom = [...sourceToTargets.entries()].filter(([, targets]) => targets.length === 0).map(([id]) => formatIntentRef(from, id));
|
|
330
|
-
const unmappedTo = [...targetLineages.entries()]
|
|
331
|
-
.filter(([id, refs]) => refs.length > 0 && !targetMappings.get(id).some((ref) => fromMap.intents[ref.intent_id]))
|
|
332
|
-
.map(([id]) => formatIntentRef(to, id));
|
|
333
|
-
return {
|
|
334
|
-
from,
|
|
335
|
-
to,
|
|
336
|
-
new: Object.entries(toMap.intents)
|
|
337
|
-
.filter(([, intent]) => !intent.lineage?.predecessors?.length)
|
|
338
|
-
.map(([id]) => formatIntentRef(to, id)),
|
|
339
|
-
revised,
|
|
340
|
-
split,
|
|
341
|
-
merged,
|
|
342
|
-
unmapped: [...unmappedFrom, ...unmappedTo],
|
|
343
|
-
unmapped_from: unmappedFrom,
|
|
344
|
-
unmapped_to: unmappedTo,
|
|
345
|
-
unchanged,
|
|
346
|
-
warnings,
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/** Compute a stable topological order, preferring the previous order where possible. */
|
|
351
|
-
export function computeTopoOrder(intents, previousOrder = []) {
|
|
352
|
-
const ids = Object.keys(intents);
|
|
353
|
-
const rank = new Map(previousOrder.map((id, index) => [id, index]));
|
|
354
|
-
const compare = (a, b) => (rank.get(a) ?? Number.MAX_SAFE_INTEGER) - (rank.get(b) ?? Number.MAX_SAFE_INTEGER) || a.localeCompare(b);
|
|
355
|
-
const indegree = new Map(ids.map((id) => [id, 0]));
|
|
356
|
-
const dependents = new Map(ids.map((id) => [id, []]));
|
|
357
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
358
|
-
for (const dependency of intent.depends_on || []) {
|
|
359
|
-
if (!indegree.has(dependency)) throw new Error(`${id} 引用了不存在的依赖: ${dependency}`);
|
|
360
|
-
indegree.set(id, indegree.get(id) + 1);
|
|
361
|
-
dependents.get(dependency).push(id);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
const ready = ids.filter((id) => indegree.get(id) === 0).sort(compare);
|
|
365
|
-
const order = [];
|
|
366
|
-
while (ready.length) {
|
|
367
|
-
const id = ready.shift();
|
|
368
|
-
order.push(id);
|
|
369
|
-
for (const dependent of dependents.get(id).sort(compare)) {
|
|
370
|
-
indegree.set(dependent, indegree.get(dependent) - 1);
|
|
371
|
-
if (indegree.get(dependent) === 0) {
|
|
372
|
-
ready.push(dependent);
|
|
373
|
-
ready.sort(compare);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
if (order.length !== ids.length) throw new Error('Intent 依赖图存在循环');
|
|
378
|
-
return order;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* 返回下一个可执行 Intent:
|
|
383
|
-
* status=pending 且 depends_on 全部 completed,按 topo_order 取第一个。
|
|
384
|
-
* @returns {object|null} Intent 对象,或 null(没有可执行的)
|
|
385
|
-
*/
|
|
386
|
-
export function getNextIntent(versionDir) {
|
|
387
|
-
const { intents, topo_order } = loadIntentMap(versionDir);
|
|
388
|
-
for (const id of topo_order) {
|
|
389
|
-
const intent = intents[id];
|
|
390
|
-
if (intent.lifecycle?.deprecation) continue;
|
|
391
|
-
if (intent.status !== 'pending') continue;
|
|
392
|
-
const depsReady = intent.depends_on.every(
|
|
393
|
-
(dep) => intents[dep]?.status === 'completed'
|
|
394
|
-
);
|
|
395
|
-
if (depsReady) return withEffectiveRevision(intent);
|
|
396
|
-
}
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* 返回进度概览:各状态的 Intent 数量 + ID 列表。
|
|
402
|
-
*/
|
|
403
|
-
export function getStatus(versionDir) {
|
|
404
|
-
const { intents } = loadIntentMap(versionDir);
|
|
405
|
-
const summary = { pending: [], in_progress: [], completed: [], blocked: [], needs_review: [] };
|
|
406
|
-
const titles = {};
|
|
407
|
-
const deprecated = [];
|
|
408
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
409
|
-
const s = intent.status;
|
|
410
|
-
if (summary[s]) summary[s].push(id);
|
|
411
|
-
titles[id] = intent.title || '';
|
|
412
|
-
if (intent.lifecycle?.deprecation) deprecated.push(id);
|
|
413
|
-
}
|
|
414
|
-
return {
|
|
415
|
-
counts: {
|
|
416
|
-
pending: summary.pending.length,
|
|
417
|
-
in_progress: summary.in_progress.length,
|
|
418
|
-
completed: summary.completed.length,
|
|
419
|
-
blocked: summary.blocked.length,
|
|
420
|
-
needs_review: summary.needs_review.length,
|
|
421
|
-
total: Object.keys(intents).length,
|
|
422
|
-
deprecated: deprecated.length,
|
|
423
|
-
},
|
|
424
|
-
ids: summary,
|
|
425
|
-
titles,
|
|
426
|
-
deprecated,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
export function dependentClosure(intents, targetId) {
|
|
431
|
-
const direct = Object.values(intents)
|
|
432
|
-
.filter((intent) => intent.depends_on?.includes(targetId))
|
|
433
|
-
.map((intent) => intent.id);
|
|
434
|
-
const seen = new Set(direct);
|
|
435
|
-
const queue = [...direct];
|
|
436
|
-
while (queue.length) {
|
|
437
|
-
const current = queue.shift();
|
|
438
|
-
for (const intent of Object.values(intents)) {
|
|
439
|
-
if (intent.depends_on?.includes(current) && !seen.has(intent.id)) {
|
|
440
|
-
seen.add(intent.id);
|
|
441
|
-
queue.push(intent.id);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
return { direct, transitive: [...seen], all: [...seen] };
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/** Validate an exact, one-time partition of an impact set. */
|
|
449
|
-
export function validateImpactPartition(impactedIds, review = [], unaffected = []) {
|
|
450
|
-
const duplicate = (ids) => ids.find((id, index) => ids.indexOf(id) !== index);
|
|
451
|
-
if (duplicate(review)) throw new Error(`--review 含重复 ID: ${duplicate(review)}`);
|
|
452
|
-
if (duplicate(unaffected)) throw new Error(`--unaffected 含重复 ID: ${duplicate(unaffected)}`);
|
|
453
|
-
const overlap = review.filter((id) => unaffected.includes(id));
|
|
454
|
-
if (overlap.length) throw new Error(`review 与 unaffected 重叠: ${overlap.join(', ')}`);
|
|
455
|
-
const related = new Set(impactedIds);
|
|
456
|
-
const unrelated = [...review, ...unaffected].filter((id) => !related.has(id));
|
|
457
|
-
if (unrelated.length) throw new Error(`分区包含无关 Intent: ${[...new Set(unrelated)].join(', ')}`);
|
|
458
|
-
const classified = new Set([...review, ...unaffected]);
|
|
459
|
-
const missing = impactedIds.filter((id) => !classified.has(id));
|
|
460
|
-
if (missing.length) throw new Error(`依赖分区不完整,缺少: ${missing.join(', ')}`);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/** Apply the canonical completed -> needs_review transition and report every reviewed state. */
|
|
464
|
-
export function applyImpactReview(data, review, { incrementPassOnce = false } = {}) {
|
|
465
|
-
const completedIds = [];
|
|
466
|
-
const reviewed = review.map((id) => {
|
|
467
|
-
const intent = data.intents[id];
|
|
468
|
-
const before = intent.status;
|
|
469
|
-
if (before !== 'pending') intent.verification_epoch = getEffectiveVerificationEpoch(intent) + 1;
|
|
470
|
-
if (before === 'completed') {
|
|
471
|
-
if (!VALID_TRANSITIONS.completed.includes('needs_review')) throw new Error('completed 不能进入 needs_review');
|
|
472
|
-
intent.status = 'needs_review';
|
|
473
|
-
completedIds.push(id);
|
|
474
|
-
}
|
|
475
|
-
return { id, status_before: before, status_after: intent.status };
|
|
476
|
-
});
|
|
477
|
-
if (completedIds.length) registerReviewCycle(data, completedIds, incrementPassOnce);
|
|
478
|
-
return { reviewed, completedReviewed: completedIds.length > 0 };
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/** Track one convergence event while retaining all Intents still being reworked. */
|
|
482
|
-
export function registerReviewCycle(data, intentIds, incrementPass = true) {
|
|
483
|
-
if (!intentIds.length) return;
|
|
484
|
-
data._meta ??= {};
|
|
485
|
-
const reviewing = new Set(Array.isArray(data._meta.reviewing_ids) ? data._meta.reviewing_ids : []);
|
|
486
|
-
for (const id of intentIds) reviewing.add(id);
|
|
487
|
-
data._meta.reviewing_ids = [...reviewing];
|
|
488
|
-
if (incrementPass) data._meta.pass_count = (data._meta.pass_count || 0) + 1;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function finishReviewCycleIntent(data, intentId) {
|
|
492
|
-
const reviewing = new Set(Array.isArray(data._meta?.reviewing_ids) ? data._meta.reviewing_ids : []);
|
|
493
|
-
if (!reviewing.delete(intentId)) return;
|
|
494
|
-
data._meta.reviewing_ids = [...reviewing];
|
|
495
|
-
if (reviewing.size === 0) {
|
|
496
|
-
data._meta.pass_count = 0;
|
|
497
|
-
delete data._meta.reviewing_ids;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function deprecationEntry(intent) {
|
|
502
|
-
return { id: intent.id, title: intent.title || '', status: intent.status };
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function validateDeprecationTarget(data, intentId, reason) {
|
|
506
|
-
if (typeof reason !== 'string' || reason.trim() === '') throw new Error('--reason 必须是非空文本');
|
|
507
|
-
const target = data.intents[intentId];
|
|
508
|
-
if (!target) throw new Error(`Intent 不存在: ${intentId}`);
|
|
509
|
-
if (target.lifecycle?.deprecation) throw new Error(`Intent ${intentId} 已弃用,不能重复确认`);
|
|
510
|
-
if (target.status !== 'completed') throw new Error(`Intent ${intentId} 必须是 completed 才能弃用(当前: ${target.status})`);
|
|
511
|
-
return target;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/** Assess or atomically confirm deprecation of a completed current-version Intent. */
|
|
515
|
-
export function deprecateIntent(versionDir, intentId, options) {
|
|
516
|
-
const filePath = join(versionDir, '04_INTENT_MAP.json');
|
|
517
|
-
const data = readJsonFile(filePath, 'Intent Map');
|
|
518
|
-
validateIntentMap(data);
|
|
519
|
-
const target = validateDeprecationTarget(data, intentId, options.reason);
|
|
520
|
-
const dependents = dependentClosure(data.intents, intentId);
|
|
521
|
-
const describe = (ids) => ids.map((id) => deprecationEntry(data.intents[id]));
|
|
522
|
-
const impact = {
|
|
523
|
-
target: deprecationEntry(target),
|
|
524
|
-
dependents: { direct: describe(dependents.direct), transitive: describe(dependents.transitive) },
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
if (!options.confirm) {
|
|
528
|
-
const ids = dependents.all.join(',');
|
|
529
|
-
const escapedReason = options.reason.trim().replace(/"/g, '\\"');
|
|
530
|
-
const command = dependents.all.length
|
|
531
|
-
? `loom intent deprecate ${intentId} --reason "${escapedReason}" --confirm --review <IDs from ${ids}> --unaffected <remaining IDs from ${ids}>`
|
|
532
|
-
: `loom intent deprecate ${intentId} --reason "${escapedReason}" --confirm`;
|
|
533
|
-
return {
|
|
534
|
-
mode: 'assessment',
|
|
535
|
-
mutated: false,
|
|
536
|
-
...impact,
|
|
537
|
-
required_partition: dependents.all,
|
|
538
|
-
follow_up: {
|
|
539
|
-
command,
|
|
540
|
-
guidance: dependents.all.length
|
|
541
|
-
? 'Classify every listed dependent exactly once between --review and --unaffected; omit an empty group.'
|
|
542
|
-
: 'This is a leaf Intent; confirm without --review or --unaffected.',
|
|
543
|
-
},
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const replacement = options.replacement || null;
|
|
548
|
-
if (replacement !== null) {
|
|
549
|
-
if (replacement === intentId) throw new Error('--replacement 必须是另一个当前 Intent');
|
|
550
|
-
if (!(replacement in data.intents)) throw new Error(`replacement 不是当前 Intent: ${replacement}`);
|
|
551
|
-
}
|
|
552
|
-
const review = options.review || [];
|
|
553
|
-
const unaffected = options.unaffected || [];
|
|
554
|
-
validateImpactPartition(dependents.all, review, unaffected);
|
|
555
|
-
|
|
556
|
-
const { reviewed } = applyImpactReview(data, review, { incrementPassOnce: true });
|
|
557
|
-
const unchanged = unaffected.map((id) => ({ id, status_before: data.intents[id].status, status_after: data.intents[id].status }));
|
|
558
|
-
target.lifecycle = {
|
|
559
|
-
...(target.lifecycle || {}),
|
|
560
|
-
deprecation: { deprecated_at: new Date().toISOString(), reason: options.reason.trim(), replacement },
|
|
561
|
-
};
|
|
562
|
-
validateIntentMap(data);
|
|
563
|
-
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
564
|
-
writeFileSync(tempPath, `${JSON.stringify(data, null, 2)}\n`, 'utf-8');
|
|
565
|
-
try {
|
|
566
|
-
renameSync(tempPath, filePath);
|
|
567
|
-
} catch (error) {
|
|
568
|
-
try { unlinkSync(tempPath); } catch {}
|
|
569
|
-
throw error;
|
|
570
|
-
}
|
|
571
|
-
return { mode: 'confirmed', mutated: true, ...impact, deprecation: target.lifecycle.deprecation, reviewed, unaffected: unchanged };
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* 输出 Mermaid 依赖图。
|
|
576
|
-
*/
|
|
577
|
-
export function getDependencyGraph(versionDir) {
|
|
578
|
-
const { intents, topo_order } = loadIntentMap(versionDir);
|
|
579
|
-
const lines = ['```mermaid', 'graph TD'];
|
|
580
|
-
for (const id of topo_order) {
|
|
581
|
-
const intent = intents[id];
|
|
582
|
-
const shape = intent.status === 'completed' ? ':::done'
|
|
583
|
-
: intent.status === 'blocked' ? ':::blocked'
|
|
584
|
-
: intent.status === 'in_progress' ? ':::active'
|
|
585
|
-
: '';
|
|
586
|
-
lines.push(` ${id}${shape}`);
|
|
587
|
-
if (intent.depends_on && intent.depends_on.length > 0) {
|
|
588
|
-
for (const dep of intent.depends_on) {
|
|
589
|
-
lines.push(` ${dep} --> ${id}`);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
lines.push('```');
|
|
594
|
-
return lines.join('\n');
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* 按 ID 返回单个 Intent 的完整信息。
|
|
599
|
-
*/
|
|
600
|
-
export function getIntent(versionDir, intentId) {
|
|
601
|
-
const { intents } = loadIntentMap(versionDir);
|
|
602
|
-
if (!(intentId in intents)) {
|
|
603
|
-
throw new Error(`Intent 不存在: ${intentId}`);
|
|
604
|
-
}
|
|
605
|
-
return withEffectiveRevision(intents[intentId]);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* 更新 Intent 的运行时 status。
|
|
610
|
-
* 只允许合法的状态转换,防止跳变(如 completed→pending)。
|
|
611
|
-
* @param {string} versionDir — .loom/v{N}/ 目录
|
|
612
|
-
* @param {string} intentId — Intent ID
|
|
613
|
-
* @param {string} newStatus — pending | in_progress | completed | blocked
|
|
614
|
-
* @returns {object} 更新后的 Intent
|
|
615
|
-
*/
|
|
616
|
-
export function updateIntentStatus(versionDir, intentId, newStatus) {
|
|
617
|
-
if (!VALID_STATUS.includes(newStatus)) {
|
|
618
|
-
throw new Error(`非法 status: "${newStatus}" (合法: ${VALID_STATUS.join('|')})`);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const filePath = join(versionDir, '04_INTENT_MAP.json');
|
|
622
|
-
const data = readJsonFile(filePath, 'Intent Map');
|
|
623
|
-
|
|
624
|
-
if (!(intentId in data.intents)) {
|
|
625
|
-
throw new Error(`Intent 不存在: ${intentId}`);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const oldStatus = data.intents[intentId].status;
|
|
629
|
-
if (!VALID_TRANSITIONS[oldStatus]?.includes(newStatus)) {
|
|
630
|
-
throw new Error(
|
|
631
|
-
`非法状态转换: ${oldStatus} → ${newStatus}` +
|
|
632
|
-
`\n合法转换: ${oldStatus} → [${VALID_TRANSITIONS[oldStatus]?.join(', ') || '无(终态)'}]`
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (oldStatus === 'pending' && newStatus === 'in_progress') {
|
|
637
|
-
const incompleteDeps = data.intents[intentId].depends_on.filter(
|
|
638
|
-
(dep) => data.intents[dep]?.status !== 'completed'
|
|
639
|
-
);
|
|
640
|
-
if (incompleteDeps.length > 0) {
|
|
641
|
-
throw new Error(
|
|
642
|
-
`Intent ${intentId} 的依赖尚未完成: ${incompleteDeps.join(', ')}` +
|
|
643
|
-
'\n先完成依赖,或运行 loom intent next 获取当前可执行 Intent。'
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// completed 是事实声明,不是自由状态标签。无当前 revision 的最后一条 passed
|
|
649
|
-
// 验证时,必须通过 `loom intent done` 的受保护闭合路径,而不是直接 update。
|
|
650
|
-
if (newStatus === 'completed') {
|
|
651
|
-
const verificationPath = join(versionDir, 'verifications', `${intentId}.json`);
|
|
652
|
-
if (!existsSync(verificationPath)) {
|
|
653
|
-
throw new Error(
|
|
654
|
-
`Intent ${intentId} 没有验证记录,不能标记 completed。` +
|
|
655
|
-
`\n先运行 loom verify pass ${intentId} --summary "...",再运行 loom intent done ${intentId}。`
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
const history = readJsonFile(verificationPath, '验证记录');
|
|
659
|
-
const latest = history?.records?.[history.records.length - 1];
|
|
660
|
-
const expectedRevision = data.intents[intentId].revision ?? 1;
|
|
661
|
-
const recordRevision = Number.isInteger(latest?.intent_revision)
|
|
662
|
-
? latest.intent_revision
|
|
663
|
-
: (data.intents[intentId].revision === undefined ? 1 : null);
|
|
664
|
-
if (latest?.verdict !== 'passed' || recordRevision !== expectedRevision) {
|
|
665
|
-
throw new Error(
|
|
666
|
-
`Intent ${intentId} 缺少当前 revision ${expectedRevision} 的最后一条 passed 验证,不能标记 completed。` +
|
|
667
|
-
`\n先重新验证,再运行 loom intent done ${intentId}。`
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
if (data.intents[intentId].quality_contract) {
|
|
671
|
-
const quality = latest.dimensions?.quality_achievement;
|
|
672
|
-
if (quality?.verdict !== 'passed') {
|
|
673
|
-
throw new Error(
|
|
674
|
-
`Intent ${intentId} 含 quality_contract,但最新验证缺少通过的 quality_achievement,不能标记 completed。`
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
try {
|
|
678
|
-
resolveQualityProofReference(versionDir, quality.quality_proof_ref);
|
|
679
|
-
} catch (error) {
|
|
680
|
-
throw new Error(`Intent ${intentId} 的 Quality Proof 无效,不能标记 completed:${error.message}`);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
if (data.intents[intentId].continuity_required) {
|
|
684
|
-
const preservation = latest.dimensions?.preservation_achievement;
|
|
685
|
-
if (preservation?.verdict !== 'passed') {
|
|
686
|
-
throw new Error(
|
|
687
|
-
`Intent ${intentId} 声明了 continuity_required,但最新验证缺少通过的 preservation_achievement,不能标记 completed。`
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
const expectedEpoch = getEffectiveVerificationEpoch(data.intents[intentId]);
|
|
692
|
-
const recordEpoch = Number.isInteger(latest?.verification_epoch)
|
|
693
|
-
? latest.verification_epoch
|
|
694
|
-
: (data.intents[intentId].verification_epoch === undefined ? 1 : null);
|
|
695
|
-
if (recordEpoch !== expectedEpoch) {
|
|
696
|
-
throw new Error(
|
|
697
|
-
`Intent ${intentId} 缺少验证代次 ${expectedEpoch} 的最新 passed 记录,不能标记 completed。` +
|
|
698
|
-
'\n回流后必须重新验证,旧证据不能闭合当前 Intent。'
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
const method = getIntentVerificationMethod(data.intents[intentId]);
|
|
702
|
-
if (method && !latest?.reproduction_command) {
|
|
703
|
-
throw new Error(`Intent ${intentId} 声明了 verification_method,但最新验证缺少 reproduction_command,不能标记 completed。`);
|
|
704
|
-
}
|
|
705
|
-
if (method && !commandCoversVerificationMethod(latest.reproduction_command, method)) {
|
|
706
|
-
throw new Error(`Intent ${intentId} 的 reproduction_command 未覆盖 verification_method,不能标记 completed。`);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// completed → needs_review 开启一轮回流;同轮跟踪到该 Intent 再次闭合。
|
|
711
|
-
if (oldStatus === 'completed' && newStatus === 'needs_review') {
|
|
712
|
-
data.intents[intentId].verification_epoch = getEffectiveVerificationEpoch(data.intents[intentId]) + 1;
|
|
713
|
-
registerReviewCycle(data, [intentId]);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
data.intents[intentId].status = newStatus;
|
|
717
|
-
if ((newStatus === 'completed' || newStatus === 'blocked') && oldStatus !== 'completed') {
|
|
718
|
-
finishReviewCycleIntent(data, intentId);
|
|
719
|
-
}
|
|
720
|
-
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
721
|
-
return data.intents[intentId];
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* 获取某 Intent 的意图叙事(解析 narrative_ref,读愿景文档对应章节)。
|
|
726
|
-
* narrative_ref 格式: "01_VISION.md#int-001" 或 "01_VISION.md#int-001"
|
|
727
|
-
* @returns {string} 意图叙事内容
|
|
728
|
-
*/
|
|
729
|
-
export function getNarrative(versionDir, intentId) {
|
|
730
|
-
const { intents } = loadIntentMap(versionDir);
|
|
731
|
-
if (!(intentId in intents)) {
|
|
732
|
-
throw new Error(`Intent 不存在: ${intentId}`);
|
|
733
|
-
}
|
|
734
|
-
const ref = intents[intentId].narrative_ref;
|
|
735
|
-
if (!ref) {
|
|
736
|
-
throw new Error(`Intent ${intentId} 没有 narrative_ref`);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// 解析 "FILE.md#section" 格式
|
|
740
|
-
const [file, section] = ref.split('#');
|
|
741
|
-
const filePath = join(versionDir, file.trim());
|
|
742
|
-
if (!existsSync(filePath)) {
|
|
743
|
-
throw new Error(`愿景文档不存在: ${filePath}`);
|
|
744
|
-
}
|
|
745
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
746
|
-
return extractMdSection(content, section ? section.trim() : null, '意图叙事');
|
|
747
|
-
}
|