@haaaiawd/loom 1.0.0 → 1.2.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 +39 -0
- package/EXTERNAL_ACQUISITION_DESIGN.md +143 -0
- package/README.md +71 -30
- package/cli/bin/loom.js +176 -7
- package/cli/help/asset.md +36 -0
- package/cli/help/atelier.md +37 -0
- package/cli/help/capability.md +73 -0
- package/cli/help/concepts.md +13 -3
- package/cli/help/expertise.md +51 -0
- package/cli/help/loop.md +4 -2
- package/cli/help/proposals.md +21 -0
- package/cli/help/workflow.md +26 -4
- package/cli/src/activate.js +85 -2
- package/cli/src/asset-library.js +384 -0
- package/cli/src/atelier.js +331 -0
- package/cli/src/capability-graph.js +416 -0
- package/cli/src/capability-proposals.js +225 -0
- package/cli/src/diagnostics.js +132 -3
- package/cli/src/expertise-pack.js +336 -0
- package/cli/src/guide.js +148 -21
- package/cli/src/init.js +11 -5
- package/cli/src/intent-map.js +20 -0
- package/cli/src/verify.js +35 -1
- package/dimensions/AUTHORSHIP.md +45 -0
- package/meta/INTENT_LOOP.md +59 -5
- package/meta/ROLE_ACTIVATION.md +17 -6
- package/package.json +11 -5
- package/roles/architect.md +24 -11
- package/roles/forge.md +30 -5
- package/roles/keeper.md +12 -0
- package/templates/ASSET_LIBRARY_MANIFEST_TEMPLATE.json +10 -0
- package/templates/ATELIER_RECORD_TEMPLATE.json +48 -0
- package/templates/CAPABILITY_BRIEF_TEMPLATE.md +35 -0
- package/templates/CAPABILITY_GRAPH_TEMPLATE.json +11 -0
- package/templates/EXPERTISE_PACK_TEMPLATE.json +22 -0
- package/templates/INTENT_MAP_TEMPLATE.json +2 -0
package/cli/src/diagnostics.js
CHANGED
|
@@ -11,6 +11,11 @@ import { validatePatches } from './patch.js';
|
|
|
11
11
|
import { formatIntentRef } from './shared/intent-ref.js';
|
|
12
12
|
import { commandCoversVerificationMethod, getIntentVerificationMethod } from './shared/verification-method.js';
|
|
13
13
|
import { resolveQualityProofReference } from './shared/proof-reference.js';
|
|
14
|
+
import { getCapabilityCoverage, getCapabilityGraphPath, loadCapabilityGraph } from './capability-graph.js';
|
|
15
|
+
import { listCapabilityProposals } from './capability-proposals.js';
|
|
16
|
+
import { getAssetManifestPath, validateAssetLibrary } from './asset-library.js';
|
|
17
|
+
import { validateAtelierRecord } from './atelier.js';
|
|
18
|
+
import { getExpertisePackState } from './expertise-pack.js';
|
|
14
19
|
|
|
15
20
|
function readIntentMapRaw(versionDir) {
|
|
16
21
|
const filePath = join(versionDir, '04_INTENT_MAP.json');
|
|
@@ -161,7 +166,22 @@ const FIX_HINTS = {
|
|
|
161
166
|
preservation_dimension_missing: '为 continuity_required 的 Intent 补写并通过 preservation_achievement,证据必须覆盖旧状态到新操作后的完整序列',
|
|
162
167
|
patch_changelog_invalid: '运行 loom patch validate 查看具体错误;修正 06_CHANGELOG.json 后重新生成 Markdown 投影',
|
|
163
168
|
patch_projection_drift: '不要手工编辑 06_CHANGELOG.md;重新运行 loom init 或下一次 loom patch record 生成投影',
|
|
164
|
-
|
|
169
|
+
capability_graph_missing: '这是 1.0 项目的兼容提示;迁移时运行 loom activate architect,创建 07_CAPABILITY_GRAPH.json 并路由高影响节点',
|
|
170
|
+
capability_graph_template: '运行 loom activate architect,先将愿景展开为 Capability Graph,再创建或修订 Intent Map',
|
|
171
|
+
capability_graph_invalid: '修正 07_CAPABILITY_GRAPH.json 的节点、关系或枚举值后运行 loom capability coverage',
|
|
172
|
+
capability_frontier_open: '为高影响节点继续展开、创建 Capability Brief、编译为 Intent,或明确延后/排除理由',
|
|
173
|
+
capability_plan_missing: '为高影响 capability 节点补齐可用 Brief 与获取计划,或改为明确的其他路由',
|
|
174
|
+
capability_route_evidence: '为该节点补齐路由所需的 Intent 回链、Brief 或延后/排除理由,避免只写一个状态标签',
|
|
175
|
+
capability_outcome_unexpanded: '将 outcome 连接到至少一个 concern,明确项目初衷需要被处理的问题面',
|
|
176
|
+
capability_outcome_unobservable: '为高影响 outcome 新建或补齐 evidence 节点,并以 validated_by 连接;evidence 必须写明观察目标、复现步骤、通过标准、证据产物,并回链承担验证的 Intent',
|
|
177
|
+
intent_graph_unmapped: '将该 Intent 回链到至少一个 Capability Graph 节点;不要让执行承诺失去项目初衷和能力来源',
|
|
178
|
+
capability_proposal_pending: '由 Architect 审核 proposal:判定已覆盖、更新 Graph、生成/修订 Intent、改变 acceptance,或升级 Minor/Major;Forge 不得静默把候选写进正式图谱。',
|
|
179
|
+
asset_library_invalid: '修复 08_ASSET_LIBRARY/manifest.json 的来源、许可、哈希、库内路径或 evidence 双向引用,然后运行 loom asset validate。',
|
|
180
|
+
atelier_record_invalid: '运行 loom atelier init {id} 创建记录,或按校验错误修正后运行 loom atelier validate {id}。',
|
|
181
|
+
atelier_verification_missing: '重新运行独立 Keeper 验证,让 passed 记录绑定当前 Atelier Record 与 stance_revision。',
|
|
182
|
+
expertise_pack_invalid: '运行 loom expertise init {id}(若尚未创建);实际执行 find skill / 网络或文档检索,补齐来源与 Capability Capsules 后运行 loom expertise validate {id}。',
|
|
183
|
+
expertise_verification_missing: '重新运行独立 Keeper 验证,让 passed 记录绑定当前 10_EXPERTISE_PACKS/{id}.json。',
|
|
184
|
+
};
|
|
165
185
|
|
|
166
186
|
/**
|
|
167
187
|
* 给 issue 补 fix_hint——把 {id} {dep} 等占位符替换成实际值。
|
|
@@ -196,8 +216,76 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
196
216
|
return { issues: issuesWithHints, summary: summarizeIssues(issuesWithHints) };
|
|
197
217
|
}
|
|
198
218
|
|
|
199
|
-
const { intents } = mapState.validMap;
|
|
200
|
-
|
|
219
|
+
const { intents } = mapState.validMap;
|
|
220
|
+
const atelierRecords = new Map();
|
|
221
|
+
|
|
222
|
+
for (const [id, intent] of Object.entries(intents)) {
|
|
223
|
+
if (intent.quality_strategy !== 'atelier' || intent.status === 'pending' || intent.status === 'blocked') continue;
|
|
224
|
+
try {
|
|
225
|
+
const record = validateAtelierRecord(versionDir, id);
|
|
226
|
+
atelierRecords.set(id, record);
|
|
227
|
+
if (intent.status === 'completed' && !['selected', 'baseline_retained'].includes(record.status)) {
|
|
228
|
+
issues.push({ id, type: 'atelier_record_invalid', severity: 'high', msg: `${id} 已 completed,但 Atelier Record 仍是 ${record.status}` });
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
issues.push({ id, type: 'atelier_record_invalid', severity: 'high', msg: error.message });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 0. Incoming Graph changes are an Architect gate, not Forge implementation scope.
|
|
236
|
+
try {
|
|
237
|
+
for (const proposal of listCapabilityProposals(versionDir, { unresolvedOnly: true })) {
|
|
238
|
+
issues.push({ id: proposal.id, type: 'capability_proposal_pending', severity: 'high', msg: `${proposal.id} 是尚未闭合的 ${proposal.origin} 候选(${proposal.candidate_kind}: ${proposal.title});必须由 Architect 决定其影响。` });
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
issues.push({ id: 'capability_proposals', type: 'capability_proposal_pending', severity: 'high', msg: `Capability Graph proposal 无法审计: ${error.message}` });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Asset Library is optional for a project, but once initialized it is a local source of truth.
|
|
245
|
+
if (existsSync(getAssetManifestPath(versionDir))) {
|
|
246
|
+
try { validateAssetLibrary(versionDir); } catch (error) {
|
|
247
|
+
issues.push({ id: 'asset_library', type: 'asset_library_invalid', severity: 'high', msg: error.message });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 0. Capability Graph:旧项目允许缺失但提示迁移;图谱一旦存在则必须可读、可路由。
|
|
252
|
+
const capabilityGraphPath = getCapabilityGraphPath(versionDir);
|
|
253
|
+
if (!existsSync(capabilityGraphPath)) {
|
|
254
|
+
issues.push({ id: 'capability_graph', type: 'capability_graph_missing', severity: 'medium', msg: '缺少 07_CAPABILITY_GRAPH.json;现有项目仍可运行,但不会获得图谱驱动的能力拆分与覆盖检查。' });
|
|
255
|
+
} else {
|
|
256
|
+
try {
|
|
257
|
+
const graph = loadCapabilityGraph(versionDir);
|
|
258
|
+
if (graph._meta?._template === true) {
|
|
259
|
+
issues.push({ id: 'capability_graph', type: 'capability_graph_template', severity: 'high', msg: 'Capability Graph 仍是模板;不得在未路由项目问题与能力缺口时宣告架构完成。' });
|
|
260
|
+
} else {
|
|
261
|
+
const coverage = getCapabilityCoverage(versionDir);
|
|
262
|
+
for (const node of coverage.high_unrouted) {
|
|
263
|
+
issues.push({ id: node.id, type: 'capability_frontier_open', severity: 'high', msg: `${node.id} 是尚未路由的高影响 Capability Graph 节点: ${node.title}` });
|
|
264
|
+
}
|
|
265
|
+
for (const item of coverage.capabilities_without_plan) {
|
|
266
|
+
issues.push({ id: item.node_id, type: 'capability_plan_missing', severity: 'high', msg: `${item.node_id} 的能力获取计划不完整: ${item.reason}` });
|
|
267
|
+
}
|
|
268
|
+
for (const item of coverage.routing_gaps) {
|
|
269
|
+
issues.push({ id: item.node_id, type: 'capability_route_evidence', severity: 'high', msg: `${item.node_id} 的图谱路由缺少依据: ${item.reason}` });
|
|
270
|
+
}
|
|
271
|
+
for (const item of coverage.outcomes_without_concern) {
|
|
272
|
+
issues.push({ id: item.node_id, type: 'capability_outcome_unexpanded', severity: 'high', msg: `${item.node_id} 没有展开为项目问题面: ${item.reason}` });
|
|
273
|
+
}
|
|
274
|
+
for (const item of coverage.high_outcomes_without_observable_evidence) {
|
|
275
|
+
issues.push({ id: item.node_id, type: 'capability_outcome_unobservable', severity: 'high', msg: `${item.node_id} 缺少真实呈现或交付的验证入口: ${item.reason}` });
|
|
276
|
+
}
|
|
277
|
+
for (const item of coverage.orphan_intent_refs) {
|
|
278
|
+
issues.push({ id: item.node_id, type: 'intent_graph_unmapped', severity: 'high', msg: `${item.node_id} 引用了不存在的 Intent: ${item.intent_id}` });
|
|
279
|
+
}
|
|
280
|
+
for (const intentId of coverage.unmapped_intents) {
|
|
281
|
+
issues.push({ id: intentId, type: 'intent_graph_unmapped', severity: 'high', msg: `${intentId} 没有 Capability Graph 回链;执行承诺缺少能力与问题来源。` });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
issues.push({ id: 'capability_graph', type: 'capability_graph_invalid', severity: 'high', msg: error.message });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
201
289
|
// 1. 状态一致性:completed 必须由当前 revision 的最后一条 passed 验证支撑。
|
|
202
290
|
for (const [id, intent] of Object.entries(intents)) {
|
|
203
291
|
const history = getVerificationHistory(verificationsDir, id);
|
|
@@ -229,6 +317,47 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
229
317
|
}
|
|
230
318
|
}
|
|
231
319
|
}
|
|
320
|
+
if (!['pending', 'blocked', 'deprecated'].includes(intent.status)) {
|
|
321
|
+
try {
|
|
322
|
+
const expertise = getExpertisePackState(versionDir, id);
|
|
323
|
+
if (expertise.required && !expertise.ready) {
|
|
324
|
+
issues.push({
|
|
325
|
+
id,
|
|
326
|
+
type: 'expertise_pack_invalid',
|
|
327
|
+
severity: intent.status === 'completed' || latest?.verdict === 'passed' ? 'high' : 'medium',
|
|
328
|
+
msg: `${id} 的外部能力获取强门尚未闭合: ${expertise.reason || 'Expertise Pack not ready'}`,
|
|
329
|
+
});
|
|
330
|
+
} else if (expertise.required && latest?.verdict === 'passed') {
|
|
331
|
+
const validation = expertise.validation;
|
|
332
|
+
if (latest.expertise?.record_ref !== `10_EXPERTISE_PACKS/${id}.json`
|
|
333
|
+
|| latest.expertise?.intent_revision !== validation.intent_revision
|
|
334
|
+
|| latest.expertise?.source_count !== validation.source_count
|
|
335
|
+
|| latest.expertise?.capsule_count !== validation.capsule_count
|
|
336
|
+
|| latest.expertise?.pack_digest !== validation.pack_digest) {
|
|
337
|
+
issues.push({
|
|
338
|
+
id,
|
|
339
|
+
type: 'expertise_verification_missing',
|
|
340
|
+
severity: 'high',
|
|
341
|
+
msg: `${id} 的最新 passed 未绑定当前 Expertise Pack、Intent revision 与来源/Capsule 计数`,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch { /* Capability Graph 的结构错误已由上方统一报告。 */ }
|
|
346
|
+
}
|
|
347
|
+
if (intent.quality_strategy === 'atelier' && latest?.verdict === 'passed') {
|
|
348
|
+
const atelier = atelierRecords.get(id);
|
|
349
|
+
if (!atelier
|
|
350
|
+
|| latest.atelier?.record_ref !== `09_ATELIER/${id}.json`
|
|
351
|
+
|| latest.atelier?.stance_revision !== atelier.stance_revision
|
|
352
|
+
|| latest.atelier?.status !== atelier.status) {
|
|
353
|
+
issues.push({
|
|
354
|
+
id,
|
|
355
|
+
type: 'atelier_verification_missing',
|
|
356
|
+
severity: 'high',
|
|
357
|
+
msg: `${id} 的最新 passed 未绑定当前 Atelier Record、status 与 stance_revision`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
232
361
|
if (intent.continuity_required && latest?.verdict === 'passed') {
|
|
233
362
|
const preservation = latest.dimensions?.preservation_achievement;
|
|
234
363
|
if (!preservation || preservation.verdict !== 'passed') {
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// expertise-pack.js — provenance-backed external capability acquisition for one Intent.
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { getIntent } from './intent-map.js';
|
|
7
|
+
import { compileCapabilityInputs } from './capability-graph.js';
|
|
8
|
+
import { getLoomRoot } from './shared/paths.js';
|
|
9
|
+
import { readJsonFile } from './shared/md-utils.js';
|
|
10
|
+
|
|
11
|
+
const PACKS_DIR = '10_EXPERTISE_PACKS';
|
|
12
|
+
const VALID_STATUS = ['draft', 'ready', 'blocked'];
|
|
13
|
+
const QUERY_CHANNELS = ['skill_registry', 'web', 'official_docs', 'research', 'tool', 'asset'];
|
|
14
|
+
const SOURCE_KINDS = ['skill', 'web', 'official_docs', 'research', 'tool', 'human'];
|
|
15
|
+
const SOURCE_AUTHORITIES = ['official', 'primary', 'expert', 'community', 'secondary'];
|
|
16
|
+
const EXTERNAL_KNOWLEDGE_KINDS = new Set(['skill', 'web', 'official_docs', 'research']);
|
|
17
|
+
|
|
18
|
+
export function getExpertisePacksDir(versionDir) {
|
|
19
|
+
return join(versionDir, PACKS_DIR);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getExpertisePackPath(versionDir, intentId) {
|
|
23
|
+
return join(getExpertisePacksDir(versionDir), `${intentId}.json`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pushText(value, label, errors) {
|
|
27
|
+
if (typeof value !== 'string' || value.trim() === '') errors.push(`${label} 必须是非空字符串`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pushTextArray(value, label, errors, { min = 1 } = {}) {
|
|
31
|
+
if (!Array.isArray(value)) {
|
|
32
|
+
errors.push(`${label} 必须是字符串数组`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (value.length < min) errors.push(`${label} 至少需要 ${min} 项`);
|
|
36
|
+
value.forEach((item, index) => pushText(item, `${label}[${index}]`, errors));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isHttpsLocator(value) {
|
|
40
|
+
try {
|
|
41
|
+
return new URL(value).protocol === 'https:';
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getExpertiseRequirement(versionDir, intentId) {
|
|
48
|
+
const intent = getIntent(versionDir, intentId);
|
|
49
|
+
const compiled = compileCapabilityInputs(versionDir, intentId);
|
|
50
|
+
const acquisition = compiled.acquisition || {
|
|
51
|
+
required: false,
|
|
52
|
+
required_node_ids: [],
|
|
53
|
+
nodes: [],
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
intent,
|
|
57
|
+
compiled,
|
|
58
|
+
required: acquisition.required === true,
|
|
59
|
+
required_node_ids: acquisition.required_node_ids || [],
|
|
60
|
+
acquisition_nodes: acquisition.nodes || [],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateSearchPlan(plan, strict, errors) {
|
|
65
|
+
if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
|
|
66
|
+
errors.push('search_plan 必须是对象');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!strict) return;
|
|
70
|
+
pushText(plan.decision_question, 'search_plan.decision_question', errors);
|
|
71
|
+
pushTextArray(plan.project_signals, 'search_plan.project_signals', errors);
|
|
72
|
+
pushTextArray(plan.constraints, 'search_plan.constraints', errors);
|
|
73
|
+
pushText(plan.stop_condition, 'search_plan.stop_condition', errors);
|
|
74
|
+
if (!Array.isArray(plan.derived_queries) || plan.derived_queries.length === 0) {
|
|
75
|
+
errors.push('search_plan.derived_queries 至少需要一条运行时派生查询');
|
|
76
|
+
} else {
|
|
77
|
+
plan.derived_queries.forEach((query, index) => {
|
|
78
|
+
const prefix = `search_plan.derived_queries[${index}]`;
|
|
79
|
+
if (!query || typeof query !== 'object' || Array.isArray(query)) {
|
|
80
|
+
errors.push(`${prefix} 必须是对象`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!QUERY_CHANNELS.includes(query.channel)) {
|
|
84
|
+
errors.push(`${prefix}.channel 必须是 ${QUERY_CHANNELS.join('|')}`);
|
|
85
|
+
}
|
|
86
|
+
pushText(query.query, `${prefix}.query`, errors);
|
|
87
|
+
pushText(query.rationale, `${prefix}.rationale`, errors);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validateSources(sources, strict, errors) {
|
|
93
|
+
const ids = new Set();
|
|
94
|
+
const valid = new Map();
|
|
95
|
+
if (!Array.isArray(sources)) {
|
|
96
|
+
errors.push('sources 必须是数组');
|
|
97
|
+
return { ids, valid, externalCount: 0 };
|
|
98
|
+
}
|
|
99
|
+
if (strict && sources.length === 0) errors.push('ready Expertise Pack 必须包含外部来源');
|
|
100
|
+
let externalCount = 0;
|
|
101
|
+
sources.forEach((source, index) => {
|
|
102
|
+
const prefix = `sources[${index}]`;
|
|
103
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
|
104
|
+
errors.push(`${prefix} 必须是对象`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
pushText(source.id, `${prefix}.id`, errors);
|
|
108
|
+
if (typeof source.id === 'string') {
|
|
109
|
+
if (ids.has(source.id)) errors.push(`${prefix}.id 重复: ${source.id}`);
|
|
110
|
+
ids.add(source.id);
|
|
111
|
+
}
|
|
112
|
+
if (!SOURCE_KINDS.includes(source.kind)) {
|
|
113
|
+
errors.push(`${prefix}.kind 必须是 ${SOURCE_KINDS.join('|')}`);
|
|
114
|
+
}
|
|
115
|
+
if (!SOURCE_AUTHORITIES.includes(source.authority)) {
|
|
116
|
+
errors.push(`${prefix}.authority 必须是 ${SOURCE_AUTHORITIES.join('|')}`);
|
|
117
|
+
}
|
|
118
|
+
pushText(source.title, `${prefix}.title`, errors);
|
|
119
|
+
pushText(source.locator, `${prefix}.locator`, errors);
|
|
120
|
+
if (source.kind !== 'human' && typeof source.locator === 'string' && !isHttpsLocator(source.locator)) {
|
|
121
|
+
errors.push(`${prefix}.locator 必须是可追溯的 https URL`);
|
|
122
|
+
}
|
|
123
|
+
pushText(source.retrieved_at, `${prefix}.retrieved_at`, errors);
|
|
124
|
+
if (typeof source.retrieved_at === 'string' && Number.isNaN(Date.parse(source.retrieved_at))) {
|
|
125
|
+
errors.push(`${prefix}.retrieved_at 必须是合法日期时间`);
|
|
126
|
+
}
|
|
127
|
+
pushText(source.why_selected, `${prefix}.why_selected`, errors);
|
|
128
|
+
pushText(source.retrieval_evidence, `${prefix}.retrieval_evidence`, errors);
|
|
129
|
+
if (EXTERNAL_KNOWLEDGE_KINDS.has(source.kind)) externalCount += 1;
|
|
130
|
+
if (typeof source.id === 'string' && source.id.trim()) valid.set(source.id, source);
|
|
131
|
+
});
|
|
132
|
+
if (strict && externalCount === 0) {
|
|
133
|
+
errors.push('ready Expertise Pack 至少需要一个 skill|web|official_docs|research 外部知识来源');
|
|
134
|
+
}
|
|
135
|
+
return { ids, valid, externalCount };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function validateCapsules(capsules, requiredNodeIds, sourceMap, strict, errors) {
|
|
139
|
+
const covered = new Set();
|
|
140
|
+
if (!Array.isArray(capsules)) {
|
|
141
|
+
errors.push('capsules 必须是数组');
|
|
142
|
+
return covered;
|
|
143
|
+
}
|
|
144
|
+
if (strict && capsules.length === 0) errors.push('ready Expertise Pack 必须包含 Capability Capsule');
|
|
145
|
+
capsules.forEach((capsule, index) => {
|
|
146
|
+
const prefix = `capsules[${index}]`;
|
|
147
|
+
if (!capsule || typeof capsule !== 'object' || Array.isArray(capsule)) {
|
|
148
|
+
errors.push(`${prefix} 必须是对象`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
pushText(capsule.capability_ref, `${prefix}.capability_ref`, errors);
|
|
152
|
+
if (typeof capsule.capability_ref === 'string') covered.add(capsule.capability_ref);
|
|
153
|
+
if (strict && requiredNodeIds.length > 0 && !requiredNodeIds.includes(capsule.capability_ref)) {
|
|
154
|
+
errors.push(`${prefix}.capability_ref 未引用当前 Intent 的外部获取能力节点: ${capsule.capability_ref}`);
|
|
155
|
+
}
|
|
156
|
+
if (!strict) return;
|
|
157
|
+
pushText(capsule.professional_problem, `${prefix}.professional_problem`, errors);
|
|
158
|
+
pushText(capsule.when_to_use, `${prefix}.when_to_use`, errors);
|
|
159
|
+
pushTextArray(capsule.rules, `${prefix}.rules`, errors);
|
|
160
|
+
pushTextArray(capsule.workflow, `${prefix}.workflow`, errors);
|
|
161
|
+
pushTextArray(capsule.decision_gates, `${prefix}.decision_gates`, errors);
|
|
162
|
+
pushTextArray(capsule.failure_modes, `${prefix}.failure_modes`, errors);
|
|
163
|
+
pushTextArray(capsule.verification_signals, `${prefix}.verification_signals`, errors);
|
|
164
|
+
if (!Array.isArray(capsule.source_refs) || capsule.source_refs.length === 0) {
|
|
165
|
+
errors.push(`${prefix}.source_refs 至少需要一个来源`);
|
|
166
|
+
} else {
|
|
167
|
+
let externalRefCount = 0;
|
|
168
|
+
capsule.source_refs.forEach((ref, refIndex) => {
|
|
169
|
+
pushText(ref, `${prefix}.source_refs[${refIndex}]`, errors);
|
|
170
|
+
if (typeof ref === 'string' && !sourceMap.has(ref)) {
|
|
171
|
+
errors.push(`${prefix}.source_refs[${refIndex}] 引用不存在的来源: ${ref}`);
|
|
172
|
+
} else if (EXTERNAL_KNOWLEDGE_KINDS.has(sourceMap.get(ref)?.kind)) {
|
|
173
|
+
externalRefCount += 1;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
if (externalRefCount === 0) {
|
|
177
|
+
errors.push(`${prefix} 必须直接引用至少一个 skill|web|official_docs|research 外部知识来源`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
if (strict) {
|
|
182
|
+
for (const nodeId of requiredNodeIds) {
|
|
183
|
+
if (!covered.has(nodeId)) errors.push(`缺少外部必需能力 ${nodeId} 的 Capability Capsule`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return covered;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function validateExpertisePack(versionDir, intentId, pack = null, { requireReady = true } = {}) {
|
|
190
|
+
const requirement = getExpertiseRequirement(versionDir, intentId);
|
|
191
|
+
const path = getExpertisePackPath(versionDir, intentId);
|
|
192
|
+
const data = pack ?? readJsonFile(path, 'Expertise Pack');
|
|
193
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
194
|
+
throw new Error('Expertise Pack 校验失败:\n - 根节点必须是对象');
|
|
195
|
+
}
|
|
196
|
+
const errors = [];
|
|
197
|
+
if (!data._meta || typeof data._meta !== 'object' || Array.isArray(data._meta)) {
|
|
198
|
+
errors.push('_meta 必须是对象');
|
|
199
|
+
} else {
|
|
200
|
+
if (data._meta._version !== '1.0') errors.push('_meta._version 必须是 1.0');
|
|
201
|
+
if (data._meta._loom_version !== basename(versionDir)) {
|
|
202
|
+
errors.push(`_meta._loom_version 必须是 ${basename(versionDir)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (data.intent_id !== intentId) errors.push(`intent_id 必须是 ${intentId}`);
|
|
206
|
+
if (data.intent_revision !== (requirement.intent.revision ?? 1)) {
|
|
207
|
+
errors.push(`intent_revision=${data.intent_revision} 已过期;当前为 ${requirement.intent.revision ?? 1}`);
|
|
208
|
+
}
|
|
209
|
+
if (!Array.isArray(data.required_capability_refs)) {
|
|
210
|
+
errors.push('required_capability_refs 必须是数组');
|
|
211
|
+
} else {
|
|
212
|
+
data.required_capability_refs.forEach((ref, index) => {
|
|
213
|
+
pushText(ref, `required_capability_refs[${index}]`, errors);
|
|
214
|
+
});
|
|
215
|
+
const declared = [...new Set(data.required_capability_refs)].sort();
|
|
216
|
+
const expected = [...new Set(requirement.required_node_ids)].sort();
|
|
217
|
+
if (JSON.stringify(declared) !== JSON.stringify(expected)) {
|
|
218
|
+
errors.push(`required_capability_refs 必须精确匹配当前强门节点: ${expected.join(', ') || '无'}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!VALID_STATUS.includes(data.status)) errors.push(`status 必须是 ${VALID_STATUS.join('|')}`);
|
|
222
|
+
const strict = data.status === 'ready';
|
|
223
|
+
validateSearchPlan(data.search_plan, data.status === 'ready' || data.status === 'blocked', errors);
|
|
224
|
+
const sourceResult = validateSources(data.sources, strict, errors);
|
|
225
|
+
validateCapsules(data.capsules, requirement.required_node_ids, sourceResult.valid, strict, errors);
|
|
226
|
+
if (data.status === 'blocked') {
|
|
227
|
+
if (!data.blocker || typeof data.blocker !== 'object' || Array.isArray(data.blocker)) {
|
|
228
|
+
errors.push('blocked 状态必须提供 blocker 对象');
|
|
229
|
+
} else {
|
|
230
|
+
pushText(data.blocker.reason, 'blocker.reason', errors);
|
|
231
|
+
pushText(data.blocker.recovery_condition, 'blocker.recovery_condition', errors);
|
|
232
|
+
}
|
|
233
|
+
} else if (data.blocker !== null && data.blocker !== undefined) {
|
|
234
|
+
errors.push('非 blocked 状态的 blocker 必须为 null 或省略');
|
|
235
|
+
}
|
|
236
|
+
if (requireReady && requirement.required && data.status !== 'ready') {
|
|
237
|
+
errors.push(`当前 Intent 需要外部能力获取,Expertise Pack 必须为 ready(当前: ${data.status})`);
|
|
238
|
+
}
|
|
239
|
+
if (errors.length) throw new Error(`Expertise Pack 校验失败:\n - ${errors.join('\n - ')}`);
|
|
240
|
+
return {
|
|
241
|
+
valid: true,
|
|
242
|
+
required: requirement.required,
|
|
243
|
+
intent_id: intentId,
|
|
244
|
+
intent_revision: data.intent_revision,
|
|
245
|
+
status: data.status,
|
|
246
|
+
required_node_ids: requirement.required_node_ids,
|
|
247
|
+
source_count: data.sources.length,
|
|
248
|
+
external_source_count: sourceResult.externalCount,
|
|
249
|
+
capsule_count: data.capsules.length,
|
|
250
|
+
pack_digest: createHash('sha256').update(JSON.stringify(data)).digest('hex'),
|
|
251
|
+
path,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function getExpertisePack(versionDir, intentId) {
|
|
256
|
+
const path = getExpertisePackPath(versionDir, intentId);
|
|
257
|
+
if (!existsSync(path)) throw new Error(`Expertise Pack 不存在: ${path}`);
|
|
258
|
+
const pack = readJsonFile(path, 'Expertise Pack');
|
|
259
|
+
validateExpertisePack(versionDir, intentId, pack, { requireReady: false });
|
|
260
|
+
return pack;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function initExpertisePack(versionDir, intentId) {
|
|
264
|
+
const requirement = getExpertiseRequirement(versionDir, intentId);
|
|
265
|
+
if (!requirement.required) {
|
|
266
|
+
throw new Error(`${intentId} 当前没有外部能力获取强门;只有显式 external_required 或未显式豁免的高影响 capability 才需要持久化 Expertise Pack`);
|
|
267
|
+
}
|
|
268
|
+
const path = getExpertisePackPath(versionDir, intentId);
|
|
269
|
+
if (existsSync(path)) throw new Error(`Expertise Pack 已存在,不会覆盖: ${path}`);
|
|
270
|
+
const templatePath = join(getLoomRoot(), 'templates', 'EXPERTISE_PACK_TEMPLATE.json');
|
|
271
|
+
const pack = JSON.parse(readFileSync(templatePath, 'utf-8'));
|
|
272
|
+
pack._meta._loom_version = basename(versionDir);
|
|
273
|
+
pack.intent_id = intentId;
|
|
274
|
+
pack.intent_revision = requirement.intent.revision ?? 1;
|
|
275
|
+
pack.required_capability_refs = requirement.required_node_ids;
|
|
276
|
+
mkdirSync(getExpertisePacksDir(versionDir), { recursive: true });
|
|
277
|
+
writeFileSync(path, `${JSON.stringify(pack, null, 2)}\n`, { encoding: 'utf-8', flag: 'wx' });
|
|
278
|
+
return pack;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function getExpertisePackState(versionDir, intentId) {
|
|
282
|
+
const requirement = getExpertiseRequirement(versionDir, intentId);
|
|
283
|
+
const path = getExpertisePackPath(versionDir, intentId);
|
|
284
|
+
if (!requirement.required) {
|
|
285
|
+
return { required: false, ready: true, path, required_node_ids: [] };
|
|
286
|
+
}
|
|
287
|
+
if (!existsSync(path)) {
|
|
288
|
+
return { required: true, ready: false, path, required_node_ids: requirement.required_node_ids, reason: 'missing' };
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const validation = validateExpertisePack(versionDir, intentId, null, { requireReady: false });
|
|
292
|
+
const ready = validation.status === 'ready';
|
|
293
|
+
const pack = ready ? null : readJsonFile(path, 'Expertise Pack');
|
|
294
|
+
const reason = ready
|
|
295
|
+
? undefined
|
|
296
|
+
: validation.status === 'blocked'
|
|
297
|
+
? `blocked: ${pack.blocker.reason}`
|
|
298
|
+
: `status=${validation.status}`;
|
|
299
|
+
return { required: true, ready, path, required_node_ids: requirement.required_node_ids, validation, reason };
|
|
300
|
+
} catch (error) {
|
|
301
|
+
return { required: true, ready: false, path, required_node_ids: requirement.required_node_ids, reason: error.message };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function assertExpertiseReady(versionDir, intentId) {
|
|
306
|
+
const state = getExpertisePackState(versionDir, intentId);
|
|
307
|
+
if (!state.required) return null;
|
|
308
|
+
if (!state.ready) {
|
|
309
|
+
const action = state.reason === 'missing'
|
|
310
|
+
? `先运行 loom expertise init ${intentId},通过 find skill 与网络搜索获取外部信息,完成后运行 loom expertise validate ${intentId}`
|
|
311
|
+
: `修正 10_EXPERTISE_PACKS/${intentId}.json 后运行 loom expertise validate ${intentId}`;
|
|
312
|
+
throw new Error(`${intentId} 的外部能力获取强门尚未闭合: ${state.reason || 'Expertise Pack not ready'}\n${action}`);
|
|
313
|
+
}
|
|
314
|
+
return state.validation;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function formatExpertisePackForPrompt(pack) {
|
|
318
|
+
const lines = [
|
|
319
|
+
`- decision_question: ${pack.search_plan.decision_question}`,
|
|
320
|
+
`- sources: ${pack.sources.map((source) => `${source.id} ${source.title} (${source.locator})`).join('; ')}`,
|
|
321
|
+
];
|
|
322
|
+
for (const capsule of pack.capsules) {
|
|
323
|
+
lines.push(
|
|
324
|
+
`\n### Capability Capsule: ${capsule.capability_ref}`,
|
|
325
|
+
`- professional_problem: ${capsule.professional_problem}`,
|
|
326
|
+
`- when_to_use: ${capsule.when_to_use}`,
|
|
327
|
+
`- rules:\n${capsule.rules.map((item) => ` - ${item}`).join('\n')}`,
|
|
328
|
+
`- workflow:\n${capsule.workflow.map((item) => ` - ${item}`).join('\n')}`,
|
|
329
|
+
`- decision_gates:\n${capsule.decision_gates.map((item) => ` - ${item}`).join('\n')}`,
|
|
330
|
+
`- failure_modes:\n${capsule.failure_modes.map((item) => ` - ${item}`).join('\n')}`,
|
|
331
|
+
`- verification_signals:\n${capsule.verification_signals.map((item) => ` - ${item}`).join('\n')}`,
|
|
332
|
+
`- source_refs: ${capsule.source_refs.join(', ')}`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
return lines.join('\n');
|
|
336
|
+
}
|