@haaaiawd/loom 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/EXTERNAL_ACQUISITION_DESIGN.md +143 -0
- package/README.md +5 -2
- package/cli/bin/loom.js +39 -1
- package/cli/help/capability.md +5 -0
- package/cli/help/concepts.md +4 -1
- package/cli/help/doctor.md +3 -1
- package/cli/help/expertise.md +51 -0
- package/cli/help/loop.md +16 -2
- package/cli/help/workflow.md +10 -0
- package/cli/src/activate.js +37 -1
- package/cli/src/capability-graph.js +81 -2
- package/cli/src/diagnostics.js +33 -0
- package/cli/src/expertise-pack.js +336 -0
- package/cli/src/guide.js +35 -1
- package/cli/src/init.js +3 -1
- package/cli/src/verify.js +32 -3
- package/meta/INTENT_LOOP.md +7 -0
- package/meta/ROLE_ACTIVATION.md +5 -0
- package/package.json +3 -2
- package/roles/architect.md +4 -2
- package/roles/forge.md +9 -2
- package/roles/keeper.md +4 -0
- package/templates/CAPABILITY_BRIEF_TEMPLATE.md +3 -2
- package/templates/CAPABILITY_GRAPH_TEMPLATE.json +1 -1
- package/templates/EXPERTISE_PACK_TEMPLATE.json +22 -0
- package/templates/INTENT_MAP_TEMPLATE.json +2 -1
|
@@ -11,6 +11,7 @@ const NODE_KINDS = ['outcome', 'concern', 'capability', 'risk', 'evidence'];
|
|
|
11
11
|
const NODE_STATUSES = ['open', 'researched', 'covered', 'deferred', 'out_of_scope'];
|
|
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
|
+
const ACQUISITION_MODES = ['adaptive', 'external_required', 'project_only'];
|
|
14
15
|
const VERIFICATION_FIELDS = ['method', 'target', 'procedure', 'pass_criteria', 'artifact'];
|
|
15
16
|
const EVIDENCE_ARTIFACT_DIRS = ['verifications', '08_ASSET_LIBRARY/files'];
|
|
16
17
|
|
|
@@ -66,6 +67,21 @@ function validateNode(id, node, allIds, errors) {
|
|
|
66
67
|
if (node.asset_refs !== undefined && node.kind !== 'evidence') {
|
|
67
68
|
errors.push(`nodes["${id}"].asset_refs 只允许写在 evidence 节点`);
|
|
68
69
|
}
|
|
70
|
+
if (node.acquisition_mode !== undefined) {
|
|
71
|
+
if (node.kind !== 'capability') {
|
|
72
|
+
errors.push(`nodes["${id}"].acquisition_mode 只允许写在 capability 节点`);
|
|
73
|
+
} else if (!ACQUISITION_MODES.includes(node.acquisition_mode)) {
|
|
74
|
+
errors.push(`nodes["${id}"].acquisition_mode 非法: ${node.acquisition_mode}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (node.acquisition_mode === 'project_only'
|
|
78
|
+
&& (typeof node.acquisition_rationale !== 'string' || !node.acquisition_rationale.trim())) {
|
|
79
|
+
errors.push(`nodes["${id}"].acquisition_mode=project_only 必须声明 acquisition_rationale`);
|
|
80
|
+
}
|
|
81
|
+
if (node.impact === 'high' && node.acquisition_mode === 'adaptive'
|
|
82
|
+
&& (typeof node.acquisition_rationale !== 'string' || !node.acquisition_rationale.trim())) {
|
|
83
|
+
errors.push(`nodes["${id}"] 为高影响 capability 且选择 adaptive 时必须声明 acquisition_rationale;说明为何此处不启用 external_required`);
|
|
84
|
+
}
|
|
69
85
|
if (node.verification !== undefined) {
|
|
70
86
|
if (node.kind !== 'evidence') {
|
|
71
87
|
errors.push(`nodes["${id}"].verification 只允许写在 evidence 节点`);
|
|
@@ -131,6 +147,13 @@ export function loadCapabilityGraph(versionDir, { required = true } = {}) {
|
|
|
131
147
|
return validateCapabilityGraph(readJsonFile(filePath, 'Capability Graph'));
|
|
132
148
|
}
|
|
133
149
|
|
|
150
|
+
function getEffectiveAcquisitionMode(node) {
|
|
151
|
+
if (node.kind !== 'capability') return null;
|
|
152
|
+
if (node.acquisition_mode) return node.acquisition_mode;
|
|
153
|
+
if (node.impact === 'high') return 'external_required';
|
|
154
|
+
return 'adaptive';
|
|
155
|
+
}
|
|
156
|
+
|
|
134
157
|
function isRouted(node) {
|
|
135
158
|
return ['brief', 'intent', 'defer', 'exclude', 'covered_by'].includes(node.route);
|
|
136
159
|
}
|
|
@@ -248,6 +271,7 @@ export function getCapabilityCoverage(versionDir) {
|
|
|
248
271
|
const capabilitiesWithoutPlan = [];
|
|
249
272
|
const routingGaps = [];
|
|
250
273
|
const outcomesWithoutConcern = [];
|
|
274
|
+
const evidenceArtifactGaps = [];
|
|
251
275
|
|
|
252
276
|
for (const node of nodes) {
|
|
253
277
|
for (const intentId of node.intent_refs || []) {
|
|
@@ -261,6 +285,19 @@ export function getCapabilityCoverage(versionDir) {
|
|
|
261
285
|
if (node.route === 'brief' && !node.brief_ref) {
|
|
262
286
|
capabilitiesWithoutPlan.push({ node_id: node.id, reason: 'route=brief 但缺少 brief_ref' });
|
|
263
287
|
}
|
|
288
|
+
if (node.kind === 'capability') {
|
|
289
|
+
const linkedIntents = (node.intent_refs || [])
|
|
290
|
+
.map((intentId) => intentMap.intents[intentId])
|
|
291
|
+
.filter(Boolean);
|
|
292
|
+
const needsExternalAcquisition = linkedIntents.length > 0
|
|
293
|
+
&& getEffectiveAcquisitionMode(node) === 'external_required';
|
|
294
|
+
if (needsExternalAcquisition && !node.brief_ref && node.route !== 'brief') {
|
|
295
|
+
capabilitiesWithoutPlan.push({
|
|
296
|
+
node_id: node.id,
|
|
297
|
+
reason: '外部能力获取为 required,但缺少 brief_ref 来定义专业问题与验收边界',
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
264
301
|
if (['defer', 'exclude'].includes(node.route) && (!node.rationale || typeof node.rationale !== 'string' || node.rationale.trim() === '')) {
|
|
265
302
|
routingGaps.push({ node_id: node.id, reason: `route=${node.route} 但缺少 rationale` });
|
|
266
303
|
}
|
|
@@ -273,6 +310,12 @@ export function getCapabilityCoverage(versionDir) {
|
|
|
273
310
|
if (node.kind === 'outcome' && !(node.relationships || []).some((relation) => graph.nodes[relation.target]?.kind === 'concern')) {
|
|
274
311
|
outcomesWithoutConcern.push({ node_id: node.id, reason: 'outcome 没有连接到 concern,项目初衷尚未展开为问题面' });
|
|
275
312
|
}
|
|
313
|
+
if (node.kind === 'evidence' && node.verification?.artifact) {
|
|
314
|
+
const ownedCompletedIntent = (node.intent_refs || []).some((intentId) => intentMap.intents[intentId]?.status === 'completed');
|
|
315
|
+
if (ownedCompletedIntent && !resolveEvidenceArtifact(versionDir, node.verification.artifact)) {
|
|
316
|
+
evidenceArtifactGaps.push({ node_id: node.id, reason: `已完成 Intent 的 evidence artifact 不存在或不可读: ${node.verification.artifact}` });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
276
319
|
if (node.brief_ref) {
|
|
277
320
|
try { resolveBrief(versionDir, node.brief_ref); } catch (error) {
|
|
278
321
|
capabilitiesWithoutPlan.push({ node_id: node.id, reason: error.message });
|
|
@@ -294,6 +337,7 @@ export function getCapabilityCoverage(versionDir) {
|
|
|
294
337
|
routing_gaps: routingGaps.length,
|
|
295
338
|
outcomes_without_concern: outcomesWithoutConcern.length,
|
|
296
339
|
high_outcomes_without_observable_evidence: highOutcomesWithoutObservableEvidence.length,
|
|
340
|
+
evidence_artifact_gaps: evidenceArtifactGaps.length,
|
|
297
341
|
intent_mapping_required: intentMappingRequired,
|
|
298
342
|
unmapped_intents: unmappedIntents.length,
|
|
299
343
|
ready: highUnrouted.length === 0
|
|
@@ -302,6 +346,7 @@ export function getCapabilityCoverage(versionDir) {
|
|
|
302
346
|
&& routingGaps.length === 0
|
|
303
347
|
&& outcomesWithoutConcern.length === 0
|
|
304
348
|
&& highOutcomesWithoutObservableEvidence.length === 0
|
|
349
|
+
&& evidenceArtifactGaps.length === 0
|
|
305
350
|
&& unmappedIntents.length === 0,
|
|
306
351
|
},
|
|
307
352
|
high_unrouted: highUnrouted,
|
|
@@ -310,6 +355,7 @@ export function getCapabilityCoverage(versionDir) {
|
|
|
310
355
|
routing_gaps: routingGaps,
|
|
311
356
|
outcomes_without_concern: outcomesWithoutConcern,
|
|
312
357
|
high_outcomes_without_observable_evidence: highOutcomesWithoutObservableEvidence,
|
|
358
|
+
evidence_artifact_gaps: evidenceArtifactGaps,
|
|
313
359
|
unmapped_intents: unmappedIntents,
|
|
314
360
|
};
|
|
315
361
|
}
|
|
@@ -339,7 +385,15 @@ function collectCompilationNodes(graph, intentId) {
|
|
|
339
385
|
|
|
340
386
|
export function compileCapabilityInputs(versionDir, intentId) {
|
|
341
387
|
const graph = loadCapabilityGraph(versionDir, { required: false });
|
|
342
|
-
if (!graph)
|
|
388
|
+
if (!graph) {
|
|
389
|
+
return {
|
|
390
|
+
available: false,
|
|
391
|
+
nodes: [],
|
|
392
|
+
briefs: [],
|
|
393
|
+
warnings: ['项目尚未建立 Capability Graph;使用 Intent 的 capability_needs 兼容路径。'],
|
|
394
|
+
acquisition: { required: false, required_node_ids: [], nodes: [] },
|
|
395
|
+
};
|
|
396
|
+
}
|
|
343
397
|
const nodes = collectCompilationNodes(graph, intentId);
|
|
344
398
|
const briefs = [];
|
|
345
399
|
const warnings = [];
|
|
@@ -347,5 +401,30 @@ export function compileCapabilityInputs(versionDir, intentId) {
|
|
|
347
401
|
if (!node.brief_ref) continue;
|
|
348
402
|
try { briefs.push({ node_id: node.id, ...resolveBrief(versionDir, node.brief_ref) }); } catch (error) { warnings.push(`${node.id}: ${error.message}`); }
|
|
349
403
|
}
|
|
350
|
-
|
|
404
|
+
const acquisitionNodes = nodes
|
|
405
|
+
.filter((node) => node.kind === 'capability')
|
|
406
|
+
.map((node) => ({
|
|
407
|
+
node_id: node.id,
|
|
408
|
+
title: node.title,
|
|
409
|
+
mode: getEffectiveAcquisitionMode(node),
|
|
410
|
+
reason: node.acquisition_mode
|
|
411
|
+
? 'Capability Graph 显式声明'
|
|
412
|
+
: node.impact === 'high'
|
|
413
|
+
? '高影响能力默认需要外部来源化'
|
|
414
|
+
: '按任务证据自适应判断',
|
|
415
|
+
}));
|
|
416
|
+
const requiredNodeIds = acquisitionNodes
|
|
417
|
+
.filter((node) => node.mode === 'external_required')
|
|
418
|
+
.map((node) => node.node_id);
|
|
419
|
+
return {
|
|
420
|
+
available: true,
|
|
421
|
+
nodes,
|
|
422
|
+
briefs,
|
|
423
|
+
warnings,
|
|
424
|
+
acquisition: {
|
|
425
|
+
required: requiredNodeIds.length > 0,
|
|
426
|
+
required_node_ids: requiredNodeIds,
|
|
427
|
+
nodes: acquisitionNodes,
|
|
428
|
+
},
|
|
429
|
+
};
|
|
351
430
|
}
|
package/cli/src/diagnostics.js
CHANGED
|
@@ -15,6 +15,7 @@ import { getCapabilityCoverage, getCapabilityGraphPath, loadCapabilityGraph } fr
|
|
|
15
15
|
import { listCapabilityProposals } from './capability-proposals.js';
|
|
16
16
|
import { getAssetManifestPath, validateAssetLibrary } from './asset-library.js';
|
|
17
17
|
import { validateAtelierRecord } from './atelier.js';
|
|
18
|
+
import { getExpertisePackState } from './expertise-pack.js';
|
|
18
19
|
|
|
19
20
|
function readIntentMapRaw(versionDir) {
|
|
20
21
|
const filePath = join(versionDir, '04_INTENT_MAP.json');
|
|
@@ -178,6 +179,8 @@ const FIX_HINTS = {
|
|
|
178
179
|
asset_library_invalid: '修复 08_ASSET_LIBRARY/manifest.json 的来源、许可、哈希、库内路径或 evidence 双向引用,然后运行 loom asset validate。',
|
|
179
180
|
atelier_record_invalid: '运行 loom atelier init {id} 创建记录,或按校验错误修正后运行 loom atelier validate {id}。',
|
|
180
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。',
|
|
181
184
|
};
|
|
182
185
|
|
|
183
186
|
/**
|
|
@@ -271,6 +274,9 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
271
274
|
for (const item of coverage.high_outcomes_without_observable_evidence) {
|
|
272
275
|
issues.push({ id: item.node_id, type: 'capability_outcome_unobservable', severity: 'high', msg: `${item.node_id} 缺少真实呈现或交付的验证入口: ${item.reason}` });
|
|
273
276
|
}
|
|
277
|
+
for (const item of coverage.evidence_artifact_gaps || []) {
|
|
278
|
+
issues.push({ id: item.node_id, type: 'capability_evidence_artifact_missing', severity: 'high', msg: `${item.node_id} 的完成证据不可用: ${item.reason}` });
|
|
279
|
+
}
|
|
274
280
|
for (const item of coverage.orphan_intent_refs) {
|
|
275
281
|
issues.push({ id: item.node_id, type: 'intent_graph_unmapped', severity: 'high', msg: `${item.node_id} 引用了不存在的 Intent: ${item.intent_id}` });
|
|
276
282
|
}
|
|
@@ -314,6 +320,33 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
|
|
|
314
320
|
}
|
|
315
321
|
}
|
|
316
322
|
}
|
|
323
|
+
if (!['pending', 'blocked', 'deprecated'].includes(intent.status)) {
|
|
324
|
+
try {
|
|
325
|
+
const expertise = getExpertisePackState(versionDir, id);
|
|
326
|
+
if (expertise.required && !expertise.ready) {
|
|
327
|
+
issues.push({
|
|
328
|
+
id,
|
|
329
|
+
type: 'expertise_pack_invalid',
|
|
330
|
+
severity: intent.status === 'completed' || latest?.verdict === 'passed' ? 'high' : 'medium',
|
|
331
|
+
msg: `${id} 的外部能力获取强门尚未闭合: ${expertise.reason || 'Expertise Pack not ready'}`,
|
|
332
|
+
});
|
|
333
|
+
} else if (expertise.required && latest?.verdict === 'passed') {
|
|
334
|
+
const validation = expertise.validation;
|
|
335
|
+
if (latest.expertise?.record_ref !== `10_EXPERTISE_PACKS/${id}.json`
|
|
336
|
+
|| latest.expertise?.intent_revision !== validation.intent_revision
|
|
337
|
+
|| latest.expertise?.source_count !== validation.source_count
|
|
338
|
+
|| latest.expertise?.capsule_count !== validation.capsule_count
|
|
339
|
+
|| latest.expertise?.pack_digest !== validation.pack_digest) {
|
|
340
|
+
issues.push({
|
|
341
|
+
id,
|
|
342
|
+
type: 'expertise_verification_missing',
|
|
343
|
+
severity: 'high',
|
|
344
|
+
msg: `${id} 的最新 passed 未绑定当前 Expertise Pack、Intent revision 与来源/Capsule 计数`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} catch { /* Capability Graph 的结构错误已由上方统一报告。 */ }
|
|
349
|
+
}
|
|
317
350
|
if (intent.quality_strategy === 'atelier' && latest?.verdict === 'passed') {
|
|
318
351
|
const atelier = atelierRecords.get(id);
|
|
319
352
|
if (!atelier
|
|
@@ -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
|
+
}
|
package/cli/src/guide.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getCapabilityCoverage } from './capability-graph.js';
|
|
|
10
10
|
import { listCapabilityProposals } from './capability-proposals.js';
|
|
11
11
|
import { isAutoOn, getAutoMode, writeHeartbeat, needsHumanReview } from './auto.js';
|
|
12
12
|
import { doctor } from './diagnostics.js';
|
|
13
|
+
import { getExpertisePackState } from './expertise-pack.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* 检测文件是否还是模板(未填充真实内容)。
|
|
@@ -145,7 +146,7 @@ export function guideProject(projectDir, options = {}) {
|
|
|
145
146
|
}
|
|
146
147
|
} else if (result.stage_num >= 4) {
|
|
147
148
|
if (auto) {
|
|
148
|
-
result.message += '\n\n> AUTO
|
|
149
|
+
result.message += '\n\n> AUTO 模式开启——可继续进入下一阶段;契约、证据与 Keeper 门禁仍不会被跳过。';
|
|
149
150
|
} else {
|
|
150
151
|
result.message += '\n\n> ⚠ AUTO 模式关闭——执行 next_command 后等人类确认再继续。';
|
|
151
152
|
}
|
|
@@ -373,6 +374,39 @@ function diagnoseStage(cwd, loomRoot, auto) {
|
|
|
373
374
|
// 状态 5: 有 in_progress
|
|
374
375
|
if (counts.in_progress > 0) {
|
|
375
376
|
const inProgressIds = allIntents.filter((i) => i.status === 'in_progress').map((i) => i.id);
|
|
377
|
+
const expertiseOpen = allIntents
|
|
378
|
+
.filter((intent) => intent.status === 'in_progress')
|
|
379
|
+
.map((intent) => ({ intent, state: getExpertisePackState(versionDir, intent.id) }))
|
|
380
|
+
.find(({ state }) => state.required && !state.ready);
|
|
381
|
+
if (expertiseOpen) {
|
|
382
|
+
const missing = expertiseOpen.state.reason === 'missing';
|
|
383
|
+
const blocked = expertiseOpen.state.reason?.startsWith('blocked:');
|
|
384
|
+
return {
|
|
385
|
+
stage: 'in_loop',
|
|
386
|
+
stage_num: 5,
|
|
387
|
+
details: {
|
|
388
|
+
version: current,
|
|
389
|
+
counts,
|
|
390
|
+
in_progress_ids: inProgressIds,
|
|
391
|
+
expertise_intent: expertiseOpen.intent.id,
|
|
392
|
+
expertise_reason: expertiseOpen.state.reason,
|
|
393
|
+
},
|
|
394
|
+
auto,
|
|
395
|
+
next_action: missing
|
|
396
|
+
? '创建搜索计划并执行外部能力获取'
|
|
397
|
+
: blocked
|
|
398
|
+
? '解决 Expertise Pack 记录的外部获取阻塞'
|
|
399
|
+
: '补齐来源化 Expertise Pack',
|
|
400
|
+
next_command: missing
|
|
401
|
+
? `loom expertise init ${expertiseOpen.intent.id}`
|
|
402
|
+
: blocked
|
|
403
|
+
? `loom expertise get ${expertiseOpen.intent.id}`
|
|
404
|
+
: `loom expertise validate ${expertiseOpen.intent.id}`,
|
|
405
|
+
message: blocked
|
|
406
|
+
? `${expertiseOpen.intent.id} 的外部能力获取已明确 blocked:${expertiseOpen.state.reason.slice('blocked: '.length)}。满足 Pack 中的 recovery_condition 后再继续;不能让模型补齐空白。`
|
|
407
|
+
: `${expertiseOpen.intent.id} 需要外部能力获取。先由任务信号派生搜索词,实际使用 find skill、网络搜索、官方文档或研究资料,再把可回查来源编译为 Capability Capsules;模型临时生成内容不能代替来源。`,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
376
410
|
const atelierWithoutRecord = allIntents.find((intent) => intent.status === 'in_progress'
|
|
377
411
|
&& intent.quality_strategy === 'atelier'
|
|
378
412
|
&& !existsSync(join(versionDir, '09_ATELIER', `${intent.id}.json`)));
|
package/cli/src/init.js
CHANGED
|
@@ -51,7 +51,8 @@ export function createVersionStructure(projectDir, version, parentVersion = null
|
|
|
51
51
|
`.loom/${v}/07_CAPABILITY_BRIEFS`,
|
|
52
52
|
`.loom/${v}/07_GRAPH_PROPOSALS`,
|
|
53
53
|
`.loom/${v}/08_ASSET_LIBRARY/files`,
|
|
54
|
-
|
|
54
|
+
`.loom/${v}/10_EXPERTISE_PACKS`,
|
|
55
|
+
];
|
|
55
56
|
|
|
56
57
|
for (const d of dirs) {
|
|
57
58
|
const path = join(cwd, d);
|
|
@@ -168,6 +169,7 @@ export function initProject(projectDir) {
|
|
|
168
169
|
'3. 只在当前角色的权限内行动;发现目标、契约或架构需要改变时,按 LOOM 回流,不要静默扩展范围。',
|
|
169
170
|
'4. 完成前运行当前 Intent 的验证方法与 `loom doctor`;声称质量提升时必须提供基线相对 Quality Proof,以磁盘证据而非会话记忆判断状态。',
|
|
170
171
|
'5. Keeper 验证必须运行在新的 Agent thread 中;同一会话切换角色不构成独立验证。',
|
|
172
|
+
'6. 协作节奏默认是手动;需要让 Agent 在已允许阶段连续推进时,显式运行 `loom auto on`。AUTO 不会跳过契约、证据或 Keeper 门禁。',
|
|
171
173
|
'',
|
|
172
174
|
'常用入口:',
|
|
173
175
|
'- `loom --help`',
|