@haaaiawd/loom 1.2.1 → 1.3.1
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 +37 -0
- package/README.md +24 -22
- package/cli/bin/loom.js +83 -76
- package/cli/help/atlas.md +48 -0
- package/cli/help/capability.md +48 -3
- package/cli/help/concepts.md +2 -0
- package/cli/help/doctor.md +8 -1
- package/cli/help/expertise.md +7 -6
- package/cli/help/workflow.md +7 -2
- package/cli/src/activate.js +126 -6
- package/cli/src/atlas.js +282 -0
- package/cli/src/capability-graph.js +300 -6
- package/cli/src/diagnostics.js +108 -15
- package/cli/src/guide.js +82 -22
- package/package.json +1 -1
- package/roles/architect.md +23 -17
- package/roles/forge.md +1 -3
- package/roles/impact-reviewer.md +37 -0
- package/templates/ATLAS_TEMPLATE.html +104 -0
- package/templates/CAPABILITY_BRIEF_TEMPLATE.md +1 -0
- package/templates/CAPABILITY_GRAPH_EXAMPLE.json +188 -0
- package/templates/CAPABILITY_GRAPH_TEMPLATE.json +69 -2
- package/cli/help/preview.md +0 -60
- package/cli/src/preview-prompt.md +0 -337
- package/cli/src/preview.js +0 -73
|
@@ -12,8 +12,22 @@ const NODE_STATUSES = ['open', 'researched', 'covered', 'deferred', 'out_of_scop
|
|
|
12
12
|
const ROUTES = ['expand', 'brief', 'intent', 'defer', 'exclude', 'covered_by'];
|
|
13
13
|
const RELATIONSHIPS = ['refines', 'requires', 'realizes', 'constrains', 'risks', 'validated_by', 'covered_by'];
|
|
14
14
|
const ACQUISITION_MODES = ['adaptive', 'external_required', 'project_only'];
|
|
15
|
+
const FAILURE_COSTS = ['low', 'material', 'hard_to_reverse'];
|
|
16
|
+
const IMPACT_REVIEWER_MODES = ['independent_agent_thread'];
|
|
15
17
|
const VERIFICATION_FIELDS = ['method', 'target', 'procedure', 'pass_criteria', 'artifact'];
|
|
16
18
|
const EVIDENCE_ARTIFACT_DIRS = ['verifications', '08_ASSET_LIBRARY/files'];
|
|
19
|
+
const LENS_STATUSES = ['applicable', 'not_applicable'];
|
|
20
|
+
// A lens is a mandatory inspection direction, not a capability category. The
|
|
21
|
+
// graph may decide that a lens does not apply, but it must make that decision
|
|
22
|
+
// visible instead of silently omitting, for example, interaction quality.
|
|
23
|
+
const STANDARD_LENSES = [
|
|
24
|
+
{ id: 'journey', title: '用户旅程', question: '用户从开始到离开,是否能完成有意义的完整路径?' },
|
|
25
|
+
{ id: 'interaction_accessibility', title: '交互与可访问性', question: '状态、反馈、失败恢复和不同使用条件是否真实可用?' },
|
|
26
|
+
{ id: 'visual_editorial', title: '视觉与信息表达', question: '视觉语言、层级、版式与资产是否帮助用户理解并形成该产品的气质?' },
|
|
27
|
+
{ id: 'content_communication', title: '内容与沟通', question: '文案、信息呈现或对话是否准确、可理解且符合产品立场?' },
|
|
28
|
+
{ id: 'system_data', title: '系统与数据', question: '数据、模型、集成、权限与运行边界是否支撑而非伤害用户结果?' },
|
|
29
|
+
{ id: 'quality_risk', title: '横切质量与风险', question: '可靠性、隐私、安全、性能、素材来源与独立验证是否被处理?' },
|
|
30
|
+
];
|
|
17
31
|
|
|
18
32
|
export function getCapabilityGraphPath(versionDir) {
|
|
19
33
|
return join(versionDir, GRAPH_FILE);
|
|
@@ -27,7 +41,193 @@ function assertString(value, label, errors) {
|
|
|
27
41
|
if (typeof value !== 'string' || value.trim() === '') errors.push(`${label} 必须是非空字符串`);
|
|
28
42
|
}
|
|
29
43
|
|
|
30
|
-
function
|
|
44
|
+
function schemaRequiresLensContract(data) {
|
|
45
|
+
const version = Number.parseFloat(String(data?._meta?._version || '1.0'));
|
|
46
|
+
return Number.isFinite(version) && version >= 1.1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function schemaRequiresCapabilityDomains(data) {
|
|
50
|
+
const version = Number.parseFloat(String(data?._meta?._version || '1.0'));
|
|
51
|
+
return Number.isFinite(version) && version >= 1.2;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function schemaRequiresImpactAssessment(data) {
|
|
55
|
+
const version = Number.parseFloat(String(data?._meta?._version || '1.0'));
|
|
56
|
+
return Number.isFinite(version) && version >= 1.3;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function minimumHighCapabilityCount(totalCapabilities) {
|
|
60
|
+
return Math.max(1, Math.ceil(totalCapabilities * 0.3));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validateImpactReview(data, capabilities, errors) {
|
|
64
|
+
if (data?._meta?._template === true) return;
|
|
65
|
+
const review = data.impact_review;
|
|
66
|
+
if (!review || typeof review !== 'object' || Array.isArray(review)) {
|
|
67
|
+
errors.push('Graph schema 1.3 要求 impact_review;Architect 提出判断后,必须由新的 Agent thread 独立审查每个 capability 的影响等级与外部获取必要性');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!IMPACT_REVIEWER_MODES.includes(review.reviewer_mode)) {
|
|
71
|
+
errors.push(`impact_review.reviewer_mode 必须为 independent_agent_thread;不能由 Architect 在同一上下文替自己确认影响等级`);
|
|
72
|
+
}
|
|
73
|
+
if (!Array.isArray(review.assessments)) {
|
|
74
|
+
errors.push('impact_review.assessments 必须是数组');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const assessmentByCapability = new Map();
|
|
78
|
+
for (const [index, assessment] of review.assessments.entries()) {
|
|
79
|
+
const prefix = `impact_review.assessments[${index}]`;
|
|
80
|
+
if (!assessment || typeof assessment !== 'object' || Array.isArray(assessment)) {
|
|
81
|
+
errors.push(`${prefix} 必须是对象`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
assertString(assessment.capability_id, `${prefix}.capability_id`, errors);
|
|
85
|
+
if (!['low', 'medium', 'high'].includes(assessment.recommended_impact)) {
|
|
86
|
+
errors.push(`${prefix}.recommended_impact 非法: ${assessment.recommended_impact}`);
|
|
87
|
+
}
|
|
88
|
+
if (typeof assessment.external_acquisition_required !== 'boolean') {
|
|
89
|
+
errors.push(`${prefix}.external_acquisition_required 必须是 boolean`);
|
|
90
|
+
}
|
|
91
|
+
assertString(assessment.rationale, `${prefix}.rationale`, errors);
|
|
92
|
+
if (assessmentByCapability.has(assessment.capability_id)) errors.push(`${prefix}.capability_id 重复: ${assessment.capability_id}`);
|
|
93
|
+
assessmentByCapability.set(assessment.capability_id, assessment);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const capabilityIds = new Set(capabilities.map((node) => node.id));
|
|
97
|
+
for (const capability of capabilities) {
|
|
98
|
+
const assessment = assessmentByCapability.get(capability.id);
|
|
99
|
+
if (!assessment) {
|
|
100
|
+
errors.push(`impact_review 缺少 ${capability.id} 的独立审查;不要让 Architect 自己裁决它是否重要`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (assessment.recommended_impact !== capability.impact) {
|
|
104
|
+
errors.push(`impact_review 对 ${capability.id} 建议 ${assessment.recommended_impact},但 Graph 写为 ${capability.impact};先按独立审查结论更新图谱或重新审查`);
|
|
105
|
+
}
|
|
106
|
+
const effectiveAcquisition = getEffectiveAcquisitionMode(capability);
|
|
107
|
+
if (assessment.external_acquisition_required && effectiveAcquisition !== 'external_required') {
|
|
108
|
+
errors.push(`impact_review 要求 ${capability.id} 外部获取,但 Graph 的 acquisition_mode 为 ${effectiveAcquisition}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const capabilityId of assessmentByCapability.keys()) {
|
|
112
|
+
if (!capabilityIds.has(capabilityId)) errors.push(`impact_review 引用不存在的 capability: ${capabilityId}`);
|
|
113
|
+
}
|
|
114
|
+
const highCount = capabilities.filter((node) => node.impact === 'high').length;
|
|
115
|
+
const requiredHighCount = minimumHighCapabilityCount(capabilities.length);
|
|
116
|
+
if (highCount < requiredHighCount) {
|
|
117
|
+
errors.push(`Impact Gate 要求至少 ${requiredHighCount}/${capabilities.length}(30%,向上取整,至少 1 个)capability 为 high;当前只有 ${highCount} 个。不要通过整体降级来跳过检索`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validateImpactAssessment(id, node, required, errors) {
|
|
122
|
+
const prefix = `nodes["${id}"].impact_assessment`;
|
|
123
|
+
const assessment = node.impact_assessment;
|
|
124
|
+
if (assessment === undefined) {
|
|
125
|
+
if (required) errors.push(`nodes["${id}"] 是 capability;Graph schema 1.3 要求 impact_assessment,先说明它影响的用户结果、错判代价、外部知识是否会改变决定与理由,再决定 impact`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!assessment || typeof assessment !== 'object' || Array.isArray(assessment)) {
|
|
129
|
+
errors.push(`${prefix} 必须是对象`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
assertString(assessment.affected_user_result, `${prefix}.affected_user_result`, errors);
|
|
133
|
+
if (!FAILURE_COSTS.includes(assessment.failure_cost)) {
|
|
134
|
+
errors.push(`${prefix}.failure_cost 非法: ${assessment.failure_cost}(可选 low|material|hard_to_reverse)`);
|
|
135
|
+
}
|
|
136
|
+
if (typeof assessment.external_knowledge_changes_decision !== 'boolean') {
|
|
137
|
+
errors.push(`${prefix}.external_knowledge_changes_decision 必须是 boolean`);
|
|
138
|
+
}
|
|
139
|
+
assertString(assessment.rationale, `${prefix}.rationale`, errors);
|
|
140
|
+
|
|
141
|
+
const mustBeHigh = assessment.failure_cost === 'hard_to_reverse'
|
|
142
|
+
|| assessment.external_knowledge_changes_decision === true;
|
|
143
|
+
if (mustBeHigh && node.impact !== 'high') {
|
|
144
|
+
errors.push(`nodes["${id}"] 的 impact_assessment 表明错判不可逆或外部知识会改变决定,impact 必须为 high;不能以 medium/low 绕过能力获取门禁`);
|
|
145
|
+
}
|
|
146
|
+
if (node.impact === 'high' && assessment.external_knowledge_changes_decision !== true) {
|
|
147
|
+
errors.push(`nodes["${id}"] 为 high capability,impact_assessment.external_knowledge_changes_decision 必须为 true;高影响能力必须进入外部获取判断`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function validateCapabilityDomains(data, allIds, errors) {
|
|
152
|
+
if (data?._meta?._template === true) return new Set();
|
|
153
|
+
const domains = data.capability_domains;
|
|
154
|
+
if (domains === undefined) return new Set();
|
|
155
|
+
if (!Array.isArray(domains)) {
|
|
156
|
+
errors.push('capability_domains 必须是数组');
|
|
157
|
+
return new Set();
|
|
158
|
+
}
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
for (const [index, domain] of domains.entries()) {
|
|
161
|
+
const prefix = `capability_domains[${index}]`;
|
|
162
|
+
if (!domain || typeof domain !== 'object' || Array.isArray(domain)) {
|
|
163
|
+
errors.push(`${prefix} 必须是对象`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
assertString(domain.id, `${prefix}.id`, errors);
|
|
167
|
+
assertString(domain.title, `${prefix}.title`, errors);
|
|
168
|
+
assertString(domain.question, `${prefix}.question`, errors);
|
|
169
|
+
assertString(domain.why_now, `${prefix}.why_now`, errors);
|
|
170
|
+
if (seen.has(domain.id)) errors.push(`${prefix}.id 重复: ${domain.id}`);
|
|
171
|
+
seen.add(domain.id);
|
|
172
|
+
if (!Array.isArray(domain.node_refs) || domain.node_refs.length === 0) {
|
|
173
|
+
errors.push(`${prefix}.node_refs 必须连接至少一个具体 capability 节点`);
|
|
174
|
+
} else {
|
|
175
|
+
for (const nodeId of domain.node_refs) {
|
|
176
|
+
if (!allIds.has(nodeId)) errors.push(`${prefix}.node_refs 指向不存在节点: ${nodeId}`);
|
|
177
|
+
else if (data.nodes[nodeId]?.kind !== 'capability') errors.push(`${prefix}.node_refs 只能引用 capability 节点: ${nodeId}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return seen;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function validateLensContract(data, allIds, errors) {
|
|
185
|
+
if (data?._meta?._template === true) return;
|
|
186
|
+
const contract = data.lens_contract;
|
|
187
|
+
if (contract === undefined) return;
|
|
188
|
+
if (!contract || typeof contract !== 'object' || Array.isArray(contract)) {
|
|
189
|
+
errors.push('lens_contract 必须是对象');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
assertString(contract.selection_basis, 'lens_contract.selection_basis', errors);
|
|
193
|
+
if (!Array.isArray(contract.lenses)) {
|
|
194
|
+
errors.push('lens_contract.lenses 必须是数组');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const seen = new Set();
|
|
198
|
+
for (const [index, lens] of contract.lenses.entries()) {
|
|
199
|
+
const prefix = `lens_contract.lenses[${index}]`;
|
|
200
|
+
if (!lens || typeof lens !== 'object' || Array.isArray(lens)) {
|
|
201
|
+
errors.push(`${prefix} 必须是对象`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
assertString(lens.id, `${prefix}.id`, errors);
|
|
205
|
+
assertString(lens.title, `${prefix}.title`, errors);
|
|
206
|
+
assertString(lens.question, `${prefix}.question`, errors);
|
|
207
|
+
if (!LENS_STATUSES.includes(lens.status)) errors.push(`${prefix}.status 非法: ${lens.status}`);
|
|
208
|
+
if (seen.has(lens.id)) errors.push(`${prefix}.id 重复: ${lens.id}`);
|
|
209
|
+
seen.add(lens.id);
|
|
210
|
+
if (lens.status === 'applicable') {
|
|
211
|
+
if (!Array.isArray(lens.node_refs) || lens.node_refs.length === 0) {
|
|
212
|
+
errors.push(`${prefix}.status=applicable 必须用 node_refs 连接至少一个具体 Graph 节点`);
|
|
213
|
+
} else {
|
|
214
|
+
for (const nodeId of lens.node_refs) {
|
|
215
|
+
if (!allIds.has(nodeId)) errors.push(`${prefix}.node_refs 指向不存在节点: ${nodeId}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (lens.status === 'not_applicable') {
|
|
220
|
+
assertString(lens.rationale, `${prefix}.rationale`, errors);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (schemaRequiresLensContract(data)) {
|
|
224
|
+
for (const lens of STANDARD_LENSES) {
|
|
225
|
+
if (!seen.has(lens.id)) errors.push(`lens_contract 缺少必审透镜: ${lens.id}(${lens.title})`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function validateNode(id, node, allIds, domainIds, requireCapabilityDomains, requireImpactAssessment, errors) {
|
|
31
231
|
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
|
32
232
|
errors.push(`nodes["${id}"] 必须是对象`);
|
|
33
233
|
return;
|
|
@@ -67,6 +267,22 @@ function validateNode(id, node, allIds, errors) {
|
|
|
67
267
|
if (node.asset_refs !== undefined && node.kind !== 'evidence') {
|
|
68
268
|
errors.push(`nodes["${id}"].asset_refs 只允许写在 evidence 节点`);
|
|
69
269
|
}
|
|
270
|
+
if (node.domain_refs !== undefined && (!Array.isArray(node.domain_refs) || node.domain_refs.some((ref) => typeof ref !== 'string' || !ref.trim()))) {
|
|
271
|
+
errors.push(`nodes["${id}"].domain_refs 必须是非空字符串数组`);
|
|
272
|
+
} else if (node.domain_refs !== undefined) {
|
|
273
|
+
for (const domainId of node.domain_refs) {
|
|
274
|
+
if (!domainIds.has(domainId)) errors.push(`nodes["${id}"].domain_refs 指向不存在能力领域: ${domainId}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (requireCapabilityDomains && node.kind === 'capability'
|
|
278
|
+
&& (!Array.isArray(node.domain_refs) || node.domain_refs.length === 0)) {
|
|
279
|
+
errors.push(`nodes["${id}"] 是 capability,必须用 domain_refs 回链至少一个 capability domain`);
|
|
280
|
+
}
|
|
281
|
+
if (node.kind === 'capability') {
|
|
282
|
+
validateImpactAssessment(id, node, requireImpactAssessment, errors);
|
|
283
|
+
} else if (node.impact_assessment !== undefined) {
|
|
284
|
+
errors.push(`nodes["${id}"].impact_assessment 只允许写在 capability 节点`);
|
|
285
|
+
}
|
|
70
286
|
if (node.acquisition_mode !== undefined) {
|
|
71
287
|
if (node.kind !== 'capability') {
|
|
72
288
|
errors.push(`nodes["${id}"].acquisition_mode 只允许写在 capability 节点`);
|
|
@@ -78,7 +294,10 @@ function validateNode(id, node, allIds, errors) {
|
|
|
78
294
|
&& (typeof node.acquisition_rationale !== 'string' || !node.acquisition_rationale.trim())) {
|
|
79
295
|
errors.push(`nodes["${id}"].acquisition_mode=project_only 必须声明 acquisition_rationale`);
|
|
80
296
|
}
|
|
81
|
-
if (node.impact === 'high' &&
|
|
297
|
+
if (node.impact === 'high' && requireImpactAssessment
|
|
298
|
+
&& node.acquisition_mode !== undefined && node.acquisition_mode !== 'external_required') {
|
|
299
|
+
errors.push(`nodes["${id}"] 为 high capability;Graph schema 1.3 只允许 acquisition_mode=external_required(或省略并使用默认值),不得以 ${node.acquisition_mode} 绕过外部获取门禁`);
|
|
300
|
+
} else if (node.impact === 'high' && node.acquisition_mode === 'adaptive'
|
|
82
301
|
&& (typeof node.acquisition_rationale !== 'string' || !node.acquisition_rationale.trim())) {
|
|
83
302
|
errors.push(`nodes["${id}"] 为高影响 capability 且选择 adaptive 时必须声明 acquisition_rationale;说明为何此处不启用 external_required`);
|
|
84
303
|
}
|
|
@@ -104,7 +323,16 @@ export function validateCapabilityGraph(data) {
|
|
|
104
323
|
errors.push('缺少 nodes 对象');
|
|
105
324
|
} else {
|
|
106
325
|
const ids = new Set(Object.keys(data.nodes));
|
|
107
|
-
|
|
326
|
+
const domainIds = validateCapabilityDomains(data, ids, errors);
|
|
327
|
+
const requireCapabilityDomains = schemaRequiresCapabilityDomains(data) && data.capability_domains !== undefined;
|
|
328
|
+
const requireImpactAssessment = schemaRequiresImpactAssessment(data);
|
|
329
|
+
validateLensContract(data, ids, errors);
|
|
330
|
+
for (const [id, node] of Object.entries(data.nodes)) {
|
|
331
|
+
validateNode(id, node, ids, domainIds, requireCapabilityDomains, requireImpactAssessment, errors);
|
|
332
|
+
}
|
|
333
|
+
if (requireImpactAssessment) {
|
|
334
|
+
validateImpactReview(data, Object.values(data.nodes).filter((node) => node?.kind === 'capability'), errors);
|
|
335
|
+
}
|
|
108
336
|
for (const [id, node] of Object.entries(data.nodes)) {
|
|
109
337
|
if (node?.route !== 'covered_by') continue;
|
|
110
338
|
const targetId = node.covered_by;
|
|
@@ -204,11 +432,31 @@ export function getCapabilityGraphProjection(versionDir) {
|
|
|
204
432
|
total: Object.keys(graph.nodes).length,
|
|
205
433
|
by_kind: Object.fromEntries(NODE_KINDS.map((kind) => [kind, Object.values(graph.nodes).filter((node) => node.kind === kind).length])),
|
|
206
434
|
frontier: getCapabilityFrontier(versionDir).length,
|
|
435
|
+
lenses: getLensSummary(graph),
|
|
436
|
+
capability_domains: getDomainSummary(graph),
|
|
207
437
|
},
|
|
208
438
|
mermaid: lines.join('\n'),
|
|
209
439
|
};
|
|
210
440
|
}
|
|
211
441
|
|
|
442
|
+
function getLensSummary(graph) {
|
|
443
|
+
const lenses = graph.lens_contract?.lenses || [];
|
|
444
|
+
return {
|
|
445
|
+
required: schemaRequiresLensContract(graph),
|
|
446
|
+
declared: lenses.length,
|
|
447
|
+
applicable: lenses.filter((lens) => lens.status === 'applicable').length,
|
|
448
|
+
not_applicable: lenses.filter((lens) => lens.status === 'not_applicable').length,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function getDomainSummary(graph) {
|
|
453
|
+
const domains = graph.capability_domains || [];
|
|
454
|
+
return {
|
|
455
|
+
required: schemaRequiresCapabilityDomains(graph),
|
|
456
|
+
declared: domains.length,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
212
460
|
function resolveBrief(versionDir, briefRef) {
|
|
213
461
|
if (!briefRef) return null;
|
|
214
462
|
if (isAbsolute(briefRef)) throw new Error(`Capability Brief 不得使用绝对路径: ${briefRef}`);
|
|
@@ -238,7 +486,7 @@ function resolveEvidenceArtifact(versionDir, artifactRef) {
|
|
|
238
486
|
return artifactPath;
|
|
239
487
|
}
|
|
240
488
|
|
|
241
|
-
function getHighOutcomesWithoutObservableEvidence(
|
|
489
|
+
function getHighOutcomesWithoutObservableEvidence(graph, intentMap, intentMappingRequired) {
|
|
242
490
|
const gaps = [];
|
|
243
491
|
for (const outcome of Object.values(graph.nodes)) {
|
|
244
492
|
if (outcome.kind !== 'outcome' || outcome.impact !== 'high') continue;
|
|
@@ -246,7 +494,6 @@ function getHighOutcomesWithoutObservableEvidence(versionDir, graph, intentMap,
|
|
|
246
494
|
const validEvidence = evidenceRelations.find((relation) => {
|
|
247
495
|
const evidence = graph.nodes[relation.target];
|
|
248
496
|
if (!evidence || evidence.kind !== 'evidence' || !evidence.verification) return false;
|
|
249
|
-
if (!resolveEvidenceArtifact(versionDir, evidence.verification.artifact)) return false;
|
|
250
497
|
const hasOwner = (evidence.intent_refs || []).some((intentId) => !intentMappingRequired || intentId in intentMap.intents);
|
|
251
498
|
return hasOwner;
|
|
252
499
|
});
|
|
@@ -265,6 +512,8 @@ export function getCapabilityCoverage(versionDir) {
|
|
|
265
512
|
const intentMap = loadIntentMap(versionDir);
|
|
266
513
|
const intentMappingRequired = intentMap._meta?._template !== true;
|
|
267
514
|
const nodes = Object.values(graph.nodes);
|
|
515
|
+
const capabilityNodes = nodes.filter((node) => node.kind === 'capability');
|
|
516
|
+
const highCapabilityCount = capabilityNodes.filter((node) => node.impact === 'high').length;
|
|
268
517
|
const highUnrouted = getCapabilityFrontier(versionDir);
|
|
269
518
|
const orphanIntentRefs = [];
|
|
270
519
|
const mappedIntentIds = new Set();
|
|
@@ -272,6 +521,19 @@ export function getCapabilityCoverage(versionDir) {
|
|
|
272
521
|
const routingGaps = [];
|
|
273
522
|
const outcomesWithoutConcern = [];
|
|
274
523
|
const evidenceArtifactGaps = [];
|
|
524
|
+
const lensContractGaps = [];
|
|
525
|
+
const capabilityDomainGaps = [];
|
|
526
|
+
|
|
527
|
+
if (schemaRequiresLensContract(graph) && !graph.lens_contract) {
|
|
528
|
+
lensContractGaps.push({
|
|
529
|
+
reason: '当前 Graph schema 要求 lens_contract;Architect 必须先审视用户旅程、交互与可访问性、视觉与信息表达、内容与沟通、系统与数据、横切质量与风险,并把每项连接到具体节点或说明为何不适用。',
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
if (schemaRequiresCapabilityDomains(graph) && !graph.capability_domains) {
|
|
533
|
+
capabilityDomainGaps.push({
|
|
534
|
+
reason: '当前 Graph schema 要求 capability_domains;Architect 必须从项目事实派生会改变方案或验证方法的专业领域(如 UI/UX、3D 与光影、网络安全、心理学或生物学),再把具体 capability 回链到这些领域。',
|
|
535
|
+
});
|
|
536
|
+
}
|
|
275
537
|
|
|
276
538
|
for (const node of nodes) {
|
|
277
539
|
for (const intentId of node.intent_refs || []) {
|
|
@@ -326,11 +588,15 @@ export function getCapabilityCoverage(versionDir) {
|
|
|
326
588
|
const unmappedIntents = intentMappingRequired
|
|
327
589
|
? Object.keys(intentMap.intents).filter((id) => !mappedIntentIds.has(id))
|
|
328
590
|
: [];
|
|
329
|
-
const highOutcomesWithoutObservableEvidence = getHighOutcomesWithoutObservableEvidence(
|
|
591
|
+
const highOutcomesWithoutObservableEvidence = getHighOutcomesWithoutObservableEvidence(graph, intentMap, intentMappingRequired);
|
|
330
592
|
return {
|
|
331
593
|
summary: {
|
|
332
594
|
nodes: nodes.length,
|
|
333
595
|
outcomes: nodes.filter((node) => node.kind === 'outcome').length,
|
|
596
|
+
capabilities: capabilityNodes.length,
|
|
597
|
+
high_capabilities: highCapabilityCount,
|
|
598
|
+
required_high_capabilities: capabilityNodes.length ? minimumHighCapabilityCount(capabilityNodes.length) : 0,
|
|
599
|
+
high_capability_ratio: capabilityNodes.length ? highCapabilityCount / capabilityNodes.length : 0,
|
|
334
600
|
high_unrouted: highUnrouted.length,
|
|
335
601
|
orphan_intent_refs: orphanIntentRefs.length,
|
|
336
602
|
capabilities_without_plan: capabilitiesWithoutPlan.length,
|
|
@@ -338,6 +604,10 @@ export function getCapabilityCoverage(versionDir) {
|
|
|
338
604
|
outcomes_without_concern: outcomesWithoutConcern.length,
|
|
339
605
|
high_outcomes_without_observable_evidence: highOutcomesWithoutObservableEvidence.length,
|
|
340
606
|
evidence_artifact_gaps: evidenceArtifactGaps.length,
|
|
607
|
+
lens_contract_gaps: lensContractGaps.length,
|
|
608
|
+
capability_domain_gaps: capabilityDomainGaps.length,
|
|
609
|
+
lenses: getLensSummary(graph),
|
|
610
|
+
capability_domains: getDomainSummary(graph),
|
|
341
611
|
intent_mapping_required: intentMappingRequired,
|
|
342
612
|
unmapped_intents: unmappedIntents.length,
|
|
343
613
|
ready: highUnrouted.length === 0
|
|
@@ -347,6 +617,8 @@ export function getCapabilityCoverage(versionDir) {
|
|
|
347
617
|
&& outcomesWithoutConcern.length === 0
|
|
348
618
|
&& highOutcomesWithoutObservableEvidence.length === 0
|
|
349
619
|
&& evidenceArtifactGaps.length === 0
|
|
620
|
+
&& lensContractGaps.length === 0
|
|
621
|
+
&& capabilityDomainGaps.length === 0
|
|
350
622
|
&& unmappedIntents.length === 0,
|
|
351
623
|
},
|
|
352
624
|
high_unrouted: highUnrouted,
|
|
@@ -356,6 +628,8 @@ export function getCapabilityCoverage(versionDir) {
|
|
|
356
628
|
outcomes_without_concern: outcomesWithoutConcern,
|
|
357
629
|
high_outcomes_without_observable_evidence: highOutcomesWithoutObservableEvidence,
|
|
358
630
|
evidence_artifact_gaps: evidenceArtifactGaps,
|
|
631
|
+
lens_contract_gaps: lensContractGaps,
|
|
632
|
+
capability_domain_gaps: capabilityDomainGaps,
|
|
359
633
|
unmapped_intents: unmappedIntents,
|
|
360
634
|
};
|
|
361
635
|
}
|
|
@@ -395,6 +669,24 @@ export function compileCapabilityInputs(versionDir, intentId) {
|
|
|
395
669
|
};
|
|
396
670
|
}
|
|
397
671
|
const nodes = collectCompilationNodes(graph, intentId);
|
|
672
|
+
const relevantLenses = (graph.lens_contract?.lenses || [])
|
|
673
|
+
.filter((lens) => lens.status === 'applicable'
|
|
674
|
+
&& (lens.node_refs || []).some((nodeId) => nodes.some((node) => node.id === nodeId)))
|
|
675
|
+
.map((lens) => ({
|
|
676
|
+
id: lens.id,
|
|
677
|
+
title: lens.title,
|
|
678
|
+
question: lens.question,
|
|
679
|
+
node_refs: lens.node_refs,
|
|
680
|
+
}));
|
|
681
|
+
const relevantDomains = (graph.capability_domains || [])
|
|
682
|
+
.filter((domain) => (domain.node_refs || []).some((nodeId) => nodes.some((node) => node.id === nodeId)))
|
|
683
|
+
.map((domain) => ({
|
|
684
|
+
id: domain.id,
|
|
685
|
+
title: domain.title,
|
|
686
|
+
question: domain.question,
|
|
687
|
+
why_now: domain.why_now,
|
|
688
|
+
node_refs: domain.node_refs,
|
|
689
|
+
}));
|
|
398
690
|
const briefs = [];
|
|
399
691
|
const warnings = [];
|
|
400
692
|
for (const node of nodes) {
|
|
@@ -419,6 +711,8 @@ export function compileCapabilityInputs(versionDir, intentId) {
|
|
|
419
711
|
return {
|
|
420
712
|
available: true,
|
|
421
713
|
nodes,
|
|
714
|
+
lenses: relevantLenses,
|
|
715
|
+
capability_domains: relevantDomains,
|
|
422
716
|
briefs,
|
|
423
717
|
warnings,
|
|
424
718
|
acquisition: {
|
package/cli/src/diagnostics.js
CHANGED
|
@@ -16,6 +16,7 @@ import { listCapabilityProposals } from './capability-proposals.js';
|
|
|
16
16
|
import { getAssetManifestPath, validateAssetLibrary } from './asset-library.js';
|
|
17
17
|
import { validateAtelierRecord } from './atelier.js';
|
|
18
18
|
import { getExpertisePackState } from './expertise-pack.js';
|
|
19
|
+
import { validateAtlas } from './atlas.js';
|
|
19
20
|
|
|
20
21
|
function readIntentMapRaw(versionDir) {
|
|
21
22
|
const filePath = join(versionDir, '04_INTENT_MAP.json');
|
|
@@ -146,12 +147,18 @@ function commandCoversMethod(actualCommand, expectedMethod) {
|
|
|
146
147
|
const FIX_HINTS = {
|
|
147
148
|
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
149
|
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 / 填必填字段)',
|
|
150
|
+
intent_map_template: '运行 loom activate architect,Architect 填充真实 Intent Map 后删除 _meta._template 标记',
|
|
151
|
+
intent_map_invalid: '按报错信息修正 04_INTENT_MAP.json 里对应字段(补 title / 加长 acceptance / 填必填字段)',
|
|
152
|
+
project_document_missing: '运行 loom activate architect,补齐当前版本缺失的项目文档;不要靠 Intent Map 或会话记忆替代 Vision、Architecture、Verification 的可审计载体',
|
|
153
|
+
project_document_template: '运行 loom activate architect,将仍带 LOOM_TEMPLATE 标记的项目文档替换为本项目的真实判断与契约后删除标记',
|
|
154
|
+
intent_narrative_invalid: '修正 {id}.narrative_ref 到 01_VISION.md 中实际存在的章节;可运行 loom intent narrative {id} 复核',
|
|
155
|
+
intent_contract_invalid: '修正 {id}.acceptance 或其对 05_VERIFICATION.md 的章节引用;可运行 loom verify contract {id} 复核',
|
|
151
156
|
completed_no_record: '在 .loom/v{N}/verifications/ 下补验证记录,或运行 loom verify pass {id} --summary "..."',
|
|
152
157
|
completed_verification_not_passed: '最新验证不是当前 revision 的 passed;重新运行 loom verify pass {id} --summary "...",再用 loom intent done {id} 闭合。',
|
|
153
158
|
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,移除或修正不存在的哲学文件引用',
|
|
159
|
+
orphan_philosophy_ref: '检查 04_INTENT_MAP.json 里 {id} 的 philosophy_anchors,移除或修正不存在的哲学文件引用',
|
|
160
|
+
orphan_philosophy_anchor: '修正 {id} 的 philosophy_anchors 中 # 后的章节锚点;可运行 loom philosophy get <file#anchor> 检查该章节是否可读取',
|
|
161
|
+
orphan_philosophy_anchor: '修正 {id} 的 philosophy_anchors 中 # 后的章节锚点;可运行 loom philosophy get <file#anchor> 检查该章节是否可读取',
|
|
155
162
|
orphan_dependency: '检查 04_INTENT_MAP.json 里 {id} 的 depends_on,移除或修正不存在的 Intent ID',
|
|
156
163
|
cycle: '打破循环:把循环链中某个 Intent 的 depends_on 里去掉前驱,或拆成更小的 Intent',
|
|
157
164
|
zombie: '检查 {id} 是否还需要——不需要就 loom intent update {id} --status completed 或 blocked',
|
|
@@ -174,6 +181,10 @@ const FIX_HINTS = {
|
|
|
174
181
|
capability_route_evidence: '为该节点补齐路由所需的 Intent 回链、Brief 或延后/排除理由,避免只写一个状态标签',
|
|
175
182
|
capability_outcome_unexpanded: '将 outcome 连接到至少一个 concern,明确项目初衷需要被处理的问题面',
|
|
176
183
|
capability_outcome_unobservable: '为高影响 outcome 新建或补齐 evidence 节点,并以 validated_by 连接;evidence 必须写明观察目标、复现步骤、通过标准、证据产物,并回链承担验证的 Intent',
|
|
184
|
+
capability_lens_contract_missing: '运行 loom activate architect;按用户旅程、交互与可访问性、视觉与信息表达、内容与沟通、系统与数据、横切质量与风险审视本项目。每项都要连接具体节点,或写明为何不适用;不要把“UI/UX”当成空泛节点。',
|
|
185
|
+
capability_domain_contract_missing: '运行 loom activate architect;从项目事实派生会改变方案或验证方法的专业领域(例如 UI/UX、3D 与光影、网络安全、心理学、生物学),并让每个具体 capability 用 domain_refs 回链。领域不是部门标签,必须说明它改变什么决定。',
|
|
186
|
+
capability_impact_gate_missing: '运行 loom activate architect;先为每个 capability 写 impact_assessment:影响的用户结果、错判代价、外部知识是否会改变决定及理由。不可逆或会受外部知识影响的能力必须标为 high,并进入 external_required。',
|
|
187
|
+
decision_atlas_missing: '当前版本的 Intent 已全部闭合,但缺少合格的 loom-atlas.html;运行 loom atlas --regen,用已装配的 Composer Pack 生成后再运行 loom atlas validate。',
|
|
177
188
|
intent_graph_unmapped: '将该 Intent 回链到至少一个 Capability Graph 节点;不要让执行承诺失去项目初衷和能力来源',
|
|
178
189
|
capability_proposal_pending: '由 Architect 审核 proposal:判定已覆盖、更新 Graph、生成/修订 Intent、改变 acceptance,或升级 Minor/Major;Forge 不得静默把候选写进正式图谱。',
|
|
179
190
|
asset_library_invalid: '修复 08_ASSET_LIBRARY/manifest.json 的来源、许可、哈希、库内路径或 evidence 双向引用,然后运行 loom asset validate。',
|
|
@@ -186,7 +197,7 @@ const FIX_HINTS = {
|
|
|
186
197
|
/**
|
|
187
198
|
* 给 issue 补 fix_hint——把 {id} {dep} 等占位符替换成实际值。
|
|
188
199
|
*/
|
|
189
|
-
function addFixHint(issue) {
|
|
200
|
+
function addFixHint(issue) {
|
|
190
201
|
const template = FIX_HINTS[issue.type];
|
|
191
202
|
if (!template) return issue;
|
|
192
203
|
let hint = template;
|
|
@@ -218,6 +229,22 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
218
229
|
|
|
219
230
|
const { intents } = mapState.validMap;
|
|
220
231
|
const atelierRecords = new Map();
|
|
232
|
+
appendProjectDocumentStructureDiagnostics(issues, versionDir, philosophyDir);
|
|
233
|
+
|
|
234
|
+
// Atlas is a completion deliverable, not a live dashboard. It becomes a
|
|
235
|
+
// hard gate only after all designed Intents are completed.
|
|
236
|
+
const allIntents = Object.values(intents);
|
|
237
|
+
if (allIntents.length > 0 && allIntents.every((intent) => intent.status === 'completed')) {
|
|
238
|
+
const atlas = validateAtlas(join(versionDir, '..', '..'), versionDir);
|
|
239
|
+
if (!atlas.valid) {
|
|
240
|
+
issues.push({
|
|
241
|
+
id: 'decision_atlas',
|
|
242
|
+
type: 'decision_atlas_missing',
|
|
243
|
+
severity: 'high',
|
|
244
|
+
msg: `当前版本所有 Intent 已完成,但决策图谱尚未成为合格交付物: ${atlas.errors.join(';')}`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
221
248
|
|
|
222
249
|
for (const [id, intent] of Object.entries(intents)) {
|
|
223
250
|
if (intent.quality_strategy !== 'atelier' || intent.status === 'pending' || intent.status === 'blocked') continue;
|
|
@@ -277,6 +304,12 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
277
304
|
for (const item of coverage.evidence_artifact_gaps || []) {
|
|
278
305
|
issues.push({ id: item.node_id, type: 'capability_evidence_artifact_missing', severity: 'high', msg: `${item.node_id} 的完成证据不可用: ${item.reason}` });
|
|
279
306
|
}
|
|
307
|
+
for (const item of coverage.lens_contract_gaps || []) {
|
|
308
|
+
issues.push({ id: 'capability_graph', type: 'capability_lens_contract_missing', severity: 'high', msg: `Capability Graph 尚未完成透镜审视: ${item.reason}` });
|
|
309
|
+
}
|
|
310
|
+
for (const item of coverage.capability_domain_gaps || []) {
|
|
311
|
+
issues.push({ id: 'capability_graph', type: 'capability_domain_contract_missing', severity: 'high', msg: `Capability Graph 尚未声明能力领域: ${item.reason}` });
|
|
312
|
+
}
|
|
280
313
|
for (const item of coverage.orphan_intent_refs) {
|
|
281
314
|
issues.push({ id: item.node_id, type: 'intent_graph_unmapped', severity: 'high', msg: `${item.node_id} 引用了不存在的 Intent: ${item.intent_id}` });
|
|
282
315
|
}
|
|
@@ -285,7 +318,13 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
285
318
|
}
|
|
286
319
|
}
|
|
287
320
|
} catch (error) {
|
|
288
|
-
|
|
321
|
+
const impactGateError = /impact_assessment|impact_review|Impact Gate|impact 必须为 high|外部获取门禁|30%/.test(error.message);
|
|
322
|
+
issues.push({
|
|
323
|
+
id: 'capability_graph',
|
|
324
|
+
type: impactGateError ? 'capability_impact_gate_missing' : 'capability_graph_invalid',
|
|
325
|
+
severity: 'high',
|
|
326
|
+
msg: error.message,
|
|
327
|
+
});
|
|
289
328
|
}
|
|
290
329
|
}
|
|
291
330
|
|
|
@@ -374,19 +413,39 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
374
413
|
}
|
|
375
414
|
}
|
|
376
415
|
|
|
377
|
-
// 2.
|
|
378
|
-
for (const [id, intent] of Object.entries(intents)) {
|
|
379
|
-
if (!intent.philosophy_anchors) continue;
|
|
416
|
+
// 2. 孤儿引用:哲学锚点必须同时指向存在的文件和可读取的章节。
|
|
417
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
418
|
+
if (!intent.philosophy_anchors) continue;
|
|
380
419
|
for (const anchor of intent.philosophy_anchors) {
|
|
381
420
|
const [file] = anchor.split('#');
|
|
382
421
|
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
|
-
|
|
422
|
+
if (!existsSync(filePath)) {
|
|
423
|
+
issues.push({ id, type: 'orphan_philosophy_ref', severity: 'high', msg: `${id} 引用不存在的哲学文件: ${file}` });
|
|
424
|
+
} else if (anchor.includes('#')) {
|
|
425
|
+
try {
|
|
426
|
+
getPhilosophy(philosophyDir, anchor);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
issues.push({ id, type: 'orphan_philosophy_anchor', severity: 'high', msg: `${id} 引用不存在或不可读取的哲学章节: ${anchor}(${error.message})` });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Intent 的叙事与完成契约不能只是字符串;它们必须能从当前版本真实解析。
|
|
435
|
+
for (const [id] of Object.entries(intents)) {
|
|
436
|
+
try {
|
|
437
|
+
getNarrative(versionDir, id);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
issues.push({ id, type: 'intent_narrative_invalid', severity: 'high', msg: `${id} 的 narrative_ref 无法解析: ${error.message}` });
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
getVerificationContract(versionDir, id);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
issues.push({ id, type: 'intent_contract_invalid', severity: 'high', msg: `${id} 的 acceptance 无法解析: ${error.message}` });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 3. 孤儿引用:depends_on 指向不存在的 Intent
|
|
390
449
|
for (const [id, intent] of Object.entries(intents)) {
|
|
391
450
|
if (!intent.depends_on) continue;
|
|
392
451
|
for (const dep of intent.depends_on) {
|
|
@@ -724,6 +783,40 @@ export function traceIntent(versionDir, verificationsDir, philosophyDir, intentI
|
|
|
724
783
|
lineage: { predecessors, successors },
|
|
725
784
|
};
|
|
726
785
|
}
|
|
786
|
+
|
|
787
|
+
const REQUIRED_PROJECT_DOCUMENTS = [
|
|
788
|
+
{ relativePath: '01_VISION.md', label: 'Vision' },
|
|
789
|
+
{ relativePath: '02_ARCHITECTURE.md', label: 'Architecture' },
|
|
790
|
+
{ relativePath: '05_VERIFICATION.md', label: 'Verification' },
|
|
791
|
+
];
|
|
792
|
+
const REQUIRED_PHILOSOPHY_DOCUMENTS = [
|
|
793
|
+
'PRODUCT_PHILOSOPHY.md',
|
|
794
|
+
'ENGINEERING_CREED.md',
|
|
795
|
+
'DECISION_RUBRIC.md',
|
|
796
|
+
];
|
|
797
|
+
|
|
798
|
+
function isLoomTemplateDocument(filePath) {
|
|
799
|
+
return existsSync(filePath) && readFileSync(filePath, 'utf-8').includes('<!-- LOOM_TEMPLATE -->');
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function appendProjectDocumentStructureDiagnostics(issues, versionDir, philosophyDir) {
|
|
803
|
+
for (const document of REQUIRED_PROJECT_DOCUMENTS) {
|
|
804
|
+
const filePath = join(versionDir, document.relativePath);
|
|
805
|
+
if (!existsSync(filePath)) {
|
|
806
|
+
issues.push({ id: document.relativePath, type: 'project_document_missing', severity: 'high', msg: `缺少 LOOM 必需项目文档: ${document.relativePath}(${document.label})` });
|
|
807
|
+
} else if (isLoomTemplateDocument(filePath)) {
|
|
808
|
+
issues.push({ id: document.relativePath, type: 'project_document_template', severity: 'high', msg: `${document.relativePath} 仍是 LOOM_TEMPLATE,尚未成为当前项目的 ${document.label} 文档` });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
for (const filename of REQUIRED_PHILOSOPHY_DOCUMENTS) {
|
|
812
|
+
const filePath = join(philosophyDir, filename);
|
|
813
|
+
if (!existsSync(filePath)) {
|
|
814
|
+
issues.push({ id: filename, type: 'project_document_missing', severity: 'high', msg: `缺少 LOOM 必需哲学文档: 00_PHILOSOPHY/${filename}` });
|
|
815
|
+
} else if (isLoomTemplateDocument(filePath)) {
|
|
816
|
+
issues.push({ id: filename, type: 'project_document_template', severity: 'high', msg: `00_PHILOSOPHY/${filename} 仍是 LOOM_TEMPLATE,尚未成为当前项目的哲学判断` });
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
727
820
|
|
|
728
821
|
// ─── reverse-dep ───────────────────────────────────────
|
|
729
822
|
// 反向依赖:哪些 Intent 依赖这个 Intent
|