@haaaiawd/loom 1.3.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -86
- package/CONTRIBUTING.md +37 -0
- package/EVIL_EVAL.md +112 -0
- package/README.md +193 -445
- package/README.zh-CN.md +174 -0
- package/SECURITY.md +11 -0
- package/cli/bin/loom.js +171 -998
- package/cli/src/protocol.js +367 -0
- package/cli/src/store.js +626 -0
- package/design.md +194 -0
- package/docs/PROMPT_CATALOG.md +99 -0
- package/docs/RELEASE_CHECKLIST.md +53 -0
- package/docs/UX_FLOW.md +171 -0
- package/docs/brand/loom-mark.svg +18 -0
- package/docs/brand/loom-readme-header.svg +34 -0
- package/docs/brand/loom-readme-header.zh-CN.svg +29 -0
- package/docs/loom-eval-loop.drawio +21 -0
- package/docs/loom-eval-loop.svg +56 -0
- package/docs/loom-production-loop.drawio +41 -0
- package/docs/loom-production-loop.svg +92 -0
- package/package.json +43 -40
- package/EXTERNAL_ACQUISITION_DESIGN.md +0 -143
- package/cli/help/asset.md +0 -36
- package/cli/help/atelier.md +0 -37
- package/cli/help/atlas.md +0 -48
- package/cli/help/capability.md +0 -118
- package/cli/help/concepts.md +0 -105
- package/cli/help/doctor.md +0 -80
- package/cli/help/expertise.md +0 -52
- package/cli/help/loop.md +0 -134
- package/cli/help/patch.md +0 -33
- package/cli/help/proposals.md +0 -21
- package/cli/help/version.md +0 -136
- package/cli/help/workflow.md +0 -116
- package/cli/src/activate.js +0 -505
- package/cli/src/asset-library.js +0 -384
- package/cli/src/atelier.js +0 -331
- package/cli/src/atlas.js +0 -282
- package/cli/src/auto.js +0 -116
- package/cli/src/capability-graph.js +0 -724
- package/cli/src/capability-proposals.js +0 -225
- package/cli/src/diagnostics.js +0 -859
- package/cli/src/expertise-pack.js +0 -336
- package/cli/src/guide.js +0 -548
- package/cli/src/help.js +0 -41
- package/cli/src/init.js +0 -187
- package/cli/src/intent-draft.js +0 -303
- package/cli/src/intent-map.js +0 -747
- package/cli/src/patch.js +0 -214
- package/cli/src/philosophy.js +0 -331
- package/cli/src/shared/intent-ref.js +0 -38
- package/cli/src/shared/md-utils.js +0 -125
- package/cli/src/shared/paths.js +0 -73
- package/cli/src/shared/proof-reference.js +0 -19
- package/cli/src/shared/verification-method.js +0 -32
- package/cli/src/verify.js +0 -394
- package/cli/src/version.js +0 -134
- package/dimensions/AUTHORSHIP.md +0 -45
- package/dimensions/PART_DECOMPOSITION.md +0 -42
- package/dimensions/SEARCH_METHODOLOGY.md +0 -101
- package/dimensions/examples/AGENT_SYSTEM/README.md +0 -219
- package/dimensions/examples/CLI_TOOL/README.md +0 -163
- package/dimensions/universal/COLLABORATION_PHILOSOPHY.md +0 -28
- package/dimensions/universal/ENGINEERING_CREED.md +0 -30
- package/dimensions/universal/PRODUCT_PHILOSOPHY.md +0 -32
- package/meta/BASELINE.md +0 -91
- package/meta/INTENT_LOOP.md +0 -296
- package/meta/PHILOSOPHY_WEAVER.md +0 -110
- package/meta/ROLE_ACTIVATION.md +0 -114
- package/roles/architect.md +0 -92
- package/roles/forge.md +0 -110
- package/roles/impact-reviewer.md +0 -37
- package/roles/keeper.md +0 -113
- package/roles/visionary.md +0 -57
- package/templates/ASSET_LIBRARY_MANIFEST_TEMPLATE.json +0 -10
- package/templates/ATELIER_RECORD_TEMPLATE.json +0 -48
- package/templates/ATLAS_TEMPLATE.html +0 -104
- package/templates/CAPABILITY_BRIEF_TEMPLATE.md +0 -36
- package/templates/CAPABILITY_GRAPH_EXAMPLE.json +0 -188
- package/templates/CAPABILITY_GRAPH_TEMPLATE.json +0 -78
- package/templates/EXPERTISE_PACK_TEMPLATE.json +0 -22
- package/templates/INTENT_MAP_TEMPLATE.json +0 -85
- package/templates/PHILOSOPHY_TEMPLATE.md +0 -44
- package/templates/VISION_TEMPLATE.md +0 -44
|
@@ -1,724 +0,0 @@
|
|
|
1
|
-
// capability-graph.js — 项目初衷到能力获取的可审计展开图。
|
|
2
|
-
|
|
3
|
-
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
4
|
-
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
|
-
import { loadIntentMap } from './intent-map.js';
|
|
6
|
-
import { readJsonFile } from './shared/md-utils.js';
|
|
7
|
-
|
|
8
|
-
const GRAPH_FILE = '07_CAPABILITY_GRAPH.json';
|
|
9
|
-
const BRIEFS_DIR = '07_CAPABILITY_BRIEFS';
|
|
10
|
-
const NODE_KINDS = ['outcome', 'concern', 'capability', 'risk', 'evidence'];
|
|
11
|
-
const NODE_STATUSES = ['open', 'researched', 'covered', 'deferred', 'out_of_scope'];
|
|
12
|
-
const ROUTES = ['expand', 'brief', 'intent', 'defer', 'exclude', 'covered_by'];
|
|
13
|
-
const RELATIONSHIPS = ['refines', 'requires', 'realizes', 'constrains', 'risks', 'validated_by', 'covered_by'];
|
|
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'];
|
|
17
|
-
const VERIFICATION_FIELDS = ['method', 'target', 'procedure', 'pass_criteria', 'artifact'];
|
|
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
|
-
];
|
|
31
|
-
|
|
32
|
-
export function getCapabilityGraphPath(versionDir) {
|
|
33
|
-
return join(versionDir, GRAPH_FILE);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function getCapabilityBriefsDir(versionDir) {
|
|
37
|
-
return join(versionDir, BRIEFS_DIR);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function assertString(value, label, errors) {
|
|
41
|
-
if (typeof value !== 'string' || value.trim() === '') errors.push(`${label} 必须是非空字符串`);
|
|
42
|
-
}
|
|
43
|
-
|
|
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) {
|
|
231
|
-
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
|
232
|
-
errors.push(`nodes["${id}"] 必须是对象`);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
if (node.id !== id) errors.push(`nodes["${id}"].id 必须与 key 一致`);
|
|
236
|
-
assertString(node.title, `nodes["${id}"].title`, errors);
|
|
237
|
-
if (!NODE_KINDS.includes(node.kind)) errors.push(`nodes["${id}"].kind 非法: ${node.kind}`);
|
|
238
|
-
if (!NODE_STATUSES.includes(node.status)) errors.push(`nodes["${id}"].status 非法: ${node.status}`);
|
|
239
|
-
if (!['low', 'medium', 'high'].includes(node.impact)) errors.push(`nodes["${id}"].impact 非法: ${node.impact}`);
|
|
240
|
-
if (!ROUTES.includes(node.route)) errors.push(`nodes["${id}"].route 非法: ${node.route}`);
|
|
241
|
-
if (node.intent_refs !== undefined && (!Array.isArray(node.intent_refs) || node.intent_refs.some((ref) => typeof ref !== 'string'))) {
|
|
242
|
-
errors.push(`nodes["${id}"].intent_refs 必须是字符串数组`);
|
|
243
|
-
}
|
|
244
|
-
if (node.proposal_refs !== undefined && (!Array.isArray(node.proposal_refs) || node.proposal_refs.some((ref) => typeof ref !== 'string' || !ref.trim()))) {
|
|
245
|
-
errors.push(`nodes["${id}"].proposal_refs 必须是非空字符串数组`);
|
|
246
|
-
}
|
|
247
|
-
if (node.covered_by !== undefined && (typeof node.covered_by !== 'string' || !node.covered_by.trim())) {
|
|
248
|
-
errors.push(`nodes["${id}"].covered_by 必须是非空节点 ID`);
|
|
249
|
-
}
|
|
250
|
-
if (node.relationships !== undefined && !Array.isArray(node.relationships)) {
|
|
251
|
-
errors.push(`nodes["${id}"].relationships 必须是数组`);
|
|
252
|
-
}
|
|
253
|
-
for (const relation of node.relationships || []) {
|
|
254
|
-
if (!relation || typeof relation !== 'object') {
|
|
255
|
-
errors.push(`nodes["${id}"].relationships 含非法关系`);
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
if (!RELATIONSHIPS.includes(relation.type)) errors.push(`nodes["${id}"].relationships.type 非法: ${relation.type}`);
|
|
259
|
-
if (!allIds.has(relation.target)) errors.push(`nodes["${id}"].relationships 指向不存在节点: ${relation.target}`);
|
|
260
|
-
}
|
|
261
|
-
if (node.brief_ref !== undefined && (typeof node.brief_ref !== 'string' || node.brief_ref.trim() === '')) {
|
|
262
|
-
errors.push(`nodes["${id}"].brief_ref 必须是非空字符串`);
|
|
263
|
-
}
|
|
264
|
-
if (node.asset_refs !== undefined && (!Array.isArray(node.asset_refs) || node.asset_refs.some((ref) => typeof ref !== 'string' || !ref.trim()))) {
|
|
265
|
-
errors.push(`nodes["${id}"].asset_refs 必须是字符串数组`);
|
|
266
|
-
}
|
|
267
|
-
if (node.asset_refs !== undefined && node.kind !== 'evidence') {
|
|
268
|
-
errors.push(`nodes["${id}"].asset_refs 只允许写在 evidence 节点`);
|
|
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
|
-
}
|
|
286
|
-
if (node.acquisition_mode !== undefined) {
|
|
287
|
-
if (node.kind !== 'capability') {
|
|
288
|
-
errors.push(`nodes["${id}"].acquisition_mode 只允许写在 capability 节点`);
|
|
289
|
-
} else if (!ACQUISITION_MODES.includes(node.acquisition_mode)) {
|
|
290
|
-
errors.push(`nodes["${id}"].acquisition_mode 非法: ${node.acquisition_mode}`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
if (node.acquisition_mode === 'project_only'
|
|
294
|
-
&& (typeof node.acquisition_rationale !== 'string' || !node.acquisition_rationale.trim())) {
|
|
295
|
-
errors.push(`nodes["${id}"].acquisition_mode=project_only 必须声明 acquisition_rationale`);
|
|
296
|
-
}
|
|
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'
|
|
301
|
-
&& (typeof node.acquisition_rationale !== 'string' || !node.acquisition_rationale.trim())) {
|
|
302
|
-
errors.push(`nodes["${id}"] 为高影响 capability 且选择 adaptive 时必须声明 acquisition_rationale;说明为何此处不启用 external_required`);
|
|
303
|
-
}
|
|
304
|
-
if (node.verification !== undefined) {
|
|
305
|
-
if (node.kind !== 'evidence') {
|
|
306
|
-
errors.push(`nodes["${id}"].verification 只允许写在 evidence 节点`);
|
|
307
|
-
} else if (!node.verification || typeof node.verification !== 'object' || Array.isArray(node.verification)) {
|
|
308
|
-
errors.push(`nodes["${id}"].verification 必须是对象`);
|
|
309
|
-
} else {
|
|
310
|
-
for (const field of VERIFICATION_FIELDS) {
|
|
311
|
-
assertString(node.verification[field], `nodes["${id}"].verification.${field}`, errors);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export function validateCapabilityGraph(data) {
|
|
318
|
-
const errors = [];
|
|
319
|
-
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
320
|
-
throw new Error('Capability Graph 必须是 JSON 对象');
|
|
321
|
-
}
|
|
322
|
-
if (!data.nodes || typeof data.nodes !== 'object' || Array.isArray(data.nodes)) {
|
|
323
|
-
errors.push('缺少 nodes 对象');
|
|
324
|
-
} else {
|
|
325
|
-
const ids = new Set(Object.keys(data.nodes));
|
|
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
|
-
}
|
|
336
|
-
for (const [id, node] of Object.entries(data.nodes)) {
|
|
337
|
-
if (node?.route !== 'covered_by') continue;
|
|
338
|
-
const targetId = node.covered_by;
|
|
339
|
-
const target = data.nodes[targetId];
|
|
340
|
-
if (!targetId) errors.push(`nodes["${id}"].route=covered_by 必须声明 covered_by`);
|
|
341
|
-
else if (targetId === id) errors.push(`nodes["${id}"].covered_by 不得指向自身`);
|
|
342
|
-
else if (!target) errors.push(`nodes["${id}"].covered_by 指向不存在节点: ${targetId}`);
|
|
343
|
-
else if (target.status !== 'covered' || target.route === 'covered_by') errors.push(`nodes["${id}"].covered_by 必须指向已覆盖且非 covered_by 的真实节点: ${targetId}`);
|
|
344
|
-
else if (!(node.relationships || []).some((relation) => relation.type === 'covered_by' && relation.target === targetId)) {
|
|
345
|
-
errors.push(`nodes["${id}"].route=covered_by 必须有 covered_by 关系指向 ${targetId}`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
if (data.constraints !== undefined) {
|
|
349
|
-
if (!Array.isArray(data.constraints)) errors.push('constraints 必须是数组');
|
|
350
|
-
else {
|
|
351
|
-
const seen = new Set();
|
|
352
|
-
for (const [index, constraint] of data.constraints.entries()) {
|
|
353
|
-
const prefix = `constraints[${index}]`;
|
|
354
|
-
if (!constraint || typeof constraint !== 'object' || Array.isArray(constraint)) { errors.push(`${prefix} 必须是对象`); continue; }
|
|
355
|
-
assertString(constraint.proposal_id, `${prefix}.proposal_id`, errors);
|
|
356
|
-
assertString(constraint.title, `${prefix}.title`, errors);
|
|
357
|
-
if (!Array.isArray(constraint.node_refs) || constraint.node_refs.length === 0) errors.push(`${prefix}.node_refs 必须包含至少一个 Graph 节点`);
|
|
358
|
-
else for (const nodeId of constraint.node_refs) if (!ids.has(nodeId)) errors.push(`${prefix}.node_refs 指向不存在节点: ${nodeId}`);
|
|
359
|
-
if (seen.has(constraint.proposal_id)) errors.push(`${prefix}.proposal_id 重复: ${constraint.proposal_id}`);
|
|
360
|
-
seen.add(constraint.proposal_id);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
if (errors.length) throw new Error(`Capability Graph 校验失败:\n - ${errors.join('\n - ')}`);
|
|
366
|
-
return data;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export function loadCapabilityGraph(versionDir, { required = true } = {}) {
|
|
370
|
-
const filePath = getCapabilityGraphPath(versionDir);
|
|
371
|
-
if (!existsSync(filePath)) {
|
|
372
|
-
if (required) throw new Error(`缺少 ${GRAPH_FILE}`);
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
return validateCapabilityGraph(readJsonFile(filePath, 'Capability Graph'));
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function getEffectiveAcquisitionMode(node) {
|
|
379
|
-
if (node.kind !== 'capability') return null;
|
|
380
|
-
if (node.acquisition_mode) return node.acquisition_mode;
|
|
381
|
-
if (node.impact === 'high') return 'external_required';
|
|
382
|
-
return 'adaptive';
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function isRouted(node) {
|
|
386
|
-
return ['brief', 'intent', 'defer', 'exclude', 'covered_by'].includes(node.route);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function incoming(nodes, nodeId) {
|
|
390
|
-
const result = [];
|
|
391
|
-
for (const [id, node] of Object.entries(nodes)) {
|
|
392
|
-
for (const relation of node.relationships || []) {
|
|
393
|
-
if (relation.target === nodeId) result.push({ source: id, type: relation.type });
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return result;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
export function getCapabilityNode(versionDir, nodeId) {
|
|
400
|
-
const graph = loadCapabilityGraph(versionDir);
|
|
401
|
-
const node = graph.nodes[nodeId];
|
|
402
|
-
if (!node) throw new Error(`Capability 节点不存在: ${nodeId}`);
|
|
403
|
-
return { node, incoming: incoming(graph.nodes, nodeId), outgoing: node.relationships || [] };
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
export function getCapabilityFrontier(versionDir) {
|
|
407
|
-
const graph = loadCapabilityGraph(versionDir);
|
|
408
|
-
return Object.values(graph.nodes)
|
|
409
|
-
.filter((node) => node.impact === 'high' && (!isRouted(node) || node.status === 'open'))
|
|
410
|
-
.map((node) => ({ id: node.id, title: node.title, kind: node.kind, status: node.status, route: node.route, question: node.question || null }));
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export function getCapabilityGraphProjection(versionDir) {
|
|
414
|
-
const graph = loadCapabilityGraph(versionDir);
|
|
415
|
-
const lines = ['```mermaid', 'flowchart LR'];
|
|
416
|
-
for (const node of Object.values(graph.nodes)) {
|
|
417
|
-
const label = `${node.id}<br/>${String(node.title).replace(/"/g, '"')}`;
|
|
418
|
-
const shape = node.kind === 'outcome' ? `(["${label}"])`
|
|
419
|
-
: node.kind === 'risk' ? `{{"${label}"}}`
|
|
420
|
-
: node.kind === 'capability' ? `[["${label}"]]`
|
|
421
|
-
: `("${label}")`;
|
|
422
|
-
lines.push(` ${node.id.replace(/[^A-Za-z0-9_]/g, '_')}${shape}`);
|
|
423
|
-
}
|
|
424
|
-
for (const node of Object.values(graph.nodes)) {
|
|
425
|
-
for (const relation of node.relationships || []) {
|
|
426
|
-
lines.push(` ${node.id.replace(/[^A-Za-z0-9_]/g, '_')} -->|${relation.type}| ${relation.target.replace(/[^A-Za-z0-9_]/g, '_')}`);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
lines.push('```');
|
|
430
|
-
return {
|
|
431
|
-
summary: {
|
|
432
|
-
total: Object.keys(graph.nodes).length,
|
|
433
|
-
by_kind: Object.fromEntries(NODE_KINDS.map((kind) => [kind, Object.values(graph.nodes).filter((node) => node.kind === kind).length])),
|
|
434
|
-
frontier: getCapabilityFrontier(versionDir).length,
|
|
435
|
-
lenses: getLensSummary(graph),
|
|
436
|
-
capability_domains: getDomainSummary(graph),
|
|
437
|
-
},
|
|
438
|
-
mermaid: lines.join('\n'),
|
|
439
|
-
};
|
|
440
|
-
}
|
|
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
|
-
|
|
460
|
-
function resolveBrief(versionDir, briefRef) {
|
|
461
|
-
if (!briefRef) return null;
|
|
462
|
-
if (isAbsolute(briefRef)) throw new Error(`Capability Brief 不得使用绝对路径: ${briefRef}`);
|
|
463
|
-
const versionRoot = resolve(versionDir);
|
|
464
|
-
const filePath = resolve(versionDir, briefRef);
|
|
465
|
-
const relation = relative(versionRoot, filePath);
|
|
466
|
-
if (relation.startsWith('..') || isAbsolute(relation)) throw new Error(`Capability Brief 不得越出当前版本目录: ${briefRef}`);
|
|
467
|
-
const briefsRoot = resolve(getCapabilityBriefsDir(versionDir));
|
|
468
|
-
const briefRelation = relative(briefsRoot, filePath);
|
|
469
|
-
if (briefRelation.startsWith('..') || isAbsolute(briefRelation)) throw new Error(`Capability Brief 必须位于 ${BRIEFS_DIR}/: ${briefRef}`);
|
|
470
|
-
if (!existsSync(filePath)) throw new Error(`Capability Brief 不存在: ${briefRef}`);
|
|
471
|
-
return { ref: briefRef, content: readFileSync(filePath, 'utf-8') };
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function resolveEvidenceArtifact(versionDir, artifactRef) {
|
|
475
|
-
if (typeof artifactRef !== 'string' || !artifactRef.trim() || isAbsolute(artifactRef)) return null;
|
|
476
|
-
const versionRoot = resolve(versionDir);
|
|
477
|
-
const artifactPath = resolve(versionDir, artifactRef);
|
|
478
|
-
const withinVersion = relative(versionRoot, artifactPath);
|
|
479
|
-
if (withinVersion.startsWith('..') || isAbsolute(withinVersion)) return null;
|
|
480
|
-
const allowed = EVIDENCE_ARTIFACT_DIRS.some((directory) => {
|
|
481
|
-
const allowedRoot = resolve(versionDir, directory);
|
|
482
|
-
const relation = relative(allowedRoot, artifactPath);
|
|
483
|
-
return !relation.startsWith('..') && !isAbsolute(relation) && relation !== '';
|
|
484
|
-
});
|
|
485
|
-
if (!allowed || !existsSync(artifactPath) || !lstatSync(artifactPath).isFile()) return null;
|
|
486
|
-
return artifactPath;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function getHighOutcomesWithoutObservableEvidence(graph, intentMap, intentMappingRequired) {
|
|
490
|
-
const gaps = [];
|
|
491
|
-
for (const outcome of Object.values(graph.nodes)) {
|
|
492
|
-
if (outcome.kind !== 'outcome' || outcome.impact !== 'high') continue;
|
|
493
|
-
const evidenceRelations = (outcome.relationships || []).filter((relation) => relation.type === 'validated_by');
|
|
494
|
-
const validEvidence = evidenceRelations.find((relation) => {
|
|
495
|
-
const evidence = graph.nodes[relation.target];
|
|
496
|
-
if (!evidence || evidence.kind !== 'evidence' || !evidence.verification) return false;
|
|
497
|
-
const hasOwner = (evidence.intent_refs || []).some((intentId) => !intentMappingRequired || intentId in intentMap.intents);
|
|
498
|
-
return hasOwner;
|
|
499
|
-
});
|
|
500
|
-
if (!validEvidence) {
|
|
501
|
-
gaps.push({
|
|
502
|
-
node_id: outcome.id,
|
|
503
|
-
reason: '高影响 outcome 必须以 validated_by 连接到带 method、target、procedure、pass_criteria、artifact 和 Intent 回链的 evidence 节点',
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
return gaps;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
export function getCapabilityCoverage(versionDir) {
|
|
511
|
-
const graph = loadCapabilityGraph(versionDir);
|
|
512
|
-
const intentMap = loadIntentMap(versionDir);
|
|
513
|
-
const intentMappingRequired = intentMap._meta?._template !== true;
|
|
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;
|
|
517
|
-
const highUnrouted = getCapabilityFrontier(versionDir);
|
|
518
|
-
const orphanIntentRefs = [];
|
|
519
|
-
const mappedIntentIds = new Set();
|
|
520
|
-
const capabilitiesWithoutPlan = [];
|
|
521
|
-
const routingGaps = [];
|
|
522
|
-
const outcomesWithoutConcern = [];
|
|
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
|
-
}
|
|
537
|
-
|
|
538
|
-
for (const node of nodes) {
|
|
539
|
-
for (const intentId of node.intent_refs || []) {
|
|
540
|
-
if (!intentMappingRequired) continue;
|
|
541
|
-
if (!(intentId in intentMap.intents)) orphanIntentRefs.push({ node_id: node.id, intent_id: intentId });
|
|
542
|
-
else mappedIntentIds.add(intentId);
|
|
543
|
-
}
|
|
544
|
-
if (node.route === 'intent' && (!node.intent_refs || node.intent_refs.length === 0)) {
|
|
545
|
-
routingGaps.push({ node_id: node.id, reason: 'route=intent 但缺少 intent_refs' });
|
|
546
|
-
}
|
|
547
|
-
if (node.route === 'brief' && !node.brief_ref) {
|
|
548
|
-
capabilitiesWithoutPlan.push({ node_id: node.id, reason: 'route=brief 但缺少 brief_ref' });
|
|
549
|
-
}
|
|
550
|
-
if (node.kind === 'capability') {
|
|
551
|
-
const linkedIntents = (node.intent_refs || [])
|
|
552
|
-
.map((intentId) => intentMap.intents[intentId])
|
|
553
|
-
.filter(Boolean);
|
|
554
|
-
const needsExternalAcquisition = linkedIntents.length > 0
|
|
555
|
-
&& getEffectiveAcquisitionMode(node) === 'external_required';
|
|
556
|
-
if (needsExternalAcquisition && !node.brief_ref && node.route !== 'brief') {
|
|
557
|
-
capabilitiesWithoutPlan.push({
|
|
558
|
-
node_id: node.id,
|
|
559
|
-
reason: '外部能力获取为 required,但缺少 brief_ref 来定义专业问题与验收边界',
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
if (['defer', 'exclude'].includes(node.route) && (!node.rationale || typeof node.rationale !== 'string' || node.rationale.trim() === '')) {
|
|
564
|
-
routingGaps.push({ node_id: node.id, reason: `route=${node.route} 但缺少 rationale` });
|
|
565
|
-
}
|
|
566
|
-
if (node.route === 'defer' && (!node.revisit_when || typeof node.revisit_when !== 'string' || node.revisit_when.trim() === '')) {
|
|
567
|
-
routingGaps.push({ node_id: node.id, reason: 'route=defer 但缺少 revisit_when' });
|
|
568
|
-
}
|
|
569
|
-
if (node.route === 'covered_by' && (node.relationships || []).length === 0) {
|
|
570
|
-
routingGaps.push({ node_id: node.id, reason: 'route=covered_by 但没有指向覆盖节点的关系' });
|
|
571
|
-
}
|
|
572
|
-
if (node.kind === 'outcome' && !(node.relationships || []).some((relation) => graph.nodes[relation.target]?.kind === 'concern')) {
|
|
573
|
-
outcomesWithoutConcern.push({ node_id: node.id, reason: 'outcome 没有连接到 concern,项目初衷尚未展开为问题面' });
|
|
574
|
-
}
|
|
575
|
-
if (node.kind === 'evidence' && node.verification?.artifact) {
|
|
576
|
-
const ownedCompletedIntent = (node.intent_refs || []).some((intentId) => intentMap.intents[intentId]?.status === 'completed');
|
|
577
|
-
if (ownedCompletedIntent && !resolveEvidenceArtifact(versionDir, node.verification.artifact)) {
|
|
578
|
-
evidenceArtifactGaps.push({ node_id: node.id, reason: `已完成 Intent 的 evidence artifact 不存在或不可读: ${node.verification.artifact}` });
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
if (node.brief_ref) {
|
|
582
|
-
try { resolveBrief(versionDir, node.brief_ref); } catch (error) {
|
|
583
|
-
capabilitiesWithoutPlan.push({ node_id: node.id, reason: error.message });
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const unmappedIntents = intentMappingRequired
|
|
589
|
-
? Object.keys(intentMap.intents).filter((id) => !mappedIntentIds.has(id))
|
|
590
|
-
: [];
|
|
591
|
-
const highOutcomesWithoutObservableEvidence = getHighOutcomesWithoutObservableEvidence(graph, intentMap, intentMappingRequired);
|
|
592
|
-
return {
|
|
593
|
-
summary: {
|
|
594
|
-
nodes: nodes.length,
|
|
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,
|
|
600
|
-
high_unrouted: highUnrouted.length,
|
|
601
|
-
orphan_intent_refs: orphanIntentRefs.length,
|
|
602
|
-
capabilities_without_plan: capabilitiesWithoutPlan.length,
|
|
603
|
-
routing_gaps: routingGaps.length,
|
|
604
|
-
outcomes_without_concern: outcomesWithoutConcern.length,
|
|
605
|
-
high_outcomes_without_observable_evidence: highOutcomesWithoutObservableEvidence.length,
|
|
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),
|
|
611
|
-
intent_mapping_required: intentMappingRequired,
|
|
612
|
-
unmapped_intents: unmappedIntents.length,
|
|
613
|
-
ready: highUnrouted.length === 0
|
|
614
|
-
&& orphanIntentRefs.length === 0
|
|
615
|
-
&& capabilitiesWithoutPlan.length === 0
|
|
616
|
-
&& routingGaps.length === 0
|
|
617
|
-
&& outcomesWithoutConcern.length === 0
|
|
618
|
-
&& highOutcomesWithoutObservableEvidence.length === 0
|
|
619
|
-
&& evidenceArtifactGaps.length === 0
|
|
620
|
-
&& lensContractGaps.length === 0
|
|
621
|
-
&& capabilityDomainGaps.length === 0
|
|
622
|
-
&& unmappedIntents.length === 0,
|
|
623
|
-
},
|
|
624
|
-
high_unrouted: highUnrouted,
|
|
625
|
-
orphan_intent_refs: orphanIntentRefs,
|
|
626
|
-
capabilities_without_plan: capabilitiesWithoutPlan,
|
|
627
|
-
routing_gaps: routingGaps,
|
|
628
|
-
outcomes_without_concern: outcomesWithoutConcern,
|
|
629
|
-
high_outcomes_without_observable_evidence: highOutcomesWithoutObservableEvidence,
|
|
630
|
-
evidence_artifact_gaps: evidenceArtifactGaps,
|
|
631
|
-
lens_contract_gaps: lensContractGaps,
|
|
632
|
-
capability_domain_gaps: capabilityDomainGaps,
|
|
633
|
-
unmapped_intents: unmappedIntents,
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function collectCompilationNodes(graph, intentId) {
|
|
638
|
-
const selected = new Set(Object.values(graph.nodes)
|
|
639
|
-
.filter((node) => (node.intent_refs || []).includes(intentId))
|
|
640
|
-
.map((node) => node.id));
|
|
641
|
-
const queue = [...selected];
|
|
642
|
-
while (queue.length) {
|
|
643
|
-
const current = queue.shift();
|
|
644
|
-
for (const { source, type } of incoming(graph.nodes, current)) {
|
|
645
|
-
if (type === 'refines' && !selected.has(source)) {
|
|
646
|
-
selected.add(source);
|
|
647
|
-
queue.push(source);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
for (const relation of graph.nodes[current].relationships || []) {
|
|
651
|
-
if (['requires', 'constrains', 'risks'].includes(relation.type) && !selected.has(relation.target)) {
|
|
652
|
-
selected.add(relation.target);
|
|
653
|
-
queue.push(relation.target);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return [...selected].map((id) => graph.nodes[id]);
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
export function compileCapabilityInputs(versionDir, intentId) {
|
|
661
|
-
const graph = loadCapabilityGraph(versionDir, { required: false });
|
|
662
|
-
if (!graph) {
|
|
663
|
-
return {
|
|
664
|
-
available: false,
|
|
665
|
-
nodes: [],
|
|
666
|
-
briefs: [],
|
|
667
|
-
warnings: ['项目尚未建立 Capability Graph;使用 Intent 的 capability_needs 兼容路径。'],
|
|
668
|
-
acquisition: { required: false, required_node_ids: [], nodes: [] },
|
|
669
|
-
};
|
|
670
|
-
}
|
|
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
|
-
}));
|
|
690
|
-
const briefs = [];
|
|
691
|
-
const warnings = [];
|
|
692
|
-
for (const node of nodes) {
|
|
693
|
-
if (!node.brief_ref) continue;
|
|
694
|
-
try { briefs.push({ node_id: node.id, ...resolveBrief(versionDir, node.brief_ref) }); } catch (error) { warnings.push(`${node.id}: ${error.message}`); }
|
|
695
|
-
}
|
|
696
|
-
const acquisitionNodes = nodes
|
|
697
|
-
.filter((node) => node.kind === 'capability')
|
|
698
|
-
.map((node) => ({
|
|
699
|
-
node_id: node.id,
|
|
700
|
-
title: node.title,
|
|
701
|
-
mode: getEffectiveAcquisitionMode(node),
|
|
702
|
-
reason: node.acquisition_mode
|
|
703
|
-
? 'Capability Graph 显式声明'
|
|
704
|
-
: node.impact === 'high'
|
|
705
|
-
? '高影响能力默认需要外部来源化'
|
|
706
|
-
: '按任务证据自适应判断',
|
|
707
|
-
}));
|
|
708
|
-
const requiredNodeIds = acquisitionNodes
|
|
709
|
-
.filter((node) => node.mode === 'external_required')
|
|
710
|
-
.map((node) => node.node_id);
|
|
711
|
-
return {
|
|
712
|
-
available: true,
|
|
713
|
-
nodes,
|
|
714
|
-
lenses: relevantLenses,
|
|
715
|
-
capability_domains: relevantDomains,
|
|
716
|
-
briefs,
|
|
717
|
-
warnings,
|
|
718
|
-
acquisition: {
|
|
719
|
-
required: requiredNodeIds.length > 0,
|
|
720
|
-
required_node_ids: requiredNodeIds,
|
|
721
|
-
nodes: acquisitionNodes,
|
|
722
|
-
},
|
|
723
|
-
};
|
|
724
|
-
}
|