@haaaiawd/loom 1.2.2 → 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.
@@ -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 validateNode(id, node, allIds, errors) {
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' && node.acquisition_mode === 'adaptive'
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
- for (const [id, node] of Object.entries(data.nodes)) validateNode(id, node, ids, errors);
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(versionDir, graph, intentMap, intentMappingRequired) {
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(versionDir, graph, intentMap, intentMappingRequired);
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: {
@@ -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
- issues.push({ id: 'capability_graph', type: 'capability_graph_invalid', severity: 'high', msg: error.message });
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
- // 3. 孤儿引用:depends_on 指向不存在的 Intent
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