@haaaiawd/loom 1.1.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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0 — 2026-07-31
4
+
5
+ ### Added
6
+
7
+ - External Acquisition Gate:Capability Graph 只声明 `acquisition_mode`,Forge 按当前
8
+ Intent 信号派生 Search Plan,并实际通过 Skill registry、网络、官方文档或研究资料获取信息。
9
+ - revision-scoped Expertise Pack 与 `loom expertise init|get|validate`,以来源、检索证据和
10
+ Capability Capsules 形成类似 Skill 的项目化核心信息组,但不复制或内置第三方内容。
11
+ - `guide`、Forge/Keeper activation、`verify pass`、`intent done` 与 `doctor` 的外部能力
12
+ 获取强门;passed 记录绑定当前 Pack 内容摘要,Keeper 必须独立重开关键来源。
13
+
14
+ ### Compatibility
15
+
16
+ - 中低影响 capability 默认为 `adaptive`;高影响 capability 未显式豁免时,以及任意 capability
17
+ 显式声明 `external_required` 时启用持久化强门。
18
+ - `project_only` 保留内部协议与机械任务路径,但必须说明 `acquisition_rationale`。
19
+
3
20
  ## 1.1.0 — 2026-07-30
4
21
 
5
22
  ### Added
@@ -0,0 +1,143 @@
1
+ # External Acquisition Gate — 来源化能力网络设计
2
+
3
+ > 状态:Implemented
4
+ > 目标:阻止 AI 在专业能力不足时闭门生成,并把真实外部检索编译成当前 Intent 可使用、可追溯、会随 revision 失效的核心信息组。
5
+
6
+ ## 1. 问题
7
+
8
+ LOOM 已经能识别任务需要哪些能力,但“识别能力”不等于“获得能力”。如果 Forge 只根据模型已有知识临时生成一段设计原则,它仍可能做出结构正确、审美普通的结果,并把自洽误当成专业。
9
+
10
+ 本设计把能力链拆成四个不同事实:
11
+
12
+ 1. Capability Graph:项目为什么需要某项能力。
13
+ 2. Search Plan:本轮要用什么问题和关键词去外部寻找。
14
+ 3. Expertise Pack:实际打开来源后,哪些规则、工作流、判断门和失败模式可用于本 Intent。
15
+ 4. Quality Proof:这些判断是否真的让结果胜过基线。
16
+
17
+ Graph 不保存网站、Skill 名称或固定关键词;Pack 不复制第三方 Skill 或网页正文。
18
+
19
+ ## 2. 核心不变量
20
+
21
+ - AI 可以生成检索计划,不能把自己生成的内容登记为外部专业知识。
22
+ - `external_required` 能力必须实际使用 Skill registry、网络、官方文档或研究资料。
23
+ - 搜索结果标题、未打开的摘要、模型记忆和“行业通常如此”都不是来源。
24
+ - 每个 Capability Capsule 必须直接引用至少一个已打开、可回查的外部来源。
25
+ - Pack 绑定 `intent_revision`;Intent 修订后旧 Pack 自动失效。
26
+ - Forge 可以使用 Pack;Keeper 不继承 Forge 的结论,必须重新打开关键来源。
27
+ - 来源化不等于正确。CLI 证明结构与追溯,Keeper 证明来源是否真的支持判断。
28
+
29
+ ## 3. 触发模型
30
+
31
+ Capability 节点可选:
32
+
33
+ ```json
34
+ {
35
+ "acquisition_mode": "adaptive | external_required | project_only",
36
+ "acquisition_rationale": "project_only 时必填"
37
+ }
38
+ ```
39
+
40
+ 有效模式按以下顺序计算:
41
+
42
+ 1. Graph 显式声明的模式优先。
43
+ 2. 未声明时,高影响 capability 自动提升为 `external_required`。
44
+ 3. 中低影响 capability 默认为 `adaptive`。
45
+
46
+ `project_only` 只适合未公开内部协议、纯机械迁移等外部知识不会改变做法的任务,必须说明理由。外部交付依赖与只读研究访问是两件事:允许检索资料,不等于允许把外部服务写进产品架构。
47
+
48
+ ## 4. 运行流程
49
+
50
+ ```text
51
+ Capability Graph
52
+ → compile acquisition requirement
53
+ → derive Search Plan from project signals
54
+ → perform real external retrieval
55
+ → synthesize Capability Capsules
56
+ → validate Expertise Pack
57
+ → Author / Atelier / implementation
58
+ → independent Keeper source check
59
+ → passed record binds current Pack
60
+ ```
61
+
62
+ 搜索词由 Forge 根据以下信号动态派生:
63
+
64
+ - capability question 与 Brief;
65
+ - Intent narrative、quality contract 和 creative scope;
66
+ - 当前媒介、宿主、技术与许可约束;
67
+ - 基线中已经观察到的具体缺口;
68
+ - Authorial Stance 尚未解决的机制问题。
69
+
70
+ 关键词属于本轮工作证据,不回写 Graph。这样 LOOM 不会把“网页设计等于某几个灵感站”固化成框架偏见。
71
+
72
+ ## 5. Expertise Pack
73
+
74
+ 位置:
75
+
76
+ ```text
77
+ .loom/vN/10_EXPERTISE_PACKS/<intent-id>.json
78
+ ```
79
+
80
+ Pack 包含:
81
+
82
+ - `search_plan`:决策问题、项目信号、派生查询、约束和停止条件;
83
+ - `sources`:类型、权威层级、HTTPS 定位、获取时间、选择理由和已打开证据;
84
+ - `capsules`:专业问题、适用时机、规则、工作流、判断门、失败模式、验证信号和来源引用;
85
+ - `required_capability_refs` 与 `intent_revision`;
86
+ - `status: draft | ready | blocked`。
87
+
88
+ Capability Capsule 是“类似 Skill 的核心信息组”,但它有三个刻意限制:
89
+
90
+ 1. 只服务当前 Intent,不假装是通用知识;
91
+ 2. 只保存项目化综合,不复制来源内容;
92
+ 3. 未经多个真实任务和 Quality Proof 支持,不自动晋升为 Skill 或 Doctrine。
93
+
94
+ ## 6. Author 与自我更正
95
+
96
+ 外部获取发生在 Author/Atelier 之前。Authorial Stance 必须建立在已经来源化的机制和项目事实之上,而不是用人格、风格词或模型偏好代替研究。
97
+
98
+ 自我更正分三层:
99
+
100
+ - Capsule 解释错误、来源不支持规则:修正 Pack,重新验证;如果已 passed,必须由 Keeper 重验。
101
+ - 当前构图、措辞、动效或候选选择失效:写入 Atelier `corrections[]`,递增 `stance_revision`。
102
+ - 出现新的用户结果、约束、风险或能力缺口:提交 Capability Graph proposal,由 Architect 裁决。
103
+
104
+ Author 不能修改 Graph 后继续自证,也不能用一次局部修正把任务长期知识化。
105
+
106
+ ## 7. 强门与验证
107
+
108
+ 以下入口共同阻止绕过:
109
+
110
+ - `loom guide`:优先引导创建或补齐 Pack;
111
+ - `loom activate forge --intent <id>`:Pack 未 ready 时显示 OPEN 门,只允许勘察、基线和检索;
112
+ - `loom expertise validate <id>`:检查 revision、真实外部来源、直接引用和 Capsule 完整性;
113
+ - `loom verify pass`:强门未闭合时拒绝 passed,并把当前 Pack 的内容摘要绑定到验证记录;
114
+ - `loom intent done`:拒绝未绑定、内容摘要变化或已经过期的 Pack;
115
+ - `loom doctor`:报告缺失、失效和 passed 绑定漂移;
116
+ - Keeper Context Pack:只给出证据入口,不注入 Forge 的 Capsule 结论。
117
+
118
+ ## 8. 搜索预算与停止条件
119
+
120
+ LOOM 不规定固定网站数量,也不鼓励无限浏览。每个 Search Plan 必须写停止条件。通常在以下条件同时成立时停止:
121
+
122
+ - 每个 required capability 都有直接来源化 Capsule;
123
+ - 来源足以回答当前决策问题,而不只是提供视觉相似物;
124
+ - 已能写出可执行的判断门、失败模式和验证信号;
125
+ - 新来源只重复已有机制,不再改变候选或验证方法。
126
+
127
+ 如果来源互相冲突、关键资料不可访问或许可不清,Pack 应进入 `blocked`,而不是让模型补齐空白。
128
+
129
+ ## 9. 与现有系统的去重
130
+
131
+ - Capability Brief 负责“项目问题与获取边界”;Pack 负责“本轮实际获得了什么”。二者不重复。
132
+ - Authorship 负责“基于材料做什么独特选择”;Pack 负责“选择前有哪些可信材料”。二者不重复。
133
+ - Atelier Record 保存候选与修正;Pack 保存外部能力证据。二者不合并。
134
+ - Quality Proof 证明结果提升;Pack 只证明专业判断有来源。来源不能替代结果。
135
+ - Doctrine 保存长期项目判断;Pack 默认短命。未经重复外部结果支持,不进入 Doctrine。
136
+
137
+ ## 10. 非目标
138
+
139
+ - 不在 LOOM 仓库内安装、镜像或维护第三方 Skill。
140
+ - 不建立固定网站白名单或 UI 专用关键词表。
141
+ - 不声称只要检索就能突破基础模型的所有智能边界。
142
+ - 不用来源数量、星标或流行度替代任务适配性。
143
+ - 不让 CLI 自动编造 Capsule;CLI 只建模、校验、编译和阻断。
package/README.md CHANGED
@@ -29,7 +29,7 @@ LOOM 的核心机制:
29
29
  | **Intent** | 一个意图单元——不是"做什么"(任务),是"为什么做"(意图)。每个 Intent 有验收契约,Keeper 据此判定实现是否忠实 |
