@haaaiawd/loom 1.2.2 → 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 -58
- package/CONTRIBUTING.md +37 -0
- package/EVIL_EVAL.md +112 -0
- package/README.md +193 -446
- package/README.zh-CN.md +174 -0
- package/SECURITY.md +11 -0
- package/cli/bin/loom.js +170 -995
- 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/capability.md +0 -73
- package/cli/help/concepts.md +0 -103
- package/cli/help/doctor.md +0 -73
- package/cli/help/expertise.md +0 -51
- package/cli/help/loop.md +0 -134
- package/cli/help/patch.md +0 -33
- package/cli/help/preview.md +0 -60
- package/cli/help/proposals.md +0 -21
- package/cli/help/version.md +0 -136
- package/cli/help/workflow.md +0 -114
- package/cli/src/activate.js +0 -473
- package/cli/src/asset-library.js +0 -384
- package/cli/src/atelier.js +0 -331
- package/cli/src/auto.js +0 -116
- package/cli/src/capability-graph.js +0 -430
- package/cli/src/capability-proposals.js +0 -225
- package/cli/src/diagnostics.js +0 -766
- package/cli/src/expertise-pack.js +0 -336
- package/cli/src/guide.js +0 -495
- 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/preview-prompt.md +0 -337
- package/cli/src/preview.js +0 -73
- 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 -86
- package/roles/forge.md +0 -112
- 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/CAPABILITY_BRIEF_TEMPLATE.md +0 -35
- package/templates/CAPABILITY_GRAPH_TEMPLATE.json +0 -11
- 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/diagnostics.js
DELETED
|
@@ -1,766 +0,0 @@
|
|
|
1
|
-
// diagnostics — LOOM 诊断与追溯工具集
|
|
2
|
-
// 提供 doctor / context / trace / reverse-dep / reverse-ref 五个聚合命令。
|
|
3
|
-
// 全部是只读的数据聚合,不做决策、不修改文件。
|
|
4
|
-
|
|
5
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
-
import { basename, dirname, join } from 'node:path';
|
|
7
|
-
import { loadIntentMap, getStatus, getNextIntent, getNarrative, getIntent } from './intent-map.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';
|
|
14
|
-
import { getCapabilityCoverage, getCapabilityGraphPath, loadCapabilityGraph } from './capability-graph.js';
|
|
15
|
-
import { listCapabilityProposals } from './capability-proposals.js';
|
|
16
|
-
import { getAssetManifestPath, validateAssetLibrary } from './asset-library.js';
|
|
17
|
-
import { validateAtelierRecord } from './atelier.js';
|
|
18
|
-
import { getExpertisePackState } from './expertise-pack.js';
|
|
19
|
-
|
|
20
|
-
function readIntentMapRaw(versionDir) {
|
|
21
|
-
const filePath = join(versionDir, '04_INTENT_MAP.json');
|
|
22
|
-
if (!existsSync(filePath)) return null;
|
|
23
|
-
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function summarizeRawIntentMap(data) {
|
|
27
|
-
const intents = data?.intents && typeof data.intents === 'object' ? data.intents : {};
|
|
28
|
-
const ids = { pending: [], in_progress: [], completed: [], blocked: [], needs_review: [] };
|
|
29
|
-
const titles = {};
|
|
30
|
-
|
|
31
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
32
|
-
const status = intent?.status;
|
|
33
|
-
if (ids[status]) ids[status].push(id);
|
|
34
|
-
titles[id] = intent?.title || '';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
counts: {
|
|
39
|
-
pending: ids.pending.length,
|
|
40
|
-
in_progress: ids.in_progress.length,
|
|
41
|
-
completed: ids.completed.length,
|
|
42
|
-
blocked: ids.blocked.length,
|
|
43
|
-
needs_review: ids.needs_review.length,
|
|
44
|
-
total: Object.keys(intents).length,
|
|
45
|
-
},
|
|
46
|
-
ids,
|
|
47
|
-
titles,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function intentMapDiagnostics(versionDir) {
|
|
52
|
-
const issues = [];
|
|
53
|
-
let raw = null;
|
|
54
|
-
let valid = null;
|
|
55
|
-
let validMap = null;
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
raw = readIntentMapRaw(versionDir);
|
|
59
|
-
} catch (e) {
|
|
60
|
-
issues.push({ id: 'intent_map', type: 'intent_map_unreadable', severity: 'fatal', msg: `Intent Map 无法读取或 JSON 损坏: ${e.message}` });
|
|
61
|
-
return { raw, valid, issues, validMap: null };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!raw) {
|
|
65
|
-
issues.push({ id: 'intent_map', type: 'intent_map_missing', severity: 'fatal', msg: '缺少 04_INTENT_MAP.json' });
|
|
66
|
-
return { raw, valid, issues, validMap: null };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const isTemplate = raw._meta?._template === true;
|
|
70
|
-
if (isTemplate) {
|
|
71
|
-
issues.push({ id: 'intent_map', type: 'intent_map_template', severity: 'high', msg: 'Intent Map 仍是模板,尚未由 Architect 产出真实意图图', is_template: true });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
validMap = loadIntentMap(versionDir);
|
|
76
|
-
valid = true;
|
|
77
|
-
} catch (e) {
|
|
78
|
-
valid = false;
|
|
79
|
-
// 模板状态下字段缺失是预期的,降级为 high 而非 fatal
|
|
80
|
-
// 非模板状态下字段缺失是真正的损坏,保持 fatal
|
|
81
|
-
issues.push({ id: 'intent_map', type: 'intent_map_invalid', severity: isTemplate ? 'high' : 'fatal', msg: e.message, is_template: isTemplate });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return { raw, valid, issues, validMap };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function normalizeVerificationCommand(command) {
|
|
88
|
-
return String(command || '')
|
|
89
|
-
.replace(/^\s*run\s+/i, '')
|
|
90
|
-
.replace(/^\s*exec\s+/i, '')
|
|
91
|
-
.replace(/\s+/g, ' ')
|
|
92
|
-
.trim();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 包管理器别名——npm test / pnpm test / bun test / yarn test 互相等价
|
|
96
|
-
const PM_ALIASES = ['npm', 'pnpm', 'bun', 'yarn'];
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* 把命令里的包管理器名归一化成 token,方便比较。
|
|
100
|
-
* "pnpm test" → "<PM> test","npm run build" → "<PM> run build"
|
|
101
|
-
*/
|
|
102
|
-
function normalizePackageManager(command) {
|
|
103
|
-
let result = command;
|
|
104
|
-
for (const pm of PM_ALIASES) {
|
|
105
|
-
result = result.replace(new RegExp(`\\b${pm}\\b`, 'g'), '<PM>');
|
|
106
|
-
}
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* 检测项目使用的包管理器。
|
|
112
|
-
* 优先级:锁文件存在性
|
|
113
|
-
* @param {string} projectDir — 项目根目录
|
|
114
|
-
* @returns {string} 'pnpm' | 'bun' | 'yarn' | 'npm'
|
|
115
|
-
*/
|
|
116
|
-
export function detectPackageManager(projectDir) {
|
|
117
|
-
if (existsSync(join(projectDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
118
|
-
if (existsSync(join(projectDir, 'bun.lockb'))) return 'bun';
|
|
119
|
-
if (existsSync(join(projectDir, 'yarn.lock'))) return 'yarn';
|
|
120
|
-
return 'npm';
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function commandCoversMethod(actualCommand, expectedMethod) {
|
|
124
|
-
const actual = normalizeVerificationCommand(actualCommand);
|
|
125
|
-
const expected = normalizeVerificationCommand(expectedMethod);
|
|
126
|
-
if (!actual || !expected) return false;
|
|
127
|
-
|
|
128
|
-
return expected.split('&&').every((part) => {
|
|
129
|
-
const expectedPart = normalizeVerificationCommand(part);
|
|
130
|
-
if (!expectedPart) return true;
|
|
131
|
-
if (actual.includes(expectedPart)) return true;
|
|
132
|
-
// 包管理器别名归一化:pnpm test = npm test = bun test = yarn test
|
|
133
|
-
const actualNorm = normalizePackageManager(actual);
|
|
134
|
-
const expectedNorm = normalizePackageManager(expectedPart);
|
|
135
|
-
if (actualNorm.includes(expectedNorm)) return true;
|
|
136
|
-
// npm test is an acceptable broader reproduction for node --test based methods.
|
|
137
|
-
if (expectedPart.startsWith('node --test') && actualNorm.includes('<PM> test')) return true;
|
|
138
|
-
return false;
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ─── doctor ────────────────────────────────────────────
|
|
143
|
-
// 全面健康检查:一致性 + 孤儿引用 + 循环依赖 + 僵尸 Intent
|
|
144
|
-
|
|
145
|
-
// 每种 issue 类型的修复提示——给 Agent 行动化建议
|
|
146
|
-
const FIX_HINTS = {
|
|
147
|
-
intent_map_unreadable: '检查 .loom/v{N}/04_INTENT_MAP.json 是否合法 JSON(jsonlint.com 或 node -e "JSON.parse(require(\'fs\').readFileSync(\'04_INTENT_MAP.json\'))")',
|
|
148
|
-
intent_map_missing: '运行 loom init 或 loom activate architect 产出 04_INTENT_MAP.json',
|
|
149
|
-
intent_map_template: '运行 loom activate architect,Architect 填充真实 Intent Map 后删除 _meta._template 标记',
|
|
150
|
-
intent_map_invalid: '按报错信息修正 04_INTENT_MAP.json 里对应字段(补 title / 加长 acceptance / 填必填字段)',
|
|
151
|
-
completed_no_record: '在 .loom/v{N}/verifications/ 下补验证记录,或运行 loom verify pass {id} --summary "..."',
|
|
152
|
-
completed_verification_not_passed: '最新验证不是当前 revision 的 passed;重新运行 loom verify pass {id} --summary "...",再用 loom intent done {id} 闭合。',
|
|
153
|
-
in_progress_no_record: '运行 loom verify pass {id} --summary "..." 写入验证记录,或 loom intent update {id} --status pending 回退',
|
|
154
|
-
orphan_philosophy_ref: '检查 04_INTENT_MAP.json 里 {id} 的 philosophy_anchors,移除或修正不存在的哲学文件引用',
|
|
155
|
-
orphan_dependency: '检查 04_INTENT_MAP.json 里 {id} 的 depends_on,移除或修正不存在的 Intent ID',
|
|
156
|
-
cycle: '打破循环:把循环链中某个 Intent 的 depends_on 里去掉前驱,或拆成更小的 Intent',
|
|
157
|
-
zombie: '检查 {id} 是否还需要——不需要就 loom intent update {id} --status completed 或 blocked',
|
|
158
|
-
completed_depends_blocked: '检查依赖 {dep} 为什么 blocked——解决阻塞或把 {id} 回退到 in_progress',
|
|
159
|
-
test_script_missing: '在 package.json 里加 test 脚本,或修正 verification_method 指向实际存在的测试命令',
|
|
160
|
-
verification_method_unverified: '运行 loom verify pass {id} --summary "..." --reproduction-command "..." 覆盖声明的验证方式',
|
|
161
|
-
verification_method_drift: '验证记录的 reproduction_command 要覆盖 verification_method 声明的命令(支持 npm/pnpm/bun 互相等价)',
|
|
162
|
-
stale_verification: '重新验证当前 Intent revision:loom verify pass {id} --summary "...",通过后再闭合 Intent',
|
|
163
|
-
inspiration_source: '在哲学文档的"证据地图/灵感来源"中写入实际使用的来源、选择理由和可追溯位置;数量由判断所需决定',
|
|
164
|
-
quality_dimension_missing: '为带 quality_contract 的 Intent 补写并通过 quality_achievement;相对提升声明在该维度中链接 Quality Proof',
|
|
165
|
-
quality_proof_invalid: '将 quality_proof_ref 改为项目内真实存在且含锚点的 Markdown 证据,例如 verifications/INT-001-quality-proof.md#int-001',
|
|
166
|
-
preservation_dimension_missing: '为 continuity_required 的 Intent 补写并通过 preservation_achievement,证据必须覆盖旧状态到新操作后的完整序列',
|
|
167
|
-
patch_changelog_invalid: '运行 loom patch validate 查看具体错误;修正 06_CHANGELOG.json 后重新生成 Markdown 投影',
|
|
168
|
-
patch_projection_drift: '不要手工编辑 06_CHANGELOG.md;重新运行 loom init 或下一次 loom patch record 生成投影',
|
|
169
|
-
capability_graph_missing: '这是 1.0 项目的兼容提示;迁移时运行 loom activate architect,创建 07_CAPABILITY_GRAPH.json 并路由高影响节点',
|
|
170
|
-
capability_graph_template: '运行 loom activate architect,先将愿景展开为 Capability Graph,再创建或修订 Intent Map',
|
|
171
|
-
capability_graph_invalid: '修正 07_CAPABILITY_GRAPH.json 的节点、关系或枚举值后运行 loom capability coverage',
|
|
172
|
-
capability_frontier_open: '为高影响节点继续展开、创建 Capability Brief、编译为 Intent,或明确延后/排除理由',
|
|
173
|
-
capability_plan_missing: '为高影响 capability 节点补齐可用 Brief 与获取计划,或改为明确的其他路由',
|
|
174
|
-
capability_route_evidence: '为该节点补齐路由所需的 Intent 回链、Brief 或延后/排除理由,避免只写一个状态标签',
|
|
175
|
-
capability_outcome_unexpanded: '将 outcome 连接到至少一个 concern,明确项目初衷需要被处理的问题面',
|
|
176
|
-
capability_outcome_unobservable: '为高影响 outcome 新建或补齐 evidence 节点,并以 validated_by 连接;evidence 必须写明观察目标、复现步骤、通过标准、证据产物,并回链承担验证的 Intent',
|
|
177
|
-
intent_graph_unmapped: '将该 Intent 回链到至少一个 Capability Graph 节点;不要让执行承诺失去项目初衷和能力来源',
|
|
178
|
-
capability_proposal_pending: '由 Architect 审核 proposal:判定已覆盖、更新 Graph、生成/修订 Intent、改变 acceptance,或升级 Minor/Major;Forge 不得静默把候选写进正式图谱。',
|
|
179
|
-
asset_library_invalid: '修复 08_ASSET_LIBRARY/manifest.json 的来源、许可、哈希、库内路径或 evidence 双向引用,然后运行 loom asset validate。',
|
|
180
|
-
atelier_record_invalid: '运行 loom atelier init {id} 创建记录,或按校验错误修正后运行 loom atelier validate {id}。',
|
|
181
|
-
atelier_verification_missing: '重新运行独立 Keeper 验证,让 passed 记录绑定当前 Atelier Record 与 stance_revision。',
|
|
182
|
-
expertise_pack_invalid: '运行 loom expertise init {id}(若尚未创建);实际执行 find skill / 网络或文档检索,补齐来源与 Capability Capsules 后运行 loom expertise validate {id}。',
|
|
183
|
-
expertise_verification_missing: '重新运行独立 Keeper 验证,让 passed 记录绑定当前 10_EXPERTISE_PACKS/{id}.json。',
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* 给 issue 补 fix_hint——把 {id} {dep} 等占位符替换成实际值。
|
|
188
|
-
*/
|
|
189
|
-
function addFixHint(issue) {
|
|
190
|
-
const template = FIX_HINTS[issue.type];
|
|
191
|
-
if (!template) return issue;
|
|
192
|
-
let hint = template;
|
|
193
|
-
// 提取 id 里的实际 Intent ID(issue.id 可能是 "INT-001" 或 "INT-002→INT-001" 等)
|
|
194
|
-
const idMatch = String(issue.id).match(/(INT-\d+)/);
|
|
195
|
-
if (idMatch) hint = hint.replace(/\{id\}/g, idMatch[1]);
|
|
196
|
-
// 提取 dep(从 msg 里找 depends_on 后的 Intent ID)
|
|
197
|
-
const depMatch = issue.msg && issue.msg.match(/依赖.*?(INT-\d+)/);
|
|
198
|
-
if (depMatch) hint = hint.replace(/\{dep\}/g, depMatch[1]);
|
|
199
|
-
return { ...issue, fix_hint: hint };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* 项目健康检查。
|
|
204
|
-
* @param {string} versionDir — 当前版本目录
|
|
205
|
-
* @param {string} verificationsDir — 验证记录目录
|
|
206
|
-
* @param {string} philosophyDir — 哲学目录
|
|
207
|
-
* @returns {{ issues: object[], summary: object }}
|
|
208
|
-
*/
|
|
209
|
-
export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
210
|
-
const mapState = intentMapDiagnostics(versionDir);
|
|
211
|
-
const issues = [...mapState.issues];
|
|
212
|
-
|
|
213
|
-
if (!mapState.validMap) {
|
|
214
|
-
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
215
|
-
const issuesWithHints = issues.map(addFixHint);
|
|
216
|
-
return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const { intents } = mapState.validMap;
|
|
220
|
-
const atelierRecords = new Map();
|
|
221
|
-
|
|
222
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
223
|
-
if (intent.quality_strategy !== 'atelier' || intent.status === 'pending' || intent.status === 'blocked') continue;
|
|
224
|
-
try {
|
|
225
|
-
const record = validateAtelierRecord(versionDir, id);
|
|
226
|
-
atelierRecords.set(id, record);
|
|
227
|
-
if (intent.status === 'completed' && !['selected', 'baseline_retained'].includes(record.status)) {
|
|
228
|
-
issues.push({ id, type: 'atelier_record_invalid', severity: 'high', msg: `${id} 已 completed,但 Atelier Record 仍是 ${record.status}` });
|
|
229
|
-
}
|
|
230
|
-
} catch (error) {
|
|
231
|
-
issues.push({ id, type: 'atelier_record_invalid', severity: 'high', msg: error.message });
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// 0. Incoming Graph changes are an Architect gate, not Forge implementation scope.
|
|
236
|
-
try {
|
|
237
|
-
for (const proposal of listCapabilityProposals(versionDir, { unresolvedOnly: true })) {
|
|
238
|
-
issues.push({ id: proposal.id, type: 'capability_proposal_pending', severity: 'high', msg: `${proposal.id} 是尚未闭合的 ${proposal.origin} 候选(${proposal.candidate_kind}: ${proposal.title});必须由 Architect 决定其影响。` });
|
|
239
|
-
}
|
|
240
|
-
} catch (error) {
|
|
241
|
-
issues.push({ id: 'capability_proposals', type: 'capability_proposal_pending', severity: 'high', msg: `Capability Graph proposal 无法审计: ${error.message}` });
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Asset Library is optional for a project, but once initialized it is a local source of truth.
|
|
245
|
-
if (existsSync(getAssetManifestPath(versionDir))) {
|
|
246
|
-
try { validateAssetLibrary(versionDir); } catch (error) {
|
|
247
|
-
issues.push({ id: 'asset_library', type: 'asset_library_invalid', severity: 'high', msg: error.message });
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// 0. Capability Graph:旧项目允许缺失但提示迁移;图谱一旦存在则必须可读、可路由。
|
|
252
|
-
const capabilityGraphPath = getCapabilityGraphPath(versionDir);
|
|
253
|
-
if (!existsSync(capabilityGraphPath)) {
|
|
254
|
-
issues.push({ id: 'capability_graph', type: 'capability_graph_missing', severity: 'medium', msg: '缺少 07_CAPABILITY_GRAPH.json;现有项目仍可运行,但不会获得图谱驱动的能力拆分与覆盖检查。' });
|
|
255
|
-
} else {
|
|
256
|
-
try {
|
|
257
|
-
const graph = loadCapabilityGraph(versionDir);
|
|
258
|
-
if (graph._meta?._template === true) {
|
|
259
|
-
issues.push({ id: 'capability_graph', type: 'capability_graph_template', severity: 'high', msg: 'Capability Graph 仍是模板;不得在未路由项目问题与能力缺口时宣告架构完成。' });
|
|
260
|
-
} else {
|
|
261
|
-
const coverage = getCapabilityCoverage(versionDir);
|
|
262
|
-
for (const node of coverage.high_unrouted) {
|
|
263
|
-
issues.push({ id: node.id, type: 'capability_frontier_open', severity: 'high', msg: `${node.id} 是尚未路由的高影响 Capability Graph 节点: ${node.title}` });
|
|
264
|
-
}
|
|
265
|
-
for (const item of coverage.capabilities_without_plan) {
|
|
266
|
-
issues.push({ id: item.node_id, type: 'capability_plan_missing', severity: 'high', msg: `${item.node_id} 的能力获取计划不完整: ${item.reason}` });
|
|
267
|
-
}
|
|
268
|
-
for (const item of coverage.routing_gaps) {
|
|
269
|
-
issues.push({ id: item.node_id, type: 'capability_route_evidence', severity: 'high', msg: `${item.node_id} 的图谱路由缺少依据: ${item.reason}` });
|
|
270
|
-
}
|
|
271
|
-
for (const item of coverage.outcomes_without_concern) {
|
|
272
|
-
issues.push({ id: item.node_id, type: 'capability_outcome_unexpanded', severity: 'high', msg: `${item.node_id} 没有展开为项目问题面: ${item.reason}` });
|
|
273
|
-
}
|
|
274
|
-
for (const item of coverage.high_outcomes_without_observable_evidence) {
|
|
275
|
-
issues.push({ id: item.node_id, type: 'capability_outcome_unobservable', severity: 'high', msg: `${item.node_id} 缺少真实呈现或交付的验证入口: ${item.reason}` });
|
|
276
|
-
}
|
|
277
|
-
for (const item of coverage.evidence_artifact_gaps || []) {
|
|
278
|
-
issues.push({ id: item.node_id, type: 'capability_evidence_artifact_missing', severity: 'high', msg: `${item.node_id} 的完成证据不可用: ${item.reason}` });
|
|
279
|
-
}
|
|
280
|
-
for (const item of coverage.orphan_intent_refs) {
|
|
281
|
-
issues.push({ id: item.node_id, type: 'intent_graph_unmapped', severity: 'high', msg: `${item.node_id} 引用了不存在的 Intent: ${item.intent_id}` });
|
|
282
|
-
}
|
|
283
|
-
for (const intentId of coverage.unmapped_intents) {
|
|
284
|
-
issues.push({ id: intentId, type: 'intent_graph_unmapped', severity: 'high', msg: `${intentId} 没有 Capability Graph 回链;执行承诺缺少能力与问题来源。` });
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
} catch (error) {
|
|
288
|
-
issues.push({ id: 'capability_graph', type: 'capability_graph_invalid', severity: 'high', msg: error.message });
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// 1. 状态一致性:completed 必须由当前 revision 的最后一条 passed 验证支撑。
|
|
293
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
294
|
-
const history = getVerificationHistory(verificationsDir, id);
|
|
295
|
-
const hasRecord = Boolean(history?.records?.length);
|
|
296
|
-
const latest = history?.records?.[history.records.length - 1];
|
|
297
|
-
if (intent.status === 'completed' && !hasRecord) {
|
|
298
|
-
issues.push({ id, type: 'completed_no_record', severity: 'high', msg: `${id} 状态为 completed 但无验证记录` });
|
|
299
|
-
} else if (intent.status === 'completed' && !hasCurrentPassedVerification(intent, history)) {
|
|
300
|
-
issues.push({ id, type: 'completed_verification_not_passed', severity: 'high', msg: `${id} 状态为 completed,但最后一条验证不是当前 revision 的 passed` });
|
|
301
|
-
}
|
|
302
|
-
if (intent.status === 'in_progress' && !hasRecord) {
|
|
303
|
-
issues.push({ id, type: 'in_progress_no_record', severity: 'medium', msg: `${id} 状态为 in_progress 但无验证记录(可能上次中断)` });
|
|
304
|
-
}
|
|
305
|
-
if (intent.quality_contract && latest?.verdict === 'passed') {
|
|
306
|
-
const quality = latest.dimensions?.quality_achievement;
|
|
307
|
-
if (!quality || quality.verdict !== 'passed') {
|
|
308
|
-
issues.push({
|
|
309
|
-
id,
|
|
310
|
-
type: 'quality_dimension_missing',
|
|
311
|
-
severity: 'high',
|
|
312
|
-
msg: `${id} 声明了 quality_contract,但最新 passed 记录缺少通过的 quality_achievement`,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
if (quality?.quality_proof_ref) {
|
|
316
|
-
try {
|
|
317
|
-
resolveQualityProofReference(versionDir, quality.quality_proof_ref);
|
|
318
|
-
} catch (error) {
|
|
319
|
-
issues.push({ id, type: 'quality_proof_invalid', severity: 'high', msg: `${id} 的 Quality Proof 无效: ${error.message}` });
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (!['pending', 'blocked', 'deprecated'].includes(intent.status)) {
|
|
324
|
-
try {
|
|
325
|
-
const expertise = getExpertisePackState(versionDir, id);
|
|
326
|
-
if (expertise.required && !expertise.ready) {
|
|
327
|
-
issues.push({
|
|
328
|
-
id,
|
|
329
|
-
type: 'expertise_pack_invalid',
|
|
330
|
-
severity: intent.status === 'completed' || latest?.verdict === 'passed' ? 'high' : 'medium',
|
|
331
|
-
msg: `${id} 的外部能力获取强门尚未闭合: ${expertise.reason || 'Expertise Pack not ready'}`,
|
|
332
|
-
});
|
|
333
|
-
} else if (expertise.required && latest?.verdict === 'passed') {
|
|
334
|
-
const validation = expertise.validation;
|
|
335
|
-
if (latest.expertise?.record_ref !== `10_EXPERTISE_PACKS/${id}.json`
|
|
336
|
-
|| latest.expertise?.intent_revision !== validation.intent_revision
|
|
337
|
-
|| latest.expertise?.source_count !== validation.source_count
|
|
338
|
-
|| latest.expertise?.capsule_count !== validation.capsule_count
|
|
339
|
-
|| latest.expertise?.pack_digest !== validation.pack_digest) {
|
|
340
|
-
issues.push({
|
|
341
|
-
id,
|
|
342
|
-
type: 'expertise_verification_missing',
|
|
343
|
-
severity: 'high',
|
|
344
|
-
msg: `${id} 的最新 passed 未绑定当前 Expertise Pack、Intent revision 与来源/Capsule 计数`,
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
} catch { /* Capability Graph 的结构错误已由上方统一报告。 */ }
|
|
349
|
-
}
|
|
350
|
-
if (intent.quality_strategy === 'atelier' && latest?.verdict === 'passed') {
|
|
351
|
-
const atelier = atelierRecords.get(id);
|
|
352
|
-
if (!atelier
|
|
353
|
-
|| latest.atelier?.record_ref !== `09_ATELIER/${id}.json`
|
|
354
|
-
|| latest.atelier?.stance_revision !== atelier.stance_revision
|
|
355
|
-
|| latest.atelier?.status !== atelier.status) {
|
|
356
|
-
issues.push({
|
|
357
|
-
id,
|
|
358
|
-
type: 'atelier_verification_missing',
|
|
359
|
-
severity: 'high',
|
|
360
|
-
msg: `${id} 的最新 passed 未绑定当前 Atelier Record、status 与 stance_revision`,
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
if (intent.continuity_required && latest?.verdict === 'passed') {
|
|
365
|
-
const preservation = latest.dimensions?.preservation_achievement;
|
|
366
|
-
if (!preservation || preservation.verdict !== 'passed') {
|
|
367
|
-
issues.push({
|
|
368
|
-
id,
|
|
369
|
-
type: 'preservation_dimension_missing',
|
|
370
|
-
severity: 'high',
|
|
371
|
-
msg: `${id} 声明了 continuity_required,但最新 passed 记录缺少通过的 preservation_achievement`,
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// 2. 孤儿引用:哲学锚点指向不存在的文件
|
|
378
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
379
|
-
if (!intent.philosophy_anchors) continue;
|
|
380
|
-
for (const anchor of intent.philosophy_anchors) {
|
|
381
|
-
const [file] = anchor.split('#');
|
|
382
|
-
const filePath = join(philosophyDir, file);
|
|
383
|
-
if (!existsSync(filePath)) {
|
|
384
|
-
issues.push({ id, type: 'orphan_philosophy_ref', severity: 'high', msg: `${id} 引用不存在的哲学文件: ${file}` });
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// 3. 孤儿引用:depends_on 指向不存在的 Intent
|
|
390
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
391
|
-
if (!intent.depends_on) continue;
|
|
392
|
-
for (const dep of intent.depends_on) {
|
|
393
|
-
if (!(dep in intents)) {
|
|
394
|
-
issues.push({ id, type: 'orphan_dependency', severity: 'high', msg: `${id} 依赖不存在的 Intent: ${dep}` });
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// 4. 循环依赖检测(DFS)
|
|
400
|
-
const cycles = detectCycles(intents);
|
|
401
|
-
for (const cycle of cycles) {
|
|
402
|
-
issues.push({ id: cycle.join('→'), type: 'cycle', severity: 'fatal', msg: `循环依赖: ${cycle.join(' → ')}` });
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// 5. 僵尸 Intent:in_progress/blocked 超过 N 天无活动(按验证记录最后修改时间)
|
|
406
|
-
const ZOMBIE_DAYS = 7;
|
|
407
|
-
const now = Date.now();
|
|
408
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
409
|
-
if (intent.status !== 'in_progress' && intent.status !== 'blocked') continue;
|
|
410
|
-
const recordPath = join(verificationsDir, `${id}.json`);
|
|
411
|
-
let lastActivity = existsSync(join(versionDir, '04_INTENT_MAP.json'))
|
|
412
|
-
? statSync(join(versionDir, '04_INTENT_MAP.json')).mtimeMs
|
|
413
|
-
: now;
|
|
414
|
-
if (existsSync(recordPath)) {
|
|
415
|
-
lastActivity = Math.max(lastActivity, statSync(recordPath).mtimeMs);
|
|
416
|
-
}
|
|
417
|
-
const daysIdle = (now - lastActivity) / (1000 * 60 * 60 * 24);
|
|
418
|
-
if (daysIdle > ZOMBIE_DAYS) {
|
|
419
|
-
issues.push({ id, type: 'zombie', severity: 'medium', msg: `${id} 状态为 ${intent.status} 已 ${Math.floor(daysIdle)} 天无活动` });
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// 6. 依赖状态一致性:completed 不能依赖 blocked
|
|
424
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
425
|
-
if (intent.status !== 'completed' || !intent.depends_on) continue;
|
|
426
|
-
for (const dep of intent.depends_on) {
|
|
427
|
-
const depIntent = intents[dep];
|
|
428
|
-
if (depIntent && depIntent.status === 'blocked') {
|
|
429
|
-
issues.push({ id, type: 'completed_depends_blocked', severity: 'high', msg: `${id} 状态为 completed 但依赖 blocked 的 ${dep}` });
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// 7. 验证脚本可执行性:检查 verification_method 引用的脚本/目录是否存在
|
|
435
|
-
const projectDir = join(versionDir, '..', '..');
|
|
436
|
-
const pm = detectPackageManager(projectDir);
|
|
437
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
438
|
-
const vm = getIntentVerificationMethod(intent);
|
|
439
|
-
if (!vm) continue;
|
|
440
|
-
// 检测任意包管理器的 test 引用(npm/pnpm/bun/yarn)
|
|
441
|
-
const pmTestRe = new RegExp(`(?:${PM_ALIASES.join('|')})\\s+(?:run\\s+)?test`);
|
|
442
|
-
if (pmTestRe.test(vm)) {
|
|
443
|
-
const pkgPath = join(projectDir, 'package.json');
|
|
444
|
-
if (!existsSync(pkgPath)) {
|
|
445
|
-
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但项目根没有 package.json` });
|
|
446
|
-
} else {
|
|
447
|
-
try {
|
|
448
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
449
|
-
const testScript = pkg.scripts && pkg.scripts.test;
|
|
450
|
-
if (!testScript) {
|
|
451
|
-
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但 package.json 没有 test 脚本` });
|
|
452
|
-
} else {
|
|
453
|
-
// 检查 test 脚本引用的目录/文件是否存在
|
|
454
|
-
const testDirMatch = testScript.match(/(?:--test|test)\s+(\S+)/);
|
|
455
|
-
if (testDirMatch) {
|
|
456
|
-
const testTarget = testDirMatch[1].replace(/['"]/g, '');
|
|
457
|
-
if (!existsSync(join(projectDir, testTarget))) {
|
|
458
|
-
issues.push({ id, type: 'test_script_missing', severity: 'medium', msg: `${id} verification_method 要求 test 但 test 脚本引用的 ${testTarget} 不存在` });
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
} catch {
|
|
463
|
-
// package.json 解析失败,不报——不是 doctor 的职责
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// 8. completed Intent 的 verification_method 必须被最新验证记录覆盖,防止契约命令漂移。
|
|
470
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
471
|
-
if (intent.status !== 'completed') continue;
|
|
472
|
-
const method = getIntentVerificationMethod(intent);
|
|
473
|
-
if (!method) continue;
|
|
474
|
-
|
|
475
|
-
const expected = normalizeVerificationCommand(method);
|
|
476
|
-
if (!expected || expected === 'human_review') continue;
|
|
477
|
-
|
|
478
|
-
const history = getVerificationHistory(verificationsDir, id);
|
|
479
|
-
const latest = history?.records?.[history.records.length - 1];
|
|
480
|
-
const actual = normalizeVerificationCommand(latest?.reproduction_command);
|
|
481
|
-
|
|
482
|
-
if (!latest) {
|
|
483
|
-
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 声明了 verification_method 但没有验证记录覆盖: ${method}` });
|
|
484
|
-
} else if (!actual) {
|
|
485
|
-
issues.push({ id, type: 'verification_method_unverified', severity: 'high', msg: `${id} 最新验证记录缺少 reproduction_command,无法复现 verification_method: ${method}` });
|
|
486
|
-
} else if (!commandCoversVerificationMethod(actual, expected)) {
|
|
487
|
-
issues.push({ id, type: 'verification_method_drift', severity: 'high', msg: `${id} verification_method 未被最新 reproduction_command 覆盖。method="${method}" reproduction_command="${latest.reproduction_command}"` });
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const patchJsonPath = join(versionDir, '06_CHANGELOG.json');
|
|
492
|
-
if (existsSync(patchJsonPath)) {
|
|
493
|
-
try {
|
|
494
|
-
validatePatches(versionDir);
|
|
495
|
-
} catch (error) {
|
|
496
|
-
const projectionDrift = String(error.message).includes('最新确定性投影') || String(error.message).includes('Markdown 投影不存在');
|
|
497
|
-
issues.push({
|
|
498
|
-
id: 'patch_changelog',
|
|
499
|
-
type: projectionDrift ? 'patch_projection_drift' : 'patch_changelog_invalid',
|
|
500
|
-
severity: projectionDrift ? 'medium' : 'high',
|
|
501
|
-
msg: error.message,
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// completed/needs_review 的旧 revision 验证不能证明当前 Intent 仍然成立。
|
|
507
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
508
|
-
if (intent.status !== 'completed' && intent.status !== 'needs_review') continue;
|
|
509
|
-
const history = getVerificationHistory(verificationsDir, id);
|
|
510
|
-
const latestPassed = getLatestPassedVerification(history);
|
|
511
|
-
if (!latestPassed || isVerificationCurrent(intent, latestPassed)) continue;
|
|
512
|
-
const verifiedRevision = getVerificationIntentRevision(intent, latestPassed);
|
|
513
|
-
issues.push({
|
|
514
|
-
id,
|
|
515
|
-
type: 'stale_verification',
|
|
516
|
-
severity: 'high',
|
|
517
|
-
msg: `${id} 最新 passed 验证 revision ${verifiedRevision ?? 'legacy/unknown'} 早于当前 Intent revision ${intent.revision ?? 1}`,
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
appendPhilosophyDiagnostics(issues, philosophyDir);
|
|
522
|
-
const issuesWithHints = issues.map(addFixHint);
|
|
523
|
-
return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function appendPhilosophyDiagnostics(issues, philosophyDir) {
|
|
527
|
-
// 哲学灵感来源校验(防止 Weaver 从训练数据"背"几个名字就交差)
|
|
528
|
-
if (!existsSync(philosophyDir)) return;
|
|
529
|
-
|
|
530
|
-
const inspirationCheck = validateInspirationSources(philosophyDir);
|
|
531
|
-
for (const issue of inspirationCheck.issues) {
|
|
532
|
-
issues.push({ id: 'philosophy', type: 'inspiration_source', severity: issue.severity, msg: issue.msg });
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function summarizeIssues(issues) {
|
|
538
|
-
return {
|
|
539
|
-
total_issues: issues.length,
|
|
540
|
-
fatal: issues.filter((i) => i.severity === 'fatal').length,
|
|
541
|
-
high: issues.filter((i) => i.severity === 'high').length,
|
|
542
|
-
medium: issues.filter((i) => i.severity === 'medium').length,
|
|
543
|
-
healthy: issues.length === 0,
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* DFS 检测循环依赖。
|
|
549
|
-
* 返回循环路径数组,每个是 [A, B, C, A] 形式。
|
|
550
|
-
*/
|
|
551
|
-
function detectCycles(intents) {
|
|
552
|
-
const cycles = [];
|
|
553
|
-
const visited = new Set();
|
|
554
|
-
const stack = new Set();
|
|
555
|
-
const path = [];
|
|
556
|
-
|
|
557
|
-
function dfs(id) {
|
|
558
|
-
if (stack.has(id)) {
|
|
559
|
-
// 找到环
|
|
560
|
-
const cycleStart = path.indexOf(id);
|
|
561
|
-
cycles.push([...path.slice(cycleStart), id]);
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
if (visited.has(id)) return;
|
|
565
|
-
visited.add(id);
|
|
566
|
-
stack.add(id);
|
|
567
|
-
path.push(id);
|
|
568
|
-
const intent = intents[id];
|
|
569
|
-
if (intent?.depends_on) {
|
|
570
|
-
for (const dep of intent.depends_on) {
|
|
571
|
-
if (dep in intents) dfs(dep);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
path.pop();
|
|
575
|
-
stack.delete(id);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
for (const id of Object.keys(intents)) {
|
|
579
|
-
if (!visited.has(id)) dfs(id);
|
|
580
|
-
}
|
|
581
|
-
return cycles;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// ─── context ───────────────────────────────────────────
|
|
585
|
-
// 一条命令拿到:进度 + 下一个 Intent + 待验证 + 不一致项 + 风险
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* 项目上下文摘要——Agent 重启后一条命令获取"我在哪"。
|
|
589
|
-
* @param {string} versionDir
|
|
590
|
-
* @param {string} verificationsDir
|
|
591
|
-
* @param {string} philosophyDir
|
|
592
|
-
* @returns {object}
|
|
593
|
-
*/
|
|
594
|
-
export function contextSummary(versionDir, verificationsDir, philosophyDir) {
|
|
595
|
-
const mapState = intentMapDiagnostics(versionDir);
|
|
596
|
-
const status = mapState.valid ? getStatus(versionDir) : summarizeRawIntentMap(mapState.raw);
|
|
597
|
-
const next = mapState.valid ? getNextIntent(versionDir) : null;
|
|
598
|
-
const pending = mapState.valid ? getPendingVerifications(versionDir, verificationsDir) : [];
|
|
599
|
-
const { issues } = doctor(versionDir, verificationsDir, philosophyDir);
|
|
600
|
-
|
|
601
|
-
// 区分模板阶段问题(待填充)和真实损坏
|
|
602
|
-
const templateIssues = issues.filter((i) => i.is_template || i.type === 'intent_map_template' || i.type === 'inspiration_source');
|
|
603
|
-
const realIssues = issues.filter((i) => !templateIssues.includes(i));
|
|
604
|
-
|
|
605
|
-
const risks = [];
|
|
606
|
-
const fatalCount = realIssues.filter((i) => i.severity === 'fatal').length;
|
|
607
|
-
const highCount = realIssues.filter((i) => i.severity === 'high').length;
|
|
608
|
-
if (fatalCount > 0) risks.push(`${fatalCount} 个致命问题(Intent Map 损坏/循环依赖)`);
|
|
609
|
-
if (highCount > 0) risks.push(`${highCount} 个高严重度问题(状态不一致/孤儿引用)`);
|
|
610
|
-
if (templateIssues.length > 0) risks.push(`${templateIssues.length} 个待填充(模板未产出,需 Weaver/Architect 填充)`);
|
|
611
|
-
if (status.counts.blocked > 0) risks.push(`${status.counts.blocked} 个阻塞 Intent`);
|
|
612
|
-
|
|
613
|
-
return {
|
|
614
|
-
intent_map_valid: mapState.valid === true,
|
|
615
|
-
progress: {
|
|
616
|
-
completed: status.counts.completed,
|
|
617
|
-
total: status.counts.total,
|
|
618
|
-
rate: `${status.counts.completed}/${status.counts.total}`,
|
|
619
|
-
},
|
|
620
|
-
deprecated_intents: status.deprecated || [],
|
|
621
|
-
next_intent: next ? next.id : null,
|
|
622
|
-
pending_verifications: pending,
|
|
623
|
-
inconsistent_states: issues.filter((i) => i.type === 'in_progress_no_record' || i.type === 'completed_no_record').map((i) => i.id),
|
|
624
|
-
risks,
|
|
625
|
-
healthy: realIssues.length === 0,
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// ─── trace ─────────────────────────────────────────────
|
|
630
|
-
// Intent 完整追溯链:依赖链 + 验证历史 + 哲学锚点内容 + 意图叙事
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* 返回某个 Intent 的完整追溯链。
|
|
634
|
-
* @param {string} versionDir
|
|
635
|
-
* @param {string} verificationsDir
|
|
636
|
-
* @param {string} philosophyDir
|
|
637
|
-
* @param {string} intentId
|
|
638
|
-
* @returns {object}
|
|
639
|
-
*/
|
|
640
|
-
export function traceIntent(versionDir, verificationsDir, philosophyDir, intentId) {
|
|
641
|
-
const intent = getIntent(versionDir, intentId);
|
|
642
|
-
if (!intent) throw new Error(`Intent 不存在: ${intentId}`);
|
|
643
|
-
|
|
644
|
-
// 意图叙事
|
|
645
|
-
let narrative = null;
|
|
646
|
-
let narrativeError = null;
|
|
647
|
-
try { narrative = getNarrative(versionDir, intentId); }
|
|
648
|
-
catch (e) { narrativeError = e.message; /* narrative_ref 可能缺失或解析失败 */ }
|
|
649
|
-
|
|
650
|
-
// 验收契约
|
|
651
|
-
let acceptance = null;
|
|
652
|
-
let acceptanceError = null;
|
|
653
|
-
try { acceptance = getVerificationContract(versionDir, intentId); }
|
|
654
|
-
catch (e) { acceptanceError = e.message; /* 引用可能缺失或解析失败 */ }
|
|
655
|
-
|
|
656
|
-
// 验证历史
|
|
657
|
-
const verificationHistory = getVerificationHistory(verificationsDir, intentId);
|
|
658
|
-
|
|
659
|
-
// 哲学锚点内容
|
|
660
|
-
const philosophyContent = {};
|
|
661
|
-
if (intent.philosophy_anchors) {
|
|
662
|
-
for (const anchor of intent.philosophy_anchors) {
|
|
663
|
-
try {
|
|
664
|
-
philosophyContent[anchor] = getPhilosophy(philosophyDir, anchor);
|
|
665
|
-
} catch (e) {
|
|
666
|
-
philosophyContent[anchor] = null;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// 依赖链(递归向上)
|
|
672
|
-
const { intents } = loadIntentMap(versionDir);
|
|
673
|
-
const dependencyChain = [];
|
|
674
|
-
function walkDeps(id, depth) {
|
|
675
|
-
const node = intents[id];
|
|
676
|
-
if (!node?.depends_on || node.depends_on.length === 0) return;
|
|
677
|
-
for (const dep of node.depends_on) {
|
|
678
|
-
dependencyChain.push({ id: dep, depth, status: intents[dep]?.status });
|
|
679
|
-
walkDeps(dep, depth + 1);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
walkDeps(intentId, 0);
|
|
683
|
-
|
|
684
|
-
const version = basename(versionDir);
|
|
685
|
-
const ref = formatIntentRef(version, intentId);
|
|
686
|
-
const loomRoot = dirname(versionDir);
|
|
687
|
-
const predecessors = (intent.lineage?.predecessors || []).map((item) => {
|
|
688
|
-
let revision = null;
|
|
689
|
-
try { revision = getIntent(join(loomRoot, item.version), item.intent_id).revision ?? 1; }
|
|
690
|
-
catch { /* Validation is structural; a referenced version may not be locally available. */ }
|
|
691
|
-
return { ...item, ref: formatIntentRef(item.version, item.intent_id), revision };
|
|
692
|
-
});
|
|
693
|
-
const successors = [];
|
|
694
|
-
for (const candidateVersion of readdirSync(loomRoot).filter((name) => /^v\d+$/.test(name))) {
|
|
695
|
-
const candidateDir = join(loomRoot, candidateVersion);
|
|
696
|
-
if (!statSync(candidateDir).isDirectory() || !existsSync(join(candidateDir, '04_INTENT_MAP.json'))) continue;
|
|
697
|
-
let candidateMap;
|
|
698
|
-
try { candidateMap = loadIntentMap(candidateDir); }
|
|
699
|
-
catch { continue; }
|
|
700
|
-
for (const [candidateId, candidate] of Object.entries(candidateMap.intents)) {
|
|
701
|
-
if (candidate.lineage?.predecessors?.some((item) => item.version === version && item.intent_id === intentId)) {
|
|
702
|
-
successors.push({
|
|
703
|
-
ref: formatIntentRef(candidateVersion, candidateId),
|
|
704
|
-
version: candidateVersion,
|
|
705
|
-
intent_id: candidateId,
|
|
706
|
-
revision: candidate.revision ?? 1,
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
return {
|
|
713
|
-
ref,
|
|
714
|
-
version,
|
|
715
|
-
revision: intent.revision ?? 1,
|
|
716
|
-
intent,
|
|
717
|
-
narrative,
|
|
718
|
-
narrative_error: narrativeError,
|
|
719
|
-
acceptance,
|
|
720
|
-
acceptance_error: acceptanceError,
|
|
721
|
-
verification_history: verificationHistory,
|
|
722
|
-
philosophy_anchors_content: philosophyContent,
|
|
723
|
-
dependency_chain: dependencyChain,
|
|
724
|
-
lineage: { predecessors, successors },
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// ─── reverse-dep ───────────────────────────────────────
|
|
729
|
-
// 反向依赖:哪些 Intent 依赖这个 Intent
|
|
730
|
-
|
|
731
|
-
/**
|
|
732
|
-
* 返回依赖指定 Intent 的所有 Intent。
|
|
733
|
-
* @param {string} versionDir
|
|
734
|
-
* @param {string} intentId
|
|
735
|
-
* @returns {string[]}
|
|
736
|
-
*/
|
|
737
|
-
export function reverseDep(versionDir, intentId) {
|
|
738
|
-
const { intents } = loadIntentMap(versionDir);
|
|
739
|
-
const result = [];
|
|
740
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
741
|
-
if (intent.depends_on?.includes(intentId)) {
|
|
742
|
-
result.push(id);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
return result;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// ─── reverse-ref ───────────────────────────────────────
|
|
749
|
-
// 反向哲学引用:哪些 Intent 引用了这个哲学锚点
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* 返回引用指定哲学锚点的所有 Intent。
|
|
753
|
-
* @param {string} versionDir
|
|
754
|
-
* @param {string} anchor — 如 "PRODUCT_PHILOSOPHY.md#core-belief"
|
|
755
|
-
* @returns {string[]}
|
|
756
|
-
*/
|
|
757
|
-
export function reverseRef(versionDir, anchor) {
|
|
758
|
-
const { intents } = loadIntentMap(versionDir);
|
|
759
|
-
const result = [];
|
|
760
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
761
|
-
if (intent.philosophy_anchors?.includes(anchor)) {
|
|
762
|
-
result.push(id);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
return result;
|
|
766
|
-
}
|