abelien-harness 0.5.0 → 0.6.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.
Files changed (95) hide show
  1. package/.env.example +1 -0
  2. package/README.md +11 -18
  3. package/package.json +2 -3
  4. package/skills/abelien-appearance/SKILL.md +1 -0
  5. package/skills/abelien-clarify/SKILL.md +12 -2
  6. package/skills/abelien-clarify/references/examples.md +22 -0
  7. package/skills/abelien-clarify/scripts/check_result.py +38 -3
  8. package/skills/abelien-clarify/scripts/test_check_result.py +32 -1
  9. package/skills/abelien-guide/SKILL.md +1 -1
  10. package/skills/abelien-hardware/SKILL.md +4 -4
  11. package/skills/abelien-hardware/domain.json +1 -1
  12. package/skills/abelien-hardware/references/search-index-format.md +4 -2
  13. package/skills/abelien-hardware/references/web-evidence-contract.md +3 -3
  14. package/skills/abelien-hardware/scripts/dimensions.mjs +23 -8
  15. package/skills/abelien-hardware/scripts/electrical.mjs +62 -19
  16. package/skills/abelien-hardware/scripts/finalize-selection.mjs +3 -5
  17. package/skills/abelien-hardware/scripts/provider-chain-contract.mjs +17 -0
  18. package/skills/abelien-hardware/scripts/public-page.mjs +11 -121
  19. package/skills/abelien-hardware/scripts/response-redaction.mjs +1 -130
  20. package/skills/abelien-hardware/scripts/selection-request.mjs +6 -5
  21. package/skills/abelien-hardware/scripts/tos-wiki-search.mjs +9 -6
  22. package/skills/abelien-hardware/scripts/volc-web-search.mjs +10 -32
  23. package/skills/abelien-hardware/scripts/web-common.mjs +4 -10
  24. package/skills/abelien-hardware/scripts/web-integrity.mjs +1 -1
  25. package/skills/abelien-hardware/scripts/web-provider-chain.mjs +2 -1
  26. package/skills/abelien-hardware/scripts/web-receipt.mjs +4 -2
  27. package/skills/abelien-hardware/scripts/web-search-plan.mjs +5 -1
  28. package/skills/abelien-hardware/scripts/web-search.mjs +4 -3
  29. package/skills/abelien-hardware-selection/domain.json +2 -2
  30. package/skills/abelien-hardware-selection/references/selection-service-contract.md +18 -10
  31. package/skills/abelien-hardware-selection/scripts/select.mjs +13 -5
  32. package/skills/abelien-harness/scripts/ab.mjs +24 -2
  33. package/skills/abelien-harness/scripts/web/common.mjs +20 -0
  34. package/skills/abelien-harness/scripts/web/public-read.mjs +168 -0
  35. package/skills/abelien-harness/scripts/web/redact.mjs +136 -0
  36. package/skills/abelien-harness/scripts/web/volc.mjs +49 -0
  37. package/skills/abelien-reference/SKILL.md +59 -0
  38. package/skills/abelien-reference/domain.json +92 -0
  39. package/skills/abelien-reference/fixtures/create.final.json +104 -0
  40. package/skills/abelien-reference/fixtures/restore.collected.json +191 -0
  41. package/skills/abelien-reference/fixtures/restore.final.json +269 -0
  42. package/skills/abelien-reference/references/research-contract.md +78 -0
  43. package/skills/abelien-reference/schemas/reference-evidence.v1.json +153 -0
  44. package/skills/abelien-reference/scripts/evidence.mjs +68 -0
  45. package/skills/abelien-reference/scripts/finalize.mjs +116 -0
  46. package/skills/abelien-reference/scripts/measure.mjs +62 -0
  47. package/skills/abelien-reference/scripts/png.mjs +86 -0
  48. package/skills/abelien-reference/scripts/research.mjs +191 -0
  49. package/skills/abelien-structure/SKILL.md +2 -2
  50. package/skills/abelien-structure/domain.json +64 -0
  51. package/skills/abelien-structure/references/build123d-execution.md +1 -1
  52. package/skills/abelien-structure/references/handoff.md +1 -1
  53. package/skills/abelien-structure/worker/abelien_cad.py +585 -0
  54. package/skills/abelien-structure/worker/assembly-validation.mjs +106 -0
  55. package/skills/abelien-structure/worker/cad.py +1623 -0
  56. package/skills/abelien-structure/worker/cli-adapter.mjs +207 -0
  57. package/skills/abelien-structure/worker/component-fit-glb.mjs +173 -0
  58. package/skills/abelien-structure/worker/component-fit.d.mts +82 -0
  59. package/skills/abelien-structure/worker/component-fit.mjs +167 -0
  60. package/skills/abelien-structure/worker/component-target-image.d.mts +23 -0
  61. package/skills/abelien-structure/worker/component-target-image.mjs +107 -0
  62. package/skills/abelien-structure/worker/component-target-render.py +58 -0
  63. package/skills/abelien-structure/worker/config.mjs +6 -0
  64. package/skills/abelien-structure/worker/engineering-fit.d.mts +48 -0
  65. package/skills/abelien-structure/worker/engineering-fit.mjs +118 -0
  66. package/skills/abelien-structure/worker/engineering-policy.json +16 -0
  67. package/skills/abelien-structure/worker/engineering-repair.py +147 -0
  68. package/skills/abelien-structure/worker/engineering.py +324 -0
  69. package/skills/abelien-structure/worker/identity.mjs +98 -0
  70. package/skills/abelien-structure/worker/inspection-geometry.mjs +71 -0
  71. package/skills/abelien-structure/worker/jobs.mjs +451 -0
  72. package/skills/abelien-structure/worker/library-runtime.mjs +15 -0
  73. package/skills/abelien-structure/worker/main.mjs +102 -0
  74. package/skills/abelien-structure/worker/manifest-digest.mjs +11 -0
  75. package/skills/abelien-structure/worker/materials.mjs +52 -0
  76. package/skills/abelien-structure/worker/native-launcher.py +13 -0
  77. package/skills/abelien-structure/worker/native.mjs +157 -0
  78. package/skills/abelien-structure/worker/representation.mjs +47 -0
  79. package/skills/abelien-structure/worker/sandbox.mjs +41 -0
  80. package/skills/abelien-structure/worker/source-contract.mjs +143 -0
  81. package/skills/abelien-structure/worker/source_call_stats.py +59 -0
  82. package/skills/abelien-structure/worker/supervisor.mjs +38 -0
  83. package/skills/abelien-workspace/SKILL.md +11 -0
  84. package/skills/abelien-workspace/assets/journey-stage-plan.json +155 -0
  85. package/skills/abelien-workspace/domain.json +25 -2
  86. package/skills/abelien-workspace/references/hardware-journey-orchestration.md +1 -1
  87. package/skills/abelien-workspace/references/harness-binding.md +2 -2
  88. package/skills/abelien-workspace/references/user-journey.md +10 -0
  89. package/skills/abelien-workspace/scripts/journey-next.mjs +38 -0
  90. package/skills/abelien-workspace/workflows/definitions/399ef372790b7330d0ef88ce5096e7f69943d4793de97b2ed6c25165a332612f.json +230 -0
  91. package/skills/abelien-workspace/workflows/definitions/591a7f21e0b0ad586e340163f7533ac910356091ba8d2a26e250c0acbc80cd8b.json +176 -0
  92. package/skills/abelien-workspace/workflows/definitions/d9f8c6cdf0b2c41cdeb1c72ac5d1e37d95a7caac3272d434a3c7e8483fc565de.json +150 -0
  93. package/skills/abelien-workspace/workflows/user-journey-hardware-algorithm.json +3 -2
  94. package/skills/abelien-workspace/workflows/user-journey-hardware-candidates.json +3 -2
  95. package/skills/abelien-workspace/workflows/user-journey-hardware.json +3 -2
package/.env.example CHANGED
@@ -33,6 +33,7 @@ ABELIEN_HARDWARE_SELECT_TOKEN=
33
33
  # Fixed prismer_then_volc policy; both Keys are trusted host-only.
34
34
  # Prismer query, URL-load and Parse are temporarily off (P2-05.02): enabled|disabled, default disabled.