30
30
  | **Intent Map** | 所有 Intent 的依赖图(JSON)。Architect 绘制,定义拓扑序和依赖关系 |
31
31
  | **Capability Graph** | 在 Intent 前展开项目问题面、能力缺口、风险和证据;高影响节点必须路由,并回链到 Intent |
32
- | **Expertise Pack** | Forge 针对当前任务临时编译的专业认知,不成为永久规则 |
32
+ | **Expertise Pack** | Forge 通过真实外部检索为当前 Intent 编译的来源化核心信息组;绑定 revision,不成为永久规则 |
33
33
  | **Authorial Stance** | 针对单个 Intent 编译的创作命题、选择、拒绝项、媒介语法和验证视角,不是 Persona 扮演 |
34
34
  | **Atelier Path** | 可选创作深路径:冻结基线、形成机制不同候选、比较或保留原版,并把证据写入唯一 Atelier Record |
35
35
  | **Quality Arena** | 以基线和机制不同候选进行探索、比较、实现与观察 |
@@ -229,6 +229,7 @@ Agent 在用户说"看看进度 / 打开 preview / 看全局"时,先跑 `loom
229
229
  | `loom capability graph\|frontier\|get\|coverage\|compile` | 查看能力图谱、未路由前沿、覆盖缺口和当前 Intent 的能力编译输入 |
230
230
  | `loom capability proposal list\|get\|submit\|decide\|close` | 以 provenance 提交新发现,并由 Architect 裁决和闭合 |
231
231
  | `loom asset import\|list\|search\|get\|validate` | 管理带来源、许可、哈希、批准状态与 evidence 回链的本地素材 |
232
+ | `loom expertise init\|get\|validate <id>` | 创建和校验 revision-scoped Search Plan、外部来源与 Capability Capsules |
232
233
  | `loom atelier init\|get\|validate <id>` | 为显式 Atelier Intent 创建、读取和校验创作记录 |
233
234
  | `loom intent deprecate <id> --reason <text>` | 只读评估当前版本弃用影响;加 `--confirm` 并完整分类依赖方后原子写入 |
234
235
  | `loom intent narrative <id>` | Intent 意图叙事 |
@@ -292,6 +293,7 @@ LOOM/
292
293
  ├── CAPABILITY_GRAPH_TEMPLATE.json Capability Graph 起点
293
294
  ├── CAPABILITY_BRIEF_TEMPLATE.md Capability Brief 起点
294
295
  ├── ASSET_LIBRARY_MANIFEST_TEMPLATE.json Asset Library 起点
296
+ ├── EXPERTISE_PACK_TEMPLATE.json 外部能力获取记录起点
295
297
  └── ATELIER_RECORD_TEMPLATE.json Atelier Record 起点
296
298
  ```
