abelien-harness 0.19.0 → 0.20.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 (67) hide show
  1. package/package.json +1 -1
  2. package/skills/abelien-algorithm/bindings.json +1 -1
  3. package/skills/abelien-hardware/assets/AGENTS.md +1 -1
  4. package/skills/abelien-hardware/bindings.json +1 -1
  5. package/skills/abelien-hardware/contract/host-tools.generated.json +12 -6
  6. package/skills/abelien-hardware/contract/search-index-format.md +1 -1
  7. package/skills/abelien-hardware/references/actions/hardware.selection-commit.generated.md +2 -2
  8. package/skills/abelien-hardware/references/actions/hardware.selection-propose.generated.md +2 -2
  9. package/skills/abelien-hardware/references/actions/hardware.selection-recall.generated.md +2 -2
  10. package/skills/abelien-hardware/references/category-default-dimensions.md +5 -5
  11. package/skills/abelien-hardware/references/selection-service-contract.md +1 -1
  12. package/skills/abelien-hardware/schemas/hardware-selection.v2.json +2 -2
  13. package/skills/abelien-hardware/scripts/dimensions.mjs +20 -4
  14. package/skills/abelien-hardware/scripts/selection-propose.mjs +10 -0
  15. package/skills/abelien-hardware/scripts/selection-recall.mjs +5 -4
  16. package/skills/abelien-hardware/scripts/selection-structure.mjs +1 -1
  17. package/skills/abelien-harness/runtime/README.md +4 -0
  18. package/skills/abelien-harness/runtime/actions.mjs +2 -0
  19. package/skills/abelien-harness/runtime/engine.mjs +23 -5
  20. package/skills/abelien-harness/runtime/errors.mjs +1 -1
  21. package/skills/abelien-harness/scripts/ab.mjs +1 -1
  22. package/skills/abelien-reference/bindings.json +1 -1
  23. package/skills/abelien-structure/contract/host-tools.generated.json +1703 -209
  24. package/skills/abelien-structure/references/actions/cad.fallback.generated.md +2 -2
  25. package/skills/abelien-structure/references/actions/structure.assets-request.generated.md +7 -2
  26. package/skills/abelien-structure/references/actions/structure.cad-assemble.generated.md +6 -0
  27. package/skills/abelien-structure/references/actions/structure.cad-check.generated.md +6 -0
  28. package/skills/abelien-structure/references/actions/structure.layout-check.generated.md +29 -24
  29. package/skills/abelien-structure/references/actions/structure.layout-request.generated.md +2 -1
  30. package/skills/abelien-structure/references/actions/structure.prelude.generated.md +9 -2
  31. package/skills/abelien-structure/references/actions/structure.swap-candidates.generated.md +1 -1
  32. package/skills/abelien-structure/references/cad-functions.generated.md +11 -8
  33. package/skills/abelien-structure/references/cad-modules.md +1 -0
  34. package/skills/abelien-structure/references/skeleton.md +13 -4
  35. package/skills/abelien-structure/schemas/layout-check-result.v1.json +99 -3
  36. package/skills/abelien-structure/schemas/layout-proposal.v1.json +1 -0
  37. package/skills/abelien-structure/schemas/layout-request-input.v1.json +1 -1
  38. package/skills/abelien-structure/schemas/layout-request.v1.json +12 -2
  39. package/skills/abelien-structure/schemas/prelude.v1.json +21 -0
  40. package/skills/abelien-structure/schemas/skeleton.v1.json +11 -3
  41. package/skills/abelien-structure/schemas/swap-candidates-input.v1.json +1 -1
  42. package/skills/abelien-structure/scripts/cad.mjs +30 -8
  43. package/skills/abelien-structure/scripts/geometry.mjs +48 -0
  44. package/skills/abelien-structure/scripts/layout.mjs +241 -21
  45. package/skills/abelien-structure/scripts/swap.mjs +3 -2
  46. package/skills/abelien-structure-worker/worker/abelien_cad.py +108 -13
  47. package/skills/abelien-structure-worker/worker/source-contract.mjs +4 -4
  48. package/skills/abelien-workspace/actions/image.generate.json +10 -0
  49. package/skills/abelien-workspace/assets/journey-stage-plan.json +246 -28
  50. package/skills/abelien-workspace/contract/host-tools.generated.json +4 -3
  51. package/skills/abelien-workspace/flows/creation.create.json +293 -34
  52. package/skills/abelien-workspace/flows/creation.modify.json +293 -34
  53. package/skills/abelien-workspace/flows/creation.remix.json +56 -18
  54. package/skills/abelien-workspace/flows/creation.restore.json +300 -34
  55. package/skills/abelien-workspace/references/actions/image.generate.generated.md +2 -0
  56. package/skills/abelien-workspace/references/actions/workspace.hardware-reselect-input.generated.md +2 -2
  57. package/skills/abelien-workspace/references/journey-stage-plan.md +59 -16
  58. package/skills/abelien-workspace/references/product-flow.md +2 -0
  59. package/skills/abelien-workspace/schemas/plan-common.v1.json +4 -1
  60. package/skills/abelien-workspace/schemas/skeleton-model-reply.v1.json +3 -2
  61. package/skills/abelien-workspace/schemas/skeleton-reply.v1.json +20 -1
  62. package/skills/abelien-workspace/schemas/version-confirmation.v1.json +22 -0
  63. package/skills/abelien-workspace/schemas/version-questions-reply.v1.json +26 -0
  64. package/skills/abelien-workspace/schemas/version-questions.v1.json +28 -0
  65. package/skills/abelien-workspace/scripts/creation-tasks.mjs +179 -14
  66. package/skills/abelien-workspace/scripts/flow-compile.mjs +4 -1
  67. package/skills/abelien-workspace/scripts/plans.mjs +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abelien-harness",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "npx-installable multi-agent skill harness: generic kernel + pluggable domain packs",
5
5
  "type": "module",