35
35
  # When disabled, Volc searches directly and sources are read over public HTTPS (PDF/image unavailable).
36
+ # Pinned per web evidence: evidence started enabled keeps using Prismer, so keep PRISMER_API_KEY until it completes.
36
37
  ABELIEN_HARDWARE_WEB_PRISMER=disabled
37
38
  # Volc subscription/pay-as-you-go search Keys are distinct from Ark generation Keys.
38
39
  PRISMER_API_KEY=
package/README.md CHANGED
@@ -243,7 +243,7 @@ skill-lint 双向守卫核对声明与实际 import 一致,打包校验再确
243
243
 
244
244
  | 层 | 篇目 | 一句话 |
245
245
  | --- | --- | --- |
246
- | architecture | [state-machine](docs/architecture/state-machine.md) / [workflow-persistence](docs/architecture/workflow-persistence.md) / [httl](docs/architecture/httl.md) / [host-adapters](docs/architecture/host-adapters.md) / [generate3d](docs/architecture/generate3d.md) / [structure](docs/architecture/structure.md) / [question-card](docs/architecture/question-card.md) / [visualization-rsi](docs/architecture/visualization-rsi.md) | 双层 FSM · 定义即数据/实例即文件 · HTTL 协议 · claude-code/codex 双入口收敛 · 声明式 provider + 异步任务引擎 · 外观/结构协作边界 · 问题卡与 ACL 前端协议(contract-ready) · `ab --visual` 可视化与自进化收口 |
246
+ | architecture | [state-machine](docs/architecture/state-machine.md) / [workflow-persistence](docs/architecture/workflow-persistence.md) / [httl](docs/architecture/httl.md) / [host-adapters](docs/architecture/host-adapters.md) / [generate3d](docs/architecture/generate3d.md) / [structure](docs/architecture/structure.md) / [structure-worker](docs/architecture/structure-worker.md) / [structure-worker-protocol](docs/architecture/structure-worker-protocol.md) / [structure-cad-library](docs/architecture/structure-cad-library.md) / [structure-component-fit](docs/architecture/structure-component-fit.md) / [structure-worker-implementation](docs/architecture/structure-worker-implementation.md) / [question-card](docs/architecture/question-card.md) / [visualization-rsi](docs/architecture/visualization-rsi.md) | 双层 FSM · 定义即数据/实例即文件 · HTTL 协议 · claude-code/codex 双入口收敛 · 声明式 provider + 异步任务引擎 · 外观/结构协作边界 · 问题卡与 ACL 前端协议(contract-ready) · `ab --visual` 可视化与自进化收口 |
247
247
  | paradigms | [README](docs/paradigms/README.md) + development / testing / evaluation / self-optimization | 场景工件是唯一事实源:测试测零件、评估测整车、优化答归因 |
248
248
  | reference | [product-journey](docs/reference/product-journey.md) / [research-notes](docs/reference/research-notes.md) / [verification-method](docs/reference/verification-method.md) | PRD 旅程转译 · 基准研究与 EaaS 方法论 · 验证方法→场景判定器(每篇带 来源/取舍) |
249
249
  | mvp | [journey](docs/mvp/journey.md) / [coverage](docs/mvp/coverage.md) | 历史 MVP(已移除的 25 步及真实实录)· 四维覆盖矩阵 + 缺口账本 |
@@ -260,24 +260,17 @@ npm run branch:new|sync|push:ready|land # 分支纪律:切分支 / rebase
260
260
 
261
261
  ### 快照来源
262
262
 
263
- `abelien-clarify`(含 guide 的 shared-response-rules)的规则正文是外部编辑源的只读快照,
264
- 来源仓 URL、分支、完整 commit 与源内路径集中登记在 [tools/snapshot-sources.json](tools/snapshot-sources.json):
263
+ `abelien-clarify` 与 guide 的 `shared-response-rules.md` 自 2026-10-11(P2-13.02)起以本仓为唯一编辑源:
264
+ 直接在 `skills/abelien-clarify/` 与 `skills/abelien-guide/references/` 修改,不再从 abelienwebapplication
265
+ `docs/agent-prd/v2` 同步;那边的副本由宿主仓移除,避免两处规则漂移。理由见[意图路由](docs/architecture/intent-routing.md)
266
+ 的职责划分:规则只在 harness 维护,宿主只消费锁定的安装包。
265
267
 