297
299
 
@@ -312,6 +314,7 @@ LOOM/
312
314
  | 哲学文档长什么样 | `templates/PHILOSOPHY_TEMPLATE.md` |
313
315
  | 愿景文档长什么样 | `templates/VISION_TEMPLATE.md` |
314
316
  | Capability Graph 与 Brief 长什么样 | `cli/help/capability.md`、`templates/CAPABILITY_GRAPH_TEMPLATE.json` |
317
+ | 为什么必须外部检索、Expertise Pack 怎么闭合 | `cli/help/expertise.md`、`EXTERNAL_ACQUISITION_DESIGN.md` |
315
318
  | Author 与 Atelier 怎么工作 | `cli/help/atelier.md`、`dimensions/AUTHORSHIP.md` |
316
319
  | Intent Map 长什么样 | `templates/INTENT_MAP_TEMPLATE.json` |
317
320
  | 怎么搜索高质量参考 | `dimensions/SEARCH_METHODOLOGY.md` |
package/cli/bin/loom.js CHANGED
@@ -28,6 +28,7 @@ import { compileCapabilityInputs, getCapabilityCoverage, getCapabilityFrontier,
28
28
  import { getAsset, importAsset, listAssets, recoverAssetImportTransaction, searchAssets, validateAssetLibrary } from '../src/asset-library.js';
29
29
  import { closeCapabilityProposal, decideCapabilityProposal, getCapabilityProposal, listCapabilityProposals, submitCapabilityProposal } from '../src/capability-proposals.js';
30
30
  import { getAtelierRecord, initAtelierRecord, validateAtelierRecord } from '../src/atelier.js';
31
+ import { assertExpertiseReady, getExpertisePack, initExpertisePack, validateExpertisePack } from '../src/expertise-pack.js';
31
32
 
32
33
  // ─── 路径解析 ──────────────────────────────────────────
33
34
  // findLoomRoot / findVersionDir / readCurrentPointer 已提取到 shared/paths.js
@@ -184,6 +185,26 @@ try {
184
185
  break;
185
186
  }
186
187
 
188
+ case 'expertise': {
189
+ const versionDir = findVersionDir();
190
+ const intentId = rest[0];
191
+ if (!intentId) die(`用法: loom expertise ${sub || '<init|get|validate>'} <intent-id>`);
192
+ switch (sub) {
193
+ case 'init':
194
+ output(initExpertisePack(versionDir, intentId));
195
+ break;
196
+ case 'get':
197
+ output(getExpertisePack(versionDir, intentId));
198
+ break;
199
+ case 'validate':
200
+ output(validateExpertisePack(versionDir, intentId));
201
+ break;
202
+ default:
203
+ die(`未知 expertise 子命令: ${sub}\n用法: loom expertise [init|get|validate] <intent-id>`);
204
+ }
205
+ break;
206
+ }
207
+
187
208
  case 'intent': {
188
209
  const versionDir = findVersionDir();
189
210
  switch (sub) {
@@ -344,6 +365,14 @@ try {
344
365
  if (!isVerificationCurrent(intent, latest)) {
345
366
  die(`${id} 最新 passed 验证不属于当前 Intent revision ${intent.revision ?? 1}。先重新验证。`);
346
367
  }
368
+ const expertise = assertExpertiseReady(versionDir, id);
369
+ if (expertise && (latest.expertise?.record_ref !== `10_EXPERTISE_PACKS/${id}.json`
370
+ || latest.expertise?.intent_revision !== expertise.intent_revision
371
+ || latest.expertise?.source_count !== expertise.source_count
372
+ || latest.expertise?.capsule_count !== expertise.capsule_count
373
+ || latest.expertise?.pack_digest !== expertise.pack_digest)) {
374
+ die(`${id} 最新 passed 未绑定当前 Expertise Pack。请在新的 Keeper task 中重新打开来源并验证。`);
375
+ }
347
376
  if (intent.quality_strategy === 'atelier') {
348
377
  const atelier = validateAtelierRecord(versionDir, id);
349
378
  if (!['selected', 'baseline_retained'].includes(atelier.status)) {
@@ -930,6 +959,9 @@ To Human:
930
959
  loom capability get <id> 返回节点、关系、Brief 与 Intent 回链
931
960
  loom capability coverage 检查图谱覆盖、Brief 和 Intent 回链
932
961
  loom capability compile <id> 只读显示会进入该 Intent 的能力输入
962
+ loom expertise init <id> 创建当前 Intent 的外部能力获取记录
963
+ loom expertise get <id> 返回并校验当前 Expertise Pack
964
+ loom expertise validate <id> 校验检索、来源、Capsule 与 revision
933
965
  loom atelier init <id> 为 atelier Intent 创建唯一创作记录
934
966
  loom atelier get <id> 返回并校验当前 Atelier Record
935
967
  loom atelier validate <id> 校验 Record、revision、候选与证据引用
@@ -66,3 +66,8 @@ Visionary 给出 outcome、角色、非目标与项目事实。Architect 用项
66
66
  `loom capability compile <intent-id>` 只读显示会进入当前 Intent 的图谱节点与 Brief。Forge 激活
67
67
  Intent 时会获得同一份输入;发现新依赖、风险或能力缺口时必须回流 Architect 更新 Graph,不能静默
68
68
  扩展实现。Keeper 以图谱回链检查高影响问题是否真的被兑现。
69
+
70
+ Capability 节点可选声明 `acquisition_mode: adaptive | external_required | project_only`。
71
+ `project_only` 必须附 `acquisition_rationale`。未声明时,高影响 capability 自动提升为
72
+ `external_required`。Graph 只保存获取必要性,不保存网站、Skill 名称或
73
+ 搜索词;Forge 在本轮 Expertise Pack 中按项目信号派生查询并记录真实来源。
@@ -54,7 +54,10 @@ Forge 针对当前 Intent 临时组装一个 Expertise Pack。先由 Capability
54
54
  6. 如何验证专业质量。
55
55
 
56
56
  能力名称只表示“可发现”;只有真实加载并转化为任务判断的内容才算进入 Expertise Pack。
57
- Pack 仅服务当前任务,不成为长期 Doctrine。
57
+ External Acquisition Gate 为 required 时,Forge 只能自行派生 Search Plan,内容必须来自
58
+ 实际打开的 Skill、网络、官方文档或研究资料;Pack 写入当前 Intent revision 的
59
+ `10_EXPERTISE_PACKS`,每个 Capability Capsule 都直接回链来源。Pack 仅服务当前任务,不成为
60
+ 长期 Doctrine。
58
61
 
59
62
  ## Quality Arena
60
63
 
@@ -0,0 +1,51 @@
1
+ # External Acquisition 与 Expertise Pack
2
+
3
+ LOOM 把“知道任务需要某项能力”和“真正获得了这项能力”分开。Capability Graph 只决定是否
4
+ 需要外部获取;Forge 在当前 Intent 中派生查询、实际检索,再把有来源的核心信息编译成
5
+ Capability Capsules。
6
+
7
+ ```bash
8
+ loom capability compile <intent-id>
9
+ loom expertise init <intent-id>
10
+ # 编辑 .loom/vN/10_EXPERTISE_PACKS/<intent-id>.json,并实际执行检索
11
+ loom expertise validate <intent-id>
12
+ loom activate forge --intent <intent-id>
13
+ ```
14
+
15
+ ## 什么时候强制
16
+
17
+ - capability 显式声明 `acquisition_mode: external_required`;
18
+ - 或高影响 capability 没有显式声明其他模式。
19
+
20
+ 中低影响任务默认为 `adaptive`。高影响任务只有在 Architect 明确写 `adaptive` 时才按证据
21
+ 决定是否检索;仅依赖未公开内部协议、外部知识不会改变做法的机械任务可以用 `project_only`,
22
+ 但必须写 `acquisition_rationale`。
23
+
24
+ ## Search Plan
25
+
26
+ Forge 根据 capability question、Capability Brief、Intent narrative、质量契约、creative
27
+ scope、媒介约束和基线缺口派生查询。关键词是运行时证据,不写回 Graph。
28
+
29
+ 必须实际使用 Skill registry、网络、官方文档或研究资料。模型自行生成的原则、没有打开的
30
+ 搜索摘要和只看标题的结果都不能登记为来源。每个计划必须写停止条件,避免无边界浏览。
31
+
32
+ ## Capability Capsule
33
+
34
+ 每个 required capability 至少有一个 Capsule,包含:
35
+
36
+ - 专业问题与适用时机;
37
+ - 规则和工作流;
38
+ - 决策门与失败模式;
39
+ - 可观察验证信号;
40
+ - 至少一个直接外部来源引用。
41
+
42
+ Pack 只保存项目化综合和定位信息,不复制第三方 Skill 或网页正文,也不会自动变成 Doctrine
43
+ 或通用 Skill。Intent revision 改变后,旧 Pack 自动失效。
44
+
45
+ ## Keeper
46
+
47
+ CLI 能检查 Pack 当前、来源可定位、Capsule 直接引用外部资料,但不能仅凭 JSON 判断来源是否
48
+ 真的支持结论。Keeper 必须在独立 task 中重新打开至少一个关键来源,核对规则与判断门,并让
49
+ `loom verify pass` 将当前 Pack 的内容摘要绑定进验证记录;之后 Pack 内容发生变化必须重验。
50
+
51
+ 更多设计边界见 `EXTERNAL_ACQUISITION_DESIGN.md`。
package/cli/help/loop.md CHANGED
@@ -28,8 +28,10 @@ Forge 先形成任务级 Expertise Pack:
28
28
  - 已实际加载的技能、资料、工具及其用途。
29
29
  - Critic 视角与验证方法。
30
30
 
31
- Pack 是临时认知配置,不写成新的长期规范。明显任务可以很短;高质量任务应足以解释为什么
32
- 某个专业手法适合这个项目。
31
+ Pack 是任务级认知配置,不写成新的长期规范。External Acquisition Gate required 时,Pack
32
+ 必须落盘到 `10_EXPERTISE_PACKS/<intent-id>.json`:Search Plan 可由 AI 派生,但 Capsule 内容
33
+ 必须来自实际打开的外部来源,并写出判断门、失败模式和验证信号。明显的机械任务可以保持
34
+ `adaptive`;高质量任务必须足以解释为什么某个专业手法适合这个项目。
33
35
 
34
36
  ## 3. Quality Arena
35
37
 
@@ -64,6 +64,16 @@ loom activate keeper --intent <id>
64
64
  Forge 编译 Expertise Pack,在 Quality Arena 中实现与比较。
65
65
  Keeper 从当前磁盘事实和契约独立验证,不继承 Forge 的解释。
66
66
 
67
+ 当 `loom capability compile <id>` 报告 `acquisition.required=true` 时,先运行:
68
+
69
+ ```bash
70
+ loom expertise init <id>
71
+ # 实际执行 find skill / web / official docs / research 检索并填写 Pack
72
+ loom expertise validate <id>
73
+ ```
74
+
75
+ 门未闭合时不能写入 passed。Keeper 会重新打开关键来源,passed 记录绑定当前 Pack。
76
+
67
77
  无质量契约时,四个基础维度通过即可闭合。存在质量契约时,额外验证
68
78
  `quality_achievement`;声明相对提升时,在该维度中链接 Quality Proof:
69
79
 
@@ -10,6 +10,7 @@ import { getVerificationContract } from './verify.js';
10
10
  import { extractMdSection } from './shared/md-utils.js';
11
11
  import { compileCapabilityInputs } from './capability-graph.js';
12
12
  import { getAtelierRecord } from './atelier.js';
13
+ import { formatExpertisePackForPrompt, getExpertisePack, getExpertisePackState } from './expertise-pack.js';
13
14
 
14
15
  const VALID_ROLES = ['weaver', 'visionary', 'architect', 'forge', 'keeper'];
15
16
 
@@ -262,8 +263,9 @@ function compileExpertiseInputs(role, versionDir, objective) {
262
263
  `- quality_strategy: ${qualityStrategy}`,
263
264
  '- Skill、工具和资产名称只代表可发现入口;实际检查并加载后才进入 Expertise Pack。',
264
265
  ];
266
+ let compiled = null;
265
267
  if (versionDir && objective.intent) {
266
- const compiled = compileCapabilityInputs(versionDir, objective.intent.id);
268
+ compiled = compileCapabilityInputs(versionDir, objective.intent.id);
267
269
  if (!compiled.available) {
268
270
  lines.push(`- Capability Graph: ${compiled.warnings.join(' ')}`);
269
271
  } else if (compiled.nodes.length === 0) {
@@ -279,6 +281,40 @@ function compileExpertiseInputs(role, versionDir, objective) {
279
281
  for (const warning of compiled.warnings) lines.push(`- Capability Graph warning: ${warning}`);
280
282
  }
281
283
  }
284
+ if (compiled?.acquisition?.nodes?.length) {
285
+ lines.push('- External acquisition modes:');
286
+ for (const node of compiled.acquisition.nodes) {
287
+ lines.push(` - ${node.node_id}: ${node.mode}(${node.reason})`);
288
+ }
289
+ }
290
+ if (versionDir && objective.intent && ['forge', 'keeper'].includes(role)) {
291
+ const state = getExpertisePackState(versionDir, subject.id);
292
+ if (state.required && !state.ready) {
293
+ lines.push(
294
+ '\n### External Acquisition Gate — OPEN',
295
+ `- required_capabilities: ${state.required_node_ids.join(', ')}`,
296
+ '- 先根据 Capability question、Authorial Stance/creative_scope、媒介约束与已观察缺口派生搜索词;搜索词属于本轮计划,不得写死进 Capability Graph。',
297
+ '- 必须实际使用外部 find skill、网络搜索、官方文档或研究资料获取信息。模型自行生成的常识、内部复述和未打开的搜索结果不能充当来源。',
298
+ `- ${state.reason === 'missing' ? `运行 \`loom expertise init ${subject.id}\`` : `修正 \`10_EXPERTISE_PACKS/${subject.id}.json\``},记录可回查来源与 Capability Capsules,再运行 \`loom expertise validate ${subject.id}\`。`,
299
+ '- 门未闭合时,只能完成机械性勘察、基线冻结与检索;不得声称专业方案已形成,也不得通过验证。',
300
+ );
301
+ } else if (state.required && role === 'forge') {
302
+ const pack = getExpertisePack(versionDir, subject.id);
303
+ lines.push(
304
+ '\n### External Acquisition Gate — READY',
305
+ '- 以下内容是本 Intent revision 的来源化核心信息组。它不是永久 Doctrine,也不是复制进仓库的第三方 Skill。',
306
+ `\n${formatExpertisePackForPrompt(pack)}`,
307
+ );
308
+ } else if (state.required && role === 'keeper') {
309
+ lines.push(
310
+ '\n### External Acquisition Evidence — PRESENT',
311
+ `- record_ref: 10_EXPERTISE_PACKS/${subject.id}.json`,
312
+ `- required_capabilities: ${state.required_node_ids.join(', ')}`,
313
+ `- source_count: ${state.validation.source_count}; capsule_count: ${state.validation.capsule_count}`,
314
+ '- 不继承 Forge 的 Capsule 结论。重新打开至少一个关键来源,检查来源确实支持规则与判断门,并把绑定写入本轮验证记录。',
315
+ );
316
+ }
317
+ }
282
318
  if (role === 'keeper') {
283
319
  lines.push('- 不继承 Forge Expertise Pack;按契约独立准备验证能力。');
284
320
  if (qualityStrategy === 'atelier') {
@@ -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,17 @@ 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
+ }
69
81
  if (node.verification !== undefined) {
70
82
  if (node.kind !== 'evidence') {
71
83
  errors.push(`nodes["${id}"].verification 只允许写在 evidence 节点`);
@@ -131,6 +143,13 @@ export function loadCapabilityGraph(versionDir, { required = true } = {}) {
131
143
  return validateCapabilityGraph(readJsonFile(filePath, 'Capability Graph'));
132
144
  }
133
145
 
146
+ function getEffectiveAcquisitionMode(node) {
147
+ if (node.kind !== 'capability') return null;
148
+ if (node.acquisition_mode) return node.acquisition_mode;
149
+ if (node.impact === 'high') return 'external_required';
150
+ return 'adaptive';
151
+ }
152
+
134
153
  function isRouted(node) {
135
154
  return ['brief', 'intent', 'defer', 'exclude', 'covered_by'].includes(node.route);
136
155
  }
@@ -261,6 +280,19 @@ export function getCapabilityCoverage(versionDir) {
261
280
  if (node.route === 'brief' && !node.brief_ref) {
262
281
  capabilitiesWithoutPlan.push({ node_id: node.id, reason: 'route=brief 但缺少 brief_ref' });
263
282
  }
283
+ if (node.kind === 'capability') {
284
+ const linkedIntents = (node.intent_refs || [])
285
+ .map((intentId) => intentMap.intents[intentId])
286
+ .filter(Boolean);
287
+ const needsExternalAcquisition = linkedIntents.length > 0
288
+ && getEffectiveAcquisitionMode(node) === 'external_required';
289
+ if (needsExternalAcquisition && !node.brief_ref && node.route !== 'brief') {
290
+ capabilitiesWithoutPlan.push({
291
+ node_id: node.id,
292
+ reason: '外部能力获取为 required,但缺少 brief_ref 来定义专业问题与验收边界',
293
+ });
294
+ }
295
+ }
264
296
  if (['defer', 'exclude'].includes(node.route) && (!node.rationale || typeof node.rationale !== 'string' || node.rationale.trim() === '')) {
265
297
  routingGaps.push({ node_id: node.id, reason: `route=${node.route} 但缺少 rationale` });
266
298
  }
@@ -339,7 +371,15 @@ function collectCompilationNodes(graph, intentId) {
339
371
 
340
372
  export function compileCapabilityInputs(versionDir, intentId) {
341
373
  const graph = loadCapabilityGraph(versionDir, { required: false });
342
- if (!graph) return { available: false, nodes: [], briefs: [], warnings: ['项目尚未建立 Capability Graph;使用 Intent 的 capability_needs 兼容路径。'] };
374
+ if (!graph) {
375
+ return {
376
+ available: false,
377
+ nodes: [],
378
+ briefs: [],
379
+ warnings: ['项目尚未建立 Capability Graph;使用 Intent 的 capability_needs 兼容路径。'],
380
+ acquisition: { required: false, required_node_ids: [], nodes: [] },
381
+ };
382
+ }
343
383
  const nodes = collectCompilationNodes(graph, intentId);
344
384
  const briefs = [];
345
385
  const warnings = [];
@@ -347,5 +387,30 @@ export function compileCapabilityInputs(versionDir, intentId) {
347
387
  if (!node.brief_ref) continue;
348
388
  try { briefs.push({ node_id: node.id, ...resolveBrief(versionDir, node.brief_ref) }); } catch (error) { warnings.push(`${node.id}: ${error.message}`); }
349
389
  }
350
- return { available: true, nodes, briefs, warnings };
390
+ const acquisitionNodes = nodes
391
+ .filter((node) => node.kind === 'capability')
392
+ .map((node) => ({
393
+ node_id: node.id,
394
+ title: node.title,
395
+ mode: getEffectiveAcquisitionMode(node),
396
+ reason: node.acquisition_mode
397
+ ? 'Capability Graph 显式声明'
398
+ : node.impact === 'high'
399
+ ? '高影响能力默认需要外部来源化'
400
+ : '按任务证据自适应判断',
401
+ }));
402
+ const requiredNodeIds = acquisitionNodes
403
+ .filter((node) => node.mode === 'external_required')
404
+ .map((node) => node.node_id);
405
+ return {
406
+ available: true,
407
+ nodes,
408
+ briefs,
409
+ warnings,
410
+ acquisition: {
411
+ required: requiredNodeIds.length > 0,
412
+ required_node_ids: requiredNodeIds,
413
+ nodes: acquisitionNodes,
414
+ },
415
+ };
351
416
  }
@@ -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
  /**
@@ -314,6 +317,33 @@ export function doctor(versionDir, verificationsDir, philosophyDir) {
314
317
  }
315
318
  }
316
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
+ }
317
347
  if (intent.quality_strategy === 'atelier' && latest?.verdict === 'passed') {
318
348
  const atelier = atelierRecords.get(id);
319
349
  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
  * 检测文件是否还是模板(未填充真实内容)。
@@ -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);
package/cli/src/verify.js CHANGED
@@ -8,6 +8,7 @@ import { getIntent, getEffectiveVerificationEpoch, hasLegacyIntentRevision } fro
8
8
  import { formatIntentRef, resolveIntentRef } from './shared/intent-ref.js';
9
9
  import { resolveQualityProofReference } from './shared/proof-reference.js';
10
10
  import { validateAtelierRecord } from './atelier.js';
11
+ import { assertExpertiseReady } from './expertise-pack.js';
11
12
 
12
13
  /** 合法判定结果 */
13
14
  const VALID_VERDICTS = ['passed', 'deviated', 'blocked', 'pending_human'];
@@ -46,6 +47,7 @@ function getRequiredDimensions(intent) {
46
47
  export function writeVerification(versionDir, verificationsDir, record) {
47
48
  const errors = [];
48
49
  let atelierEvidence = null;
50
+ let expertiseEvidence = null;
49
51
  if (!record.intent_id) errors.push('缺少 intent_id');
50
52
  if (!record.verdict || !VALID_VERDICTS.includes(record.verdict)) {
51
53
  errors.push(`verdict 非法: "${record.verdict}" (合法: ${VALID_VERDICTS.join('|')})`);
@@ -104,6 +106,13 @@ export function writeVerification(versionDir, verificationsDir, record) {
104
106
  errors.push(error.message);
105
107
  }
106
108
  }
109
+ if (record.verdict === 'passed' && intent) {
110
+ try {
111
+ expertiseEvidence = assertExpertiseReady(versionDir, record.intent_id);
112
+ } catch (error) {
113
+ errors.push(`passed 前必须闭合外部能力获取强门: ${error.message}`);
114
+ }
115
+ }
107
116
  if (intent?.quality_strategy === 'atelier' && record.verdict === 'passed') {
108
117
  try {
109
118
  atelierEvidence = validateAtelierRecord(versionDir, record.intent_id);
@@ -162,6 +171,14 @@ export function writeVerification(versionDir, verificationsDir, record) {
162
171
  stance_revision: atelierEvidence.stance_revision,
163
172
  status: atelierEvidence.status,
164
173
  } : undefined,
174
+ expertise: expertiseEvidence ? {
175
+ record_ref: `10_EXPERTISE_PACKS/${record.intent_id}.json`,
176
+ intent_revision: expertiseEvidence.intent_revision,
177
+ required_node_ids: expertiseEvidence.required_node_ids,
178
+ source_count: expertiseEvidence.source_count,
179
+ capsule_count: expertiseEvidence.capsule_count,
180
+ pack_digest: expertiseEvidence.pack_digest,
181
+ } : undefined,
165
182
  reproduction_command: record.reproduction_command,
166
183
  deviation_detail: record.deviation_detail,
167
184
  reset_suggested: record.reset_suggested,
@@ -110,6 +110,13 @@ Forge 在实现前形成临时 Expertise Pack:
110
110
 
111
111
  Capability Graph 先提供当前 Intent 相关的项目事实、风险、约束和 Capability Brief;Expertise Compiler 再按 Brief 的获取计划加载真实技能、工具或资料。它不把整张图或历史会话当成当前任务上下文。
112
112
 
113
+ 当 capability 显式为 `external_required`,或高影响 capability 未显式豁免时,External
114
+ Acquisition Gate 启用。Forge 只能自行生成 Search Plan,必须实际使用 find skill、
115
+ 网络、官方文档或研究资料获取内容,再把可回查来源与项目化 Capability Capsules 写入
116
+ `.loom/vN/10_EXPERTISE_PACKS/<intent-id>.json`。Graph 不保存固定站点、Skill 或关键词。
117
+ 模型记忆、未打开的搜索摘要和自生成原则不能替代来源。Pack 绑定 Intent revision;Keeper
118
+ 不继承 Capsule 结论,而是重新打开关键来源并在 passed 记录中绑定当前 Pack。
119
+
113
120
  `quality_strategy=atelier` 时运行 Identity Compiler:将项目判断编译为可执行的 Authorial
114
121
  Stance,而不是模仿名人的 Persona。Forge 在 `.loom/vN/09_ATELIER/<intent-id>.json`
115
122
  保存唯一 Atelier Record;普通 Intent 不创建该文件。
@@ -69,6 +69,11 @@ Context Pack 不会清除 Agent 既有记忆。发生冲突时,以 system、de
69
69
  Expertise Pack。Domain、Taste、Author、Critic、Verifier 是按需认知职能,不是新增角色。
70
70
  Author 只在需要形成创作命题时启用,不是常驻 Persona。
71
71
 
72
+ 未显式豁免的高影响专业能力会让 Context Pack 先打开 External Acquisition Gate:Forge
73
+ 从项目事实派生查询并实际检索,将来源化 Capability Capsules 写入当前
74
+ Intent revision 的 `10_EXPERTISE_PACKS`;门闭合后才注入 Author/Atelier。Keeper 只接收
75
+ Pack 的证据入口和计数,必须独立重开关键来源,不继承 Forge 的综合结论。
76
+
72
77
  ### Quality Arena
73
78
 
74
79
  答案明确时直接实现;声明质量提升时保存基线、比较机制不同的候选。完成契约是
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/loom",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "LOOM — 哲学驱动开发框架。Agent 通过 CLI 访问 Intent Map / 哲学 / 验证记录,不直接读文件",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,8 @@
15
15
  "templates",
16
16
  "dimensions",
17
17
  "README.md",
18
- "CHANGELOG.md"
18
+ "CHANGELOG.md",
19
+ "EXTERNAL_ACQUISITION_DESIGN.md"
19
20
  ],
20
21
  "publishConfig": {
21
22
  "access": "public",
@@ -29,7 +29,7 @@
29
29
  1. 一个 Intent 产生可观察的完整结果,能独立验证,并可在一次受控工作周期内完成。
30
30
  2. 在创建正式 Intent Map 前,先用 Capability Graph 检查适用的用户旅程、体验、系统、资产、横切质量和风险;每个透镜必须展开、覆盖或明确排除。
31
31
  3. Graph 不是任务列表:未知与调研留在 Graph;只有边界清楚、可独立验证的结果才进入 Intent Map。
32
- 4. 高影响 capability 节点需要外部知识、专业方法或会进入当前 Intent 时,创建短小的 Capability Brief;不要为每个低价值叶子制造文档。
32
+ 4. 高影响 capability 默认进入外部获取强门并创建短小的 Capability Brief;只有证据表明按任务自适应更合适时才显式写 `acquisition_mode: adaptive`,纯内部机械任务可写带理由的 `project_only`。Graph 不保存网站、Skill 或关键词,不为低价值叶子制造文档。
33
33
  5. 每个 Intent 必须回链至少一个 Graph 节点;每个高影响 Graph 节点必须有明确路由。
34
34
  6. 只引入当前目标确实需要的边界和抽象;不为想象中的扩展性提前付费。
35
35
  7. `acceptance` 是完成契约:包含功能承诺、关键失败边界和防御承诺。
package/roles/forge.md CHANGED
@@ -45,8 +45,15 @@ Stance。Stance 至少包含 creative thesis、gaze、tension、signature bet、
45
45
  medium grammar、surprise budget、anti-fixation 与 verification lens。人格故事、设计师
46
46
  名号和风格形容词不能替代这些决策。
47
47
 
48
- Context Pack 中出现 Skill 名称只代表可发现。只有实际检查环境并加载后,才算进入
49
- Expertise Pack。Pack 默认只存在于当前工作上下文,不新增项目文件。
48
+ Context Pack 中出现 Skill 名称只代表可发现。若 External Acquisition Gate 为 OPEN,
49
+ 先从 Capability question、项目事实、媒介约束与已观察缺口派生 Search Plan,并实际使用
50
+ find skill、网络、官方文档或研究资料。模型自行生成的常识、未打开的搜索摘要和只有标题的
51
+ 结果不能成为来源。
52
+
53
+ required Pack 写入 `.loom/vN/10_EXPERTISE_PACKS/<intent-id>.json`,只保存来源定位和
54
+ 项目化 Capability Capsules,不复制第三方内容。每个 Capsule 必须直接引用已打开的外部
55
+ 来源并写出规则、决策门、失败模式与验证信号。运行 `loom expertise validate <intent-id>`
56
+ 闭合强门后,才能进入 Author/Atelier 或非机械性实现。
50
57
 
51
58
  Author 的观察先分流:当前命题、机制、媒介语法或候选选择失效,写入 Atelier Record 的
52
59
  `corrections[]` 并递增 `stance_revision`;新的用户结果、约束、研究证据、风险、能力缺口
package/roles/keeper.md CHANGED
@@ -33,6 +33,8 @@ Keeper 默认运行在新的 Agent thread 中。只接收:
33
33
  - BASELINE、acceptance 与按需的 quality_contract。
34
34
  - verification_method、当前产物与可复现入口。
35
35
  - 当前 revision 的验证历史。
36
+ - External Acquisition Gate required 时,读取当前 Expertise Pack 的 evidence binding,
37
+ 重新打开至少一个决定性来源;不继承 Forge 的 Capsule 结论。
36
38
  - `quality_strategy=atelier` 时,读取当前 revision 的 Atelier Record 及其产物引用;不接收
37
39
  Forge 为结果辩护的隐藏推理。
38
40
 
@@ -19,8 +19,9 @@
19
19
 
20
20
  ## 能力获取计划
21
21
 
22
- - 需要的 Skill、工具、官方资料、项目事实或人类输入:
23
- - 为什么这些入口适用于当前节点:
22
+ - 需要回答的专业决策问题:
23
+ - 可用的检索通道、工具、项目事实或人类输入(不在 Brief 写死网站和关键词):
24
+ - Forge 应依据哪些项目信号派生本轮查询:
24
25
  - 不可用时的降级或回流:
25
26
 
26
27
  ## 产出与验证入口
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_meta": {
3
- "_description": "Capability Graph 起点:从项目初衷展开问题面、能力缺口、风险与证据,再编译为 Intent。不是待办列表。",
3
+ "_description": "Capability Graph 起点:从项目初衷展开问题面、能力缺口、风险与证据,再编译为 Intent。capability 可选 acquisition_mode=adaptive|external_required|project_only;Graph 不保存网站、Skill 或搜索词。",
4
4
  "_version": "1.0",
5
5
  "_loom_version": "v1",
6
6
  "_generated_by": "architect",
@@ -0,0 +1,22 @@
1
+ {
2
+ "_meta": {
3
+ "_description": "当前 Intent 的外部能力获取记录。只保存检索计划、来源定位与项目化核心信息,不复制第三方 Skill 或网站内容。",
4
+ "_version": "1.0",
5
+ "_loom_version": "v1",
6
+ "_generated_by": "forge"
7
+ },
8
+ "intent_id": "INT-001",
9
+ "intent_revision": 1,
10
+ "required_capability_refs": [],
11
+ "status": "draft",
12
+ "search_plan": {
13
+ "decision_question": "",
14
+ "project_signals": [],
15
+ "derived_queries": [],
16
+ "constraints": [],
17
+ "stop_condition": ""
18
+ },
19
+ "sources": [],
20
+ "capsules": [],
21
+ "blocker": null
22
+ }