6
6
  "engines": {
@@ -10,7 +10,7 @@
10
10
  "cancel": "unsupported",
11
11
  "submission": {
12
12
  "idempotent_submit": true,
13
- "query_by_operation": true,
13
+ "query_by_operation": false,
14
14
  "remote_task_identity": "none",
15
15
  "not_found_definitive": false
16
16
  }
@@ -5,6 +5,6 @@
5
5
  - category-default-dimensions.v1.json:三级叶子分类的估算外包络;仅布局预留,圆形直接带直径/高度,来源与假设随行。
6
6
  - pdf-drawing-prompt.txt:厂家 PDF 图纸读取提示;不得把默认尺寸当作读图或厂家证据。
7
7
 
8
- 表由同级 ../schemas/category-default-dimensions.v1.json 校验;消费规则在 ../references/category-default-dimensions.md,实测/资料事实仍由现有 dimensions.mjs 处理。
8
+ 表由同级 ../schemas/category-default-dimensions.v1.json 校验;消费规则在 ../references/category-default-dimensions.md,实测/资料事实及 incomplete/risk 等检查状态仍由现有 dimensions.mjs 处理,分类占位不改写事实或完整度。
9
9
 
10
10
  [PROTOCOL]: 变更时更新此头部,然后检查 AGENTS.md
@@ -36,7 +36,7 @@
36
36
  "cancel": "unsupported",
37
37
  "submission": {
38
38
  "idempotent_submit": true,
39
- "query_by_operation": true,
39
+ "query_by_operation": false,
40
40
  "remote_task_identity": "submit_response",
41
41
  "not_found_definitive": false
42
42
  }
@@ -4783,12 +4783,13 @@
4783
4783
  ]
4784
4784
  },
4785
4785
  "basis": {
4786
- "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
4786
+ "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;database 器件库登记的尺寸,有经核对的 v2 快照绑定且资料完整(三个轴与 evidence_ref 齐全,由代码判定),算可信尺寸;recorded 库内或既有资料记录(没有核对过的绑定或资料不全的库内尺寸也在这里),不算可信。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
4787
4787
  "enum": [
4788
4788
  "manufacturer",
4789
4789
  "measured",
4790
4790
  "estimate",
4791
4791
  "engineering_design",
4792
+ "database",
4792
4793
  "recorded",
4793
4794
  "unknown"
4794
4795
  ]
@@ -9219,12 +9220,13 @@
9219
9220
  ]
9220
9221
  },
9221
9222
  "basis": {
9222
- "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
9223
+ "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;database 器件库登记的尺寸,有经核对的 v2 快照绑定且资料完整(三个轴与 evidence_ref 齐全,由代码判定),算可信尺寸;recorded 库内或既有资料记录(没有核对过的绑定或资料不全的库内尺寸也在这里),不算可信。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
9223
9224
  "enum": [
9224
9225
  "manufacturer",
9225
9226
  "measured",
9226
9227
  "estimate",
9227
9228
  "engineering_design",
9229
+ "database",
9228
9230
  "recorded",
9229
9231
  "unknown"
9230
9232
  ]
@@ -12360,12 +12362,13 @@
12360
12362
  ]
12361
12363
  },
12362
12364
  "basis": {
12363
- "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
12365
+ "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;database 器件库登记的尺寸,有经核对的 v2 快照绑定且资料完整(三个轴与 evidence_ref 齐全,由代码判定),算可信尺寸;recorded 库内或既有资料记录(没有核对过的绑定或资料不全的库内尺寸也在这里),不算可信。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
12364
12366
  "enum": [
12365
12367
  "manufacturer",
12366
12368
  "measured",
12367
12369
  "estimate",
12368
12370
  "engineering_design",
12371
+ "database",
12369
12372
  "recorded",
12370
12373
  "unknown"
12371
12374
  ]
@@ -12760,12 +12763,13 @@
12760
12763
  ]
12761
12764
  },
12762
12765
  "basis": {
12763
- "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
12766
+ "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;database 器件库登记的尺寸,有经核对的 v2 快照绑定且资料完整(三个轴与 evidence_ref 齐全,由代码判定),算可信尺寸;recorded 库内或既有资料记录(没有核对过的绑定或资料不全的库内尺寸也在这里),不算可信。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
12764
12767
  "enum": [
12765
12768
  "manufacturer",
12766
12769
  "measured",
12767
12770
  "estimate",
12768
12771
  "engineering_design",
12772
+ "database",
12769
12773
  "recorded",
12770
12774
  "unknown"
12771
12775
  ]
@@ -13151,12 +13155,13 @@
13151
13155
  ]
13152
13156
  },
13153
13157
  "basis": {
13154
- "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
13158
+ "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;database 器件库登记的尺寸,有经核对的 v2 快照绑定且资料完整(三个轴与 evidence_ref 齐全,由代码判定),算可信尺寸;recorded 库内或既有资料记录(没有核对过的绑定或资料不全的库内尺寸也在这里),不算可信。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
13155
13159
  "enum": [
13156
13160
  "manufacturer",
13157
13161
  "measured",
13158
13162
  "estimate",
13159
13163
  "engineering_design",
13164
+ "database",
13160
13165
  "recorded",
13161
13166
  "unknown"
13162
13167
  ]
@@ -14137,12 +14142,13 @@
14137
14142
  ]
14138
14143
  },
