@haaaiawd/loom 0.9.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 +438 -149
- 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/auto.js +41 -18
- package/cli/src/diagnostics.js +223 -50
- package/cli/src/guide.js +127 -38
- 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 +181 -156
- 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 +204 -51
- 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/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');
|
|
@@ -59,7 +63,7 @@ function intentMapDiagnostics(versionDir) {
|
|
|
59
63
|
|
|
60
64
|
const isTemplate = raw._meta?._template === true;
|
|
61
65
|
if (isTemplate) {
|
|
62
|
-
issues.push({ id: 'intent_map', type: 'intent_map_template', severity: 'high', msg: 'Intent Map 仍是模板,尚未由 Architect 产出真实意图图' });
|
|
66
|
+
issues.push({ id: 'intent_map', type: 'intent_map_template', severity: 'high', msg: 'Intent Map 仍是模板,尚未由 Architect 产出真实意图图', is_template: true });
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
try {
|
|
@@ -69,16 +73,12 @@ function intentMapDiagnostics(versionDir) {
|
|
|
69
73
|
valid = false;
|
|
70
74
|
// 模板状态下字段缺失是预期的,降级为 high 而非 fatal
|
|
71
75
|
// 非模板状态下字段缺失是真正的损坏,保持 fatal
|
|
72
|
-
issues.push({ id: 'intent_map', type: 'intent_map_invalid', severity: isTemplate ? 'high' : 'fatal', msg: e.message });
|
|
76
|
+
issues.push({ id: 'intent_map', type: 'intent_map_invalid', severity: isTemplate ? 'high' : 'fatal', msg: e.message, is_template: isTemplate });
|
|
73
77
|
}
|
|
74
78
|
|
|
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, '')
|
|
@@ -87,6 +87,34 @@ function normalizeVerificationCommand(command) {
|
|
|
87
87
|
.trim();
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// 包管理器别名——npm test / pnpm test / bun test / yarn test 互相等价
|
|
91
|
+
const PM_ALIASES = ['npm', 'pnpm', 'bun', 'yarn'];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 把命令里的包管理器名归一化成 token,方便比较。
|
|
95
|
+
* "pnpm test" → "<PM> test","npm run build" → "<PM> run build"
|
|
96
|
+
*/
|
|
97
|
+
function normalizePackageManager(command) {
|
|
98
|
+
let result = command;
|
|
99
|
+
for (const pm of PM_ALIASES) {
|
|
100
|
+
result = result.replace(new RegExp(`\\b${pm}\\b`, 'g'), '<PM>');
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 检测项目使用的包管理器。
|
|
107
|
+
* 优先级:锁文件存在性
|
|
108
|
+
* @param {string} projectDir — 项目根目录
|
|
109
|
+
* @returns {string} 'pnpm' | 'bun' | 'yarn' | 'npm'
|
|
110
|
+
*/
|
|
111
|
+
export function detectPackageManager(projectDir) {
|
|
112
|
+
if (existsSync(join(projectDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
113
|
+
if (existsSync(join(projectDir, 'bun.lockb'))) return 'bun';
|
|
114
|
+
if (existsSync(join(projectDir, 'yarn.lock'))) return 'yarn';
|
|
115
|
+
return 'npm';
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
function commandCoversMethod(actualCommand, expectedMethod) {
|
|
91
119
|
const actual = normalizeVerificationCommand(actualCommand);
|
|
92
120
|
const expected = normalizeVerificationCommand(expectedMethod);
|
|
@@ -96,8 +124,12 @@ function commandCoversMethod(actualCommand, expectedMethod) {
|
|
|
96
124
|
const expectedPart = normalizeVerificationCommand(part);
|
|
97
125
|
if (!expectedPart) return true;
|
|
98
126
|
if (actual.includes(expectedPart)) return true;
|
|
127
|
+
// 包管理器别名归一化:pnpm test = npm test = bun test = yarn test
|
|
128
|
+
const actualNorm = normalizePackageManager(actual);
|
|
129
|
+
const expectedNorm = normalizePackageManager(expectedPart);
|
|
130
|
+
if (actualNorm.includes(expectedNorm)) return true;
|
|
99
131
|
// npm test is an acceptable broader reproduction for node --test based methods.
|
|
100
|
-
if (expectedPart.startsWith('node --test') &&
|
|
132
|
+
if (expectedPart.startsWith('node --test') && actualNorm.includes('<PM> test')) return true;
|
|
101
133
|
return false;
|
|
102
134
|
});
|
|
103
135
|
}
|
|
@@ -105,6 +137,48 @@ function commandCoversMethod(actualCommand, expectedMethod) {
|
|
|
105
137
|
// ─── doctor ────────────────────────────────────────────
|
|
106
138
|
// 全面健康检查:一致性 + 孤儿引用 + 循环依赖 + 僵尸 Intent
|
|
107
139
|
|
|
140
|
+
// 每种 issue 类型的修复提示——给 Agent 行动化建议
|
|
141
|
+
const FIX_HINTS = {
|
|
142
|
+
intent_map_unreadable: '检查 .loom/v{N}/04_INTENT_MAP.json 是否合法 JSON(jsonlint.com 或 node -e "JSON.parse(require(\'fs\').readFileSync(\'04_INTENT_MAP.json\'))")',
|
|
143
|
+
intent_map_missing: '运行 loom init 或 loom activate architect 产出 04_INTENT_MAP.json',
|
|
144
|
+
intent_map_template: '运行 loom activate architect,Architect 填充真实 Intent Map 后删除 _meta._template 标记',
|
|
145
|
+
intent_map_invalid: '按报错信息修正 04_INTENT_MAP.json 里对应字段(补 title / 加长 acceptance / 填必填字段)',
|
|
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} 闭合。',
|
|
148
|
+
in_progress_no_record: '运行 loom verify pass {id} --summary "..." 写入验证记录,或 loom intent update {id} --status pending 回退',
|
|
149
|
+
orphan_philosophy_ref: '检查 04_INTENT_MAP.json 里 {id} 的 philosophy_anchors,移除或修正不存在的哲学文件引用',
|
|
150
|
+
orphan_dependency: '检查 04_INTENT_MAP.json 里 {id} 的 depends_on,移除或修正不存在的 Intent ID',
|
|
151
|
+
cycle: '打破循环:把循环链中某个 Intent 的 depends_on 里去掉前驱,或拆成更小的 Intent',
|
|
152
|
+
zombie: '检查 {id} 是否还需要——不需要就 loom intent update {id} --status completed 或 blocked',
|
|
153
|
+
completed_depends_blocked: '检查依赖 {dep} 为什么 blocked——解决阻塞或把 {id} 回退到 in_progress',
|
|
154
|
+
test_script_missing: '在 package.json 里加 test 脚本,或修正 verification_method 指向实际存在的测试命令',
|
|
155
|
+
verification_method_unverified: '运行 loom verify pass {id} --summary "..." --reproduction-command "..." 覆盖声明的验证方式',
|
|
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 生成投影',
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 给 issue 补 fix_hint——把 {id} {dep} 等占位符替换成实际值。
|
|
168
|
+
*/
|
|
169
|
+
function addFixHint(issue) {
|
|
170
|
+
const template = FIX_HINTS[issue.type];
|
|
171
|
+
if (!template) return issue;
|
|
172
|
+
let hint = template;
|
|
173
|
+
// 提取 id 里的实际 Intent ID(issue.id 可能是 "INT-001" 或 "INT-002→INT-001" 等)
|
|
174
|
+
const idMatch = String(issue.id).match(/(INT-\d+)/);
|
|
175
|
+
if (idMatch) hint = hint.replace(/\{id\}/g, idMatch[1]);
|
|
176
|
+
// 提取 dep(从 msg 里找 depends_on 后的 Intent ID)
|
|
177
|
+
const depMatch = issue.msg && issue.msg.match(/依赖.*?(INT-\d+)/);
|
|
178
|
+
if (depMatch) hint = hint.replace(/\{dep\}/g, depMatch[1]);
|
|
179
|
+
return { ...issue, fix_hint: hint };
|
|
180
|
+
}
|
|
181
|
+
|
|
108
182
|
/**
|
|
109
183
|
* 项目健康检查。
|
|
110
184
|
* @param {string} versionDir — 当前版本目录
|
|
@@ -118,21 +192,55 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
118
192
|
|
|
119
193
|
if (!mapState.validMap) {
|
|
120
194
|
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
121
|
-
|
|
195
|
+
const issuesWithHints = issues.map(addFixHint);
|
|
196
|
+
return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
|
|
122
197
|
}
|
|
123
198
|
|
|
124
199
|
const { intents } = mapState.validMap;
|
|
125
200
|
|
|
126
|
-
// 1. 状态一致性:
|
|
127
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
}
|
|
136
244
|
|
|
137
245
|
// 2. 孤儿引用:哲学锚点指向不存在的文件
|
|
138
246
|
for (const [id, intent] of Object.entries(intents)) {
|
|
@@ -193,28 +301,29 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
193
301
|
|
|
194
302
|
// 7. 验证脚本可执行性:检查 verification_method 引用的脚本/目录是否存在
|
|
195
303
|
const projectDir = join(versionDir, '..', '..');
|
|
304
|
+
const pm = detectPackageManager(projectDir);
|
|
196
305
|
for (const [id, intent] of Object.entries(intents)) {
|
|
197
306
|
const vm = getIntentVerificationMethod(intent);
|
|
198
307
|
if (!vm) continue;
|
|
199
|
-
//
|
|
200
|
-
|
|
308
|
+
// 检测任意包管理器的 test 引用(npm/pnpm/bun/yarn)
|
|
309
|
+
const pmTestRe = new RegExp(`(?:${PM_ALIASES.join('|')})\\s+(?:run\\s+)?test`);
|
|
310
|
+
if (pmTestRe.test(vm)) {
|
|
201
311
|
const pkgPath = join(projectDir, 'package.json');
|
|
202
312
|
if (!existsSync(pkgPath)) {
|
|
203
|
-
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求
|
|
313
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但项目根没有 package.json` });
|
|
204
314
|
} else {
|
|
205
315
|
try {
|
|
206
316
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
207
317
|
const testScript = pkg.scripts && pkg.scripts.test;
|
|
208
318
|
if (!testScript) {
|
|
209
|
-
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求
|
|
319
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但 package.json 没有 test 脚本` });
|
|
210
320
|
} else {
|
|
211
321
|
// 检查 test 脚本引用的目录/文件是否存在
|
|
212
|
-
// 常见模式: "node --test test/" / "mocha test/" / "jest" 等
|
|
213
322
|
const testDirMatch = testScript.match(/(?:--test|test)\s+(\S+)/);
|
|
214
323
|
if (testDirMatch) {
|
|
215
324
|
const testTarget = testDirMatch[1].replace(/['"]/g, '');
|
|
216
325
|
if (!existsSync(join(projectDir, testTarget))) {
|
|
217
|
-
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求
|
|
326
|
+
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但 test 脚本引用的 ${testTarget} 不存在` });
|
|
218
327
|
}
|
|
219
328
|
}
|
|
220
329
|
}
|
|
@@ -242,13 +351,44 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
242
351
|
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 声明了 verification_method 但没有验证记录覆盖: ${method}` });
|
|
243
352
|
} else if (!actual) {
|
|
244
353
|
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 最新验证记录缺少 reproduction_command,无法复现 verification_method: ${method}` });
|
|
245
|
-
} else if (!
|
|
354
|
+
} else if (!commandCoversVerificationMethod(actual, expected)) {
|
|
246
355
|
issues.push({ id, type: 'verification_method_drift', severity: 'high', msg: `${id} verification_method 未被最新 reproduction_command 覆盖。method="${method}" reproduction_command="${latest.reproduction_command}"` });
|
|
247
356
|
}
|
|
248
357
|
}
|
|
249
358
|
|
|
250
|
-
|
|
251
|
-
|
|
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);
|
|
390
|
+
const issuesWithHints = issues.map(addFixHint);
|
|
391
|
+
return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
|
|
252
392
|
}
|
|
253
393
|
|
|
254
394
|
function appendPhilosophyDiagnostics(issues, philosophyDir) {
|
|
@@ -260,11 +400,6 @@ function appendPhilosophyDiagnostics(issues, philosophyDir) {
|
|
|
260
400
|
issues.push({ id: 'philosophy', type: 'inspiration_source', severity: issue.severity, msg: issue.msg });
|
|
261
401
|
}
|
|
262
402
|
|
|
263
|
-
// 实现部分拆解校验(防止 Weaver 跳过拆解步骤)
|
|
264
|
-
const decompositionCheck = validatePartDecomposition(philosophyDir);
|
|
265
|
-
for (const issue of decompositionCheck.issues) {
|
|
266
|
-
issues.push({ id: 'philosophy', type: 'part_decomposition', severity: issue.severity, msg: issue.msg });
|
|
267
|
-
}
|
|
268
403
|
}
|
|
269
404
|
|
|
270
405
|
function summarizeIssues(issues) {
|
|
@@ -331,25 +466,31 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
|
|
|
331
466
|
const pending = mapState.valid ? getPendingVerifications(versionDir, verificationsDir) : [];
|
|
332
467
|
const { issues } = doctor(versionDir, verificationsDir, philosophyDir);
|
|
333
468
|
|
|
469
|
+
// 区分模板阶段问题(待填充)和真实损坏
|
|
470
|
+
const templateIssues = issues.filter((i) => i.is_template || i.type === 'intent_map_template' || i.type === 'inspiration_source');
|
|
471
|
+
const realIssues = issues.filter((i) => !templateIssues.includes(i));
|
|
472
|
+
|
|
334
473
|
const risks = [];
|
|
335
|
-
const fatalCount =
|
|
336
|
-
const highCount =
|
|
474
|
+
const fatalCount = realIssues.filter((i) => i.severity === 'fatal').length;
|
|
475
|
+
const highCount = realIssues.filter((i) => i.severity === 'high').length;
|
|
337
476
|
if (fatalCount > 0) risks.push(`${fatalCount} 个致命问题(Intent Map 损坏/循环依赖)`);
|
|
338
477
|
if (highCount > 0) risks.push(`${highCount} 个高严重度问题(状态不一致/孤儿引用)`);
|
|
478
|
+
if (templateIssues.length > 0) risks.push(`${templateIssues.length} 个待填充(模板未产出,需 Weaver/Architect 填充)`);
|
|
339
479
|
if (status.counts.blocked > 0) risks.push(`${status.counts.blocked} 个阻塞 Intent`);
|
|
340
480
|
|
|
341
481
|
return {
|
|
342
482
|
intent_map_valid: mapState.valid === true,
|
|
343
|
-
progress: {
|
|
483
|
+
progress: {
|
|
344
484
|
completed: status.counts.completed,
|
|
345
485
|
total: status.counts.total,
|
|
346
|
-
rate: `${status.counts.completed}/${status.counts.total}`,
|
|
347
|
-
},
|
|
486
|
+
rate: `${status.counts.completed}/${status.counts.total}`,
|
|
487
|
+
},
|
|
488
|
+
deprecated_intents: status.deprecated || [],
|
|
348
489
|
next_intent: next ? next.id : null,
|
|
349
490
|
pending_verifications: pending,
|
|
350
491
|
inconsistent_states: issues.filter((i) => i.type === 'in_progress_no_record' || i.type === 'completed_no_record').map((i) => i.id),
|
|
351
492
|
risks,
|
|
352
|
-
healthy:
|
|
493
|
+
healthy: realIssues.length === 0,
|
|
353
494
|
};
|
|
354
495
|
}
|
|
355
496
|
|
|
@@ -364,7 +505,7 @@ export function contextSummary(versionDir, verificationsDir, philosophyDir) {
|
|
|
364
505
|
* @param {string} intentId
|
|
365
506
|
* @returns {object}
|
|
366
507
|
*/
|
|
367
|
-
export function traceIntent(versionDir, verificationsDir, philosophyDir, intentId) {
|
|
508
|
+
export function traceIntent(versionDir, verificationsDir, philosophyDir, intentId) {
|
|
368
509
|
const intent = getIntent(versionDir, intentId);
|
|
369
510
|
if (!intent) throw new Error(`Intent 不存在: ${intentId}`);
|
|
370
511
|
|
|
@@ -406,19 +547,51 @@ export function traceIntent(versionDir, verificationsDir, philosophyDir, intentI
|
|
|
406
547
|
walkDeps(dep, depth + 1);
|
|
407
548
|
}
|
|
408
549
|
}
|
|
409
|
-
walkDeps(intentId, 0);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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,
|
|
413
585
|
narrative,
|
|
414
586
|
narrative_error: narrativeError,
|
|
415
587
|
acceptance,
|
|
416
588
|
acceptance_error: acceptanceError,
|
|
417
589
|
verification_history: verificationHistory,
|
|
418
|
-
philosophy_anchors_content: philosophyContent,
|
|
419
|
-
dependency_chain: dependencyChain,
|
|
420
|
-
|
|
421
|
-
}
|
|
590
|
+
philosophy_anchors_content: philosophyContent,
|
|
591
|
+
dependency_chain: dependencyChain,
|
|
592
|
+
lineage: { predecessors, successors },
|
|
593
|
+
};
|
|
594
|
+
}
|
|
422
595
|
|
|
423
596
|
// ─── reverse-dep ───────────────────────────────────────
|
|
424
597
|
// 反向依赖:哪些 Intent 依赖这个 Intent
|
package/cli/src/guide.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { readCurrentPointer } from './version.js';
|
|
8
|
-
import { loadIntentMap } from './intent-map.js';
|
|
9
|
-
import { isAutoOn, writeHeartbeat, needsHumanReview } from './auto.js';
|
|
8
|
+
import { loadIntentMap } from './intent-map.js';
|
|
9
|
+
import { isAutoOn, getAutoMode, writeHeartbeat, needsHumanReview } from './auto.js';
|
|
10
|
+
import { doctor } from './diagnostics.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* 检测文件是否还是模板(未填充真实内容)。
|
|
@@ -36,17 +37,78 @@ function isTemplate(filePath) {
|
|
|
36
37
|
/**
|
|
37
38
|
* 诊断项目当前阶段。
|
|
38
39
|
* @param {string} projectDir — 项目根目录
|
|
39
|
-
* @returns {{ stage: string, stage_num: number, details: object, auto: boolean, next_action: string, next_command: string, message: string, needs_human_review: boolean }}
|
|
40
|
-
*/
|
|
41
|
-
export function guideProject(projectDir, options = {}) {
|
|
42
|
-
const cwd = projectDir || process.cwd();
|
|
43
|
-
const loomRoot = join(cwd, '.loom');
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
* @returns {{ stage: string, stage_num: number, details: object, auto: boolean, next_action: string, next_command: string, message: string, needs_human_review: boolean }}
|
|
41
|
+
*/
|
|
42
|
+
export function guideProject(projectDir, options = {}) {
|
|
43
|
+
const cwd = projectDir || process.cwd();
|
|
44
|
+
const loomRoot = join(cwd, '.loom');
|
|
45
|
+
const autoMode = getAutoMode(loomRoot);
|
|
46
|
+
const auto = autoMode !== 'manual'; // 向后兼容 boolean
|
|
47
|
+
const result = diagnoseStage(cwd, loomRoot, auto);
|
|
48
|
+
result.auto_mode = autoMode;
|
|
49
|
+
|
|
50
|
+
// 按 stage 补充可执行信息:要读什么、要产出什么、完成后跑什么校验
|
|
51
|
+
const current = result.details.version || 'v1';
|
|
52
|
+
const stageMeta = {
|
|
53
|
+
not_initialized: {
|
|
54
|
+
inputs: [],
|
|
55
|
+
outputs: ['.loom/v1/'],
|
|
56
|
+
verify_command: 'loom guide',
|
|
57
|
+
},
|
|
58
|
+
no_version: {
|
|
59
|
+
inputs: [],
|
|
60
|
+
outputs: ['.loom/v1/'],
|
|
61
|
+
verify_command: 'loom guide',
|
|
62
|
+
},
|
|
63
|
+
need_philosophy: {
|
|
64
|
+
inputs: ['meta/PHILOSOPHY_WEAVER.md', 'meta/BASELINE.md', 'dimensions/SEARCH_METHODOLOGY.md'],
|
|
65
|
+
outputs: [`.loom/${current}/00_PHILOSOPHY/PRODUCT_PHILOSOPHY.md`, `.loom/${current}/00_PHILOSOPHY/ENGINEERING_CREED.md`, `.loom/${current}/00_PHILOSOPHY/DECISION_RUBRIC.md`],
|
|
66
|
+
verify_command: 'loom philosophy check',
|
|
67
|
+
},
|
|
68
|
+
need_vision: {
|
|
69
|
+
inputs: ['roles/visionary.md', `.loom/${current}/00_PHILOSOPHY/`],
|
|
70
|
+
outputs: [`.loom/${current}/01_VISION.md`],
|
|
71
|
+
verify_command: 'loom guide',
|
|
72
|
+
},
|
|
73
|
+
need_architecture: {
|
|
74
|
+
inputs: ['roles/architect.md', `.loom/${current}/01_VISION.md`],
|
|
75
|
+
outputs: [`.loom/${current}/02_ARCHITECTURE.md`, `.loom/${current}/04_INTENT_MAP.json`],
|
|
76
|
+
verify_command: 'loom doctor',
|
|
77
|
+
},
|
|
78
|
+
intent_map_broken: {
|
|
79
|
+
inputs: [`.loom/${current}/04_INTENT_MAP.json`],
|
|
80
|
+
outputs: [`.loom/${current}/04_INTENT_MAP.json`],
|
|
81
|
+
verify_command: 'loom intent validate',
|
|
82
|
+
},
|
|
83
|
+
in_loop: {
|
|
84
|
+
inputs: ['roles/forge.md', 'roles/keeper.md', `.loom/${current}/04_INTENT_MAP.json`],
|
|
85
|
+
outputs: ['代码文件', `.loom/${current}/verifications/INT-*.json`],
|
|
86
|
+
verify_command: 'loom verify pending',
|
|
87
|
+
},
|
|
88
|
+
ready_for_loop: {
|
|
89
|
+
inputs: ['roles/forge.md', 'roles/keeper.md', `.loom/${current}/04_INTENT_MAP.json`],
|
|
90
|
+
outputs: ['代码文件', `.loom/${current}/verifications/INT-*.json`],
|
|
91
|
+
verify_command: 'loom verify pending',
|
|
92
|
+
},
|
|
93
|
+
done: {
|
|
94
|
+
inputs: [`.loom/${current}/04_INTENT_MAP.json`, `.loom/${current}/verifications/`],
|
|
95
|
+
outputs: [`.loom/${current}/06_CHANGELOG.json`, `.loom/${current}/06_CHANGELOG.md`],
|
|
96
|
+
verify_command: 'loom doctor',
|
|
97
|
+
},
|
|
98
|
+
unknown: {
|
|
99
|
+
inputs: [],
|
|
100
|
+
outputs: [],
|
|
101
|
+
verify_command: 'loom doctor',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
const meta = stageMeta[result.stage] || {};
|
|
105
|
+
result.inputs = meta.inputs || [];
|
|
106
|
+
result.outputs = meta.outputs || [];
|
|
107
|
+
result.verify_command = meta.verify_command || null;
|
|
108
|
+
// 统一后处理:写心跳 + 加 AUTO 提示词 + 判断是否需要人类 review
|
|
109
|
+
if (existsSync(loomRoot) && !options.dryRun) {
|
|
110
|
+
try {
|
|
111
|
+
writeHeartbeat(loomRoot, {
|
|
50
112
|
stage: result.stage,
|
|
51
113
|
stage_num: result.stage_num,
|
|
52
114
|
next_command: result.next_command,
|
|
@@ -57,13 +119,19 @@ export function guideProject(projectDir, options = {}) {
|
|
|
57
119
|
result.needs_human_review = needsHumanReview(loomRoot, result.stage_num);
|
|
58
120
|
// 在 message 末尾加 AUTO 提示
|
|
59
121
|
if (result.stage_num > 0 && result.stage_num < 4) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
122
|
+
if (autoMode === 'auto-design') {
|
|
123
|
+
result.message += '\n\n> AUTO 模式 auto-design——设计阶段也自动执行,无需人类确认。';
|
|
124
|
+
} else if (autoMode === 'auto-loop') {
|
|
125
|
+
result.message += '\n\n> ⚠ 这是设计阶段(哲学/愿景/架构),auto-loop 模式下需要人类 review 后再继续。';
|
|
126
|
+
} else {
|
|
127
|
+
result.message += '\n\n> ⚠ AUTO 模式 manual——每步都需要人类确认后再继续。';
|
|
128
|
+
}
|
|
129
|
+
} else if (result.stage_num >= 4) {
|
|
130
|
+
if (auto) {
|
|
131
|
+
result.message += '\n\n> AUTO 模式开启——直接执行 next_command,无需人类确认。';
|
|
132
|
+
} else {
|
|
133
|
+
result.message += '\n\n> ⚠ AUTO 模式关闭——执行 next_command 后等人类确认再继续。';
|
|
134
|
+
}
|
|
67
135
|
}
|
|
68
136
|
return result;
|
|
69
137
|
}
|
|
@@ -145,11 +213,11 @@ function diagnoseStage(cwd, loomRoot, auto) {
|
|
|
145
213
|
}
|
|
146
214
|
|
|
147
215
|
// 状态 4-7: Intent Map 已设计,根据 Intent 状态判断
|
|
148
|
-
let intentMap;
|
|
149
|
-
let intents;
|
|
150
|
-
try {
|
|
151
|
-
intentMap = loadIntentMap(versionDir);
|
|
152
|
-
intents = intentMap.intents;
|
|
216
|
+
let intentMap;
|
|
217
|
+
let intents;
|
|
218
|
+
try {
|
|
219
|
+
intentMap = loadIntentMap(versionDir);
|
|
220
|
+
intents = intentMap.intents;
|
|
153
221
|
} catch (e) {
|
|
154
222
|
return {
|
|
155
223
|
stage: 'intent_map_broken',
|
|
@@ -189,18 +257,39 @@ function diagnoseStage(cwd, loomRoot, auto) {
|
|
|
189
257
|
};
|
|
190
258
|
}
|
|
191
259
|
|
|
192
|
-
// 状态 6: 全部 completed
|
|
193
|
-
if (counts.completed === total && total > 0) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
260
|
+
// 状态 6: 全部 completed
|
|
261
|
+
if (counts.completed === total && total > 0) {
|
|
262
|
+
const health = doctor(versionDir, join(versionDir, 'verifications'), philosophyDir);
|
|
263
|
+
const blocking = health.issues.filter((issue) => issue.severity === 'fatal' || issue.severity === 'high');
|
|
264
|
+
if (blocking.length) {
|
|
265
|
+
return {
|
|
266
|
+
stage: 'needs_review',
|
|
267
|
+
stage_num: 5.5,
|
|
268
|
+
details: { version: current, counts, blocking_issues: blocking.map((issue) => issue.type) },
|
|
269
|
+
auto,
|
|
270
|
+
next_action: '修复完成门或验证证据后重新运行 doctor',
|
|
271
|
+
next_command: 'loom doctor',
|
|
272
|
+
message: `当前版本 ${current} 的 Intent 均标为 completed,但健康检查仍有 ${blocking.length} 个高风险问题;不能宣告阶段完成。先运行 loom doctor。`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const doneMessage = [
|
|
276
|
+
`当前版本 ${current}:全部 ${total} 个 Intent 已完成。`,
|
|
277
|
+
'',
|
|
278
|
+
'如果有新需求,先判断变更档位:',
|
|
279
|
+
'- Patch:不触及 Intent,只修 bug / 样式 / 实现细节;跑验证并记录 changelog。',
|
|
280
|
+
'- Minor:新增或修改 Intent,但不改变哲学前提、愿景北极星、架构边界;在当前版本内变更并重验受影响 Intent。',
|
|
281
|
+
'- Major:哲学前提、愿景北极星或架构边界变化;运行 loom version new。',
|
|
282
|
+
].join('\n');
|
|
283
|
+
return {
|
|
284
|
+
stage: 'done',
|
|
285
|
+
stage_num: 6,
|
|
286
|
+
details: { version: current, counts, total },
|
|
287
|
+
auto,
|
|
288
|
+
next_action: '项目阶段完成,按 Patch / Minor / Major 判断下一步',
|
|
289
|
+
next_command: 'loom help version',
|
|
290
|
+
message: doneMessage,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
204
293
|
|
|
205
294
|
// 状态 5: 有 in_progress
|
|
206
295
|
if (counts.in_progress > 0) {
|
|
@@ -220,7 +309,7 @@ function diagnoseStage(cwd, loomRoot, auto) {
|
|
|
220
309
|
if (counts.needs_review > 0) {
|
|
221
310
|
const reviewIds = allIntents.filter((i) => i.status === 'needs_review').map((i) => i.id);
|
|
222
311
|
// 读 _meta.pass_count 收敛趟计数(最大 3 趟)
|
|
223
|
-
const passCount = intentMap._meta?.pass_count || 1;
|
|
312
|
+
const passCount = intentMap._meta?.pass_count || 1;
|
|
224
313
|
const MAX_PASSES = 3;
|
|
225
314
|
const isOverLimit = passCount > MAX_PASSES;
|
|
226
315
|
const passMsg = ` [Pass ${passCount}/${MAX_PASSES}]`;
|