266
- | 快照 | 来源 | 本地挂载(`checkout`,相对本仓) |
267
- | --- | --- | --- |
268
- | clarify | abelienwebapplication `develop` 的 `docs/agent-prd/v2` | `../abelienwebapplication`,`git fetch` 到锚定 commit 即可 |
269
-
270
- - `npm run sync:clarify` 默认从锚定 commit 的 git 对象同步,加 `--check` 只校验;`prepack` 跑它的 `--check`,
271
- 工作区里未提交的源改动不影响结果。硬件分类规则不再是外部快照:唯一正文就在
272
- `skills/abelien-hardware/references/hardware-selection/`,`prepack` 以 `validate:hardware-selection-snapshot` 校验其完整性。
273
- - 升级快照:设置 `ABELIEN_AGENT_PRD_DIR` 指向新版源目录同步,经内容责任人审过后,
274
- 再把 `snapshot-sources.json` 的 commit 改成新版源已提交的 commit(未提交的源不能作为锚点)。
275
- - 锚定 commit 在本地不可得时,`--check` 打印挂载提示并跳过(fail-open,未设 CI 兜底)。
276
- - 2026-10-03 核对:clarify 五个同步文件在锚点 `f370e49e`→`f269c4b0` 间逐字节一致,更新的是
277
- 锚点记录而非快照正文。必要问题的选项上限沿用 clarify 的 `result / questions / note` 契约
278
- (最多三个,可少于三个或纯自由输入),workspace 不设“恰好三个”下限;B3 的两项确认邀请与
279
- 确认门仍归 workspace。该对齐收口 [!2 note_545](https://git.abelien.cn/abelien/abelienharness/-/merge_requests/2#note_545),
280
- 不代表真实产品旅程已验收。
268
+ [tools/snapshot-sources.json](tools/snapshot-sources.json) 现在只登记 hardware-selection 的历史锚点。硬件分类规则的唯一正文在
269
+ `skills/abelien-hardware/references/hardware-selection/`,`prepack` 以 `validate:hardware-selection-snapshot` 校验其完整性。
270
+
271
+ - 必要问题的选项上限沿用 clarify 的 `result / questions / note` 契约(最多三个,可少于三个或纯自由输入),
272
+ workspace 不设“恰好三个”下限;B3 的两项确认邀请与确认门仍归 workspace。该对齐收口
273
+ [!2 note_545](https://git.abelien.cn/abelien/abelienharness/-/merge_requests/2#note_545),不代表真实产品旅程已验收。
281
274
 
282
275
  ## License
283
276
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abelien-harness",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "npx-installable multi-agent skill harness: generic kernel + pluggable domain packs",
5
5
  "type": "module",
6
6
  "engines": {
@@ -33,8 +33,7 @@
33
33
  "verify:hardware-search-index": "node skills/abelien-hardware/scripts/verify-search-index.mjs",
34
34
  "smoke:hardware-search": "node skills/abelien-hardware/scripts/smoke-search-index.mjs",
35
35
  "sync:hardware-selection": "node tools/sync-hardware-selection.mjs",
36
- "sync:clarify": "node tools/sync-clarify.mjs",
37
- "prepack": "npm run validate:hardware-selection-snapshot && node tools/sync-hardware-selection.mjs --check --strict && node tools/sync-clarify.mjs --check"
36
+ "prepack": "npm run validate:hardware-selection-snapshot && node tools/sync-hardware-selection.mjs --check --strict"
38
37
  },
39
38
  "license": "MIT"
40
39
  }
@@ -17,6 +17,7 @@ compatibility:
17
17
  - A1/A2:从文字和参考媒体提取外观方向,形成首版轮廓、比例、CMF、部位与辨识特征目标。
18
18
  - B1 修改:处理颜色、材质、表面语言、整体轮廓、局部造型、灯效和装饰件;保留未被修改的目标。
19
19
  - 还原参考产品:区分照片、视频、源 CAD、渲染与概念图,只对有证据的可见特征提出匹配要求。
20
+ - 型号识别属于推断:采用官网尺寸图、参数页等外部规格作为外观基准前,先从参考媒体量取可测比例(整体高宽比、标志性特征的相对位置),与规格换算的比例比较;超出测量误差时记为型号冲突,列出各候选及依据,不以最先找到的资料为准。只有一个视角时,比较只覆盖该视角可见的尺寸。向用户展示识别结果与来源不等于要求确认:高置信时展示后继续,冲突时保留候选供切换。
20
21
  - structure 返回模型和实际渲染后:按匹配视角执行视觉审查;工程冲突则共同形成可见取舍,必要时由 guide 交给用户决定。
21
22
  - 同版视觉结果和外观提案交回 workspace 采用;本包不创建自己的作品版本或确认流程。
22
23
 
@@ -33,8 +33,16 @@ compatibility:
33
33
  type Envelope =
34
34
  | {shape: "box", max_mm: [number, number, number], min_mm?: [number, number, number], wall_mm?: {minimum: number, estimate?: number}}
35
35
  | {shape: "cylinder", max_diameter_mm: number, max_height_mm: number, min_diameter_mm?: number, min_height_mm?: number, wall_mm?: {minimum: number, estimate?: number}};
36
+ type Intent = {
37
+ kind: "restore" | "create" | "modify";
38
+ source: "user" | "system";
39
+ keep: string[]; // 保留的来源产品特征;create 为空
40
+ changeable: string[]; // 允许重新设计的特征
41
+ depth?: "appearance" | "assembly" | "functional"; // 目标深度:外观模型、可装配结构、功能样机
42
+ };
36
43
  Requirements = {
37
44
  context?: {text: string, source: "user" | "system"};
45
+ intent?: Intent; // 交回的 result 必须带;只有历史需求可能缺
38
46
  reaction: {id: string, text: string, source: "user" | "system"}[];
39
47
  limits: {id: string, text: string, source: "user" | "system", strength: "hard" | "preference", envelope?: Envelope}[];
40
48
  }
@@ -72,6 +80,8 @@ ID 是不透明标识,不能按数组位置重新编号;新 ID 不得与现
72
80
 
73
81
  没有明确尺寸要求,不主动索要尺寸;“必须塞进这个格子”缺尺寸且挡住选型时才问。品类默认和器件分类用输入中的数据,不在 skill 固化型号、价位或默认值表。工程资料缺失交给总控补充,不让用户提供接口、料号或测试证明。
74
82
 
83
+ **意图与目标深度。** 用户给出图片、链接或产品名,或说“还原/复刻/同款/拆解/这款”时,先按原话判定本次意图:还原现成产品(restore)、创造新产品(create)或基于现成产品修改(modify)。判定依据是原话,不是输入形式:商品图配“参考这个风格做一个”是 create;“参考某产品,再加一个屏幕”是 modify。判定结果写入 `result.intent`:`kind` 为意图;`keep` 列出保留的来源产品特征(restore 默认含外形尺寸与比例,modify 逐项列出,create 没有来源产品,必须为空);`changeable` 列出允许重新设计的特征;`depth` 为还原或修改的目标深度(appearance 外观模型、assembly 可装配结构、functional 功能样机),原话推断不出时省略。原话直接说明意图(如“还原这款”“参考它再加一个屏幕”“做一个新的”),或者根本没有提到任何现成产品时,`source` 记 user;需要从图片、链接或措辞推断时记 system,并在 note 写明依据。宿主按 `intent.kind` 安排旅程,参考研究的输入也从这里取;它不单独构成首版之前的问题,以系统假设展示,用户随时可改,改动就是一次需求修订。来源产品的功能只能以 source=system 写入 reaction,并注明来自来源产品;“原产品有这个功能”不等于“本次要实现”,按目标深度决定是否纳入,外观模型不纳入语音等功能实现。
84
+
75
85
  只提出现在必须由用户决定的问题:闭环歧义、落点歧义或挡住当前选择的硬要求缺值/相互矛盾;缺少 PRD 字段本身不是问题。提问前能在 note 中说明:不回答会卡住哪个选择、为何现有事实和授权不足以代定。默认一题,相互独立才合并,最多三题;每题最多三个实质选项,表达遵循已注入的共用规则。用户意图选项可以来自原话支持的不同解释,不冒充已有硬件或工程可行候选。
76
86
 
77
87
  暂停追问时不返回新问题,在 note 保留当前缺口及影响;用户对某项说“别帮我猜”时停用该项默认。未答、跳过或催促不构成授权。卡片部分回答只采用明确答复,其余根据本轮情况重新判断;不把未选项当作撤回。每轮重新判断,不保存问题队列。
@@ -82,7 +92,7 @@ ID 是不透明标识,不能按数组位置重新编号;新 ID 不得与现
82
92
  { result?: Requirements; questions?: {text: string, options?: string[]}[]; note?: string }
83
93
  ```
84
94
 
85
- result 是完整的建议需求,保留未修改部分;没有变更就省略。questions 是本轮必要用户决定;已确定的事实可以与问题同时返回,不预先写入未选答案。note 说明假设及依据、问题阻塞的选择、咨询结论、撤回对象或上下文缺口;没有 result 时至少提供非空 questions 或 note。不返回 null、检查结果、工具信封或额外的 ready / status 字段。
95
+ result 是完整的建议需求,保留未修改部分,必须带 intent;没有变更就省略。questions 是本轮必要用户决定;已确定的事实可以与问题同时返回,不预先写入未选答案。note 说明假设及依据、问题阻塞的选择、咨询结论、撤回对象或上下文缺口;没有 result 时至少提供非空 questions 或 note。不返回 null、检查结果、工具信封或额外的 ready / status 字段。
86
96
 
87
97
  ## 本地校验
88
98
 
@@ -96,7 +106,7 @@ python3 scripts/check_result.py - --current current-requirements.json
96
106
 
97
107
  `proposal.json` 是上述 SkillResult;`current-requirements.json` 是当前 Requirements 本身,不是整个草稿。脚本只读输入、向标准输出写诊断,不覆盖文件。返回 `valid / errors / changes / review`;这份报告供修正与复核,**不是 SkillResult,也不是工程 Check**。退出码 0 表示结构合法,1 表示契约错误,2 表示文件或 JSON 读取失败。
98
108
 
99
- - 检查字段、类型、来源、限制强度、ID 唯一性及题目数量;新增/修改系统假设和撤回项须有 note。
109
+ - 检查字段、类型、来源、限制强度、ID 唯一性、intent 结构及题目数量;交回的 result 缺 intent 即为错误,当前需求可以是缺 intent 的历史版本;新增/修改系统假设(含 source=system 的 intent)和撤回项须有 note。
100
110
  - 提供当前需求时按稳定 ID 列出前后差异,提醒复核删除、来源和硬要求变化;保留前后值,不自动修复或采用。
101
111
  - 先修正 errors,再依据原话核对 changes / review。`valid=true` 不证明理解正确、得到授权、没有阻塞问题或可以确认;脚本不会验证原话真假、给问题排序或判断工程可行性。
102
112
 
@@ -10,6 +10,7 @@
10
10
  {
11
11
  "result": {
12
12
  "context": {"text": "放在桌面上", "source": "user"},
13
+ "intent": {"kind": "create", "source": "user", "keep": [], "changeable": []},
13
14
  "reaction": [{"id": "r-turn", "text": "有人靠近时转头看向对方", "source": "user"}],
14
15
  "limits": []
15
16
  }
@@ -26,6 +27,7 @@
26
27
  {
27
28
  "result": {
28
29
  "context": {"text": "放在桌面上", "source": "user"},
30
+ "intent": {"kind": "create", "source": "user", "keep": [], "changeable": []},
29
31
  "reaction": [{"id": "r-remind", "text": "每天提醒我吃药", "source": "user"}],
30
32
  "limits": []
31
33
  },
@@ -42,6 +44,7 @@
42
44
  ```json
43
45
  {
44
46
  "context": {"text": "放在桌面上", "source": "user"},
47
+ "intent": {"kind": "create", "source": "user", "keep": [], "changeable": []},
45
48
  "reaction": [
46
49
  {"id": "r-remind", "text": "每天八点提醒吃药,按下按钮后停止", "source": "user"},
47
50
  {"id": "r-recognize", "text": "认出我妈时向她问好", "source": "user"}
@@ -56,6 +59,7 @@
56
59
  {
57
60
  "result": {
58
61
  "context": {"text": "放在桌面上", "source": "user"},
62
+ "intent": {"kind": "create", "source": "user", "keep": [], "changeable": []},
59
63
  "reaction": [{"id": "r-remind", "text": "每天九点提醒吃药,按下按钮后停止", "source": "user"}],
60
64
  "limits": [{"id": "l-network", "text": "不能联网", "source": "user", "strength": "hard"}]
61
65
  },
@@ -87,6 +91,24 @@
87
91
  - “确认,就这样,但不要摄像头”:先交明确需求变更,确认由总控针对新版本重新处理。
88
92
  - 过期卡的答复不直接更新当前卡。用户在同一消息里给出明确独立的新要求时可单独整理,note 交回卡片版本问题;不能假装旧卡已经消费。
89
93
 
94
+ ## 6. 还原现成产品:意图写进需求
95
+
96
+ 当前需求为空。用户上传一张智能音箱照片,说:“帮我根据图片还原小爱音箱。”
97
+
98
+ ```json
99
+ {
100
+ "result": {
101
+ "context": {"text": "还原图片中的小爱音箱", "source": "user"},
102
+ "intent": {"kind": "restore", "source": "user", "keep": ["外形尺寸与比例", "顶部按键布局", "底部灯环"], "changeable": [], "depth": "appearance"},
103
+ "reaction": [{"id": "r-voice", "text": "语音唤醒与播放(来源产品功能)", "source": "system"}],
104
+ "limits": []
105
+ },
106
+ "note": "原话“还原小爱音箱”直接说明意图,记 restore、source=user,外形尺寸与比例默认锁定。目标深度没有说明,按“还原”的通常含义推断为外观模型(depth=appearance)。来源产品的语音功能只作为候选写入 reaction(source=system),外观模型不实现它。型号待参考研究识别,不在这里猜。"
107
+ }
108
+ ```
109
+
110
+ `intent.depth` 是推断时,依据写在 note 里,不单独发问。用户之后说“要能装起来”,改为 `assembly`,这就是一次需求修订。
111
+
90
112
  ## 验证方式
91
113
 
92
114
  将上述当前需求和输出分别保存为临时 JSON,运行 `scripts/check_result.py`;检查 ID、完整快照及差异。单元测试只覆盖这些确定性约束。模型验收还要核对原话完整性、假设依据、提问必要性、卡片上下文与真实授权;本文件未宣称模型已经通过这些案例。
@@ -13,6 +13,10 @@ MAX_QUESTIONS = 3
13
13
  MAX_OPTIONS = 3
14
14
  SOURCES = ("user", "system")
15
15
  STRENGTHS = ("hard", "preference")
16
+ INTENT_KINDS = ("restore", "create", "modify")
17
+ DEPTHS = ("appearance", "assembly", "functional")
18
+ MAX_FEATURES = 20
19
+ MAX_FEATURE_CHARS = 500
16
20
 
17
21
 
18
22
  def nonempty_string(value: Any) -> bool:
@@ -91,9 +95,38 @@ def check_envelope(value: Any, path: str, errors: list) -> None:
91
95
  issue(errors, f"{path}.wall_mm.estimate", "估算壁厚必须有限且不小于保证下界")
92
96
 
93
97
 
94
- def check_requirements(value: Any, path: str, errors: list) -> None:
95
- if not object_fields(value, ("reaction", "limits"), ("context",), path, errors):
98
+ def check_features(value: Any, path: str, errors: list) -> None:
99
+ if not isinstance(value, list) or len(value) > MAX_FEATURES:
100
+ issue(errors, path, f"必须是数组,至多 {MAX_FEATURES} 项")
96
101
  return
102
+ for index, item in enumerate(value):
103
+ if not nonempty_string(item) or len(item) > MAX_FEATURE_CHARS:
104
+ issue(errors, f"{path}[{index}]", f"必须是 1–{MAX_FEATURE_CHARS} 字的非空字符串")
105
+
106
+
107
+ def check_intent(value: Any, path: str, errors: list) -> None:
108
+ """Intent drives host routing before any reference research; clarify is its only producer."""
109
+ if not object_fields(value, ("kind", "source", "keep", "changeable"), ("depth",), path, errors):
110
+ return
111
+ if value.get("kind") not in INTENT_KINDS:
112
+ issue(errors, f"{path}.kind", "必须是 restore、create 或 modify")
113
+ if value.get("source") not in SOURCES:
114
+ issue(errors, f"{path}.source", "必须是 user 或 system")
115
+ if "depth" in value and value["depth"] not in DEPTHS:
116
+ issue(errors, f"{path}.depth", "必须是 appearance、assembly 或 functional")
117
+ check_features(value.get("keep"), f"{path}.keep", errors)
118
+ check_features(value.get("changeable"), f"{path}.changeable", errors)
119
+ if value.get("kind") == "create" and value.get("keep"):
120
+ issue(errors, f"{path}.keep", "create 没有来源产品,keep 必须为空")
121
+
122
+
123
+ def check_requirements(value: Any, path: str, errors: list, require_intent: bool = False) -> None:
124
+ if not object_fields(value, ("reaction", "limits"), ("context", "intent"), path, errors):
125
+ return
126
+ if "intent" in value:
127
+ check_intent(value["intent"], f"{path}.intent", errors)
128
+ elif require_intent:
129
+ issue(errors, f"{path}.intent", "缺少必需字段:按原话判定 restore/create/modify")
97
130
  if "context" in value:
98
131
  context = value["context"]
99
132
  location = f"{path}.context"
@@ -156,6 +189,8 @@ def requirement_items(requirements: dict) -> dict:
156
189
  items = {}
157
190
  if "context" in requirements:
158
191
  items[("context", None)] = requirements["context"]
192
+ if "intent" in requirements:
193
+ items[("intent", None)] = requirements["intent"]
159
194
  for group in ("reaction", "limits"):
160
195
  for entry in requirements[group]:
161
196
  items[(group, entry["id"])] = entry
@@ -183,7 +218,7 @@ def validate(proposal: Any, current: Any = None) -> dict:
183
218
  if not object_fields(proposal, (), ("result", "questions", "note"), "$", errors):
184
219
  return report
185
220
  if "result" in proposal:
186
- check_requirements(proposal["result"], "$.result", errors)
221
+ check_requirements(proposal["result"], "$.result", errors, require_intent=True)
187
222
  if "questions" in proposal:
188
223
  check_questions(proposal["questions"], errors)
189
224
  if "note" in proposal:
@@ -17,6 +17,7 @@ SCRIPT = Path(__file__).with_name("check_result.py")
17
17
  def current_requirements():
18
18
  return {
19
19
  "context": {"text": "放在桌面", "source": "user"},
20
+ "intent": {"kind": "create", "source": "user", "keep": [], "changeable": []},
20
21
  "reaction": [
21
22
  {"id": "r-remind", "text": "每天八点提醒吃药,按下按钮后停止", "source": "user"},
22
23
  {"id": "r-turn", "text": "有人靠近时转头看向对方", "source": "user"},
@@ -71,12 +72,42 @@ class ContractTests(unittest.TestCase):
71
72
 
72
73
  def test_partial_requirements_and_questions_can_coexist(self):
73
74
  proposal = {
74
- "result": {"reaction": [], "limits": []},
75
+ "result": {"intent": {"kind": "create", "source": "user", "keep": [], "changeable": []},
76
+ "reaction": [], "limits": []},
75
77
  "questions": [{"text": "你希望它怎样陪伴?", "options": ["和我说话", "提醒日常安排"]}],
76
78
  "note": "行为方向会改变当前选型,尚无依据代定。",
77
79
  }
78
80
  self.assertTrue(validate(proposal, {"reaction": [], "limits": []})["valid"])
79
81
 
82
+ def test_result_must_state_intent_but_current_may_predate_it(self):
83
+ result = current_requirements()
84
+ del result["intent"]
85
+ report = validate({"result": result})
86
+ self.assertFalse(report["valid"])
87
+ self.assertTrue(any(item["path"] == "$.result.intent" for item in report["errors"]))
88
+ old = copy.deepcopy(result)
89
+ added = current_requirements()
90
+ added["intent"] = {"kind": "restore", "source": "system", "keep": ["外形尺寸与比例"], "changeable": []}
91
+ self.assertFalse(validate({"result": added}, old)["valid"], "a system intent needs a note")
92
+ report = validate({"result": added, "note": "原话“还原小爱音箱”且点名型号,判定为 restore,外形锁定。"}, old)
93
+ self.assertTrue(report["valid"], report["errors"])
94
+ self.assertEqual([c["field"] for c in report["changes"]], ["intent"])
95
+
96
+ def test_intent_shape_is_strict(self):
97
+ base = {"kind": "modify", "source": "user", "keep": ["机身外形"], "changeable": ["顶部加屏幕"], "depth": "assembly"}
98
+ result = current_requirements()
99
+ result["intent"] = base
100
+ self.assertTrue(validate({"result": result})["valid"])
101
+ bad = [{**base, "kind": "copy"}, {**base, "source": "agent"}, {**base, "depth": "full"},
102
+ {**base, "keep": "机身外形"}, {**base, "keep": [""]}, {**base, "keep": ["x"] * 21}, {**base, "changeable": ["x" * 501]},
103
+ {**base, "extra": True}, {k: v for k, v in base.items() if k != "changeable"},
104
+ {"kind": "create", "source": "user", "keep": ["机身外形"], "changeable": []}, None]
105
+ for intent in bad:
106
+ with self.subTest(intent=intent):
107
+ result = current_requirements()
108
+ result["intent"] = intent
109
+ self.assertFalse(validate({"result": result})["valid"])
110
+
80
111
  def test_consultation_does_not_create_a_diff(self):
81
112
  report = validate({"note": "能否认出家人需要选型证据,当前不增加识别要求。"}, current_requirements())
82
113
  self.assertTrue(report["valid"])
@@ -23,7 +23,7 @@ compatibility:
23
23
  5. **选择下一步。** 依据 workspace 的[完整旅程](../abelien-workspace/references/user-journey.md)核对推进条件,按其能力绑定选择领域 skill、workflow 或产品服务。目标旅程先复用符合需求的真实 CAD;没有现成 CAD 时依宿主阶段生成概念图并自动选型、绑定原图、构建 CAD/审查;实际安装版本不支持时报告能力缺口,不调用未注册工具。必要问题发持久卡并承接原任务。可选交互邀请遵守宿主实际展示门(核对实际版本的 3D 加载与适用审查),不得因需求足够就抢先覆盖必要问题卡;按旅程 §4.1 的通用目标告知随时可收尾,不插入需求版 PRD 展示关卡。点击“随时收尾”后按 §4.2 调用宿主成果体验模式,核对仿真或 Three.js 行为演示能力,并保留 PRD 查看/导出入口。调用前读对应 skill;能力不存在时报告具体缺口。实际能力返回后回到当前事实重判,不自行选择型号、算法或编造工程结论。
24
24
  6. **向用户交代结果。** 根据真实产物说明本次变化、当前缺口和下一步,区分 PRD 已登记的需求与仍待实现/检查的方案;没有写入回执就明确尚未保存。首版展示、必要取舍及确认邀请的时机均采用 workspace 规则;表达方式采用共用表达规则。
25
25
 
26
- 图片、链接或“做同款”仍走同一入口,不自动开启另一条整机还原流程。改轮廓、颜色、材质、表面语言或参考匹配时调用 [appearance](../abelien-appearance/SKILL.md);空间约束、布局、装配、Build123d 或几何检查调用 [structure](../abelien-structure/SKILL.md)。外壳形状同时涉及两者时,由 appearance 定义目标、structure 实现并回传实际渲染、appearance 做视觉验收。只把必须由用户决定的差异交回用户,不让内部视觉审查成为额外的必答关卡。
26
+ 图片、链接或“做同款”仍走同一入口,不自动开启另一条整机还原流程。入口按本轮意图调度:还原现成产品(restore)、创造新产品(create)或基于现成产品修改(modify)共用同一旅程,差别只在首先展示什么、参考资料怎么用、哪些特征锁定,依据是原话而不是输入形式,详见[意图路由](../../docs/architecture/intent-routing.md)。还原与改现成先识别产品,展示原产品图及来源后继续;新创先参考相似产品再出概念图,用户已有明确设计图时直接沿用。意图、目标深度与识别结果在首版之前不单独发问:按推断继续,作为系统假设展示并可随时修改;型号识别冲突时保留候选,用户切换即视为需求修订。改轮廓、颜色、材质、表面语言或参考匹配时调用 [appearance](../abelien-appearance/SKILL.md);空间约束、布局、装配、Build123d 或几何检查调用 [structure](../abelien-structure/SKILL.md)。外壳形状同时涉及两者时,由 appearance 定义目标、structure 实现并回传实际渲染、appearance 做视觉验收。只把必须由用户决定的差异交回用户,不让内部视觉审查成为额外的必答关卡。
27
27
 
28
28
  用户要让局部外观件更有趣时,调用 appearance 的[局部自由造型流程](../abelien-appearance/references/local-component-edit.md)。默认由当前部件图或视角渲染生成单张修改参考,上传图可选,四视图只作增强;整机和未选部件保留。先解析真实目标和基线,不能把一句“改成猫头”变成全产品重新生成。
29
29
 
@@ -20,9 +20,9 @@ allowed-tools:
20
20
 
21
21
  ## 与结构协作
22
22
 
23
- 接收同级 [structure](../abelien-structure/SKILL.md) 提出的尺寸、开口、安装及运动空间约束;约束只是选型输入,不授权结构域自行换型号。选定后交出真实组件 ID、数量、版本和已有 CAD/尺寸依据;缺少几何资产须明示,BOM 抽取不代表 CAD 已逐件拆出。装不下或接口冲突时按原要求回到本包比较替换方案,不能悄悄缩放实体组件。
23
+ 接收同级 [structure](../abelien-structure/SKILL.md) 提出的尺寸、开口、安装及运动空间约束;约束只是选型输入,不授权结构域自行换型号。结构分区的可用空间写在槽位 `fit_envelope`(`envelope`、`basis`、可选 `zone`),只作用于该槽位:`locked` 与用户硬尺寸上限同权,`inferred` 只让放不下的件排在后面。选定后交出真实组件 ID、数量、版本和已有 CAD/尺寸依据;缺少几何资产须明示,BOM 抽取不代表 CAD 已逐件拆出。装不下或接口冲突时按原要求回到本包比较替换方案,不能悄悄缩放实体组件。
24
24
 
25
- “还原参考产品”另保留有来源的原始 BOM 与所选方案逐项对照,区分原件、替代件与未知;替代的可见外观影响交 appearance,空间/接口影响交 structure,行为影响交 algorithm。没有原始证据时可以提出候选,不声称候选即原机。不要把额外研究记录塞进未支持的选型 schema;作为关联证据交 workspace。
25
+ “还原参考产品”的产品识别、规格与拆解资料由同级 [reference](../abelien-reference/SKILL.md) 收集并核验;本包读取其 final 证据,保留有来源的原始 BOM 与所选方案逐项对照,区分原件、替代件与未知;替代的可见外观影响交 appearance,空间/接口影响交 structure,行为影响交 algorithm。没有原始证据时可以提出候选,不声称候选即原机。不要把额外研究记录塞进未支持的选型 schema;作为关联证据交 workspace。
26
26
 
27
27
  ## 硬件选型
28
28
 
@@ -57,9 +57,9 @@ An ordinary failure of an authorized public read is recorded as an unavailable s
57
57
 
58
58
  ## 结构化尺寸粗筛
59
59
 
60
- 数据库尺寸快照是尺寸编辑源;生成与迁移见 [search-index-format](references/search-index-format.md)。搜索先对全部分类索引做保守筛选再取 Top-K,unknown/estimate 保留。使用候选的 dimension_check 和来源,不从正文猜 dimensions,不把 clear、占用比例、用户确认或 CAD 可用性当作工程验证;all_indexed_candidates_rejected 只指已评估分类。risk 候选排在可装/未知候选之后且不占 Top-K 名额:有满足功能与硬约束的非 risk 候选时选它;只有都不满足才选 risk 件,并在 selection_reason 写明可能放不下的原因。正式选择会重算尺寸,incompatible 必须换候选;只有 decision_required(用户硬尺寸上限+实测/datasheet 尺寸,且没有轴对齐装法)返回待确认,交用户在换件与放宽尺寸间取舍;unknown 与其他 risk 只随 dimension_check 提示,不阻塞。
60
+ 数据库尺寸快照是尺寸编辑源;生成与迁移见 [search-index-format](references/search-index-format.md)。搜索先对全部分类索引做保守筛选再取 Top-K,unknown/estimate 保留。使用候选的 dimension_check 和来源,不从正文猜 dimensions,不把 clear、占用比例、用户确认或 CAD 可用性当作工程验证;all_indexed_candidates_rejected 只指已评估分类。risk 候选排在可装/未知候选之后且不占 Top-K 名额:有满足功能与硬约束的非 risk 候选时选它;只有都不满足才选 risk 件,并在 selection_reason 写明可能放不下的原因。正式选择会重算尺寸,incompatible 必须换候选;只有 decision_required(用户硬尺寸上限或 locked 分区+实测/datasheet 尺寸,且没有轴对齐装法)返回待确认,交用户在换件与放宽尺寸间取舍;分区原因带 `limit_id=fit:<slot_id>` 与 `zone`。locked 分区下全部候选被拒时,先在原要求下换件,再由 structure 调整骨架并给出新的 `fit_envelope`,改外形由用户决定;unknown 与其他 risk 只随 dimension_check 提示,不阻塞。
61
61
 
62
62
  ## 电气兼容
63
63
 
64
- 多型号选型的电源与接口由确定性检查先判(星形拓扑,主控为中心),规则见 [selection-service-contract](../abelien-hardware-selection/references/selection-service-contract.md#电气兼容与兼容报告)。可信数据证明总线缺失或数量不足时必须替换一侧;`ELECTRICAL_SUPPLY_GAP` 表示要补电源或稳压件;结构、散热、软件只作提示。不要从正文猜电压或接口,电气数据只来自候选的 `electrical` 字段。
64
+ 多型号选型的电源与接口由确定性检查先判(星形拓扑,主控为中心),规则见 [selection-service-contract](../abelien-hardware-selection/references/selection-service-contract.md#电气兼容与兼容报告)。数据没记录不等于不需要:缺电流、缺供电输入、缺逻辑电平、未选电源件都只得 unknown,不能当作通过。星形假设下主控总线缺失或数量不足不是证明(扩展板、驱动板后的外设会被算到主控上),记 `ELECTRICAL_BUS_UNCONFIRMED` 待确认,可换一侧或补扩展件并说明;`ELECTRICAL_SUPPLY_GAP` 表示要补电源或稳压件;电气检查不再以 VALIDATION 拒绝。结构、散热、软件只作提示,未给有效引用时列入结果的 `advisories`。不要从正文猜电压或接口,电气数据只来自候选的 `electrical` 字段。
65
65
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abelien-hardware",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Hardware library recall, English Volc query and source evidence (Prismer off by default) with envelope candidates; formal selection remains hardware.select",
5
5
  "resources": [
6
6
  "hardware-recall",
@@ -64,7 +64,9 @@ source 与 output 须是互不嵌套的目录。每个 Wiki record_id 必须命
64
64
 
65
65
  `requirements.limits[].envelope` 支持 box.max_mm/min_mm 与 cylinder.max_diameter_mm/max_height_mm/min_diameter_mm/min_height_mm;保留同一条 text/id/source/strength。范围按最大外形减去两倍明确保证的 wall_mm.minimum(缺省 0)求最大腔体;estimate 不参与硬拒绝。所有尺寸有限正数,范围有序,无混合形状字段;最低壁厚必须留出正的最大腔体。较小范围端或估算壁厚不可行不扩大硬拒绝范围。
66
66
 
67
- 只有 user+hard 边界和 measured/datasheet 组件可硬拒绝。solid_box 可用最小边、对角线、实体体积三项必要条件;任意 bounding_box 只可用最大支撑跨度超过容器最大弦长,不能用包围盒最小边、对角线或体积。六轴置换、圆柱 footprint 只是可装下粗筛,失败是 risk,未知形体是 unknown。clear 只表示单件粗筛,不代表多件装配、CAD 或工程通过。
67
+ 槽位 `fit_envelope` 是结构分区给该槽位的额外容器,形状规则同上:basis=locked 视同 user+hard 边界,basis=inferred 视同非硬边界;原因以 `limit_id=fit:<slot_id>` 标出,有 zone 时带 `zone`。没有 fit_envelope 的槽位判定结果不变。
68
+
69
+ 只有 user+hard 边界(含 locked 分区)和 measured/datasheet 组件可硬拒绝。solid_box 可用最小边、对角线、实体体积三项必要条件;任意 bounding_box 只可用最大支撑跨度超过容器最大弦长,不能用包围盒最小边、对角线或体积。六轴置换、圆柱 footprint 只是可装下粗筛,失败是 risk,未知形体是 unknown。clear 只表示单件粗筛,不代表多件装配、CAD 或工程通过。
68
70
 
69
71
 
70
72
  新域包 hardware 1.2.0 / selection 0.5.0 / workspace 1.3.0 使用此加法契约。已有持久结果只读原样;旧 workflow JSON 摘要不固定模块代码,因此不可用旧定义配新包宣称旧行为仍相同。宿主必须固定完整发布包,为尺寸筛选开新运行,不能原地改写旧 CAD 输入或已失败/未知副作用。
@@ -73,7 +75,7 @@ source 与 output 须是互不嵌套的目录。每个 Wiki record_id 必须命
73
75
 
74
76
  生成 Wiki 头部可带两行严格内联 JSON:
75
77
 
76
- - `electrical`:`{schema:"abelien.component-electrical.v1", basis, power_inputs, power_outputs, buses}`。`basis` 为 measured / datasheet / estimate / unknown;供电项为 `{id, voltage_v:[min,max], connector?, max_current_a?}`,id 与连接器为小写短横线标识,电压为有序正数;总线项为 `{type, role, count, logic_level_v?}`,type 取 i2c / spi / uart / usb / i2s / can / pwm / gpio / adc,role 为 controller 或 peripheral。不接受多余字段。
78
+ - `electrical`:`{schema:"abelien.component-electrical.v1", basis, power_inputs, power_outputs, buses}`。`basis` 为 measured / datasheet / estimate / unknown;供电项为 `{id, voltage_v:[min,max], connector?, max_current_a?}`,id 与连接器为小写短横线标识,电压为有序正数;输入项的 `max_current_a` 是该输入端最大取用电流(含经本件输出转供的负载),输出项的是该电压轨最大供给电流,缺失按未记录处理;`power_inputs` 为空表示未记录,不表示无需供电;总线项为 `{type, role, count, logic_level_v?}`,type 取 i2c / spi / uart / usb / i2s / can / pwm / gpio / adc,role 为 controller 或 peripheral。不接受多余字段。
77
79
  - `electrical_source`:`{component_id, revision, snapshot_id, evidence_ref?}`,`revision` 是 `{component_id, ...electrical}` 规范 JSON 的 SHA-256。
78
80
 
79
81
  只有两行都在、源绑定一致的记录才进入候选;只有 `electrical` 没有来源时按未知处理,摘要不符则拒绝。组件数据库是电气数据唯一编辑源,生产方(P2-04.03)沿尺寸快照同一条只读导出与构建管道写入,禁止在 Wiki 上手改。占位、默认或无出处的数据只能标 estimate 或 unknown。
@@ -2,11 +2,11 @@
2
2
 
3
3
  Read the [architecture](../../../docs/architecture/hardware-online-fallback.md) and [official API inventory](../../../docs/architecture/prismer-api-inventory.md). The trusted current-workdir reader/checkpoint owns all source bodies. A model cannot submit URL/key/provider/command/source content or default envelopes.
4
4
 
5
- hardware.web-search input is exactly {requirements,slots,recall_id,unresolved_slots}. Each unresolved item is {slot_id,reason,query,count?}; every zero-candidate slot must occur and all-unsuitable candidates need a reason. Query is nonempty; Count is a positive integer. New plans (P2-05.02) must write the query in English (no Chinese/Japanese/Korean characters; keep model numbers and manufacturer names) and Count is 1–5, default 5; validateNewHardwareWebPlan rejects violations with VALIDATION. A plan already frozen in saved evidence keeps the rules it was accepted under (validateFrozenHardwareWebPlan). Full selectionRequestDigest preserves original requirements, quantities and constraints. unresolved_slots lists only slots needing search: omit suitable slots entirely, never submit empty query/count0 placeholders; all suitable means []. Empty unresolved list with suitable candidates uses zero network/credentials. The shared validateHardwareWebPlan({input,readResource}) in scripts/web-search-plan.mjs returns {source} after the original-recall, query/count and new-plan checks used by the preflight helper; search applies the new-plan checks only when the plan is not the one frozen in existing evidence, before configuration, checkpoints or provider calls. It does not filter or default invalid entries.
5
+ hardware.web-search input is exactly {requirements,slots,recall_id,unresolved_slots,plan_rules?}. plan_rules is optional and its only valid value is the literal volc-english-5 (any other value is VALIDATION); the current hardware workflow definitions write it, and it does not enter the evidence identity or plan digest. Each unresolved item is {slot_id,reason,query,count?}; every zero-candidate slot must occur and all-unsuitable candidates need a reason. Query is nonempty; Count is a positive integer. New plans (P2-05.02) opt in with plan_rules=volc-english-5: they must write the query in English (no Chinese/Japanese/Korean characters; keep model numbers and manufacturer names) and Count is 1–5, default 5; validateNewHardwareWebPlan rejects violations with VALIDATION. The English check is a heuristic that rejects only Han, Hiragana, Katakana and Hangul characters; it does not prove the query is English. A plan already frozen in saved evidence keeps the rules it was accepted under (validateFrozenHardwareWebPlan). An input without plan_rules comes from a run pinned to a definition archived before P2-05.02 (whose review instruction still asks for Count 10 and any language); it keeps the pre-P2-05.02 rules (shape check only, Volc Count 1–50 at dispatch), so such a run is not invalidated by the upgrade. Full selectionRequestDigest preserves original requirements, quantities and constraints. unresolved_slots lists only slots needing search: omit suitable slots entirely, never submit empty query/count0 placeholders; all suitable means []. Empty unresolved list with suitable candidates uses zero network/credentials. The shared validateHardwareWebPlan({input,readResource}) in scripts/web-search-plan.mjs returns {source} after the original-recall, query/count and new-plan checks used by the preflight helper; the preflight helper always applies the new-plan checks, so a host still running it for a run pinned to an old definition must handle that run itself (outside the harness); search applies the new-plan checks only when plan_rules opts in and the plan is not the one frozen in existing evidence, before configuration, checkpoints or provider calls. It does not filter or default invalid entries.
6
6
 
7
- The trusted fixed policy is prismer_then_volc. Prismer is temporarily off (P2-05.02): unless the trusted host sets ABELIEN_HARDWARE_WEB_PRISMER=enabled (default disabled; any other value is HARDWARE_WEB_CONFIG), every prismer attempt (query, context_url, parse) is saved unsent as status=unavailable, reason=prismer_disabled with no receipt, and that marker authorizes the same group's volcengine/query or direct_https/direct_page attempt; PDF/image sources then stay unavailable (parse_unavailable). Volc requests use Count = plan count or 5; Volc receipts saved by 0.4.0 keep their frozen default Count 10 request. When enabled, the fixed Prismer query POST /api/context/load sends inputType=query, search.topK and explicit return.topK/format=both. URL-load sends inputType=url/format=both; only result.raw supplies source text. cached/cachedAt/meta and complete raw/response/summary/cost/time are retained. HQCC-only is unavailable. PDF/image suffixes from original query URLs use actual Parse {url,mode:fast,output:markdown,wait:true}. Synchronous requestId is nullable; async taskId is persisted before fixed status/result GET. Callback/endpoints are ignored. Source text is API raw/parsed markdown, not original HTTP bytes or a verified realtime/final URL.
7
+ The trusted fixed policy is prismer_then_volc. Prismer is temporarily off (P2-05.02): unless the trusted host sets ABELIEN_HARDWARE_WEB_PRISMER=enabled (default disabled; any other value is HARDWARE_WEB_CONFIG), every prismer attempt (query, context_url, parse) is saved unsent as status=unavailable, reason=prismer_disabled with no receipt, and that marker authorizes the same group's volcengine/query or direct_https/direct_page attempt; PDF/image sources then stay unavailable (parse_unavailable). Volc requests use Count = plan count or 5; Volc receipts saved by 0.4.0 keep their frozen default Count 10 request. The switch belongs to one evidence, not to the host environment at resume time: when the evidence first needs provider IO it records prismer_switch=enabled|disabled, and every later resume of that evidence uses the recorded value. Evidence saved before the pin (0.4.0/0.5.0) is inferred from its own attempts: any prismer_disabled marker means disabled, any other Prismer attempt (sent or unsent missing Key) means enabled, and with neither the current host value is used and pinned at once; a mix of both fails closed with HARDWARE_WEB_RECONCILIATION_REQUIRED. Integrity rejects a pin outside enabled|disabled or one that contradicts the attempts, and a later checkpoint cannot rewrite it. Consequently disabling the host switch does not stop evidence already pinned enabled: its pending Parse keeps the fixed status/result GET and its remaining frozen work still POSTs Prismer, and prismer-context-parse-v1 evidence always GETs its original task. Keep PRISMER_API_KEY configured until such evidence has completed. Enabling the switch does not restore the pre-P2-05.02 plan rules: a plan with plan_rules=volc-english-5 is checked for English and Count ≤ 5 before any provider runs, Prismer included. When enabled, the fixed Prismer query POST /api/context/load sends inputType=query, search.topK and explicit return.topK/format=both. URL-load sends inputType=url/format=both; only result.raw supplies source text. cached/cachedAt/meta and complete raw/response/summary/cost/time are retained. HQCC-only is unavailable. PDF/image suffixes from original query URLs use actual Parse {url,mode:fast,output:markdown,wait:true}. Synchronous requestId is nullable; async taskId is persisted before fixed status/result GET. Callback/endpoints are ignored. Source text is API raw/parsed markdown, not original HTTP bytes or a verified realtime/final URL.
8
8
 
9
- New hardware-web-evidence has provider=prismer, protocol=prismer-volc-source-v2, schema_version=2, provider_policy=prismer_then_volc; original recall_id/recall_digest/request_digest/requirements/slots; plan_digest/unresolved_slots/query_plans; all searches; all pages; unknowns. Its stable ID depends only on original recall_id/request_digest. Every paid attempt has stable id/status/request/raw/raw_sha256/canonical response_sha256/received_at/http_status/request_id/response. Ordered searches[].attempts use prismer/query then the optional volcengine/query; pages[].attempts use prismer/context_url|parse then optional direct_https/direct_page. Each group retains selected_attempt_id/selected_provider and the exact selected receipt alias. Page primary attempt.kind is context_url or parse; pending Parse includes task_id and observations of each fixed GET. Page has source_url/returned_url (nullable), text/text_sha256/content_sha256/retrieval/cached/cachedAt/meta/loaded_at. Prismer source_bytes_verified/remote_redirects_verified remain false. Direct receipts retain actual wire and safe persisted raw/text digests, final_url/fetched_at, DNS/redirect/IP/TLS trace; redaction modifies only the safe copy and raw_redacted=true makes source_bytes_verified=false. PDF/image denial remains unavailable without a direct Parse substitute.
9
+ New hardware-web-evidence has provider=prismer, protocol=prismer-volc-source-v2, schema_version=2, provider_policy=prismer_then_volc; prismer_switch once provider IO is needed (absent on evidence that never needed IO and on evidence saved before the pin); original recall_id/recall_digest/request_digest/requirements/slots; plan_digest/unresolved_slots/query_plans; all searches; all pages; unknowns. Its stable ID depends only on original recall_id/request_digest. Every paid attempt has stable id/status/request/raw/raw_sha256/canonical response_sha256/received_at/http_status/request_id/response. Ordered searches[].attempts use prismer/query then the optional volcengine/query; pages[].attempts use prismer/context_url|parse then optional direct_https/direct_page. Each group retains selected_attempt_id/selected_provider and the exact selected receipt alias. Page primary attempt.kind is context_url or parse; pending Parse includes task_id and observations of each fixed GET. Page has source_url/returned_url (nullable), text/text_sha256/content_sha256/retrieval/cached/cachedAt/meta/loaded_at. Prismer source_bytes_verified/remote_redirects_verified remain false. Direct receipts retain actual wire and safe persisted raw/text digests, final_url/fetched_at, DNS/redirect/IP/TLS trace; redaction modifies only the safe copy and raw_redacted=true makes source_bytes_verified=false. PDF/image denial remains unavailable without a direct Parse substitute.
10
10
 
11
11
  page.request_id equals discovery_refs[0].request_id, which equals the first original query's nullable request_id. Later discoveries may reuse that page without replacing its first query identity; all refs still bind their own original query response, selected provider and attempt ID. Merge and formal rebuilding reject an altered or deleted duplicate page identity without provider I/O.
12
12
 
@@ -32,6 +32,15 @@ export function validateEnvelope(envelope) {
32
32
  if (max.some(v => v - 2 * (envelope.wall_mm?.minimum ?? 0) <= 0)) invalid('guaranteed minimum wall leaves no positive maximum cavity');
33
33
  return envelope;
34
34
  }
35
+ // A structure zone budget for one slot (P2-06.03). locked: the zone is fixed by the user or the restore target and
36
+ // rejects like a user hard limit; inferred: a structure estimate that only demotes candidates, never rejects.
37
+ export function validateFitEnvelope(fit) {
38
+ keys(fit, ['envelope', 'basis'], ['zone'], 'fit_envelope');
39
+ if (!['locked', 'inferred'].includes(fit.basis)) invalid('fit_envelope.basis must be locked or inferred');
40
+ if (fit.zone !== undefined && !(typeof fit.zone === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/.test(fit.zone))) invalid('fit_envelope.zone must be a short identifier');
41
+ validateEnvelope(fit.envelope);
42
+ return fit;
43
+ }
35
44
  export function validateRequirementEnvelopes(requirements) {
36
45
  for (const row of requirements?.limits ?? []) if (row?.envelope !== undefined) validateEnvelope(row.envelope);
37
46
  }
@@ -102,11 +111,16 @@ function coarseFit(dims, container) {
102
111
  // status is the coarse fit result only: clear = every coarse check passed, risk = some check failed without proof,
103
112
  // incompatible = proof under a user hard limit with measured/datasheet sizes. Data/limit authority stays in basis and limit source.
104
113
  // decision_required = user hard limit + trusted sizes, yet no axis-aligned mounting fits: only the user can trade the part against the limit.
105
- export function evaluateDimensions(candidate, requirements) {
114
+ export function evaluateDimensions(candidate, requirements, slot) {
106
115
  validateRequirementEnvelopes(requirements);
116
+ const fit = slot?.fit_envelope;
117
+ if (fit !== undefined) validateFitEnvelope(fit);
107
118
  const fields = dimensionFields(candidate), reasons = [], method = 'coarse-single-part-v2';
108
119
  const unknown = code => ({ status: 'unknown', decision_required: false, reasons: [{ code }], method });
109
- const limits = (requirements?.limits ?? []).filter(row => row?.envelope !== undefined);
120
+ // Requirement limits keep their own authority rule; a slot zone budget adds one more container for this slot only.
121
+ const limits = [...(requirements?.limits ?? []).filter(row => row?.envelope !== undefined)
122
+ .map(row => ({ id: row.id, envelope: row.envelope, hardLimit: row.source === 'user' && row.strength === 'hard' })),
123
+ ...(fit ? [{ id: `fit:${slot.slot_id}`, envelope: fit.envelope, hardLimit: fit.basis === 'locked', ...(fit.zone ? { zone: fit.zone } : {}) }] : [])];
110
124
  if (!limits.length) return unknown('PRODUCT_ENVELOPE_MISSING');
111
125
  if (!fields.dimensions_mm || fields.dimension_basis === 'unknown' || fields.dimension_geometry === 'unknown') return unknown('COMPONENT_DIMENSIONS_UNKNOWN');
112
126
  const dims = fields.dimensions_mm;
@@ -115,27 +129,28 @@ export function evaluateDimensions(candidate, requirements) {
115
129
  return unknown('NUMERIC_RANGE_UNSUPPORTED');
116
130
  let incompatible = false, risk = false, decisionRequired = false;
117
131
  for (const row of limits) {
118
- const hard = row.source === 'user' && row.strength === 'hard' && ['measured', 'datasheet'].includes(fields.dimension_basis);
132
+ const hard = row.hardLimit && ['measured', 'datasheet'].includes(fields.dimension_basis);
133
+ const at = row.zone ? { limit_id: row.id, zone: row.zone } : { limit_id: row.id };
119
134
  const container = cavity(row.envelope, row.envelope.wall_mm?.minimum ?? 0);
120
135
  const failures = fields.dimension_geometry === 'solid_box'
121
136
  ? [['MIN_SUPPORT_WIDTH', Math.min(...dims), container.width], ['SOLID_DIAGONAL', Math.hypot(...dims), container.chord], ['SOLID_VOLUME', dims.reduce((a, b) => a * b, 1), container.volume]]
122
137
  : [['MAX_SUPPORT_SPAN', Math.max(...dims), container.chord]];
123
138
  for (const [code, observed, maximum] of failures) if (exceeds(observed, maximum)) {
124
- reasons.push({ code, limit_id: row.id, observed, maximum, hard }); if (hard) incompatible = true; else risk = true;
139
+ reasons.push({ code, ...at, observed, maximum, hard }); if (hard) incompatible = true; else risk = true;
125
140
  }
126
141
  // Parts are mounted axis-aligned in practice; failing that is a risk, never a proof, because a tilted part may still fit.
127
142
  if (!coarseFit(dims, container)) {
128
143
  risk = true; if (hard) decisionRequired = true;
129
- reasons.push({ code: 'AXIS_ALIGNED_FIT_FAILS', limit_id: row.id, hard: false });
144
+ reasons.push({ code: 'AXIS_ALIGNED_FIT_FAILS', ...at, hard: false });
130
145
  } else if (row.envelope.wall_mm?.estimate !== undefined && !coarseFit(dims, cavity(row.envelope, row.envelope.wall_mm.estimate))) {
131
146
  // Estimated walls only hint; they never reject and never ask the user to decide.
132
- risk = true; reasons.push({ code: 'ESTIMATED_WALL_FIT_FAILS', limit_id: row.id, hard: false });
147
+ risk = true; reasons.push({ code: 'ESTIMATED_WALL_FIT_FAILS', ...at, hard: false });
133
148
  }
134
149
  }
135
150
  return { status: incompatible ? 'incompatible' : risk ? 'risk' : 'clear', decision_required: !incompatible && decisionRequired, reasons, method };
136
151
  }
137
- export function screenDimensions(records, requirements) {
138
- const checks = records.map(record => ({ record, check: evaluateDimensions(record, requirements) }));
152
+ export function screenDimensions(records, requirements, slot) {
153
+ const checks = records.map(record => ({ record, check: evaluateDimensions(record, requirements, slot) }));
139
154
  const rejected = checks.filter(item => item.check.status === 'incompatible');
140
155
  const survivors = checks.filter(item => item.check.status !== 'incompatible');
141
156
  return { survivors, summary: {