14139
14144
  "basis": {
14140
- "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
14145
+ "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;database 器件库登记的尺寸,有经核对的 v2 快照绑定且资料完整(三个轴与 evidence_ref 齐全,由代码判定),算可信尺寸;recorded 库内或既有资料记录(没有核对过的绑定或资料不全的库内尺寸也在这里),不算可信。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
14141
14146
  "enum": [
14142
14147
  "manufacturer",
14143
14148
  "measured",
14144
14149
  "estimate",
14145
14150
  "engineering_design",
14151
+ "database",
14146
14152
  "recorded",
14147
14153
  "unknown"
14148
14154
  ]
@@ -74,7 +74,7 @@ source 与 output 须是互不嵌套的目录。每个 Wiki record_id 必须命
74
74
 
75
75
  槽位 `fit_envelope` 是结构分区给该槽位的额外容器,形状规则同上:basis=locked 不是证明(分区由结构声明、无可复核出处),可信尺寸放不下时记 risk 且 decision_required=true、从不硬拒绝,原因带 `basis: locked`;basis=inferred 视同非硬边界;原因以 `limit_id=fit:<slot_id>` 标出,有 zone 时带 `zone`。没有 fit_envelope 的槽位判定结果不变。
76
76
 
77
- 只有 user+hard 边界和 measured/datasheet 组件可硬拒绝;locked 分区只要求用户裁决。solid_box 可用最小边、对角线、实体体积三项必要条件;任意 bounding_box 只可用最大支撑跨度超过容器最大弦长,不能用包围盒最小边、对角线或体积。六轴置换、圆柱 footprint 只是可装下粗筛,失败是 risk;没有实心形体记录时按包络判据。clear 只表示单件粗筛,不代表多件装配、CAD 或工程通过。
77
+ 只有 user+hard 边界和可信尺寸的组件可硬拒绝;可信尺寸为 `TRUSTED_SIZE_BASES`(measured、datasheet、database;database 是有经核对的 v2 快照绑定、资料完整含 evidence_ref 的器件库尺寸,由 `sizeBasis` 判定,快照里仍写 catalog);其余 catalog、估算与分类默认尺寸都不可信。locked 分区只要求用户裁决。solid_box 可用最小边、对角线、实体体积三项必要条件;任意 bounding_box 只可用最大支撑跨度超过容器最大弦长,不能用包围盒最小边、对角线或体积。六轴置换、圆柱 footprint 只是可装下粗筛,失败是 risk;没有实心形体记录时按包络判据。clear 只表示单件粗筛,不代表多件装配、CAD 或工程通过。
78
78
 
79
79
 
80
80
  新域包 hardware 1.2.0 / selection 0.5.0 / workspace 1.3.0 使用此加法契约。已有持久结果只读原样;旧 workflow JSON 摘要不固定模块代码,因此不可用旧定义配新包宣称旧行为仍相同。宿主必须固定完整发布包,为尺寸筛选开新运行,不能原地改写旧 CAD 输入或已失败/未知副作用。
@@ -203,7 +203,7 @@ array<Identifier>;3 项。
203
203
  | `subject` | Subject | 是 | 事实描述的对象。显示有效区、模组 PCB、裸芯片、完整设备外形是不同对象,不共享一个含糊的“尺寸”。尺寸适配只认 device_outline 与 module_outline |
204
204
  | `value` | 任意 | 是 | — |
205
205
  | `unit` | Unit | 是 | 受控单位。px 用来如实记录没有尺度依据的读图结果,不能当毫米用;none 用于无量纲值或结构化记录 |
206
- | `basis` | Basis | 是 | 事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项 |
206
+ | `basis` | Basis | 是 | 事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;database 器件库登记的尺寸,有经核对的 v2 快照绑定且资料完整(三个轴与 evidence_ref 齐全,由代码判定),算可信尺寸;recorded 库内或既有资料记录(没有核对过的绑定或资料不全的库内尺寸也在这里),不算可信。unknown 仅兼容历史记录,新资料按完整度列明缺项 |
207
207
  | `source_ref` | null / Ref | 是 | — |
208
208
  | `locator` | null / object | 是 | — |
209
209
  | `conditions` | object | 是 | — |
@@ -344,7 +344,7 @@ string;受控单位。px 用来如实记录没有尺度依据的读图结果
344
344
 
345
345
  ### Basis
346
346
 
347
- string;事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项;取值 `"manufacturer"` / `"measured"` / `"estimate"` / `"engineering_design"` / `"recorded"` / `"unknown"`。
347
+ string;事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;database 器件库登记的尺寸,有经核对的 v2 快照绑定且资料完整(三个轴与 evidence_ref 齐全,由代码判定),算可信尺寸;recorded 库内或既有资料记录(没有核对过的绑定或资料不全的库内尺寸也在这里),不算可信。unknown 仅兼容历史记录,新资料按完整度列明缺项;取值 `"manufacturer"` / `"measured"` / `"estimate"` / `"engineering_design"` / `"database"` / `"recorded"` / `"unknown"`。
348
348
 
349
349
  ### GeometryFrame
350
350
 
@@ -243,7 +243,7 @@ array<Identifier>;3 项。
243
243
  | `subject` | Subject | 是 | 事实描述的对象。显示有效区、模组 PCB、裸芯片、完整设备外形是不同对象,不共享一个含糊的“尺寸”。尺寸适配只认 device_outline 与 module_outline |
244
244
  | `value` | 任意 | 是 | — |
245
245
  | `unit` | Unit | 是 | 受控单位。px 用来如实记录没有尺度依据的读图结果,不能当毫米用;none 用于无量纲值或结构化记录 |
246
- | `basis` | Basis | 是 | 事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项 |
246
+ | `basis` | Basis | 是 | 事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;database 器件库登记的尺寸,有经核对的 v2 快照绑定且资料完整(三个轴与 evidence_ref 齐全,由代码判定),算可信尺寸;recorded 库内或既有资料记录(没有核对过的绑定或资料不全的库内尺寸也在这里),不算可信。unknown 仅兼容历史记录,新资料按完整度列明缺项 |
247
247
  | `source_ref` | null / Ref | 是 | — |
248
248
  | `locator` | null / object | 是 | — |
249
249
  | `conditions` | object | 是 | — |
@@ -379,7 +379,7 @@ string;受控单位。px 用来如实记录没有尺度依据的读图结果
379
379
 
380
380
  ### Basis
381
381
 
382
- string;事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项;取值 `"manufacturer"` / `"measured"` / `"estimate"` / `"engineering_design"` / `"recorded"` / `"unknown"`。
382
+ string;事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;database 器件库登记的尺寸,有经核对的 v2 快照绑定且资料完整(三个轴与 evidence_ref 齐全,由代码判定),算可信尺寸;recorded 库内或既有资料记录(没有核对过的绑定或资料不全的库内尺寸也在这里),不算可信。unknown 仅兼容历史记录,新资料按完整度列明缺项;取值 `"manufacturer"` / `"measured"` / `"estimate"` / `"engineering_design"` / `"database"` / `"recorded"` / `"unknown"`。
383
383
 
384
384
  ### GeometryFrame
385
385
 
@@ -238,7 +238,7 @@ string;sha256 小写十六进制(64 位,不带 sha256: 前缀)。对 JSO
238
238
  | `subject` | Subject | 是 | 事实描述的对象。显示有效区、模组 PCB、裸芯片、完整设备外形是不同对象,不共享一个含糊的“尺寸”。尺寸适配只认 device_outline 与 module_outline |
239
239
  | `value` | 任意 | 是 | — |
240
240
  | `unit` | Unit | 是 | 受控单位。px 用来如实记录没有尺度依据的读图结果,不能当毫米用;none 用于无量纲值或结构化记录 |
241
- | `basis` | Basis | 是 | 事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项 |
241
+ | `basis` | Basis | 是 | 事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;database 器件库登记的尺寸,有经核对的 v2 快照绑定且资料完整(三个轴与 evidence_ref 齐全,由代码判定),算可信尺寸;recorded 库内或既有资料记录(没有核对过的绑定或资料不全的库内尺寸也在这里),不算可信。unknown 仅兼容历史记录,新资料按完整度列明缺项 |
242
242
  | `source_ref` | null / Ref | 是 | — |
243
243
  | `locator` | null / object | 是 | — |
244
244
  | `conditions` | object | 是 | — |
@@ -300,7 +300,7 @@ string;受控单位。px 用来如实记录没有尺度依据的读图结果
300
300
 
301
301
  ### Basis
302
302
 
303
- string;事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项;取值 `"manufacturer"` / `"measured"` / `"estimate"` / `"engineering_design"` / `"recorded"` / `"unknown"`。
303
+ string;事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;database 器件库登记的尺寸,有经核对的 v2 快照绑定且资料完整(三个轴与 evidence_ref 齐全,由代码判定),算可信尺寸;recorded 库内或既有资料记录(没有核对过的绑定或资料不全的库内尺寸也在这里),不算可信。unknown 仅兼容历史记录,新资料按完整度列明缺项;取值 `"manufacturer"` / `"measured"` / `"estimate"` / `"engineering_design"` / `"database"` / `"recorded"` / `"unknown"`。
304
304
 
305
305
  ### GeometryFrame
306
306
 
@@ -1,6 +1,6 @@
1
1
  # 按分类的默认尺寸表
2
2
 
3
- > INPUT:受控三级分类与尺寸未知检查;OUTPUT:布局估算外包络及来源;POS:hardware → r30 骨架的静态数据契约,不是组件事实。
3
+ > INPUT:受控三级分类与尺寸缺项/粗筛检查;OUTPUT:布局估算外包络及来源,保留原完整度/风险状态;POS:hardware → r30 骨架的静态数据契约,不是组件事实。
4
4
  >
5
5
  > [PROTOCOL]: 变更时更新此头部,然后检查 AGENTS.md
6
6
 
@@ -9,7 +9,7 @@
9
9
  - 数据:`../assets/category-default-dimensions.v1.json`。
10
10
  - 校验:`../schemas/category-default-dimensions.v1.json`(JSON Schema 2020-12)。
11
11
  - `schema` 固定 `abelien.hardware.category-default-dimensions.v1`;`revision` 从表顶层读取并随布局冻结;统一 mm。
12
- - `category_path` 是完整三级 slug 数组,按三段精确匹配,一叶一行。不按标签、父类、型号或模糊词匹配;不存在的叶子继续 unknown,不套邻类。
12
+ - `category_path` 是完整三级 slug 数组,按三段精确匹配,一叶一行。不按标签、父类、型号或模糊词匹配;不存在的叶子不造分类占位,保留原有尺寸缺项与检查结果,不套邻类。
13
13
  - 首批是 pilot:覆盖主控、电源、麦克风阵列、喇叭、功放、语音处理、红外收发、Wi-Fi、灯环、触摸按键。主控拆 MCU/SBC,电源拆通用模组/DC-DC,共 12 行,不代表完整分类覆盖。
14
14
 
15
15
  消费者可构建只读索引:
@@ -44,12 +44,12 @@ const estimate = byPath.get(component.category_path.join('/'));
44
44
 
45
45
  ## 使用门与信任边界
46
46
 
47
- 1. 只对 `COMPONENT_DIMENSIONS_UNKNOWN`,或明确指出缺的是尺寸的 `FACT_MISSING` 使用。其他缺事实错误不触发尺寸回退;已有适用的 CAD、厂家/实测或显式器件估算优先,不能被分类默认值覆盖。
48
- 2. 在布局记录上携带 `dimension_basis: "category_default"`、完整 `category_path`、表 revision、shape、外包络及完整 source。未知检查仍是 unknown,不能改成 clear/verified。
47
+ 1. 只在确实缺少尺寸时使用:现行检查为 `COMPONENT_DIMENSIONS_INCOMPLETE`,旧上游也可能返回 `COMPONENT_DIMENSIONS_UNKNOWN` 或明确指出缺的是尺寸的 `FACT_MISSING`。其他缺事实错误不触发尺寸回退;已有适用的 CAD、厂家/实测或显式器件估算优先,已有尺寸值、缺项及来源不能被分类默认值覆盖。
48
+ 2. 在布局记录上携带 `dimension_basis: "category_default"`、完整 `category_path`、表 revision、shape、外包络及完整 source。保留原检查语义,不强制改成 unknown:有有效空间约束时,缺少完整尺寸返回 `incomplete`;注入分类默认尺寸后若超出 locked 分区,返回 `risk`。这两种情况均为 `decision_required=false`,不阻断、不触发换件。缺产品包络等其他情况仍保留原状态与原因;`clear` 只是粗筛结果,默认占位不能证明资料齐全或工程 verified。
49
49
  3. source 只说明典型器件与估算假设,不证明所选型号的尺寸或形状。部分来源仅有平面尺寸,预留高度是 note 中明示的假设;默认值不是该分类的最大尺寸,不能当保证放得下的上界。
50
50
  4. 估算放不进 locked 分区时只显示 advisory;不得阻断、设置换件决策、自动替换、缩放零件或放宽用户约束。真实尺寸的既有检查规则不变。
51
51
  5. 不写回组件库、Wiki、索引、签名事实快照或来源尺寸字段;不新增采购/BOM 行,不借此认定 CAD、散热、接线或安装可行。
52
52
 
53
53
  ## r30 接入边界
54
54
 
55
- 本 MR 交付表、schema 和离线回归;骨架端按以上字段接入。当前结构输出契约尚未接入 `category_default`,不能把表的测试通过当作 r30 布局已跑通。接入方需在自己的布局契约中保存上述 provenance,并验证:尺寸未知占位、圆形占位、缺分类仍 unknown、真实尺寸优先,以及 locked 溢出仅提示且不触发换件。无需改选型事实 schema 或降低现有准入检查。
55
+ 本 MR 交付表、schema 和离线回归;骨架端按以上字段接入。当前结构输出契约尚未接入 `category_default`,不能把表的测试通过当作 r30 布局已跑通。接入方需在自己的布局契约中保存上述 provenance,并验证:尺寸缺项占位、圆形占位、未覆盖分类不造占位且保留原检查结果、真实尺寸优先,以及 incomplete/risk 两种场景不阻断、locked 溢出仅提示且不触发换件。无需改选型事实 schema 或降低现有准入检查。
@@ -65,7 +65,7 @@ verified 只代表同源、完整数量和有归属证据的 Agent 选型判断
65
65
 
66
66
  ## 结构化尺寸与正式门禁
67
67
 
68
- limits 可附 envelope(形状与字段见 clarify 的 Requirements 定义)。hardware.search 在分类所有索引记录上先筛尺寸再 Top-K,实际缺轴和估算保留;资料完整度按已填/应填及缺项输出,已有数值不因历史来源标签丢失。hardware.select 读取冻结 recall 并核对 request_digest、正文 SHA-256 后,复用纯判定器重算,忽略模型伪造的尺寸、来源、名称和 dimension_check。incompatible 以 VALIDATION 拒绝所选件,必须选其他存活候选;dimension_check.decision_required=true(用户 hard 上限+measured/datasheet 尺寸,且轴对齐装法全部失败,仍不足以证明放不下)时加入 DIMENSION_DECISION_REQUIRED 并返回 needs_confirmation,模型引用、确认或机械 verified 不能覆盖。unknown、估算尺寸、system/preference 上限或估算壁厚下的 risk 只随 selected_models.dimension_check 提示,不进入 unknowns,也不单独阻止 verified;尺寸限制仍须经原 constraint_checks 的 Wiki 证据门。
68
+ limits 可附 envelope(形状与字段见 clarify 的 Requirements 定义)。hardware.search 在分类所有索引记录上先筛尺寸再 Top-K,实际缺轴和估算保留;资料完整度按已填/应填及缺项输出,已有数值不因历史来源标签丢失。hardware.select 读取冻结 recall 并核对 request_digest、正文 SHA-256 后,复用纯判定器重算,忽略模型伪造的尺寸、来源、名称和 dimension_check。incompatible 以 VALIDATION 拒绝所选件,必须选其他存活候选;dimension_check.decision_required=true(用户 hard 上限+可信尺寸 measured/datasheet/database,且轴对齐装法全部失败,仍不足以证明放不下)时加入 DIMENSION_DECISION_REQUIRED 并返回 needs_confirmation,模型引用、确认或机械 verified 不能覆盖。unknown、估算尺寸、system/preference 上限或估算壁厚下的 risk 只随 selected_models.dimension_check 提示,不进入 unknowns,也不单独阻止 verified;尺寸限制仍须经原 constraint_checks 的 Wiki 证据门。
69
69
 
70
70
  槽位可附 `fit_envelope: {envelope, basis: locked|inferred, zone?}`,envelope 与 limits 的 envelope 同一校验,zone 为短标识;字段不合法以 VALIDATION 拒绝。它只对本槽位追加一个容器,原因的 `limit_id` 为 `fit:<slot_id>`,有 zone 时同时带 `zone`。locked 由结构声明、没有可复核出处,不是证明:可信尺寸放不下时只 risk 并 decision_required(返回 needs_confirmation,由用户在换件与放宽分区/外形间取舍),从不 incompatible;inferred 与 system/preference 上限相同(只 risk)。硬拒绝只属于 user+hard 上限。它属于结构输入,不进入 constraint_checks 的 expected ID,由判定器确定性复核;槽位已计入 request_digest,改分区须重新检索。只有 fit_envelope、没有产品 envelope 时也做尺寸检查。
71
71
 
@@ -23,8 +23,8 @@
23
23
  "pattern": "^sha256:[a-f0-9]{64}$"
24
24
  },
