abelien-harness 0.6.3 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/abelien-appearance/SKILL.md +2 -1
- package/skills/abelien-clarify/SKILL.md +11 -9
- package/skills/abelien-clarify/references/examples.md +3 -3
- package/skills/abelien-clarify/scripts/check_result.py +3 -2
- package/skills/abelien-clarify/scripts/test_check_result.py +13 -0
- package/skills/abelien-guide/SKILL.md +5 -3
- package/skills/abelien-hardware/SKILL.md +2 -2
- package/skills/abelien-hardware/domain.json +1 -1
- package/skills/abelien-hardware/references/hardware-selection/SKILL.md +1 -1
- package/skills/abelien-hardware/references/hardware-selection/references/query-contract.md +6 -4
- package/skills/abelien-hardware/references/planning-modes.md +1 -1
- package/skills/abelien-hardware/references/search-index-format.md +2 -2
- package/skills/abelien-hardware/scripts/dimensions.mjs +13 -8
- package/skills/abelien-hardware-selection/references/hardware-selection-v2/SKILL.md +1 -1
- package/skills/abelien-hardware-selection/references/hardware-selection-v2/references/query-contract.md +6 -4
- package/skills/abelien-hardware-selection/references/selection-service-contract.md +2 -2
- package/skills/abelien-harness/references/adapter-contract.md +1 -0
- package/skills/abelien-harness/scripts/ab.mjs +6 -0
- package/skills/abelien-harness/scripts/web/public-read.mjs +32 -11
- package/skills/abelien-reference/SKILL.md +7 -4
- package/skills/abelien-reference/domain.json +4 -3
- package/skills/abelien-reference/fixtures/create.final.json +20 -0
- package/skills/abelien-reference/fixtures/restore.collected.json +20 -0
- package/skills/abelien-reference/fixtures/restore.final.json +24 -2
- package/skills/abelien-reference/references/research-contract.md +20 -11
- package/skills/abelien-reference/schemas/reference-evidence.v1.json +18 -4
- package/skills/abelien-reference/schemas/reference-judgement.v1.json +6 -6
- package/skills/abelien-reference/scripts/evidence.mjs +6 -3
- package/skills/abelien-reference/scripts/finalize.mjs +60 -14
- package/skills/abelien-reference/scripts/measure.mjs +4 -2
- package/skills/abelien-reference/scripts/png.mjs +20 -4
- package/skills/abelien-reference/scripts/research.mjs +93 -22
- package/skills/abelien-structure/SKILL.md +2 -2
- package/skills/abelien-structure/domain.json +1 -1
- package/skills/abelien-structure/references/build123d-execution.md +2 -2
- package/skills/abelien-structure/references/handoff.md +1 -1
- package/skills/abelien-structure/worker/cli-adapter.mjs +56 -13
- package/skills/abelien-structure/worker/main.mjs +15 -4
- package/skills/abelien-structure/worker/supervisor.mjs +6 -2
- package/skills/abelien-workspace/SKILL.md +2 -1
- package/skills/abelien-workspace/assets/journey-stage-plan.json +21 -15
- package/skills/abelien-workspace/domain.json +1 -1
- package/skills/abelien-workspace/references/harness-binding.md +7 -2
- package/skills/abelien-workspace/references/prd-contract.md +1 -1
- package/skills/abelien-workspace/references/user-journey.md +40 -12
- package/skills/abelien-workspace/scripts/journey-next.mjs +17 -5
- package/skills/abelien-workspace/workflows/definitions/748a70482fb2f775f6e5fd942c7873534cf3e59614c77381f812a9710d479743.json +1 -0
- package/skills/abelien-workspace/workflows/definitions/a1021c217f2894da423f8591842ea9f349bc526ac3119352b4bea02e8dbd42f9.json +1 -0
- package/skills/abelien-workspace/workflows/definitions/df147960a3f7c4d631f66a6b91a7891f8a37d25a50322cf09bdc9304195963b0.json +1 -0
- package/skills/abelien-workspace/workflows/user-journey-hardware-candidates.json +1 -1
- package/skills/abelien-workspace/workflows/user-journey.json +4 -4
package/README.md
CHANGED
|
@@ -128,7 +128,7 @@ tool-receipt successor 与已有可信 runtime decision 也可以取得后续 co
|
|
|
128
128
|
|
|
129
129
|
- 对话调度与沟通由 [abelien-guide](skills/abelien-guide/SKILL.md) 负责:组织上下文、调用领域 skill、呈现问题和结果;阶段、版本和确认条件统一读取 workspace。
|
|
130
130
|
- 作品规则由 [abelien-workspace](skills/abelien-workspace/SKILL.md) 维护:完整 A1–C、采用与版本、组合检查、确认和交付恢复。其 `user-journey` 是单个 Agent 步骤,Harness 提供通用运行机制;产品服务的实际接入边界见 workspace 调用说明。
|
|
131
|
-
- 需求澄清为 [abelien-clarify](skills/abelien-clarify/SKILL.md):把用户原话整理成 requirements、区分用户事实与系统假设、只问阻塞当前操作的问题,附只读校验脚本;共用表达规则随 guide 的 references
|
|
131
|
+
- 需求澄清为 [abelien-clarify](skills/abelien-clarify/SKILL.md):把用户原话整理成 requirements、区分用户事实与系统假设、只问阻塞当前操作的问题,附只读校验脚本;共用表达规则随 guide 的 references 分发。两者自 2026-10-11 起以本仓为唯一编辑源,直接在 `skills/abelien-clarify/` 与 `skills/abelien-guide/references/` 修改,见下文「快照来源」。
|
|
132
132
|
- 各 bundle 均为标准 `SKILL.md`(含 name/description/license/compatibility,
|
|
133
133
|
兼容 claude-code / hermes / pi),agent 按 description 触发词路由;claude-code 与
|
|
134
134
|
codex 的双入口收敛(根 `CLAUDE.md` / `AGENTS.md`)见 [docs/architecture/host-adapters.md](docs/architecture/host-adapters.md)。
|
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@ compatibility:
|
|
|
28
28
|
1. **核对对象与版本。** 明确是整机还是部位、当前作品/基线、固定区域、允许变化范围、用户要求和参考来源。过期图片或旧渲染不能覆盖新版。
|
|
29
29
|
2. **建立外观目标。** 将整体轮廓、比例、分件、特征位置、颜色、材质、纹理、透明/发光表现整理成稳定目标;区分硬要求、偏好、观察事实、推断和未知。
|
|
30
30
|
3. **提出编辑。** 输出改变前后、影响部位、所用参考及预期视图。纯 CMF 变化不假装结构改变;新增摄像头、开口或可动部位时通知 hardware/structure。
|
|
31
|
-
4. **按需生成视觉资产。** 无现成 CAD 的文字 Studio 目标是在需求足够后先出概念图,默认沿用原图绑定真实硬件并交 structure
|
|
31
|
+
4. **按需生成视觉资产。** 无现成 CAD 的文字 Studio 目标是在需求足够后先出概念图,默认沿用原图绑定真实硬件并交 structure;缺图、自然语言重试与重画统一按 workspace 的[图片请求规则](../abelien-workspace/references/user-journey.md#image-retry-intent)承接。已有符合需求的真实 CAD 时,概念图不应成为首次展示的硬性前置。概念探索可使用图像生成;需要图生网格时调用同级 `abelien-3dgen`。生成图或网格是外观候选,不是可制造 CAD,也不代表已经采用。
|
|
32
32
|
5. **交给 structure 实现。** 提供形态目标、视图、比例基准、必须保留的特征和允许妥协范围。structure 返回空间/制造冲突、实际模型、相机信息和真实渲染。
|
|
33
33
|
6. **实际视觉审查。** 读[视觉审查](references/visual-review.md),真正查看目标参考与本版实际渲染;分别记录 pass、conflict、unknown、not_checked。脚本通过或文字自述“很像”不能替代看图。
|
|
34
34
|
7. **迭代或交回。** 可在授权范围内修正外观目标,受工程约束影响的方案回到 structure。只有影响用户目标或需要其取舍时进入 B2;完成后把同版提案、证据和视觉结论交回 workspace。
|
|
@@ -39,6 +39,7 @@ compatibility:
|
|
|
39
39
|
|
|
40
40
|
- appearance 决定外观意图与视觉是否符合;structure 决定如何实现以及几何是否成立。
|
|
41
41
|
- appearance 可以编辑外壳的“形”,但参数化实体、壁厚、装配、开孔、碰撞和导出由 structure 负责。
|
|
42
|
+
- 外形未锁定而硬件装不下时,appearance 给出可接受的外观容差(尺寸与比例可调整的范围),structure 只在容差内调整外壳;超出容差只能换件或交用户取舍,不自行放大外壳。外形锁定时不得改变外形,见 structure 的[交接规则](../abelien-structure/references/handoff.md)。
|
|
42
43
|
- 视觉审查与几何检查是独立门;任何一方通过都不能代替另一方。
|
|
43
44
|
- 纯颜色/材质变化通常只使相关渲染与视觉报告失效;轮廓、分件、开口或尺寸变化会使相关结构检查失效。
|
|
44
45
|
- 用户喜欢某个预览不自动等于工程视觉审查 pass;视觉 pass 也不等于用户确认或 workspace 已采用。
|
|
@@ -36,9 +36,9 @@ type Envelope =
|
|
|
36
36
|
type Intent = {
|
|
37
37
|
kind: "restore" | "create" | "modify";
|
|
38
38
|
source: "user" | "system";
|
|
39
|
-
keep: string[]; // 保留的来源产品特征;create
|
|
40
|
-
changeable: string[]; //
|
|
41
|
-
depth?: "appearance" | "assembly" | "functional"; //
|
|
39
|
+
keep: string[]; // 保留的来源产品特征;create 为空;各至多 20 条,每条去首尾空白后非空、至多 500 个码点
|
|
40
|
+
changeable: string[]; // 允许重新设计的特征;条数与长度同 keep
|
|
41
|
+
depth?: "appearance" | "assembly" | "functional" | null; // 目标深度:外观模型、可装配结构、功能样机;推断不出时省略,null 与省略同义
|
|
42
42
|
};
|
|
43
43
|
Requirements = {
|
|
44
44
|
context?: {text: string, source: "user" | "system"};
|
|
@@ -72,19 +72,21 @@ ID 是不透明标识,不能按数组位置重新编号;新 ID 不得与现
|
|
|
72
72
|
|
|
73
73
|
1. 枚举原话直接支持的解释,最多三个,每个都有原话依据;不添加未提及的生活背景。
|
|
74
74
|
2. 临时比较器件方向,再核对对象、方向、时序等完整语义。器件标签相同但实现不同的解释不能归并。
|
|
75
|
-
3.
|
|
75
|
+
3. 只剩一个解释则继续;多个解释会改变当前选择时,首版之前按最可能的解释继续并标 source=system、在 note 说明,首版之后才提出问题;没有解释则按“已有事实推导 → 有依据且可撤销的默认 → 必要问题”补全。提问一律受下文“提问时机”约束。
|
|
76
76
|
|
|
77
|
-
能说明拟探索行为的落点、事件/状态和输出,且不存在阻塞这次选择的歧义,就可以交给总控推进。其他行为与未定项继续保留,不生成全局 ready / complete
|
|
77
|
+
能说明拟探索行为的落点、事件/状态和输出,且不存在阻塞这次选择的歧义,就可以交给总控推进。其他行为与未定项继续保留,不生成全局 ready / complete 状态;“可以开始搜索”不代表需求已满足。一个假设会改变用户明确要求、替换保留的底座或突破已有授权时,不能用“可撤销”绕过用户决定:首版之前改走不触碰这些要求的默认方向,把该决定留到首版之后;只有符合“提问时机”例外时才在首版之前提问。
|
|
78
78
|
|
|
79
79
|
尺寸是推导变量,从落点与行为提出可见、可修改的范围;可提出 source=system 的可见估算,必须说明依据且不得冒充用户确认、实测或图片比例尺。envelope 所有尺寸用毫米有限正数,范围最小值不超过最大值;精确值可令 min=max。只使用匹配 shape 的字段,不混用盒体和圆柱字段。wall_mm.minimum 是明确保证的非负壁厚下界,未提供 wall_mm 时按 0;estimate 不得小于 minimum,只参与风险提示,不硬拒绝。保证下界扣除后最大腔体各轴必须为正;范围较小端或估算壁厚不可行仍保留供风险评审。只有 source=user 且 strength=hard 的最大范围具有硬筛选权限;系统估算和偏好不能硬拒绝。未知形状不填写 envelope,不编造 box。
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
没有明确尺寸要求,不主动索要尺寸;“必须塞进这个格子”缺尺寸且无法合理默认时才问,这正是首版之前允许提问的例外情形。品类默认和器件分类用输入中的数据,不在 skill 固化型号、价位或默认值表。工程资料缺失交给总控补充,不让用户提供接口、料号或测试证明。
|
|
82
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`
|
|
83
|
+
**意图与目标深度。** 用户给出图片、链接或产品名,或说“还原/复刻/同款/拆解/这款”时,先按原话判定本次意图:还原现成产品(restore)、创造新产品(create)或基于现成产品修改(modify)。判定依据是原话,不是输入形式:商品图配“参考这个风格做一个”是 create;“参考某产品,再加一个屏幕”是 modify。判定结果写入 `result.intent`:`kind` 为意图;`keep` 列出保留的来源产品特征(restore 默认含外形尺寸与比例,modify 逐项列出,create 没有来源产品,必须为空);`changeable` 列出允许重新设计的特征;`depth` 为还原或修改的目标深度(appearance 外观模型、assembly 可装配结构、functional 功能样机),原话推断不出时省略。原话直接说明意图(如“还原这款”“参考它再加一个屏幕”“做一个新的”),或者根本没有提到任何现成产品时,`source` 记 user;需要从图片、链接或措辞推断时记 system,并在 note 写明依据。宿主按 `intent.kind` 安排旅程,参考研究的输入也从这里取;它不单独构成首版之前的问题,以系统假设展示,用户随时可改,改动就是一次需求修订。`intent` 是需求的一部分,硬件召回与联网证据的 `request_digest` 绑定整份需求,所以只改 `intent`(包括 `keep`、`changeable`、`depth`)也会让进行中的召回与证据失效,须重新检索并再次产生检索费用;不要为了省检索而把意图改动挪到需求之外。来源产品的功能只能以 source=system 写入 reaction,并注明来自来源产品;“原产品有这个功能”不等于“本次要实现”,按目标深度决定是否纳入,外观模型不纳入语音等功能实现。
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
**提问时机(2026-10-12 裁定)。** 首版 CAD 之前默认不向用户提问:按原话和合理默认继续,按默认处理的事项登记为系统假设或待确认项,由宿主展示、用户可随时修改;首版之后,把仍按默认处理的事项合并成一张问题卡。唯一例外:用户提出的硬要求缺少取值、无法合理默认,且按默认继续会做出错误结果(例如“必须放进这个卡槽”却没给尺寸)时,可以在首版之前提问,至多一张卡,沿用问题卡上限(最多三题、每题最多三个选项)。调用方说明首版是否已形成;未说明时按首版之前处理。首版之前返回的 questions 只能属于这一例外,其余未定项按默认写入 result(source=system)并在 note 说明,留到首版之后合并询问。意图、目标深度与型号识别不属于例外。
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
只提出现在必须由用户决定、且符合提问时机的问题:闭环歧义、落点歧义或挡住当前选择的硬要求缺值/相互矛盾;缺少 PRD 字段本身不是问题。提问前能在 note 中说明:不回答会卡住哪个选择、为何现有事实和授权不足以代定。默认一题,相互独立才合并,最多三题;每题最多三个实质选项,表达遵循已注入的共用规则。用户意图选项可以来自原话支持的不同解释,不冒充已有硬件或工程可行候选。
|
|
88
|
+
|
|
89
|
+
暂停追问时不返回新问题,在 note 保留当前缺口及影响;用户对某项说“别帮我猜”时停用该项默认。未答、跳过或催促不构成授权。卡片部分回答只采用明确答复,其余根据本轮情况重新判断;不把未选项当作撤回。每轮重新判断,不保存问题队列;首版之后的合并问题卡同样每次按当前仍按默认处理的事项重新生成,不是保存的队列,用户已改掉或不再适用的默认不再出现。
|
|
88
90
|
|
|
89
91
|
## 交付
|
|
90
92
|
|
|
@@ -73,10 +73,10 @@
|
|
|
73
73
|
|
|
74
74
|
| 本轮输入与上下文 | 处理 |
|
|
75
75
|
|---|---|
|
|
76
|
-
|
|
|
77
|
-
| 已有提醒行为,“必须塞进这个格子”,格子尺寸确实挡住当前硬件选择 |
|
|
76
|
+
| “陪我妈的机器人”,还不知道需要什么行为,首版尚未形成 | 保留“给妈妈用”;按最常见的陪伴方式补一条 source=system 的行为假设,note 说明依据及它会改变选型,首版之前不提问,首版之后并入合并问题卡确认。不推断她独居、年老或有疾病 |
|
|
77
|
+
| 已有提醒行为,“必须塞进这个格子”,格子尺寸确实挡住当前硬件选择 | 登记该硬要求;只问格子可用空间(硬要求缺值、无法合理默认,属于首版之前也允许提问的例外)。不要求用户提供料号、接口或测试报告 |
|
|
78
78
|
| “没网,但要看画面” | 保留原话,探索本地显示;没有证据不把它判为矛盾 |
|
|
79
|
-
|
|
|
79
|
+
| 首版已形成,已有“不能联网”,本轮明确增加“必须调用云端服务”,未表示撤回离线要求 | 保留两项明确要求,问离线还是云端哪项必须保留;不静默放宽旧要求 |
|
|
80
80
|
| “先别问了,也别替我猜尺寸” | 不返回 questions;note 说明尺寸未定及其实际影响,独立工作可交总控继续。没有得到代选授权 |
|
|
81
81
|
| “颜色你定”,同时已有“不能联网” | 代选颜色的内容标 system、强度不升为 hard;授权只针对颜色,离线要求保留 |
|
|
82
82
|
|
|
@@ -112,8 +112,9 @@ def check_intent(value: Any, path: str, errors: list) -> None:
|
|
|
112
112
|
issue(errors, f"{path}.kind", "必须是 restore、create 或 modify")
|
|
113
113
|
if value.get("source") not in SOURCES:
|
|
114
114
|
issue(errors, f"{path}.source", "必须是 user 或 system")
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
# null 与省略同义(未推断出目标深度),与 reference-evidence 的 depth 口径一致。
|
|
116
|
+
if value.get("depth") is not None and value["depth"] not in DEPTHS:
|
|
117
|
+
issue(errors, f"{path}.depth", "必须是 appearance、assembly、functional 或 null")
|
|
117
118
|
check_features(value.get("keep"), f"{path}.keep", errors)
|
|
118
119
|
check_features(value.get("changeable"), f"{path}.changeable", errors)
|
|
119
120
|
if value.get("kind") == "create" and value.get("keep"):
|
|
@@ -108,6 +108,19 @@ class ContractTests(unittest.TestCase):
|
|
|
108
108
|
result["intent"] = intent
|
|
109
109
|
self.assertFalse(validate({"result": result})["valid"])
|
|
110
110
|
|
|
111
|
+
def test_intent_depth_null_reads_as_not_inferred(self):
|
|
112
|
+
# reference-evidence writes an uninferred depth as null; the host may carry it back into requirements.
|
|
113
|
+
result = current_requirements()
|
|
114
|
+
result["intent"] = {"kind": "restore", "source": "user", "keep": ["外形尺寸与比例"], "changeable": [], "depth": None}
|
|
115
|
+
self.assertTrue(validate({"result": result})["valid"])
|
|
116
|
+
|
|
117
|
+
def test_intent_features_count_code_points_like_reference(self):
|
|
118
|
+
result = current_requirements()
|
|
119
|
+
result["intent"] = {"kind": "modify", "source": "user", "keep": ["\U0001F600" * 500], "changeable": []}
|
|
120
|
+
self.assertTrue(validate({"result": result})["valid"])
|
|
121
|
+
result["intent"]["keep"] = ["\U0001F600" * 501]
|
|
122
|
+
self.assertFalse(validate({"result": result})["valid"])
|
|
123
|
+
|
|
111
124
|
def test_consultation_does_not_create_a_diff(self):
|
|
112
125
|
report = validate({"note": "能否认出家人需要选型证据,当前不增加识别要求。"}, current_requirements())
|
|
113
126
|
self.assertTrue(report["valid"])
|
|
@@ -18,9 +18,9 @@ compatibility:
|
|
|
18
18
|
|
|
19
19
|
1. **识别本轮操作。** 区分咨询、新创作、需求变更、答复、收尾/返回编辑、确认和工具结果。纯咨询先回答;需要细化需求语义、指代或混合意图时交 clarify,不在 guide 再实现一套提取规则。
|
|
20
20
|
2. **取得当前上下文。** 按 workspace 的[领域模型](../abelien-workspace/references/domain-model.md)及[调用与恢复说明](../abelien-workspace/references/harness-binding.md)恢复作品、事件、任务和证据。已有运行先恢复,读取失败交回具体缺口。
|
|
21
|
-
3. **按需调用 clarify。**
|
|
21
|
+
3. **按需调用 clarify。** 新增、修改、撤回需求或影响需求的答复需要整理时,读取其入口,提供同版原话、当前完整需求、本次操作、相关方案、授权与会话约束;答卡附原题和真实答复事件;工具结果中的用户原文仍按 user 来源处理,只采明确子集,不用生成的聊天 message 冒充回答事件。纯确认或仅工具回传不机械重跑 clarify。
|
|
22
22
|
4. **承接需求建议。** clarify 返回 `result / questions / note`;按其契约复核语义与结构,再按 workspace 的采用规则交给实际服务。没有 result 时沿用当前需求。需求变更按 workspace 的 [PRD 契约](../abelien-workspace/references/prd-contract.md)在本轮登记/修改/撤回条目并回读,不等最终确认;只调度该契约,不在 guide 复制模板或登记逻辑。必要问题结合当前上下文呈现,不把问题提出或用户回答当作方案已修改。
|
|
23
|
-
5. **选择下一步。** 依据 workspace 的[完整旅程](../abelien-workspace/references/user-journey.md)核对推进条件,按其能力绑定选择领域 skill、workflow 或产品服务。目标旅程先复用符合需求的真实 CAD;没有现成 CAD 时依宿主阶段生成概念图并自动选型、绑定原图、构建 CAD
|
|
23
|
+
5. **选择下一步。** 依据 workspace 的[完整旅程](../abelien-workspace/references/user-journey.md)核对推进条件,按其能力绑定选择领域 skill、workflow 或产品服务。目标旅程先复用符合需求的真实 CAD;没有现成 CAD 时依宿主阶段生成概念图并自动选型、绑定原图、构建 CAD/审查;实际安装版本不支持时报告能力缺口,不调用未注册工具。必要问题只调用宿主已注册的 `publish_agent_card(kind=question)`,持久绑定原模型调用后结束执行,用户独立提交后以原调用的工具结果承接原任务;不等待 HTTP/Worker、不先把 card_id 返回为最终答案、不继续执行依赖答案的工具批次。可选交互邀请遵守宿主实际展示门(核对实际版本的 3D 加载与适用审查),不得因需求足够就抢先覆盖必要问题卡;按旅程 §4.1 的通用目标告知随时可收尾,不插入需求版 PRD 展示关卡。点击“随时收尾”后按 §4.2 调用宿主成果体验模式,核对仿真或 Three.js 行为演示能力,并保留 PRD 查看/导出入口。调用前读对应 skill;能力不存在时报告具体缺口。实际能力返回后回到当前事实重判,不自行选择型号、算法或编造工程结论。
|
|
24
24
|
6. **向用户交代结果。** 根据真实产物说明本次变化、当前缺口和下一步,区分 PRD 已登记的需求与仍待实现/检查的方案;没有写入回执就明确尚未保存。首版展示、必要取舍及确认邀请的时机均采用 workspace 规则;表达方式采用共用表达规则。
|
|
25
25
|
|
|
26
26
|
图片、链接或“做同款”仍走同一入口,不自动开启另一条整机还原流程。入口按本轮意图调度:还原现成产品(restore)、创造新产品(create)或基于现成产品修改(modify)共用同一旅程,差别只在首先展示什么、参考资料怎么用、哪些特征锁定,依据是原话而不是输入形式,详见[意图路由](../../docs/architecture/intent-routing.md)。还原与改现成先识别产品,展示原产品图及来源后继续;新创先参考相似产品再出概念图,用户已有明确设计图时直接沿用。意图、目标深度与识别结果在首版之前不单独发问:按推断继续,作为系统假设展示并可随时修改;型号识别冲突时保留候选,用户切换即视为需求修订。改轮廓、颜色、材质、表面语言或参考匹配时调用 [appearance](../abelien-appearance/SKILL.md);空间约束、布局、装配、Build123d 或几何检查调用 [structure](../abelien-structure/SKILL.md)。外壳形状同时涉及两者时,由 appearance 定义目标、structure 实现并回传实际渲染、appearance 做视觉验收。只把必须由用户决定的差异交回用户,不让内部视觉审查成为额外的必答关卡。
|
|
@@ -29,7 +29,7 @@ compatibility:
|
|
|
29
29
|
|
|
30
30
|
## 交互邀请与成果呈现
|
|
31
31
|
|
|
32
|
-
交互细化与收尾采用 workspace §4.1–§4.2;下列话术补充共用表达规则的起点开口,具体时机以 workspace
|
|
32
|
+
交互细化与收尾采用 workspace §4.1–§4.2;下列话术补充共用表达规则的起点开口,具体时机以 workspace 为准。共用表达规则与本包特有的引导都在 harness 仓直接维护(共用表达文件在本包 references/ 中,不是外部同步的快照)。
|
|
33
33
|
|
|
34
34
|
通过选择题卡片呈现“核心需求已经清楚了,接下来你想?”,选项为“继续细化交互”和“先收尾,体验成果”,分别说明可以细化触发/回应/停止,或先体验成果、以后再补。用户选择继续后才围绕真正未定的交互细节询问;选择收尾即切换成果体验。卡片属于可选操作,不变成 B2 必答阻塞,不自动代选,也不覆盖必要问题。同轮起点邀请合并到同一卡片;用户已要求停止追问时不再邀请。仅使用宿主真实支持的卡片接口,未接通时如实说明,不能把普通文字说成已弹出卡片。
|
|
35
35
|
|
|
@@ -39,6 +39,8 @@ compatibility:
|
|
|
39
39
|
|
|
40
40
|
当前输入、证据与授权仍支持下一步时继续推进。需要用户决定、等待真实任务、缺必要能力或已达到本轮交接条件时,交代结果与下一步并结束本轮;无新依据时不重复请求。
|
|
41
41
|
|
|
42
|
+
原生答卡按[问题工具接续契约](../../docs/architecture/question-card.md)消费:选项 ID 和自由原文走原 toolCallId 的 tool 结果,不能包装成新 User 消息。旧卡缺可信原调用时只走明确旧协议或说明缺口;不伪造调用,不把答卡、暂停追问或物理接续 run 当作品确认。可选交互邀请沿用非阻塞接口,不使用必要问题的等待机制。
|
|
43
|
+
|
|
42
44
|
workflow 的恢复、advance、cancel 及确认后的交接均按 workspace 的运行约定执行。guide 只消费真实回执,不把工具成功、需求建议或自己的总结当成作品已采用、已确认或已交付。
|
|
43
45
|
|
|
44
46
|
用户看到的是方案、变化、问题和产物;内部阶段编号、HTTL、cursor 与资源 ID 留在工作上下文。
|
|
@@ -20,7 +20,7 @@ allowed-tools:
|
|
|
20
20
|
|
|
21
21
|
## 与结构协作
|
|
22
22
|
|
|
23
|
-
接收同级 [structure](../abelien-structure/SKILL.md) 提出的尺寸、开口、安装及运动空间约束;约束只是选型输入,不授权结构域自行换型号。结构分区的可用空间写在槽位 `fit_envelope`(`envelope`、`basis`、可选 `zone`),只作用于该槽位:`locked
|
|
23
|
+
接收同级 [structure](../abelien-structure/SKILL.md) 提出的尺寸、开口、安装及运动空间约束;约束只是选型输入,不授权结构域自行换型号。结构分区的可用空间写在槽位 `fit_envelope`(`envelope`、`basis`、可选 `zone`),只作用于该槽位:`locked`(结构声明的固定分区)在可信尺寸放不下时把该件标为风险并交用户在换件与放宽分区/外形之间裁决(`DIMENSION_DECISION_REQUIRED`),不硬拒绝;`inferred` 只让放不下的件排在后面。硬拒绝仍只属于用户硬尺寸上限。选定后交出真实组件 ID、数量、版本和已有 CAD/尺寸依据;缺少几何资产须明示,BOM 抽取不代表 CAD 已逐件拆出。装不下或接口冲突时按原要求回到本包比较替换方案,不能悄悄缩放实体组件。
|
|
24
24
|
|
|
25
25
|
“还原参考产品”的产品识别、规格与拆解资料由同级 [reference](../abelien-reference/SKILL.md) 收集并核验;本包读取其 final 证据,保留有来源的原始 BOM 与所选方案逐项对照,区分原件、替代件与未知;替代的可见外观影响交 appearance,空间/接口影响交 structure,行为影响交 algorithm。没有原始证据时可以提出候选,不声称候选即原机。不要把额外研究记录塞进未支持的选型 schema;作为关联证据交 workspace。
|
|
26
26
|
|
|
@@ -36,7 +36,7 @@ allowed-tools:
|
|
|
36
36
|
不要一次加载整个分类目录或大量 Wiki 全文。分类阶段只产出功能槽位、数量、受控三级路径和本次硬约束,不指定 SKU。
|
|
37
37
|
|
|
38
38
|
- 新建方案可先调用 `hardware.search`,按受控三级分类读取 TOS `_catalog.json` 和对应的压缩分类索引,只下载索引 Top-K 的完整 Wiki;详细协议见[Wiki 检索契约](references/knowledge-recall-service-contract.md)。也可直接执行 `ab search_hardware <query> --category-path <l1/l2/l3>`。它只产生候选召回,不能替代正式选型服务。
|
|
39
|
-
- Agent 必须读取检索结果中的最新完整 Wiki
|
|
39
|
+
- Agent 必须读取检索结果中的最新完整 Wiki,按功能槽位逐槽位选择零部件;同类整机配置只作参考,不先比较或套用整机(还原、基于现成产品修改或用户明确要求沿用整机时除外);只能提交候选中真实的 `component_id`。
|
|
40
40
|
- 候选比较如需保留,调用本地 `hardware.finalize` 归一化为 `hardware-proposal`;`proposed`/`needs_confirmation` 只代表候选提案,不能通过 `hardware-selection` 门,也不能直接开始算法阶段。
|
|
41
41
|
- 正式新建选型走同级 `abelien-hardware-selection` 的本地 `hardware.select`:回读真实召回资源校验 Agent 逐项证据判断,返回版本化 `hardware-selection`。`hardware.finalize` 本身仍不授予 verified。具体契约及未实现工程能力见其 selection-service-contract;主流程不需要 HTTP 服务地址。
|
|
42
42
|
- 已有方案只改指定部件时,先读[规划模式与修订边界](references/planning-modes.md),再调用 `hardware.revise`;未点名槽位保持不变。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abelien-hardware",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
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",
|
|
@@ -35,6 +35,6 @@ description: 为已澄清的产品需求选择数据库中的硬件型号。用
|
|
|
35
35
|
- 新型号归类先核对实际交付物(独立芯片、已组装模组或板卡)和主要用途;兼具功能放在能力字段,不因支持某协议或包含某芯片就重复写入两个主分类。遇到 NFC/RFID、处理器/控制板、充电/电源管理交界时读取相邻二级页的边界;旧 Wiki 的 `hardware_form` 或名称单独不足以证明交付形态。
|
|
36
36
|
- 把 `compare_by` 当作“应比较哪些规格”的提示;真正的硬约束来自需求和逐型号事实。`roles`、场景标签、Wiki prose 不能单独证明兼容。
|
|
37
37
|
- 用户约定库内硬件和 CAD 均为标准可选资源;`draft` 等维护状态、`selection_gate`、`selection_completeness`、CAD 时间/质量标记均不参与本选型过滤。未知规格记 `unknown`,不得写成通过或失败。
|
|
38
|
-
-
|
|
38
|
+
- 整机配置只作参考:新建产品默认按功能槽位逐槽位选型(`assemble_new`),同类整机的功能组合、板卡划分与布局可作参考,但不先套整机、不抵扣槽位。只有还原现成产品、基于现成产品修改,或用户明确要求沿用某台整机时,才评估整机改造,并按已验证的功能与数量覆盖逐槽位扣减;整机不必位于 `robot-system` 组,也可能属于移动底盘、机械臂等路径。细则随检索阶段加载 [query-contract](references/query-contract.md)。电气/端口/机械兼容、候选排序与补件由确定性后端执行;Agent 只提交功能槽位和分类路径,解释后端返回的证据与缺口,不凭记忆指定 SKU。
|
|
39
39
|
|
|
40
40
|
输出每个槽位的路径、数量、候选型号与 ID、已验证/不兼容/未知条件、替代路径及未覆盖功能。若没有可证实满足硬约束的型号,明确返回缺口,不伪造结果。
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
## 查询顺序
|
|
10
10
|
|
|
11
11
|
1. 用届时的运行时受控分类目录校验路径。若某路径不存在或不可选,报告目录不一致;不要偷换到相近类。离线 Wiki 演练仅核对本目录路径,不等于已通过线上校验。
|
|
12
|
-
2.
|
|
12
|
+
2. 整机配置只作参考,不先检索整机再扣减槽位。新建产品默认按全部需求槽位、精确 `category text[]` 查询非整机候选(`assemble_new`);`integration_level=system` 的候选可一并召回,用来参考同类产品的功能组合、板卡划分与布局,但不先套用、不抵扣槽位。只有还原现成产品、基于现成产品修改,或用户明确要求沿用某台整机时,才按下文评估整机改造与覆盖扣减(整机不限于 `robot-system` 组)。
|
|
13
13
|
3. 查询须稳定分页至完整候选集,或让后端证明其召回上限不会漏掉合格型号。不得以首批 `LIMIT 50` 为空/不合格就判断全库无货;可在后端做确定性预筛和 Top-K,但需保留召回计数及淘汰原因。
|
|
14
14
|
4. 按本次用户约定,不用 `status`、`draft`、`selection_gate`、`data_quality`、CAD provenance/时间筛除型号。`roles`/场景标签仅作辅助召回,不作为必要条件。未来服务若按这些字段过滤,应标记“接口行为与本契约不一致”。
|
|
15
15
|
|
|
16
16
|
## 新建产品的规划模式
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
新建产品默认 `assemble_new`。只有还原现成产品、基于现成产品修改,或用户明确要求沿用某台整机时,才另行评估 `adapt_system`,且必须满足下列边界,不能看到相似整机就直接采用:
|
|
19
19
|
|
|
20
20
|
- `adapt_system`:选择一个 `integration_level=system` 整机作为基线,只替换或补充少量、明确可更换的槽位。
|
|
21
21
|
- `assemble_new`:不继承整机基线,从需求槽位重新组成完整硬件方案。
|
|
@@ -26,12 +26,14 @@
|
|
|
26
26
|
|
|
27
27
|
`adapt_system` 的结果须给出基线整机、继承部件、替换/补充清单、修改闭包和每项证据;`assemble_new` 须明确没有继承整机。两种模式最终都生成完整 BOM,不能只返回差异件。
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## 整机参考与功能覆盖
|
|
30
|
+
|
|
31
|
+
以下覆盖与扣减规则只在评估 `adapt_system` 时适用;`assemble_new` 中整机只作配置参考,不抵扣任何槽位。
|
|
30
32
|
|
|
31
33
|
1. 保留需求分解后的完整槽位清单及每槽位 `quantity`、硬约束和是否要求独立可拆部件。整机候选的主要功能须命中至少一个可由整机交付的需求槽位;不能因为它附带多种传感器,就用不相关的整机代替用户要做的产品。若需求明确要求自行组装或独立部件,不把预装整机强加给该槽位。
|
|
32
34
|
2. 先核对整机本身满足其主功能、数量与不可放宽的性能、电气、接口、外形要求。再逐条核对它对其他槽位的显式覆盖:以届时数据库中的结构化覆盖记录(当前契约预期为 `component_spec.functional.system_coverage`)所列受控三级路径、覆盖数量及可核验事实为准,并验证内置功能在本次安装/使用条件下可用;接入时要核对实际字段映射。Wiki `capability_coverage` 可提示哪些路径值得检查,但须与当前数据库记录核对;`functional_capabilities`、标题或描述本身不能证明已包含某数量的部件。
|
|
33
35
|
3. 对每个需求槽位建立覆盖账本:`remaining_quantity = max(0, requested_quantity - verified_allocated_quantity)`。只分配已选中整机的覆盖,且同一内置资源不能被重复用于两个需要独立实例的槽位;部分覆盖只扣实际数量,不删除整条分类。例如需求两组电池而整机仅证实含一组,仍检索另一组。
|
|
34
|
-
4.
|
|
36
|
+
4. 整机不是优先候选,只在 `adapt_system` 评估中参与比较,也不是无条件选中。先排除已证实违反硬约束的整机;在可行整机方案之间,优先满足主功能并覆盖更多**本次需求中、可核验的不同槽位及其数量**,再比较剩余补件、兼容风险与用户优先级。不能只按 Wiki 所列能力总数或匹配类别个数选型;多个整机同时入选时要防止重复抵扣。
|
|
35
37
|
5. 缺失覆盖记录或关键兼容证据时该部分为 `unknown`,不能最终抵扣;可给出待确认的整机方案,同时保留未确认槽位及补件候选。只有覆盖和兼容已验证时,才把相应数量从后续零部件选型中移除。整机主功能和附带能力都按此证据原则判定,不能把“整机自带”推定为已满足用户的具体指标。
|
|
36
38
|
|
|
37
39
|
输出覆盖账本中的原始需求、所选整机 ID、每槽位已验证抵扣量、待确认量和剩余量;保留未入选整机及不适用原因,避免前端将“跳过零部件”误解为漏选。
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- `assemble_new`:新建产品,不继承整机基线,按全部功能槽位形成完整 BOM。
|
|
9
9
|
- `revise_existing`:已有已验证选型的版本化局部修订。它不重新分类或重选未点名槽位。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
新建请求始终提交完整需求和槽位,默认 `assemble_new`;只有还原现成产品、基于现成产品修改,或用户明确要求沿用某台整机时,才同时评估 `adapt_system`。其他情况下整机配置只作参考。Agent 不按“相似度高”“只差几个”自行选模式。服务必须验证整机 manifest、可更换关系和修改闭包;判定细则随检索时读取[检索契约](hardware-selection/references/query-contract.md)。
|
|
12
12
|
|
|
13
13
|
已有方案的后续修改必须引用 `base_selection_id` 与 `base_revision`。Agent 从最新用户指令抽取 `change_set[]`,每项只包含目标引用、指定型号/新约束和数量;目标引用优先使用已知 `slot_id` 或当前 `component_id`,不知道时保留用户原始 `function_ref`,由服务在基线中解析,歧义时返回待确认。调用方固定设置 `preserve_unmentioned: true`,不接受模型改写。不要把完整长对话、完整旧 BOM 或从记忆重述的规格作为变更集。基线由服务按 ID/版本读取,版本不匹配即停止。
|
|
14
14
|
|
|
@@ -64,9 +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
|
-
槽位 `fit_envelope` 是结构分区给该槽位的额外容器,形状规则同上:basis=locked
|
|
67
|
+
槽位 `fit_envelope` 是结构分区给该槽位的额外容器,形状规则同上:basis=locked 不是证明(分区由结构声明、无可复核出处),可信尺寸放不下时记 risk 且 decision_required=true、从不硬拒绝,原因带 `basis: locked`;basis=inferred 视同非硬边界;原因以 `limit_id=fit:<slot_id>` 标出,有 zone 时带 `zone`。没有 fit_envelope 的槽位判定结果不变。
|
|
68
68
|
|
|
69
|
-
只有 user+hard
|
|
69
|
+
只有 user+hard 边界和 measured/datasheet 组件可硬拒绝;locked 分区只要求用户裁决。solid_box 可用最小边、对角线、实体体积三项必要条件;任意 bounding_box 只可用最大支撑跨度超过容器最大弦长,不能用包围盒最小边、对角线或体积。六轴置换、圆柱 footprint 只是可装下粗筛,失败是 risk,未知形体是 unknown。clear 只表示单件粗筛,不代表多件装配、CAD 或工程通过。
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
新域包 hardware 1.2.0 / selection 0.5.0 / workspace 1.3.0 使用此加法契约。已有持久结果只读原样;旧 workflow JSON 摘要不固定模块代码,因此不可用旧定义配新包宣称旧行为仍相同。宿主必须固定完整发布包,为尺寸筛选开新运行,不能原地改写旧 CAD 输入或已失败/未知副作用。
|
|
@@ -32,8 +32,10 @@ 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).
|
|
36
|
-
//
|
|
35
|
+
// A structure zone budget for one slot (P2-06.03). inferred: a structure estimate that only demotes candidates.
|
|
36
|
+
// locked: structure declares the zone fixed (locked shape, restore target), but the declaration carries no checkable
|
|
37
|
+
// provenance, so it is not proof: a trusted part that misses it is a risk the user must decide on, never a rejection.
|
|
38
|
+
// Hard rejection stays with user hard limits plus measured/datasheet sizes.
|
|
37
39
|
export function validateFitEnvelope(fit) {
|
|
38
40
|
keys(fit, ['envelope', 'basis'], ['zone'], 'fit_envelope');
|
|
39
41
|
if (!['locked', 'inferred'].includes(fit.basis)) invalid('fit_envelope.basis must be locked or inferred');
|
|
@@ -110,7 +112,8 @@ function coarseFit(dims, container) {
|
|
|
110
112
|
}
|
|
111
113
|
// status is the coarse fit result only: clear = every coarse check passed, risk = some check failed without proof,
|
|
112
114
|
// incompatible = proof under a user hard limit with measured/datasheet sizes. Data/limit authority stays in basis and limit source.
|
|
113
|
-
// decision_required =
|
|
115
|
+
// decision_required = trusted sizes that miss either a user hard limit (no axis-aligned mounting fits) or a locked zone:
|
|
116
|
+
// only the user can trade the part against the limit or zone.
|
|
114
117
|
export function evaluateDimensions(candidate, requirements, slot) {
|
|
115
118
|
validateRequirementEnvelopes(requirements);
|
|
116
119
|
const fit = slot?.fit_envelope;
|
|
@@ -120,7 +123,7 @@ export function evaluateDimensions(candidate, requirements, slot) {
|
|
|
120
123
|
// Requirement limits keep their own authority rule; a slot zone budget adds one more container for this slot only.
|
|
121
124
|
const limits = [...(requirements?.limits ?? []).filter(row => row?.envelope !== undefined)
|
|
122
125
|
.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 } : {}) }] : [])];
|
|
126
|
+
...(fit ? [{ id: `fit:${slot.slot_id}`, envelope: fit.envelope, hardLimit: false, decides: fit.basis === 'locked', basis: fit.basis, ...(fit.zone ? { zone: fit.zone } : {}) }] : [])];
|
|
124
127
|
if (!limits.length) return unknown('PRODUCT_ENVELOPE_MISSING');
|
|
125
128
|
if (!fields.dimensions_mm || fields.dimension_basis === 'unknown' || fields.dimension_geometry === 'unknown') return unknown('COMPONENT_DIMENSIONS_UNKNOWN');
|
|
126
129
|
const dims = fields.dimensions_mm;
|
|
@@ -129,18 +132,20 @@ export function evaluateDimensions(candidate, requirements, slot) {
|
|
|
129
132
|
return unknown('NUMERIC_RANGE_UNSUPPORTED');
|
|
130
133
|
let incompatible = false, risk = false, decisionRequired = false;
|
|
131
134
|
for (const row of limits) {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
135
|
+
const trustedSize = ['measured', 'datasheet'].includes(fields.dimension_basis);
|
|
136
|
+
const hard = row.hardLimit && trustedSize, decides = Boolean(row.decides) && trustedSize;
|
|
137
|
+
const at = { limit_id: row.id, ...(row.zone ? { zone: row.zone } : {}), ...(row.basis ? { basis: row.basis } : {}) };
|
|
134
138
|
const container = cavity(row.envelope, row.envelope.wall_mm?.minimum ?? 0);
|
|
135
139
|
const failures = fields.dimension_geometry === 'solid_box'
|
|
136
140
|
? [['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]]
|
|
137
141
|
: [['MAX_SUPPORT_SPAN', Math.max(...dims), container.chord]];
|
|
138
142
|
for (const [code, observed, maximum] of failures) if (exceeds(observed, maximum)) {
|
|
139
|
-
reasons.push({ code, ...at, observed, maximum, hard });
|
|
143
|
+
reasons.push({ code, ...at, observed, maximum, hard });
|
|
144
|
+
if (hard) incompatible = true; else { risk = true; if (decides) decisionRequired = true; }
|
|
140
145
|
}
|
|
141
146
|
// Parts are mounted axis-aligned in practice; failing that is a risk, never a proof, because a tilted part may still fit.
|
|
142
147
|
if (!coarseFit(dims, container)) {
|
|
143
|
-
risk = true; if (hard) decisionRequired = true;
|
|
148
|
+
risk = true; if (hard || decides) decisionRequired = true;
|
|
144
149
|
reasons.push({ code: 'AXIS_ALIGNED_FIT_FAILS', ...at, hard: false });
|
|
145
150
|
} else if (row.envelope.wall_mm?.estimate !== undefined && !coarseFit(dims, cavity(row.envelope, row.envelope.wall_mm.estimate))) {
|
|
146
151
|
// Estimated walls only hint; they never reject and never ask the user to decide.
|
|
@@ -35,6 +35,6 @@ description: 为已澄清的产品需求选择数据库中的硬件型号。用
|
|
|
35
35
|
- 新型号归类先核对实际交付物(独立芯片、已组装模组或板卡)和主要用途;兼具功能放在能力字段,不因支持某协议或包含某芯片就重复写入两个主分类。遇到 NFC/RFID、处理器/控制板、充电/电源管理交界时读取相邻二级页的边界;旧 Wiki 的 `hardware_form` 或名称单独不足以证明交付形态。
|
|
36
36
|
- 把 `compare_by` 当作“应比较哪些规格”的提示;真正的硬约束来自需求和逐型号事实。`roles`、场景标签、Wiki prose 不能单独证明兼容。
|
|
37
37
|
- 用户约定库内硬件和 CAD 均为标准可选资源;`draft` 等维护状态、`selection_gate`、`selection_completeness`、CAD 时间/质量标记均不参与本选型过滤。未知规格记 `unknown`,不得写成通过或失败。
|
|
38
|
-
-
|
|
38
|
+
- 整机配置只作参考:新建产品默认按功能槽位逐槽位选型(`assemble_new`),同类整机的功能组合、板卡划分与布局可作参考,但不先套整机、不抵扣槽位。只有还原现成产品、基于现成产品修改,或用户明确要求沿用某台整机时,才评估整机改造,并按已验证的功能与数量覆盖逐槽位扣减;整机不必位于 `robot-system` 组,也可能属于移动底盘、机械臂等路径。细则随检索阶段加载 [query-contract](references/query-contract.md)。电气/端口/机械兼容、候选排序与补件由确定性后端执行;Agent 只提交功能槽位和分类路径,解释后端返回的证据与缺口,不凭记忆指定 SKU。
|
|
39
39
|
|
|
40
40
|
输出每个槽位的路径、数量、候选型号与 ID、已验证/不兼容/未知条件、替代路径及未覆盖功能。若没有可证实满足硬约束的型号,明确返回缺口,不伪造结果。
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
## 查询顺序
|
|
10
10
|
|
|
11
11
|
1. 用届时的运行时受控分类目录校验路径。若某路径不存在或不可选,报告目录不一致;不要偷换到相近类。离线 Wiki 演练仅核对本目录路径,不等于已通过线上校验。
|
|
12
|
-
2.
|
|
12
|
+
2. 整机配置只作参考,不先检索整机再扣减槽位。新建产品默认按全部需求槽位、精确 `category text[]` 查询非整机候选(`assemble_new`);`integration_level=system` 的候选可一并召回,用来参考同类产品的功能组合、板卡划分与布局,但不先套用、不抵扣槽位。只有还原现成产品、基于现成产品修改,或用户明确要求沿用某台整机时,才按下文评估整机改造与覆盖扣减(整机不限于 `robot-system` 组)。
|
|
13
13
|
3. 查询须稳定分页至完整候选集,或让后端证明其召回上限不会漏掉合格型号。不得以首批 `LIMIT 50` 为空/不合格就判断全库无货;可在后端做确定性预筛和 Top-K,但需保留召回计数及淘汰原因。
|
|
14
14
|
4. 按本次用户约定,不用 `status`、`draft`、`selection_gate`、`data_quality`、CAD provenance/时间筛除型号。`roles`/场景标签仅作辅助召回,不作为必要条件。未来服务若按这些字段过滤,应标记“接口行为与本契约不一致”。
|
|
15
15
|
|
|
16
16
|
## 新建产品的规划模式
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
新建产品默认 `assemble_new`。只有还原现成产品、基于现成产品修改,或用户明确要求沿用某台整机时,才另行评估 `adapt_system`,且必须满足下列边界,不能看到相似整机就直接采用:
|
|
19
19
|
|
|
20
20
|
- `adapt_system`:选择一个 `integration_level=system` 整机作为基线,只替换或补充少量、明确可更换的槽位。
|
|
21
21
|
- `assemble_new`:不继承整机基线,从需求槽位重新组成完整硬件方案。
|
|
@@ -26,12 +26,14 @@
|
|
|
26
26
|
|
|
27
27
|
`adapt_system` 的结果须给出基线整机、继承部件、替换/补充清单、修改闭包和每项证据;`assemble_new` 须明确没有继承整机。两种模式最终都生成完整 BOM,不能只返回差异件。
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## 整机参考与功能覆盖
|
|
30
|
+
|
|
31
|
+
以下覆盖与扣减规则只在评估 `adapt_system` 时适用;`assemble_new` 中整机只作配置参考,不抵扣任何槽位。
|
|
30
32
|
|
|
31
33
|
1. 保留需求分解后的完整槽位清单及每槽位 `quantity`、硬约束和是否要求独立可拆部件。整机候选的主要功能须命中至少一个可由整机交付的需求槽位;不能因为它附带多种传感器,就用不相关的整机代替用户要做的产品。若需求明确要求自行组装或独立部件,不把预装整机强加给该槽位。
|
|
32
34
|
2. 先核对整机本身满足其主功能、数量与不可放宽的性能、电气、接口、外形要求。再逐条核对它对其他槽位的显式覆盖:以届时数据库中的结构化覆盖记录(当前契约预期为 `component_spec.functional.system_coverage`)所列受控三级路径、覆盖数量及可核验事实为准,并验证内置功能在本次安装/使用条件下可用;接入时要核对实际字段映射。Wiki `capability_coverage` 可提示哪些路径值得检查,但须与当前数据库记录核对;`functional_capabilities`、标题或描述本身不能证明已包含某数量的部件。
|
|
33
35
|
3. 对每个需求槽位建立覆盖账本:`remaining_quantity = max(0, requested_quantity - verified_allocated_quantity)`。只分配已选中整机的覆盖,且同一内置资源不能被重复用于两个需要独立实例的槽位;部分覆盖只扣实际数量,不删除整条分类。例如需求两组电池而整机仅证实含一组,仍检索另一组。
|
|
34
|
-
4.
|
|
36
|
+
4. 整机不是优先候选,只在 `adapt_system` 评估中参与比较,也不是无条件选中。先排除已证实违反硬约束的整机;在可行整机方案之间,优先满足主功能并覆盖更多**本次需求中、可核验的不同槽位及其数量**,再比较剩余补件、兼容风险与用户优先级。不能只按 Wiki 所列能力总数或匹配类别个数选型;多个整机同时入选时要防止重复抵扣。
|
|
35
37
|
5. 缺失覆盖记录或关键兼容证据时该部分为 `unknown`,不能最终抵扣;可给出待确认的整机方案,同时保留未确认槽位及补件候选。只有覆盖和兼容已验证时,才把相应数量从后续零部件选型中移除。整机主功能和附带能力都按此证据原则判定,不能把“整机自带”推定为已满足用户的具体指标。
|
|
36
38
|
|
|
37
39
|
输出覆盖账本中的原始需求、所选整机 ID、每槽位已验证抵扣量、待确认量和剩余量;保留未入选整机及不适用原因,避免前端将“跳过零部件”误解为漏选。
|
|
@@ -6,7 +6,7 @@ load-guide 的各层响应均附 runtimeBoundary 提示当前执行范围;只
|
|
|
6
6
|
|
|
7
7
|
## 请求和来源
|
|
8
8
|
|
|
9
|
-
请求仅含 requirements、slots、recall_id、selection_proposal。requirements 原样保留 clarify 的 context/reaction/limits;槽位带 slot_id/function/quantity/category_path[3]/hard_constraints 对象,可选 fit_envelope(见下文「结构化尺寸与正式门禁」)。recall_id 必须指向调用 workdir 内真实 hardware-recall,校验器通过受信 readResource 前后读回,比对完整需求/槽位 request_digest。不能提交召回正文替代真实来源。旧召回没有 request_digest 时须重新检索,不得修改原资源。
|
|
9
|
+
请求仅含 requirements、slots、recall_id、selection_proposal。requirements 原样保留 clarify 的 context/reaction/limits;槽位带 slot_id/function/quantity/category_path[3]/hard_constraints 对象,可选 fit_envelope(见下文「结构化尺寸与正式门禁」)。recall_id 必须指向调用 workdir 内真实 hardware-recall,校验器通过受信 readResource 前后读回,比对完整需求/槽位 request_digest。不能提交召回正文替代真实来源。旧召回没有 request_digest 时须重新检索,不得修改原资源。request_digest 绑定的是整份 requirements,包括 clarify 写入的 `intent`(kind、keep、changeable、depth)。因此只改意图也会让进行中的召回与联网证据失效,hardware.select 以摘要不符拒绝,须重新执行 hardware.search 与联网检索,并会再次产生检索费用。这是有意保留的口径:改摘要算法会让 0.6.x 已存的全部绑定失效。宿主不得把 `intent` 回填进已保存的需求或召回资源来"迁移"历史数据;缺 intent 的历史需求原样保留,下一次需求修订由 clarify 写入 intent,再按新需求重新检索。
|
|
10
10
|
|
|
11
11
|
选型前校验全部需求条目:context 必须带非空 text 和 source=user|system;reaction 与每一项 limits 都必须是含唯一 id、非空 text 和上述 source 的对象,ID 在两组之间也不得重复。每项 limit 的 strength 必须是 hard|preference;null、数值、缺归属或拼错 strength 均明确以 VALIDATION 拒绝,不可在筛选 hard 时静默消失。合法的系统假设/偏好原样保留;未解决证据仍保存在 unknowns 并返回 needs_confirmation。
|
|
12
12
|
|
|
@@ -61,7 +61,7 @@ V2 hardware evidence uses fixed prismer_then_volc ordered primary/fallback recei
|
|
|
61
61
|
|
|
62
62
|
limits 可附 envelope;严格形状与来源规则见 [索引尺寸契约](../../abelien-hardware/references/search-index-format.md)。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 证据门。
|
|
63
63
|
|
|
64
|
-
槽位可附 `fit_envelope: {envelope, basis: locked|inferred, zone?}`,envelope 与 limits 的 envelope 同一校验,zone 为短标识;字段不合法以 VALIDATION 拒绝。它只对本槽位追加一个容器,原因的 `limit_id` 为 `fit:<slot_id>`,有 zone 时同时带 `zone`。locked
|
|
64
|
+
槽位可附 `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 时也做尺寸检查。
|
|
65
65
|
|
|
66
66
|
没有 envelope 时旧流程仍可在原 Wiki 证据范围 verified,但 selected_models.dimension_check 始终显式为 unknown(PRODUCT_ENVELOPE_MISSING;槽位有 fit_envelope 时除外),不能宣传尺寸检查通过。clear 也只表示单件粗筛;device_tested 仍 false。hardware.finalize 仍只返回 proposed/needs_confirmation 的提案,不创建硬件正式资源。
|
|
67
67
|
|
|
@@ -90,6 +90,7 @@ adapter 返回值整体作为新资源 body:
|
|
|
90
90
|
- 适合必须随 Skill 分发且需要本地 SDK/协议实现的能力,不把领域代码写进通用内核;
|
|
91
91
|
- 凭据只从 `env` 读取,禁止写入 domain、workflow 或能力结果;
|
|
92
92
|
- 模块加载失败返回 `ADAPTER_MODULE`,执行失败返回模块自身错误码或 `ADAPTER_MODULE_FAILED`。
|
|
93
|
+
- 模块抛出的 Node 系统错误(带 `syscall`,如 `EACCES`、`ENOENT`)保留错误码,message 改为 `<syscall> failed: <code>`,不含原文里的绝对路径;原文只在设置 `AB_DEBUG=1` 时以 `ab_debug {...}` 一行写到 stderr。
|
|
93
94
|
|
|
94
95
|
## 错误码速查
|
|
95
96
|
|
|
@@ -534,6 +534,12 @@ async function moduleAdapter(a, ctx) {
|
|
|
534
534
|
try {
|
|
535
535
|
return await handler({ input: ctx.input ?? {}, env: ctx.env ?? {}, fetchImpl: ctx.fetchImpl ?? fetch, readResource: ctx.readResource, readEvidence: ctx.readEvidence, readBlob: ctx.readBlob, writeBlob: ctx.writeBlob, ports: ctx.ports, checkpoint: ctx.checkpoint });
|
|
536
536
|
} catch (e) {
|
|
537
|
+
// Node 系统错误(EACCES、ENOENT 等)的 message 带绝对路径。stdout 只留 code 与 syscall;
|
|
538
|
+
// 原文仅在显式 AB_DEBUG=1 时写到 stderr。错误码不变,结构仍是 {code,message}。
|
|
539
|
+
if (e?.code && typeof e.syscall === "string") {
|
|
540
|
+
if ((ctx.env ?? {}).AB_DEBUG === "1") process.stderr.write(`ab_debug ${JSON.stringify({ code: e.code, message: String(e.message) })}\n`);
|
|
541
|
+
throw fail(e.code, `${e.syscall} failed: ${e.code}`);
|
|
542
|
+
}
|
|
537
543
|
if (e?.code) throw e;
|
|
538
544
|
throw fail("ADAPTER_MODULE_FAILED", String(e?.message ?? e), { path: a.path, export: exportName });
|
|
539
545
|
}
|
|
@@ -70,10 +70,11 @@ export function createPublicReader({ codes, userAgent, accept, decodeCharset = f
|
|
|
70
70
|
|| /(^|\.)(localhost|local|internal|test|invalid)$/.test(url.hostname)) throw failure(codes.url, "only public HTTPS source URLs without credentials are allowed");
|
|
71
71
|
url.hash = ""; return url;
|
|
72
72
|
}
|
|
73
|
-
function pageRequest(url, { address, family, signal, headers = {}, maxBytes = Infinity }) {
|
|
73
|
+
function pageRequest(url, { address, family, signal, headers = {}, maxBytes = Infinity, onHeaders }) {
|
|
74
74
|
return new Promise((resolve, reject) => {
|
|
75
75
|
const req = https.get(url, { signal, agent: false, servername: new URL(url).hostname, rejectUnauthorized: true, headers: { "user-agent": userAgent, accept, ...headers },
|
|
76
76
|
lookup: (_hostname, opts, callback) => callback(null, ...(opts.all ? [[{ address, family }]] : [address, family])) }, response => {
|
|
77
|
+
onHeaders?.();
|
|
77
78
|
const chunks = []; let size = 0, truncated = false;
|
|
78
79
|
response.on("data", chunk => {
|
|
79
80
|
if (truncated) return;
|
|
@@ -84,7 +85,9 @@ export function createPublicReader({ codes, userAgent, accept, decodeCharset = f
|
|
|
84
85
|
response.on("error", () => { if (!truncated) reject(readFailure("transport")); });
|
|
85
86
|
response.on("end", () => { if (!truncated) resolve({ status: response.statusCode, headers: response.headers, bytes: Buffer.concat(chunks) }); });
|
|
86
87
|
});
|
|
87
|
-
|
|
88
|
+
// A connection-level network error (refused, unreachable, reset before a response) is passed through as is, so
|
|
89
|
+
// fetchPinned can try the next validated address; it maps the last one to the transport read failure as before.
|
|
90
|
+
req.on("error", error => reject(networkError(error) ? error : readFailure("transport")));
|
|
88
91
|
});
|
|
89
92
|
}
|
|
90
93
|
async function dohLookup(hostname, signal, request) {
|
|
@@ -117,7 +120,12 @@ export function createPublicReader({ codes, userAgent, accept, decodeCharset = f
|
|
|
117
120
|
return url.href;
|
|
118
121
|
}
|
|
119
122
|
// One pinned-address fetch loop shared by page and byte reads. Returns the final response and its provenance.
|
|
120
|
-
|
|
123
|
+
/**
|
|
124
|
+
* connectTimeoutMs(可选,缺省不启用):每个地址单独计时,到点还没收到响应头就放弃这个地址、试下一个已校验的地址,
|
|
125
|
+
* 最后一个也没连上记为 timeout;收到响应头之后只受整次读取的 timeoutMs 约束。连接慢与正文慢因此分开计时。
|
|
126
|
+
*/
|
|
127
|
+
async function fetchPinned(target, { lookup, pageRequest: request = pageRequest, dnsRequest = pageRequest, dnsTransport = "system", timeoutMs = 30000, connectTimeoutMs, maxRedirects = 5, maxBytes, checkUrl = () => {} } = {}) {
|
|
128
|
+
if (connectTimeoutMs !== undefined && (!Number.isSafeInteger(connectTimeoutMs) || connectTimeoutMs <= 0)) throw new Error("connectTimeoutMs must be a positive integer");
|
|
121
129
|
checkUrl(target);
|
|
122
130
|
let url = normalizePublicSourceUrl(target); const redirects = [], connections = [], fetchedAt = new Date().toISOString();
|
|
123
131
|
if (!["system", "doh"].includes(dnsTransport)) throw failure(codes.config, "web DNS transport must be system or doh");
|
|
@@ -134,12 +142,23 @@ export function createPublicReader({ codes, userAgent, accept, decodeCharset = f
|
|
|
134
142
|
let response;
|
|
135
143
|
for (const [index, pinned] of ordered.entries()) {
|
|
136
144
|
connections.push({ url: url.href, tls_hostname: url.hostname, addresses, address: pinned.address, family: pinned.family });
|
|
137
|
-
|
|
145
|
+
// Per-address connect budget: an address that has not answered with headers in time is abandoned like a refused one.
|
|
146
|
+
const attempt = connectTimeoutMs === undefined ? null : new AbortController();
|
|
147
|
+
let answered = false, stalled = false;
|
|
148
|
+
const timer = attempt && setTimeout(() => { if (!answered) { stalled = true; attempt.abort(); } }, connectTimeoutMs);
|
|
149
|
+
const attemptSignal = attempt ? AbortSignal.any([signal, attempt.signal]) : signal;
|
|
150
|
+
try {
|
|
151
|
+
response = await request(url.href, { address: pinned.address, family: pinned.family, signal: attemptSignal,
|
|
152
|
+
...(attempt ? { onHeaders: () => { answered = true; clearTimeout(timer); } } : {}), ...(maxBytes === undefined ? {} : { maxBytes }) });
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
138
155
|
catch (error) {
|
|
139
|
-
if (
|
|
140
|
-
if (!
|
|
141
|
-
if (
|
|
156
|
+
if (signal?.aborted) throw error;
|
|
157
|
+
if (!stalled && publicReadFailure(error)) throw error;
|
|
158
|
+
if (!stalled && !networkError(error)) throw error;
|
|
159
|
+
if (index === ordered.length - 1) throw readFailure(stalled ? "timeout" : "transport");
|
|
142
160
|
}
|
|
161
|
+
finally { if (timer) clearTimeout(timer); }
|
|
143
162
|
}
|
|
144
163
|
const bytes = Buffer.from(response.bytes ?? []), headers = response.headers ?? {};
|
|
145
164
|
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
@@ -158,7 +177,8 @@ export function createPublicReader({ codes, userAgent, accept, decodeCharset = f
|
|
|
158
177
|
const { response, bytes, common } = await fetchPinned(target, options);
|
|
159
178
|
const mime = common.content_type;
|
|
160
179
|
// Only callers that pass maxBytes can see truncation; hardware never does, so its results are unchanged.
|
|
161
|
-
|
|
180
|
+
// The received length is rechecked too: an injected pageRequest may return more than maxBytes without the flag.
|
|
181
|
+
if (response.truncated || (options.maxBytes !== undefined && bytes.length > options.maxBytes)) return { ...common, status: "unavailable", reason: "too_large", text: null, text_sha256: null };
|
|
162
182
|
if (response.status < 200 || response.status >= 300) return { ...common, status: "unavailable", reason: "http_status", text: null, text_sha256: null };
|
|
163
183
|
if (mime.includes("pdf") || bytes.subarray(0, 5).toString() === "%PDF-") return { ...common, status: "unavailable", reason: "pdf_parser_unavailable", text: null, text_sha256: null };
|
|
164
184
|
if (!(mime.startsWith("text/") || mime.includes("xhtml"))) return { ...common, status: "unavailable", reason: "unsupported_content_type", text: null, text_sha256: null };
|
|
@@ -168,11 +188,12 @@ export function createPublicReader({ codes, userAgent, accept, decodeCharset = f
|
|
|
168
188
|
// Raw bytes (for example a product image) with the same address pinning; never decodes or interprets them.
|
|
169
189
|
async function fetchPublicBytes(target, { maxBytes, contentTypes = [], ...options } = {}) {
|
|
170
190
|
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new Error("fetchPublicBytes requires a positive integer maxBytes");
|
|
171
|
-
const { response, common } = await fetchPinned(target, { ...options, maxBytes });
|
|
172
|
-
if (response.truncated) return { ...common, status: "unavailable", reason: "too_large" };
|
|
191
|
+
const { response, bytes, common } = await fetchPinned(target, { ...options, maxBytes });
|
|
192
|
+
if (response.truncated || bytes.length > maxBytes) return { ...common, status: "unavailable", reason: "too_large" };
|
|
173
193
|
if (response.status < 200 || response.status >= 300) return { ...common, status: "unavailable", reason: "http_status" };
|
|
174
194
|
if (contentTypes.length && !contentTypes.some(type => common.content_type.startsWith(type))) return { ...common, status: "unavailable", reason: "unsupported_content_type" };
|
|
175
195
|
return { ...common, status: "read", reason: null };
|
|
176
196
|
}
|
|
177
|
-
|
|
197
|
+
// pageRequest is the default pinned HTTPS transport, exposed so callers and tests can wrap the real streaming cut-off.
|
|
198
|
+
return { publicReadFailureCodes, publicReadFailure, normalizePublicSourceUrl, validatePublicSource, fetchPublicPage, fetchPublicBytes, pageRequest };
|
|
178
199
|
}
|