25
25
  "Basis": {
26
- "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;recorded 库内或既有资料记录。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
27
- "enum": ["manufacturer", "measured", "estimate", "engineering_design", "recorded", "unknown"]
26
+ "description": "事实来源类型:manufacturer 厂商资料;measured 测量记录;estimate 估算;engineering_design 工程设计;database 器件库登记的尺寸,有经核对的 v2 快照绑定且资料完整(三个轴与 evidence_ref 齐全,由代码判定),算可信尺寸;recorded 库内或既有资料记录(没有核对过的绑定或资料不全的库内尺寸也在这里),不算可信。unknown 仅兼容历史记录,新资料按完整度列明缺项。",
27
+ "enum": ["manufacturer", "measured", "estimate", "engineering_design", "database", "recorded", "unknown"]
28
28
  },
29
29
  "Unit": {
30
30
  "description": "受控单位。px 用来如实记录没有尺度依据的读图结果,不能当毫米用;none 用于无量纲值或结构化记录。",
@@ -9,6 +9,10 @@ const tuple = v => Array.isArray(v) && v.length === 3 && v.every(positive);
9
9
  const partialTuple = v => Array.isArray(v) && v.length === 3 && v.some(positive) && v.every(axis => axis === null || positive(axis));
10
10
  const DATA_FIELDS = ['x_mm', 'y_mm', 'z_mm', 'source_ref'];
11
11
  const SNAPSHOT_V2 = 'abelien.component-dimensions.v2';
12
+ // Sizes a check may rely on (r30, user 2026-10-19): measured, from a datasheet, or `database` — a component-database size that is
13
+ // source-bound and complete. The one trust rule for sizes: hardware's checks, the skeleton check and the swap screen read this list.
14
+ // catalog (a database size without a verified binding or evidence), estimates and category defaults never decide anything.
15
+ export const TRUSTED_SIZE_BASES = Object.freeze(['measured', 'datasheet', 'database']);
12
16
  /** Availability only: axes and a reference, with an explicit denominator; no confidence state. */
13
17
  export function dimensionCompleteness(value, sourceRef) {
14
18
  const present = [0, 1, 2].map(axis => Array.isArray(value) && value.length <= 3 && positive(value[axis])).concat(text(sourceRef));
@@ -44,7 +48,7 @@ export function validateEnvelope(envelope) {
44
48
  // A structure zone budget for one slot (P2-06.03). inferred: a structure estimate that only demotes candidates.
45
49
  // locked: structure declares the zone fixed (locked shape, restore target), but the declaration carries no checkable
46
50
  // provenance, so it is not proof: a trusted part that misses it is a risk the user must decide on, never a rejection.
47
- // Hard rejection stays with user hard limits plus measured/datasheet sizes.
51
+ // Hard rejection stays with user hard limits plus trusted sizes (TRUSTED_SIZE_BASES).
48
52
  export function validateFitEnvelope(fit) {
49
53
  keys(fit, ['envelope', 'basis'], ['zone'], 'fit_envelope');
50
54
  if (!['locked', 'inferred'].includes(fit.basis)) invalid('fit_envelope.basis must be locked or inferred');
@@ -91,6 +95,18 @@ export function validateComponentSnapshot(snapshot) {
91
95
  if (snapshot.snapshot_id !== digest(snapshot.records)) invalid('component snapshot hash does not match records');
92
96
  return snapshot;
93
97
  }
98
+ /**
99
+ * The basis a check reads for a candidate's size: `database` for a component-database size (snapshot basis catalog) bound to a v2
100
+ * snapshot record whose revision dimensionFields verified, with complete data (all three axes and the evidence_ref); otherwise the
101
+ * fields' own basis. A legacy candidate without dimension_geometry keeps its binding unverified, so it is never `database`.
102
+ * The snapshot keeps writing catalog (the export's format) and dimensionFields keeps returning it; `database` is decided only here.
103
+ */
104
+ export function sizeBasis(value) {
105
+ const fields = dimensionFields(value);
106
+ const verified = fields.dimension_source !== null && value.dimension_geometry !== undefined;
107
+ return verified && fields.dimension_basis === 'catalog' && fields.dimension_source.record_schema === SNAPSHOT_V2 && fields.data_completeness.complete
108
+ ? 'database' : fields.dimension_basis;
109
+ }
94
110
  // Only source-bound records can contribute a trusted basis. Legacy unbound fields stay display-only unknown.
95
111
  export function dimensionFields(value) {
96
112
  const source = value?.dimension_source;
@@ -136,14 +152,14 @@ function coarseFit(dims, container) {
136
152
  : !exceeds(dims[p[2]], container.height) && !exceeds(Math.hypot(dims[p[0]], dims[p[1]]), container.diameter));
137
153
  }
138
154
  // status is the coarse fit result only: clear = every coarse check passed, risk = some check failed without proof,
139
- // incompatible = proof under a user hard limit with measured/datasheet sizes. Data/limit authority stays in basis and limit source.
155
+ // incompatible = proof under a user hard limit with trusted sizes (TRUSTED_SIZE_BASES). Data/limit authority stays in basis and limit source.
140
156
  // decision_required = trusted sizes that miss either a user hard limit (no axis-aligned mounting fits) or a locked zone:
141
157
  // only the user can trade the part against the limit or zone.
142
158
  export function evaluateDimensions(candidate, requirements, slot) {
143
159
  validateRequirementEnvelopes(requirements);
144
160
  const fit = slot?.fit_envelope;
145
161
  if (fit !== undefined) validateFitEnvelope(fit);
146
- const fields = dimensionFields(candidate), reasons = [], method = 'coarse-single-part-v2';
162
+ const fields = dimensionFields(candidate), basis = sizeBasis(candidate), reasons = [], method = 'coarse-single-part-v2';
147
163
  const unknown = code => ({ status: 'unknown', decision_required: false, reasons: [{ code }], method });
148
164
  // Requirement limits keep their own authority rule; a slot zone budget adds one more container for this slot only.
149
165
  const limits = [...(requirements?.limits ?? []).filter(row => row?.envelope !== undefined)
@@ -158,7 +174,7 @@ export function evaluateDimensions(candidate, requirements, slot) {
158
174
  return unknown('NUMERIC_RANGE_UNSUPPORTED');
159
175
  let incompatible = false, risk = false, decisionRequired = false;
160
176
  for (const row of limits) {
161
- const trustedSize = ['measured', 'datasheet'].includes(fields.dimension_basis);
177
+ const trustedSize = TRUSTED_SIZE_BASES.includes(basis);
162
178
  const hard = row.hardLimit && trustedSize, decides = Boolean(row.decides) && trustedSize;
163
179
  const at = { limit_id: row.id, ...(row.zone ? { zone: row.zone } : {}), ...(row.basis ? { basis: row.basis } : {}) };
164
180
  const container = cavity(row.envelope, row.envelope.wall_mm?.minimum ?? 0);
@@ -50,7 +50,17 @@ function pickOutline(candidate) {
50
50
  return { fact: outline.find(trustedFact) ?? outline[0] };
51
51
  }
52
52
  // The shared size evaluator reads component-record fields; build them from the frozen fact so its rules apply unchanged.
53
+ // A database fact is rebuilt as the bound v2 record it was recalled from (basis catalog, its evidence_ref and completeness), so
54
+ // sizeBasis judges it by the same rule as a search candidate; without its evidence_ref it is incomplete and stays untrusted.
53
55
  function sizeRecord(componentId, fact) {
56
+ if (fact.basis === "database") {
57
+ const evidence_ref = fact.locator?.evidence_ref;
58
+ const record = { component_id: componentId, dimensions_mm: fact.value, dimension_basis: "catalog", dimension_geometry: fact.shape ?? null,
59
+ ...(evidence_ref !== undefined ? { evidence_ref } : {}), data_completeness: dimensionCompleteness(fact.value, evidence_ref) };
60
+ const { evidence_ref: _evidence, ...fields } = record;
61
+ return { ...fields, dimension_source: { component_id: componentId, revision: digest(record), snapshot_id: fact.locator?.snapshot_id ?? NO_SNAPSHOT,
62
+ ...(evidence_ref !== undefined ? { evidence_ref } : {}), record_schema: "abelien.component-dimensions.v2" } };
63
+ }
54
64
  const fields = { component_id: componentId, dimensions_mm: fact.value, dimension_basis: RECORD_BASIS[fact.basis], dimension_geometry: fact.shape ?? "unknown" };
55
65
  return { ...fields, dimension_source: { component_id: componentId, revision: digest(fields), snapshot_id: fact.source_ref?.digest ?? NO_SNAPSHOT } };
56
66
  }
@@ -1,7 +1,7 @@
1
1
  // hardware.selection-recall: freeze a persisted library/web recall, the caller's registered custom parts and placeholders,
2
2
  // and optionally the parts of a stored v1 selection (hardware.select) into one candidate set. Every fact keeps its own
3
3
  // value, unit, basis, source and locator. No network.
4
- import { dimensionFields } from "./dimensions.mjs";
4
+ import { dimensionFields, sizeBasis } from "./dimensions.mjs";
5
5
  import { electricalFields } from "./electrical.mjs";
6
6
  import { selectionRequestDigest, validateSelectionSlots } from "./selection-request.mjs";
7
7
  import { libraryCandidateText } from "./wiki-content.mjs";
@@ -12,8 +12,9 @@ import {
12
12
  } from "./selection-model.mjs";
13
13
 
14
14
  const UNSPECIFIED_MANUFACTURER = "Unspecified";
15
- // Component database bases map onto fact bases; datasheet values are the manufacturer's.
16
- const FACT_BASIS = { datasheet: "manufacturer", measured: "measured", estimate: "estimate", catalog: "recorded", unknown: "recorded" };
15
+ // Component database bases map onto fact bases; datasheet values are the manufacturer's. A size sizeBasis judges `database`
16
+ // (source-bound and complete) stays database; any other catalog size is only recorded.
17
+ const FACT_BASIS = { datasheet: "manufacturer", measured: "measured", estimate: "estimate", database: "database", catalog: "recorded", unknown: "recorded" };
17
18
  const LOCAL_FRAME = { coordinate_system: "component_local", datum: "unspecified", orientation: "unspecified", tolerance_mm: null };
18
19
  const candidateId = (slotId, kind, key) => `cand-${digest({ slot_id: slotId, item_kind: kind, key }).slice(0, 24)}`;
19
20
  const normalized = value => value.toLowerCase().replace(/[\s_]+/g, "");
@@ -33,7 +34,7 @@ function recordFacts(part, component, addEvidence) {
33
34
  const ref = { id: refId("dimension-record:", component), version: `r-${dims.dimension_source.revision.slice(0, 12)}`, digest: dims.dimension_source.revision };
34
35
  addEvidence({ ref, kind: "dimension_record", content: null, content_sha256: null, locator: { snapshot_id: dims.dimension_source.snapshot_id } });
35
36
  facts.push({ fact_id: property, property, subject: "device_outline", value: dims.dimensions_mm, unit: "mm",
36
- basis: FACT_BASIS[dims.dimension_basis], source_ref: ref,
37
+ basis: FACT_BASIS[sizeBasis(part)], source_ref: ref,
37
38
  locator: { snapshot_id: dims.dimension_source.snapshot_id, field: "dimensions_mm", ...(dims.dimension_source.evidence_ref ? { evidence_ref: dims.dimension_source.evidence_ref } : {}) },
38
39
  conditions: {}, frame: LOCAL_FRAME, method: "component_dimension_record",
39
40
  ...(["solid_box", "bounding_box"].includes(dims.dimension_geometry) ? { shape: dims.dimension_geometry } : {}) });
@@ -5,7 +5,7 @@
5
5
  import { OUTLINE_SUBJECTS, digest, factOf, identityOf, invalid, refId, trustedFact, verifySelectionRevision } from "./selection-model.mjs";
6
6
  import { dimensionCompleteness } from "./dimensions.mjs";
7
7
 
8
- const SIZE_BASIS = { manufacturer: "datasheet", measured: "measured", estimate: "estimate", engineering_design: "estimate", recorded: "catalog", unknown: "catalog" };
8
+ const SIZE_BASIS = { manufacturer: "datasheet", measured: "measured", estimate: "estimate", engineering_design: "estimate", database: "database", recorded: "catalog", unknown: "catalog" };
9
9
 
10
10
  /** The frozen part object behind a candidate: its identity plus its facts, versioned by the identity's own version. */
11
11
  export function componentRefOf(candidate) {
@@ -106,6 +106,10 @@ v1(线性节点序列,旧 workflow 编译来的)原样保留;v2 是新
106
106
 
107
107
  接管(租约过期或同机执行者已死)时,`submitting`/`unknown` 的操作先看已到的回执,再按绑定声明对账:能按操作查询就先查(查不到且“查不到”有确定含义才重新提交);支持幂等提交才按原身份重发;都没有就保持 `unknown`,节点 `blocked`,绝不盲目重发。
108
108
 
109
+ 远端提交或查询返回 unknown/reconcile(含 `STUDIO_WRITE_UNKNOWN`、暂时的 `STUDIO_READ_UNAVAILABLE`)时,原操作保留为非终态 `unknown`,原错误详情与 remote task ID 留在账本,节点 blocked;它不是已确认失败,不生成失败回执。宿主经原身份显式重排后查询或 guarded resume,真实终态回执才完成操作;重复成功回执不会推进两次。外部传入的 failed 回执若仍声明 unknown/reconcile,`deliverReceipt` 以 `RECEIPT_INVALID` 拒绝保存。历史终态记录不因此重写。
110
+
111
+ `recover` 只为已有 pending receipts 的 parked 操作重排;它不是供应商轮询器。无回执时由宿主 recheck/query 调度原身份,`drain` 不增加自动轮询或新出图。已取消/被改版的操作不恢复执行权,迟到真实结果只记历史。`createActionResolver` 拒绝声明 `query_by_operation=true` 却没有 query 函数的 executor;基于原 checkpoint 的模块恢复单独走 resume,不冒充 query。
112
+
109
113
  `cli` 节点的绑定由 `workflow/compile.mjs` 的 `actions.resolve` 给出:写了 `actions` 索引的域包,动作身份、契约版本、实现身份与摘要、提交与取消语义和错误归类都取动作注册表的有效动作(与 `ab action describe` 同一份,见 [动作注册表](../contract/action-registry.md)),本轮只执行与能力 adapter 相同的 module 绑定;没写索引的域包与内核动作仍由能力声明合成身份。注册表有问题时这些动作按配置缺失(`BINDING_MISSING`)停下,不退回合成身份。宿主自己组装 runtime 时用 `createActionResolver({ skillsDir, bindings, executors })`(公开入口导出):身份同样取有效动作,`bindings` 是 bindings.json 形状的部署覆盖,`executors` 按传输方式给执行函数,用法见 [动作注册表](../contract/action-registry.md#宿主侧动作解析web-等宿主)。
110
114
 
111
115
  ## 错误
@@ -78,6 +78,8 @@ export function createActionResolver({ skillsDir, domains, workdir, bindings, ex
78
78
  throw runtimeError("BINDING_MISSING", `no executor for ${effective.binding.transport} bindings (${action_id})`, { action_id, transport: effective.binding.transport });
79
79
  const execution = await executor({ action: effective, node, flow });
80
80
  if (typeof execution?.submit !== "function") throw runtimeError("BINDING_MISSING", `the ${effective.binding.transport} executor gave no submit for ${action_id}`, { action_id });
81
+ if (effective.binding.submission?.query_by_operation && typeof execution.query !== "function")
82
+ throw runtimeError("BINDING_MISSING", `the ${effective.binding.transport} executor gave no query for queryable action ${action_id}`, { action_id });
81
83
  return { ...bindingIdentity(effective), audience: effective.definition.audience ?? "agent",
82
84
  input_schema: (await load()).schema(action_id, "input"), output_schema: (await load()).schema(action_id, "output"),
83
85
  ...(inputRestrictions?.[action_id] ? { input_restrictions: structuredClone(inputRestrictions[action_id]) } : {}),
@@ -975,6 +975,14 @@ export function createRuntime(options) {
975
975
  if ((await pendingReceipts({ ...state.op, state: "accepted" })).length) await ledger.requeueDispatch({ tenant_id: state.op.identity.tenant_id, operation_id: state.op.identity.operation_id });
976
976
  return { accepted: true, remote_task_id: remote ?? null };
977
977
  }
978
+ if (outcome.status === "failed") {
979
+ const error = toActionError(outcome.error, {
980
+ submission: base.binding.location === "remote" ? "unknown" : "accepted", declarations: base.binding.errors,
981
+ });
982
+ if (error.category === "unknown" || error.recovery.action === "reconcile")
983
+ return markUnknown(base, state, error, outcome.remote_task_id);
984
+ outcome = { ...outcome, error };
985
+ }
978
986
  const receipt = await recordOutcome(state.op, base.binding, outcome, claim);
979
987
  return applyReceipt({ ...base, op: state.op }, receipt, claim);
980
988
  }
@@ -999,20 +1007,25 @@ export function createRuntime(options) {
999
1007
  async function resubmit(base, state) {
1000
1008
  return submitOperation(base, state, "resubmit");
1001
1009
  }
1002
- async function markUnknown(base, state) {
1010
+ async function markUnknown(base, state, diagnostic, remoteTaskId) {
1003
1011
  const { claim, run } = base;
1004
- if (state.op.state === "unknown") { await releaseOnly(state.op, claim, "waiting"); return { unknown: true }; }
1005
- const error = toActionError(runtimeError("SUBMISSION_UNKNOWN", `operation ${state.op.identity.operation_id} may have been submitted; reconcile it, never resend`), { category: "unknown", submission: "unknown" });
1012
+ const receipts = await pendingReceipts(state.op);
1013
+ if (receipts.length) return applyReceipt({ ...base, op: state.op }, receipts[0], claim);
1014
+ if (state.op.state === "unknown" && !diagnostic) { await releaseOnly(state.op, claim, "waiting"); return { unknown: true }; }
1015
+ const error = diagnostic ?? toActionError(runtimeError("SUBMISSION_UNKNOWN", `operation ${state.op.identity.operation_id} may have been submitted; reconcile it, never resend`), { category: "unknown", submission: "unknown" });
1006
1016
  const tx = batch(run.root_run_id, scopeOf(state.op));
1007
- tx.op(state.op, { state: "unknown", submission_state: "unknown", error });
1017
+ const remote = typeof remoteTaskId === "string" && remoteTaskId ? remoteTaskId : state.op.remote_task_id;
1018
+ tx.op(state.op, { state: "unknown", submission_state: "unknown", error, ...(remote ? { remote_task_id: remote } : {}) });
1008
1019
  const step = await ledger.getStepExecution(scopeOf(run), base.step.step_execution_id);
1009
1020
  const latest = await ledger.getRun(scopeOf(run), run.run_id);
1010
1021
  if (!TERMINAL_STEP.has(step.state)) {
1011
- tx.step(step, { state: "blocked", wait: { kind: "external_task", operation_id: state.op.identity.operation_id, remote_task_id: state.op.remote_task_id ?? null }, error });
1022
+ tx.step(step, { state: "blocked", wait: { kind: "external_task", operation_id: state.op.identity.operation_id, remote_task_id: remote ?? null }, error });
1012
1023
  if (!TERMINAL_RUN.has(latest.state) && !isGraph(base.flow)) tx.run(latest, { state: "blocked" }, { error });
1013
1024
  }
1014
1025
  await tx.commit(release(claim, "waiting"));
1015
1026
  if (isGraph(base.flow)) await afterCommit(tx, { runs: [latest] });
1027
+ if ((await pendingReceipts({ ...state.op, state: "unknown" })).length)
1028
+ await ledger.requeueDispatch({ tenant_id: state.op.identity.tenant_id, operation_id: state.op.identity.operation_id });
1016
1029
  return { unknown: true };
1017
1030
  }
1018
1031
 
@@ -1238,6 +1251,11 @@ export function createRuntime(options) {
1238
1251
  async function deliverReceipt(receipt, { result } = {}) {
1239
1252
  const op = await ledger.getOperation({ tenant_id: receipt.tenant_id }, receipt.operation_id);
1240
1253
  if (!op) throw runtimeError("NOT_FOUND", `receipt ${receipt.receipt_id} names an unknown operation`);
1254
+ // A model step has no query (MODEL_BINDING): nothing can reconcile it, so the host's unknown verdict is final and blocks the step.
1255
+ // Only actions take the unknown → reconcile path.
1256
+ if (receipt.outcome === "failed" && op.identity.action_id !== MODEL_BINDING.action_id
1257
+ && (receipt.error?.category === "unknown" || receipt.error?.recovery?.action === "reconcile"))
1258
+ throw runtimeError("RECEIPT_INVALID", "an unknown outcome is a reconciliation diagnostic, not a confirmed failure receipt");
1241
1259
  if (receipt.outcome === "succeeded") {
1242
1260
  if (digest(normalize(result)) !== receipt.result_digest) throw runtimeError("RECEIPT_INVALID", "receipt result does not match result_digest");
1243
1261
  await putResult(op, result);
@@ -21,7 +21,7 @@ const KNOWN = {
21
21
  integrity: ["CAPABILITY_RESULT_INVALID", "CONTENT_BLOB_INVALID", "DIGEST_MISMATCH", "RECEIPT_CONFLICT", "RECEIPT_INVALID", "WORKFLOW_DRIFTED",
22
22
  "CAPABILITY_CHECKPOINT_CONFLICT", "OPERATION_CONFLICT", "RESULT_INVALID"],
23
23
  terminal: ["NOT_FOUND", "RESOURCE_NOT_FOUND", "INSTANCE_NOT_FOUND", "INVALID_EVENT", "GUARD_FAILED", "HTTP_STATUS", "DOMAIN_INVALID", "WORKFLOW_INVALID", "CANCELLED", "RESULT_STORE_REFUSED"],
24
- unknown: ["HTTP_FAILED", "RESULT_UNKNOWN", "SUBMISSION_UNKNOWN"],
24
+ unknown: ["HTTP_FAILED", "RESULT_UNKNOWN", "SUBMISSION_UNKNOWN", "STUDIO_WRITE_UNKNOWN", "STUDIO_READ_UNAVAILABLE"],
25
25
  };
26
26
  const DEFAULT_CATEGORY = new Map(Object.entries(KNOWN).flatMap(([category, codes]) => codes.map(code => [code, category])));
27
27
  const CODE_RE = /^[A-Z][A-Z0-9_]{1,63}$/;
@@ -25,7 +25,7 @@ export { interpolate, findWorkflow, loadWorkflow, workflowDefinitionDigest, pars
25
25
  // doctor must report the shipped package version, never a frozen constant (0.2.3 drifted across
26
26
  // six releases). The npm tree always ships package.json at the skill root; standalone skill copies
27
27
  // fall back to FALLBACK_VERSION, which scripts/release-check.mjs pins to package.json.
28
- const FALLBACK_VERSION = "0.19.0";
28
+ const FALLBACK_VERSION = "0.20.1";
29
29
  export let VERSION = FALLBACK_VERSION;
30
30
  try {
31
31
  VERSION = String(JSON.parse(fs.readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version || FALLBACK_VERSION);
@@ -10,7 +10,7 @@
10
10
  "cancel": "unsupported",
11
11
  "submission": {
12
12
  "idempotent_submit": true,
13
- "query_by_operation": true,
13
+ "query_by_operation": false,
14
14
  "remote_task_identity": "none",
15
15
  "not_found_definitive": false
16
16
  }