abelien-harness 0.10.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/README.md +6 -6
- package/package.json +2 -1
- package/skills/abelien-clarify/SKILL.md +1 -1
- package/skills/abelien-guide/SKILL.md +2 -2
- package/skills/abelien-hardware/domain.json +1 -1
- package/skills/abelien-hardware/references/selection-service-contract.md +1 -1
- package/skills/abelien-hardware/references/web-evidence-contract.md +1 -1
- package/skills/abelien-hardware/scripts/admission.mjs +149 -0
- package/skills/abelien-hardware/scripts/dimensions.mjs +6 -0
- package/skills/abelien-hardware/scripts/finalize-selection.mjs +6 -2
- package/skills/abelien-hardware-selection/SKILL.md +1 -1
- package/skills/abelien-hardware-selection/domain.json +3 -3
- package/skills/abelien-hardware-selection/references/selection-service-contract.md +12 -0
- package/skills/abelien-hardware-selection/scripts/select.mjs +9 -0
- package/skills/abelien-harness/references/workflow-spec.md +1 -3
- package/skills/abelien-harness/scripts/ab.mjs +5 -16
- package/skills/abelien-workspace/SKILL.md +9 -16
- package/skills/abelien-workspace/assets/journey-stage-plan.json +87 -18
- package/skills/abelien-workspace/domain.json +4 -3
- package/skills/abelien-workspace/references/hardware-journey-orchestration.md +3 -3
- package/skills/abelien-workspace/references/harness-binding.md +23 -17
- package/skills/abelien-workspace/references/journey-stage-plan.md +220 -0
- package/skills/abelien-workspace/references/user-journey.md +12 -8
- package/skills/abelien-workspace/scripts/journey-next.mjs +14 -0
- package/skills/abelien-workspace/workflows/definitions/0852e4949f5a6246880c1a26bf5ccaa42f8cda06b960e7c7ce234a0a176878f9.json +0 -229
- package/skills/abelien-workspace/workflows/definitions/1179e62586cf6d88c6820022a933f719b3b4eb3c0df0856fb7a0cc6341a9d630.json +0 -1
- package/skills/abelien-workspace/workflows/definitions/1a9afef5463f53e58222b3476c0002aa9703b5ced16f531e3224beb14a28f9a3.json +0 -229
- package/skills/abelien-workspace/workflows/definitions/32ed16198839c06c36b43acbada5ba74403e37b2d351058bd7ad3d5269f76722.json +0 -1
- package/skills/abelien-workspace/workflows/definitions/399ef372790b7330d0ef88ce5096e7f69943d4793de97b2ed6c25165a332612f.json +0 -230
- package/skills/abelien-workspace/workflows/definitions/748a70482fb2f775f6e5fd942c7873534cf3e59614c77381f812a9710d479743.json +0 -1
- package/skills/abelien-workspace/workflows/definitions/b780bf280d61596b43eb6dbc6a417cf8df1ec9a7ae79a6de10f048da9ed58729.json +0 -229
- package/skills/abelien-workspace/workflows/definitions/c1b5c6c653f0c4761d51efee923aafbbeea05512e7f35c60f7f7d36ec49fc1d1.json +0 -229
- package/skills/abelien-workspace/workflows/definitions/df147960a3f7c4d631f66a6b91a7891f8a37d25a50322cf09bdc9304195963b0.json +0 -1
- package/skills/abelien-workspace/workflows/user-journey-hardware-algorithm.json +0 -230
- package/skills/abelien-workspace/workflows/user-journey.json +0 -391
package/.env.example
CHANGED
|
@@ -25,6 +25,13 @@ OSS_ALLOW_LEGACY_MANIFEST_FALLBACK=false
|
|
|
25
25
|
# Optional external hardware revision service
|
|
26
26
|
ABELIEN_HARDWARE_REVISE_URL=
|
|
27
27
|
|
|
28
|
+
# Optional component admission (P2-05): a selected web part enters the component library before the
|
|
29
|
+
# selection is saved. Set URL and token together (HTTPS, or HTTP on loopback; token at least 32 chars).
|
|
30
|
+
# Leave both empty to keep web parts under their external: ID with an EXTERNAL_NOT_ADMITTED unknown.
|
|
31
|
+
ABELIEN_HARDWARE_ADMISSION_URL=
|
|
32
|
+
ABELIEN_HARDWARE_ADMISSION_TOKEN=
|
|
33
|
+
ABELIEN_HARDWARE_ADMISSION_TIMEOUT_MS=30000
|
|
34
|
+
|
|
28
35
|
# Optional loopback HTTP wrapper only; main hardware.select executes locally without a URL.
|
|
29
36
|
# At least 32 bytes, keep the real token in a private env file. Do not expose this server publicly.
|
|
30
37
|
ABELIEN_HARDWARE_SELECT_TOKEN=
|
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ tool-receipt successor 与已有可信 runtime decision 也可以取得后续 co
|
|
|
127
127
|
现行入口不再维护独立的“主控业务提示词”。宿主指令只交代身份、skills 的用途、读取/调用方式与授权边界;具体对话调度放在 guide,作品旅程与业务规则放在 workspace,领域工作放在对应 skill。它们可以由同一个宿主 Agent 执行,不需要额外的主控 Agent;旧 controller 文档仅作历史参考,不与 skills 流程重复注入。
|
|
128
128
|
|
|
129
129
|
- 对话调度与沟通由 [abelien-guide](skills/abelien-guide/SKILL.md) 负责:组织上下文、调用领域 skill、呈现问题和结果;阶段、版本和确认条件统一读取 workspace。
|
|
130
|
-
- 作品规则由 [abelien-workspace](skills/abelien-workspace/SKILL.md) 维护:完整 A1–C
|
|
130
|
+
- 作品规则由 [abelien-workspace](skills/abelien-workspace/SKILL.md) 维护:完整 A1–C、采用与版本、组合检查、确认和交付恢复。作品推进没有 workflow:宿主持有作品状态,A1 通过之后的阶段与规则取 `workspace.journey-next`;产品服务的实际接入边界见 workspace 调用说明。
|
|
131
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 与
|
|
@@ -142,19 +142,19 @@ tool-receipt successor 与已有可信 runtime decision 也可以取得后续 co
|
|
|
142
142
|
|
|
143
143
|
## 产品旅程与 PRD
|
|
144
144
|
|
|
145
|
-
完整规则统一维护在 [workspace 用户旅程](skills/abelien-workspace/references/user-journey.md)
|
|
145
|
+
完整规则统一维护在 [workspace 用户旅程](skills/abelien-workspace/references/user-journey.md),旅程规则只有两处来源:正文维护 A1–C 判定与产品规则;A1 通过之后到交付首版的阶段顺序、逐阶段规则与允许动作只在[阶段计划](skills/abelien-workspace/assets/journey-stage-plan.json)维护,正文 §5 的一览由 `npm run render:journey-plan` 从它生成、测试比对。作品推进没有 workflow:任何宿主都持有自己的作品状态,按 `workspace.journey-next` 取下一阶段,再运行该阶段需要的领域 workflow;workflow 只承载单一领域内的多步流程,不替宿主判定作品完成。具体做法及旧实例处理见 [调用说明](skills/abelien-workspace/references/harness-binding.md)。
|
|
146
146
|
|
|
147
147
|
```bash
|
|
148
|
-
node skills/abelien-harness/scripts/ab.mjs --workdir <作品工作目录>
|
|
149
|
-
#
|
|
148
|
+
node skills/abelien-harness/scripts/ab.mjs --workdir <作品工作目录> capability run workspace.journey-next --input '{"intent":"create","artifacts":{"reference_final":true}}'
|
|
149
|
+
# → 下一阶段、goal、directives 与 actions;宿主按它执行,硬件阶段再运行 user-journey-hardware
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
### PRD 随需求对话维护
|
|
153
153
|
|
|
154
154
|
PRD 以用户指定的飞书模板 revision 191 为结构依据,保留七块总览、逐条需求清单与图片引用;从 A1 起每轮登记/修改/撤回需求,保留 ID、来源与历史,选型和检查回来后再更新关联行。定义与写入语义集中在 [PRD 契约](skills/abelien-workspace/references/prd-contract.md),模板随 workspace 安装。产品写入/回读接口尚未接入,不能把规则声明说成自动写入已实现。
|
|
155
155
|
|
|
156
|
-
`user-journey-mock`、专属 workspace mock 能力和 draft/quote 样例状态机已移除,旧记录与 [MVP 实录](docs/mvp/journey.md) 仅用于历史核对,不再提供运行入口。当前 eval
|
|
157
|
-
基础需求足够后推进首版;guide 在宿主实际展示条件满足时通过可选选择题卡片邀请交互细化并告知可“随时收尾”。PRD 从已保存需求起可读,不强制展开或增加文档确认关卡。收尾切入同一作品的[成果体验](skills/abelien-workspace/references/user-journey.md#result-experience):收起全部编辑面板,有可运行仿真就进入仿真,否则按真实播放器能力提供 Three.js 状态动作演示;保留 PRD
|
|
156
|
+
`user-journey-mock`、`user-journey`、`user-journey-hardware-algorithm`、专属 workspace mock 能力和 draft/quote 样例状态机已移除,旧记录与 [MVP 实录](docs/mvp/journey.md) 仅用于历史核对,不再提供运行入口。当前 eval 共四条场景:`user-journey-hardware-cancelled` 验证取消是**终态**(在硬件分类暂停点取消,落 `cancelled` 后 `advance` 与再次 `cancel` 都被拒);`user-journey-hardware-verified` 与 `user-journey-hardware-pending-confirmation` 经**场景 http 桩**端到端驱动硬件阶段的接线(Agent 槽位真的进了请求体、选型资源落库、draft 记下 id 与 status)与门禁(`verified` 过门落 `selected`;`needs_confirmation` 不再被拒——不执行算法,以 `selected_pending_confirmation` 正常完成,仍不宣称 `verified`;`failed`/`unknown` 依旧 `GUARD_FAILED`);`user-journey-hardware-candidates` 驱动**候选提案**路径(本地 `hardware.search` 按受控三级路径读 OSS,`hardware.finalize` 归一化为提案),三条实况断言锁 Catalog/索引/Wiki 各被真实读一次,且 draft 不带 `hardwareSelectionId`(候选不产生正式选型)。桩是评估进程内的假服务,**不**证明选型质量,也**不**证明服务端已就位。
|
|
157
|
+
基础需求足够后推进首版;guide 在宿主实际展示条件满足时通过可选选择题卡片邀请交互细化并告知可“随时收尾”。PRD 从已保存需求起可读,不强制展开或增加文档确认关卡。收尾切入同一作品的[成果体验](skills/abelien-workspace/references/user-journey.md#result-experience):收起全部编辑面板,有可运行仿真就进入仿真,否则按真实播放器能力提供 Three.js 状态动作演示;保留 PRD 查看/草稿或确认版导出及返回编辑入口。展示模式与方案确认独立,不能用收尾、演示状态或草稿导出代替确认。宿主 UI/播放器仍须实际接入,历史入口测试不证明体验已实现。
|
|
158
158
|
|
|
159
159
|
### 真实硬件阶段
|
|
160
160
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abelien-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "npx-installable multi-agent skill harness: generic kernel + pluggable domain packs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"test": "node --test",
|
|
22
22
|
"eval": "node eval/run.mjs",
|
|
23
|
+
"render:journey-plan": "node tools/render-journey-plan.mjs",
|
|
23
24
|
"prepare": "node scripts/install-hooks.mjs",
|
|
24
25
|
"branch:new": "node scripts/new-branch.mjs",
|
|
25
26
|
"sync": "node scripts/sync.mjs",
|
|
@@ -82,7 +82,7 @@ ID 是不透明标识,不能按数组位置重新编号;新 ID 不得与现
|
|
|
82
82
|
|
|
83
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
|
-
**提问时机(2026-10-12 裁定)。** 首版 CAD 之前默认不向用户提问:按原话和合理默认继续,按默认处理的事项登记为系统假设或待确认项,由宿主展示、用户可随时修改;首版之后,把仍按默认处理的事项合并成一张问题卡。唯一例外:用户提出的硬要求缺少取值、无法合理默认,且按默认继续会做出错误结果(例如“必须放进这个卡槽”却没给尺寸)时,可以在首版之前提问,至多一张卡,沿用问题卡上限(最多三题、每题最多三个选项)。调用方说明首版是否已形成;未说明时按首版之前处理。首版之前返回的 questions 只能属于这一例外,其余未定项按默认写入 result(source=system)并在 note
|
|
85
|
+
**提问时机(2026-10-12 裁定)。** 首版 CAD 之前默认不向用户提问:按原话和合理默认继续,按默认处理的事项登记为系统假设或待确认项,由宿主展示、用户可随时修改;首版之后,把仍按默认处理的事项合并成一张问题卡。唯一例外:用户提出的硬要求缺少取值、无法合理默认,且按默认继续会做出错误结果(例如“必须放进这个卡槽”却没给尺寸)时,可以在首版之前提问,至多一张卡,沿用问题卡上限(最多三题、每题最多三个选项)。调用方说明首版是否已形成;未说明时按首版之前处理。首版之前返回的 questions 只能属于这一例外,其余未定项按默认写入 result(source=system)并在 note 说明,留到首版之后合并询问。意图、目标深度与型号识别不属于例外。确认卡(2026-10-14 决定)不属于这里的提问:它由宿主在识别、找参考、概念图、选硬件这几个环节结束后按阶段计划的 gate 固定发出,问题与选项事先写死(继续 / 重新生成 / 改需求),首版之前也会出现;卡片不等人,只有阶段计划 waits_for 声明要用这个结果的阶段才停下等答复。首版之后的合并卡第一题固定问“这一版怎么样?”。用户在确认卡上选了改需求的选项(换候选、换参考、调整方向、换零件、改要求)时,按普通需求修改处理。
|
|
86
86
|
|
|
87
87
|
只提出现在必须由用户决定、且符合提问时机的问题:闭环歧义、落点歧义或挡住当前选择的硬要求缺值/相互矛盾;缺少 PRD 字段本身不是问题。提问前能在 note 中说明:不回答会卡住哪个选择、为何现有事实和授权不足以代定。默认一题,相互独立才合并,最多三题;每题最多三个实质选项,表达遵循已注入的共用规则。用户意图选项可以来自原话支持的不同解释,不冒充已有硬件或工程可行候选。
|
|
88
88
|
|
|
@@ -20,7 +20,7 @@ compatibility:
|
|
|
20
20
|
2. **取得当前上下文。** 按 workspace 的[领域模型](../abelien-workspace/references/domain-model.md)及[调用与恢复说明](../abelien-workspace/references/harness-binding.md)恢复作品、事件、任务和证据。已有运行先恢复,读取失败交回具体缺口。
|
|
21
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)
|
|
23
|
+
5. **选择下一步。** 依据 workspace 的[完整旅程](../abelien-workspace/references/user-journey.md)核对推进条件。A1 通过之后到交付首版这一段,下一阶段与该阶段的规则、允许动作一律取 `workspace.journey-next` 的返回,不凭记忆或旅程摘要自排阶段;再按能力绑定选择领域 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 做视觉验收。只把必须由用户决定的差异交回用户,不让内部视觉审查成为额外的必答关卡。
|
|
@@ -41,6 +41,6 @@ compatibility:
|
|
|
41
41
|
|
|
42
42
|
原生答卡按[问题工具接续契约](../../docs/architecture/question-card.md)消费:选项 ID 和自由原文走原 toolCallId 的 tool 结果,不能包装成新 User 消息。旧卡缺可信原调用时只走明确旧协议或说明缺口;不伪造调用,不把答卡、暂停追问或物理接续 run 当作品确认。可选交互邀请沿用非阻塞接口,不使用必要问题的等待机制。
|
|
43
43
|
|
|
44
|
-
workflow 的恢复、advance、cancel
|
|
44
|
+
领域 workflow 的恢复、advance、cancel 按 workspace 的运行约定执行;作品推进与确认后的交接由宿主回执决定,不推进任何 workflow。guide 只消费真实回执,不把工具成功、需求建议或自己的总结当成作品已采用、已确认或已交付。
|
|
45
45
|
|
|
46
46
|
用户看到的是方案、变化、问题和产物;内部阶段编号、HTTL、cursor 与资源 ID 留在工作上下文。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abelien-hardware",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.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",
|
|
@@ -22,7 +22,7 @@ URL 来自 `ABELIEN_HARDWARE_REVISE_URL`。请求包含 `operation: "revise_exis
|
|
|
22
22
|
|
|
23
23
|
## Supplemental source binding
|
|
24
24
|
|
|
25
|
-
For new journeys, `hardware.web-search` and `hardware.web-merge` follow [web-evidence-contract](web-evidence-contract.md). Proposal-only finalize consumes the merged recall; formal selection uses its persisted ID and revalidates original recall/evidence. Finalize now keeps only model assessment fields from the proposal and uses candidate facts for manufacturer/model/classification/source/geometry/CAD. No self-reported source or CAD claim can override an external candidate.
|
|
25
|
+
For new journeys, `hardware.web-search` and `hardware.web-merge` follow [web-evidence-contract](web-evidence-contract.md). Proposal-only finalize consumes the merged recall; formal selection uses its persisted ID and revalidates original recall/evidence. Finalize now keeps only model assessment fields from the proposal and uses candidate facts for manufacturer/model/classification/source/geometry/CAD. No self-reported source or CAD claim can override an external candidate. Proposal-only finalize never admits anything into the component library; only formal `hardware.select` does, and only for selected parts (「联网件入组件库」 in the [local selection contract](../../abelien-hardware-selection/references/selection-service-contract.md)).
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
Dimension metadata is copied only from frozen candidates, with deterministic single-part screening shared by search/finalize/select. Name comes from the candidate title; `bom_item_id` is explicitly a selection-line identity, not a catalog BOM identity. Active envelope output may add advisory occupancy. The [local selection contract](../../abelien-hardware-selection/references/selection-service-contract.md#结构化尺寸与正式门禁) defines unknown/pending behavior and the [index contract](search-index-format.md#数据库尺寸快照-v1) defines database provenance. Finalize never upgrades its proposed result to formal verified authority.
|
|
@@ -20,7 +20,7 @@ After original source/task/hash/monotonic checks, an already saved kind=result o
|
|
|
20
20
|
|
|
21
21
|
hardware.web-merge input is exactly {requirements,slots,recall_id,evidence_id,candidates}. Each candidate is {slot_id,page_id,manufacturer,manufacturer_quote,model,model_quote,parts}. Each physical part is {name,name_quote,shape,shape_quote,dimensions_mm,dimension_quotes?}, optionally dimensions_quote/dimension_axes. All claims match persisted raw/Parse source text, including its original provider response. Manufacturer/model quotes have at least eight characters and contain the literal. Shape is box/cylinder/sphere/unknown; one attributed positive finite measurement/unit per axis, or an exact attributed tuple with explicit original-order axes. No values borrowed from another part/imperial alternative. Missing coil thickness, source conflicts, OCR/tolerance/shape/orientation and assembly stay unknown; box only represents the dimensional envelope.
|
|
22
22
|
|
|
23
|
-
The merged hardware-recall retains original library candidates/unmet requirements, source IDs/digests and web_extractions for formal deterministic rebuilding. external:<hash> is separate from catalog IDs. Wiki object_key/relative_path/sha256 explicitly represent HTTPS document provenance. CAD_available and engineering_verified remain false. The same formal hardware.select covers quantity/all constraints/compatibility and emits ten root fields with external needs_confirmation; proposal fields cannot override candidate source/geometry/model. No verified/adopted/delivered or true CAD claim follows from a document parse.
|
|
23
|
+
The merged hardware-recall retains original library candidates/unmet requirements, source IDs/digests and web_extractions for formal deterministic rebuilding. external:<hash> is separate from catalog IDs. Wiki object_key/relative_path/sha256 explicitly represent HTTPS document provenance. CAD_available and engineering_verified remain false. The same formal hardware.select covers quantity/all constraints/compatibility and emits ten root fields with external needs_confirmation; proposal fields cannot override candidate source/geometry/model. No verified/adopted/delivered or true CAD claim follows from a document parse. When the host configures component admission, `hardware.select` admits each *selected* external part into the component library and saves it under the library UUID (`source_type=admitted_web`, provenance unchanged, size as `estimate` + `bounding_box` only for one fully quoted part); unselected candidates keep their `external:` identity. See 「联网件入组件库」 in the [selection contract](../../abelien-hardware-selection/references/selection-service-contract.md).
|
|
24
24
|
|
|
25
25
|
Configuration: PRISMER_API_KEY; WEB_SEARCH_API_KEY; ABELIEN_HARDWARE_WEB_SEARCH_TIMEOUT_MS=30000; ABELIEN_HARDWARE_WEB_PAGE_TIMEOUT_MS=30000; ABELIEN_HARDWARE_WEB_PARSE_TIMEOUT_MS=120000; ABELIEN_HARDWARE_WEB_DNS_TRANSPORT=system|doh. The submitted public URL/DNS check does not certify remote Prismer redirects. Authorized direct reads recheck all answers/redirects and pin original-hostname TLS to the chosen public IP. HTTP errors, success=false/error, malformed JSON, raw missing and timeout are explicit evidence/unknowns, never empty successful facts. Secret echoes are redacted; headers are never stored.
|
|
26
26
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Selected web parts enter the component library before their selection is saved (P2-05, 2026-10-14).
|
|
2
|
+
// The component database issues the ID: this module builds the request from source-bound web evidence,
|
|
3
|
+
// calls the host's admission endpoint, checks the reply, and rewrites the selected rows to the library ID.
|
|
4
|
+
// It never invents dimensions: only one fully quoted, unconflicted part becomes an outer bounding box.
|
|
5
|
+
import { assemblyOccupancy, dimensionRevision, evaluateDimensions } from "./dimensions.mjs";
|
|
6
|
+
import { selectionLineId } from "./finalize-selection.mjs";
|
|
7
|
+
import { digest } from "./selection-request.mjs";
|
|
8
|
+
|
|
9
|
+
export const ADMISSION_REQUEST_SCHEMA = "abelien.hardware-admission-request.v1";
|
|
10
|
+
export const ADMISSION_RESPONSE_SCHEMA = "abelien.hardware-admission-response.v1";
|
|
11
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
12
|
+
const HEX64 = /^[a-f0-9]{64}$/;
|
|
13
|
+
// Provenance stays as recorded on the web candidate; only identity references move to the library ID.
|
|
14
|
+
const PROVENANCE_KEYS = new Set(["source", "geometry", "wiki", "admission"]);
|
|
15
|
+
const record = value => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
const text = value => typeof value === "string" && value.trim().length > 0;
|
|
17
|
+
const fail = (code, message) => { throw Object.assign(new Error(message), { code }); };
|
|
18
|
+
const UNKNOWN_SIZE = Object.freeze({ dimensions_mm: null, dimension_basis: "unknown", dimension_geometry: "unknown" });
|
|
19
|
+
|
|
20
|
+
// A third-party page of unknown authority gives an estimate of the outer box, never a datasheet fact.
|
|
21
|
+
export function admissionEnvelope(geometry) {
|
|
22
|
+
const parts = Array.isArray(geometry?.parts) ? geometry.parts : [];
|
|
23
|
+
if (parts.length !== 1 || parts[0]?.status !== "source_dimensions" || !record(parts[0].dimensions_mm)) return { ...UNKNOWN_SIZE };
|
|
24
|
+
const { shape, dimensions_mm: size } = parts[0];
|
|
25
|
+
const box = shape === "box" ? [size.length, size.width, size.height]
|
|
26
|
+
: shape === "cylinder" ? [size.diameter, size.diameter, size.height]
|
|
27
|
+
: shape === "sphere" ? [size.diameter, size.diameter, size.diameter] : null;
|
|
28
|
+
if (!box || !box.every(value => typeof value === "number" && Number.isFinite(value) && value > 0)) return { ...UNKNOWN_SIZE };
|
|
29
|
+
return { dimensions_mm: box, dimension_basis: "estimate", dimension_geometry: "bounding_box" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function admissionItem(model) {
|
|
33
|
+
const source = model?.source, wiki = model?.wiki;
|
|
34
|
+
if (model?.source_type !== "external_web" || !String(model.component_id).startsWith("external:") || !record(source)
|
|
35
|
+
|| !text(source.evidence_id) || !text(source.page_id) || !HEX64.test(wiki?.sha256 ?? ""))
|
|
36
|
+
fail("VALIDATION", "only a source-bound external web selection can be admitted");
|
|
37
|
+
const [part] = model.geometry?.parts?.length === 1 ? model.geometry.parts : [];
|
|
38
|
+
return {
|
|
39
|
+
external_id: model.component_id, slot_id: model.slot_id,
|
|
40
|
+
manufacturer: model.manufacturer, model: model.model, category_path: [...model.category_path],
|
|
41
|
+
...admissionEnvelope(model.geometry),
|
|
42
|
+
evidence: {
|
|
43
|
+
// The page text the Agent quoted from, as persisted with the web evidence.
|
|
44
|
+
evidence_ref: `web-evidence:${source.evidence_id}/${source.page_id}/sha256/${wiki.sha256}`,
|
|
45
|
+
evidence_sha256: wiki.sha256,
|
|
46
|
+
source_url: source.source_url ?? null, url: source.url ?? null, retrieval: source.retrieval ?? null,
|
|
47
|
+
manufacturer_quote: source.manufacturer_quote, model_quote: source.model_quote,
|
|
48
|
+
part: part ? { name: part.name, shape: part.shape, dimensions_mm: part.dimensions_mm,
|
|
49
|
+
...(part.dimension_quotes ? { dimension_quotes: part.dimension_quotes } : {}),
|
|
50
|
+
...(part.dimension_tuple ? { dimension_tuple: part.dimension_tuple } : {}) } : null,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function admissionTarget(env) {
|
|
56
|
+
const url = env?.ABELIEN_HARDWARE_ADMISSION_URL, token = env?.ABELIEN_HARDWARE_ADMISSION_TOKEN;
|
|
57
|
+
if (!text(url) && !text(token)) return null;
|
|
58
|
+
if (!text(url) || !text(token) || token.length < 32) fail("ADMISSION_UNAVAILABLE", "ABELIEN_HARDWARE_ADMISSION_URL and a token of at least 32 characters must be set together");
|
|
59
|
+
let target;
|
|
60
|
+
try { target = new URL(url); } catch { fail("ADMISSION_UNAVAILABLE", "ABELIEN_HARDWARE_ADMISSION_URL is not a URL"); }
|
|
61
|
+
const loopback = ["127.0.0.1", "[::1]", "localhost"].includes(target.hostname);
|
|
62
|
+
if (target.username || target.password || !(target.protocol === "https:" || (target.protocol === "http:" && loopback)))
|
|
63
|
+
fail("ADMISSION_UNAVAILABLE", "the admission endpoint must be HTTPS, or HTTP on loopback, without credentials in the URL");
|
|
64
|
+
const timeout = Number(env.ABELIEN_HARDWARE_ADMISSION_TIMEOUT_MS ?? 30000);
|
|
65
|
+
if (!Number.isSafeInteger(timeout) || timeout < 1000 || timeout > 120000) fail("ADMISSION_UNAVAILABLE", "ABELIEN_HARDWARE_ADMISSION_TIMEOUT_MS must be 1000-120000");
|
|
66
|
+
return { target, token, timeout };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseAdmissionResponse(reply, items) {
|
|
70
|
+
const invalid = message => fail("ADMISSION_RESPONSE_INVALID", message);
|
|
71
|
+
if (!record(reply) || reply.schema !== ADMISSION_RESPONSE_SCHEMA || !Array.isArray(reply.items) || reply.items.length !== items.length)
|
|
72
|
+
invalid("admission reply must answer every requested item once");
|
|
73
|
+
const requested = new Map(items.map(item => [item.external_id, item])), admitted = new Map();
|
|
74
|
+
for (const row of reply.items) {
|
|
75
|
+
const asked = requested.get(row?.external_id);
|
|
76
|
+
if (!asked || admitted.has(row.external_id)) invalid("admission reply names an unrequested or repeated part");
|
|
77
|
+
if (!UUID.test(row.component_id ?? "") || !text(row.urn) || !row.urn.startsWith("urn:abelien:component:")
|
|
78
|
+
|| !["created", "existing"].includes(row.outcome) || (row.evidence_ref !== undefined && !text(row.evidence_ref)))
|
|
79
|
+
invalid("admission reply needs a library UUID, URN and outcome");
|
|
80
|
+
const size = { component_id: row.component_id, dimensions_mm: row.dimensions_mm ?? null, dimension_basis: row.dimension_basis,
|
|
81
|
+
dimension_geometry: row.dimension_geometry, ...(row.evidence_ref !== undefined ? { evidence_ref: row.evidence_ref } : {}) };
|
|
82
|
+
try { dimensionRevision(size); } catch { invalid("admission reply carries an invalid dimension record"); }
|
|
83
|
+
// A new library row stores exactly what was sent; an existing row keeps the library's own facts.
|
|
84
|
+
if (row.outcome === "created" && (JSON.stringify(size.dimensions_mm) !== JSON.stringify(asked.dimensions_mm)
|
|
85
|
+
|| size.dimension_basis !== asked.dimension_basis || size.dimension_geometry !== asked.dimension_geometry
|
|
86
|
+
|| (asked.dimensions_mm !== null && size.evidence_ref !== asked.evidence.evidence_ref)))
|
|
87
|
+
invalid("a created library part must store the requested envelope and evidence");
|
|
88
|
+
// Two pages of one product may resolve to the same library part; each slot still keeps its own selection line.
|
|
89
|
+
admitted.set(row.external_id, { ...size, urn: row.urn, outcome: row.outcome });
|
|
90
|
+
}
|
|
91
|
+
return admitted;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function remap(value, ids, key) {
|
|
95
|
+
if (key !== undefined && PROVENANCE_KEYS.has(key)) return value;
|
|
96
|
+
if (typeof value === "string") return ids.get(value) ?? value;
|
|
97
|
+
if (Array.isArray(value)) return value.map(item => remap(item, ids));
|
|
98
|
+
if (record(value)) return Object.fromEntries(Object.entries(value).map(([name, item]) => [name, remap(item, ids, name)]));
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function applyAdmission(selection, admitted, { requirements, slots }) {
|
|
103
|
+
const ids = new Map(), issues = [];
|
|
104
|
+
const models = selection.selected_models.map(model => {
|
|
105
|
+
const row = model.source_type === "external_web" ? admitted.get(model.component_id) : undefined;
|
|
106
|
+
if (!row) return model;
|
|
107
|
+
const bom = selectionLineId(model.slot_id, row.component_id);
|
|
108
|
+
ids.set(model.component_id, row.component_id); ids.set(model.bom_item_id, bom);
|
|
109
|
+
const evidenceRef = row.evidence_ref !== undefined ? { evidence_ref: row.evidence_ref } : {};
|
|
110
|
+
const source = { component_id: row.component_id, revision: dimensionRevision(row),
|
|
111
|
+
snapshot_id: digest({ schema: ADMISSION_RESPONSE_SCHEMA, admitted: row }), ...evidenceRef };
|
|
112
|
+
const next = { ...model, component_id: row.component_id, bom_item_id: bom, resource_urn: row.urn,
|
|
113
|
+
dimensions_mm: row.dimensions_mm, dimension_basis: row.dimension_basis, dimension_geometry: row.dimension_geometry,
|
|
114
|
+
dimension_source: source, source_type: "admitted_web", admission: { external_component_id: model.component_id, outcome: row.outcome } };
|
|
115
|
+
const slot = slots.find(item => item.slot_id === model.slot_id);
|
|
116
|
+
next.dimension_check = evaluateDimensions(next, requirements, slot);
|
|
117
|
+
// An existing library row may carry trusted sizes the web page did not: they can now prove a misfit.
|
|
118
|
+
if (next.dimension_check.status === "incompatible") fail("VALIDATION", "the admitted library part is dimensionally incompatible; choose another candidate");
|
|
119
|
+
if (next.dimension_check.decision_required) issues.push({ code: "DIMENSION_DECISION_REQUIRED", slot_id: slot.slot_id, component_id: row.component_id, dimension_check: next.dimension_check });
|
|
120
|
+
return next;
|
|
121
|
+
});
|
|
122
|
+
const { selected_models: _models, occupancy, ...rest } = selection;
|
|
123
|
+
const rewritten = remap(rest, ids);
|
|
124
|
+
return { ...rewritten, selected_models: models.map(model => remap(model, ids)),
|
|
125
|
+
...(occupancy !== undefined ? { occupancy: assemblyOccupancy(models, requirements) } : {}),
|
|
126
|
+
unknowns: [...rewritten.unknowns, ...issues] };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Hosts that configure admission must admit every selected web part; hosts without it keep the web ID and say so.
|
|
130
|
+
export async function admitSelectedHardware({ selection, requirements, slots, env, fetchImpl = fetch }) {
|
|
131
|
+
const external = selection.selected_models.filter(model => model.source_type === "external_web");
|
|
132
|
+
if (!external.length) return selection;
|
|
133
|
+
const endpoint = admissionTarget(env);
|
|
134
|
+
if (!endpoint) return { ...selection, unknowns: [...selection.unknowns,
|
|
135
|
+
...external.map(model => ({ code: "EXTERNAL_NOT_ADMITTED", slot_id: model.slot_id, component_id: model.component_id }))] };
|
|
136
|
+
const items = external.map(admissionItem);
|
|
137
|
+
let response, reply;
|
|
138
|
+
try {
|
|
139
|
+
response = await fetchImpl(endpoint.target.href, { method: "POST", redirect: "error",
|
|
140
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${endpoint.token}` },
|
|
141
|
+
body: JSON.stringify({ schema: ADMISSION_REQUEST_SCHEMA, selection_id: selection.id, items }),
|
|
142
|
+
signal: AbortSignal.timeout(endpoint.timeout) });
|
|
143
|
+
reply = await response.json();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
fail("ADMISSION_FAILED", `component admission request failed (${error?.name === "TimeoutError" ? "timeout" : "network or reply"})`);
|
|
146
|
+
}
|
|
147
|
+
if (!response.ok) fail("ADMISSION_FAILED", `component admission returned HTTP ${response.status}${text(reply?.error?.code) ? ` ${reply.error.code}` : ""}`);
|
|
148
|
+
return applyAdmission(selection, parseAdmissionResponse(reply, items), { requirements, slots });
|
|
149
|
+
}
|
|
@@ -59,6 +59,12 @@ function validRecord(value) {
|
|
|
59
59
|
&& (value.evidence_ref === undefined || text(value.evidence_ref))
|
|
60
60
|
&& (value.dimensions_mm !== null || (value.dimension_basis === 'unknown' && value.dimension_geometry === 'unknown'));
|
|
61
61
|
}
|
|
62
|
+
// Same revision the component database export writes, for a record that did not come from a snapshot file.
|
|
63
|
+
export function dimensionRevision(value) {
|
|
64
|
+
const record = dimensionRecord(value);
|
|
65
|
+
if (!validRecord(record)) invalid('component dimension record is invalid');
|
|
66
|
+
return digest(record);
|
|
67
|
+
}
|
|
62
68
|
export function validateComponentSnapshot(snapshot) {
|
|
63
69
|
keys(snapshot, ['schema', 'snapshot_id', 'records'], [], 'component snapshot');
|
|
64
70
|
if (snapshot.schema !== 'abelien.component-dimensions.v1' || !Array.isArray(snapshot.records) || !hash(snapshot.snapshot_id)) invalid('unsupported component dimension snapshot');
|
|
@@ -14,6 +14,11 @@ function digest(value) {
|
|
|
14
14
|
return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// A deterministic selection-line identity, NOT an upstream catalog BOM identity.
|
|
18
|
+
export function selectionLineId(slotId, componentId) {
|
|
19
|
+
return `selection-line-${digest({ slot_id: slotId, component_id: componentId }).slice(0, 24)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
function requireArray(value, name) {
|
|
18
23
|
if (!Array.isArray(value)) throw error(`selection_proposal.${name} must be an array`);
|
|
19
24
|
return value;
|
|
@@ -79,8 +84,7 @@ export async function finalizeHardwareSelection({ input }) {
|
|
|
79
84
|
if (dimensionCheck.status === "incompatible") throw error("selected component is dimensionally incompatible; choose a surviving candidate", { slot_id: selected.slot_id, component_id: selected.component_id, dimension_check: dimensionCheck });
|
|
80
85
|
return {
|
|
81
86
|
name: String(candidate.title ?? candidate.name ?? candidate.model ?? candidate.component_id),
|
|
82
|
-
|
|
83
|
-
bom_item_id: `selection-line-${digest({ slot_id: selected.slot_id, component_id: candidate.component_id }).slice(0, 24)}`,
|
|
87
|
+
bom_item_id: selectionLineId(selected.slot_id, candidate.component_id),
|
|
84
88
|
...dimensionFields(candidate), dimension_check: dimensionCheck, ...electricalFields(candidate),
|
|
85
89
|
...(candidate.interfaces !== undefined ? { interfaces: candidate.interfaces } : {}),
|
|
86
90
|
slot_id: selected.slot_id, component_id: selected.component_id, quantity: selected.quantity,
|
|
@@ -24,6 +24,6 @@ compatibility:
|
|
|
24
24
|
|
|
25
25
|
## Source-bound online fallback
|
|
26
26
|
|
|
27
|
-
The new hardware journeys review library suitability, supplement unresolved slots with [web evidence](../abelien-hardware/references/web-evidence-contract.md), then consume the merged recall through this same `hardware.select`. Select rereads original recall and web evidence and rebuilds the merge; its trusted reader must resolve all three current-workdir resources. External identity is `external:<hash>` with envelope-only geometry and source/engineering unknowns. All quantities, hard constraints and compatibility checks remain complete; an external choice stays `needs_confirmation`. The old fixed 256 KiB input gate is removed without relaxing shape or provenance.
|
|
27
|
+
The new hardware journeys review library suitability, supplement unresolved slots with [web evidence](../abelien-hardware/references/web-evidence-contract.md), then consume the merged recall through this same `hardware.select`. Select rereads original recall and web evidence and rebuilds the merge; its trusted reader must resolve all three current-workdir resources. External identity is `external:<hash>` with envelope-only geometry and source/engineering unknowns. All quantities, hard constraints and compatibility checks remain complete; an external choice stays `needs_confirmation`. When the host configures component admission, a selected external part enters the component library first and is saved under its library UUID (`source_type=admitted_web`); see 「联网件入组件库」 in the [selection contract](references/selection-service-contract.md). The old fixed 256 KiB input gate is removed without relaxing shape or provenance.
|
|
28
28
|
|
|
29
29
|
新运行显式开启 `AB_HARNESS_AUDIT_V1=true` 后,正式选型的 needs_confirmation 可进入 selected_pending_confirmation 并结束本次选型,仍待用户确认、不执行算法。普通关闭开关的运行保留原 verified-only 门禁;安装前无摘要/新格式标记的硬件运行保持只读,不能补造摘要续跑。网页页引用通过 `resource get <evidenceId> --page <pageId>` 读取所需完整原文,再做语义判断,不能把整页抄进回复。接口与迁移见 [选型服务契约](references/selection-service-contract.md)。
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abelien-hardware-selection",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "本地硬件选型:真实 OSS 召回、Agent 证据判断与受信资源校验;不是数据库兼容性求解器",
|
|
5
5
|
"resources": ["hardware-selection"],
|
|
6
6
|
"requires": ["abelien-harness", "abelien-hardware"],
|
|
7
7
|
"capabilities": {
|
|
8
8
|
"hardware.select": {
|
|
9
|
-
"description": "回读同需求的真实召回资源,验证 Agent 选择的型号、数量、逐约束引用与兼容评估(电源与接口可由可信电气数据确定性判定,其未知仍阻断;结构/散热/软件只提示并列入 advisories
|
|
9
|
+
"description": "回读同需求的真实召回资源,验证 Agent 选择的型号、数量、逐约束引用与兼容评估(电源与接口可由可信电气数据确定性判定,其未知仍阻断;结构/散热/软件只提示并列入 advisories),保存选型、可选尺寸占用摘要与兼容报告;宿主配置了组件入库端点时,选中的联网件先入组件库、以库内 ID 保存",
|
|
10
10
|
"adapter": {
|
|
11
11
|
"type": "module",
|
|
12
12
|
"path": "scripts/select.mjs",
|
|
13
|
-
"export": "
|
|
13
|
+
"export": "selectAndAdmitHardware"
|
|
14
14
|
},
|
|
15
15
|
"input": { "from": "input" },
|
|
16
16
|
"resultContract": {
|
|
@@ -32,6 +32,18 @@ constraint_checks 非数组、ID 重复或不属于当前槽位时仍以 VALIDAT
|
|
|
32
32
|
|
|
33
33
|
verified 只代表同源、完整数量和有归属证据的 Agent 选型判断通过,不代表 CAD、几何、电气实测、采用或交付。缺关键评估、非空 unknowns、未覆盖槽位均返回 needs_confirmation;机器门拒绝,但保留结果。整机继承/覆盖 manifest 求解尚未实现,adapt_system 始终待确认;不能借 assemble_new 自动抵扣其他槽位。换型仍走 hardware.revise,不重新排序未点名组件。
|
|
34
34
|
|
|
35
|
+
## 联网件入组件库(P2-05,2026-10-14)
|
|
36
|
+
|
|
37
|
+
组件数据库是硬件 ID 的唯一权威。联网补位选中的件(`external:<hash>`)在选型保存前先入组件库,以库内 UUID 保存,下游(登记、CAD 输入、BOM)只见库内 ID。
|
|
38
|
+
|
|
39
|
+
- **谁做**:`hardware.select` 能力入口 `selectAndAdmitHardware` = 纯选型 `selectHardware` + 入库。宿主预检与可选 HTTP 包装仍调用纯 `selectHardware`,不发任何写请求。
|
|
40
|
+
- **何时做**:只在选中的型号里有 `source_type=external_web` 时;没选中的联网候选不入库。全部校验(数量、约束、证据、召回回读)通过之后才发入库请求,校验失败不会留下库内行。
|
|
41
|
+
- **宿主配置**:`ABELIEN_HARDWARE_ADMISSION_URL`(HTTPS,或回环地址上的 HTTP,URL 不带凭据)与 `ABELIEN_HARDWARE_ADMISSION_TOKEN`(至少 32 字符,作 Bearer)必须同时设置;可选 `ABELIEN_HARDWARE_ADMISSION_TIMEOUT_MS`(1000–120000,默认 30000)。两项都不设:保持原行为,联网件保留 `external:` 身份,并为每个这样的型号追加 unknowns `EXTERNAL_NOT_ADMITTED`。只设一项或格式不对:`ADMISSION_UNAVAILABLE`,不静默跳过。
|
|
42
|
+
- **请求** `abelien.hardware-admission-request.v1`:`{schema, selection_id, items:[{external_id, slot_id, manufacturer, model, category_path[3], dimensions_mm, dimension_basis, dimension_geometry, evidence}]}`。尺寸只取**唯一一个**引用完整、无冲突的物理件:box 取长宽高,cylinder 取 `[直径, 直径, 高]`,sphere 取三个直径;记为 `estimate` + `bounding_box`(第三方页面来源权威未知,不当规格书)。多件、缺轴、冲突一律 `null` + `unknown`,不拼整机包络。`evidence` 带 `evidence_ref = web-evidence:<evidence_id>/<page_id>/sha256/<页面正文 SHA-256>`、`evidence_sha256`、来源 URL、厂商与型号引用、该件的原始尺寸与引用。
|
|
43
|
+
- **回复** `abelien.hardware-admission-response.v1`:`{schema, items:[{external_id, component_id(UUID), urn, outcome:created|existing, dimensions_mm, dimension_basis, dimension_geometry, evidence_ref?}]}`,每个请求项恰好一条。`created` 必须原样存下请求的尺寸、依据、几何与 `evidence_ref`;`existing` 是库里已有的同件,以库内事实为准(库内可信尺寸可以据此判出放不下,此时选型以 `VALIDATION` 拒绝,要求换件)。
|
|
44
|
+
- **改写**:入库件的 `component_id` 换成库内 UUID,`bom_item_id` 按新 ID 重新派生,`resource_urn` 取回复的 URN,尺寸四字段与 `dimension_source{component_id, revision, snapshot_id, evidence_ref?}` 取回复(`revision` 与组件库尺寸快照同一公式),`dimension_check` 与 `occupancy` 重算;`source_type=admitted_web`,新增 `admission{external_component_id, outcome}`。覆盖账本、unknowns、兼容报告与约束证据里的旧 ID 一并换成新 ID;`source`、`geometry`、`wiki` 这些联网出处原样保留。状态不变:联网件照旧 `needs_confirmation`(来源权威与工程验证仍未知)。
|
|
45
|
+
- **失败**:网络错误、超时或非 2xx 为 `ADMISSION_FAILED`;回复缺项、多项、非 UUID、`created` 改了尺寸等为 `ADMISSION_RESPONSE_INVALID`。两者都使选型步骤失败、不保存选型;入库按同件去重,重跑安全。
|
|
46
|
+
|
|
35
47
|
## 可选本地 HTTP 包装
|
|
36
48
|
|
|
37
49
|
scripts/server.mjs 使用同一 selectHardware,固定一个已存在作品 workdir,只读该目录的 Harness store,不另建状态。显式设置至少 32 字节的 ABELIEN_HARDWARE_SELECT_TOKEN(仅私有环境文件),在该环境文件所在目录运行:
|
|
@@ -4,6 +4,7 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import { mergeHardwareWeb } from "../../abelien-hardware/scripts/web-merge.mjs";
|
|
5
5
|
import { finalizeHardwareSelection } from "../../abelien-hardware/scripts/finalize-selection.mjs";
|
|
6
6
|
import { evaluateElectrical } from "../../abelien-hardware/scripts/electrical.mjs";
|
|
7
|
+
import { admitSelectedHardware } from "../../abelien-hardware/scripts/admission.mjs";
|
|
7
8
|
import { canonical, digest, selectionRequestDigest, validateSelectionSlots } from "../../abelien-hardware/scripts/selection-request.mjs";
|
|
8
9
|
const record = value => value && typeof value === "object" && !Array.isArray(value);
|
|
9
10
|
const text = value => typeof value === "string" && value.trim().length > 0;
|
|
@@ -153,3 +154,11 @@ export async function selectHardware({ input, readResource, readBlob }) {
|
|
|
153
154
|
...(advisories.length ? { advisories } : {}) };
|
|
154
155
|
}
|
|
155
156
|
export const run = selectHardware;
|
|
157
|
+
|
|
158
|
+
// Capability entry (P2-05): the pure selection above, then any selected web part enters the component library
|
|
159
|
+
// through the host's admission endpoint and is saved under its library ID. Host preflights keep calling the
|
|
160
|
+
// pure selectHardware, which never writes anywhere.
|
|
161
|
+
export async function selectAndAdmitHardware({ input, readResource, readBlob, env, fetchImpl }) {
|
|
162
|
+
const selection = await selectHardware({ input, readResource, readBlob });
|
|
163
|
+
return admitSelectedHardware({ selection, requirements: input.requirements, slots: input.slots, env, fetchImpl });
|
|
164
|
+
}
|
|
@@ -32,8 +32,7 @@ create 输入为 `schemaVersion:1, creationOperationId, inputRevision:0, manifes
|
|
|
32
32
|
`args` 支持 `{{expr}}` 插值
|
|
33
33
|
- `executor: "agent"`:`instruction` 必填;运行到此暂停(`awaiting_agent`),
|
|
34
34
|
instruction 随实例返回给 agent
|
|
35
|
-
-
|
|
36
|
-
- `main(argv,{verifyCompletion})`、底层 workflow deps 接受该回调。内核不解释业务 guard 名称。当前 user-journey 声明 `workspace.confirmed-snapshot`;产品检查/同版确认/快照服务未接时禁止 done。未声明 guard 的一般工作流行为不变。
|
|
35
|
+
- **范围:** 一条 workflow 只承载单一领域内的多步流程——`capability.run` 步骤只调用同一个能力命名空间(如全是 `hardware.*`),不跨领域串接。作品推进、采用、确认与交付由宿主持有,不写成 workflow;`completionGuard` 已移除,定义里出现即 `WORKFLOW_INVALID`。随包定义由 `test/workflow-scope.test.mjs` 把守。
|
|
37
36
|
|
|
38
37
|
## 运行实例(`.abelien/workflows/<runId>.json`)
|
|
39
38
|
|
|
@@ -188,7 +187,6 @@ abelien-harness receipt get --wait <waitId> --digest <resultDigest> --workdir <
|
|
|
188
187
|
| `WORKFLOW_INVALID` | 步骤定义非法(id 重复、action/instruction 缺失等) |
|
|
189
188
|
| `WORKFLOW_RUN_NOT_FOUND` | runId 不存在 |
|
|
190
189
|
| `WORKFLOW_STATE` | 状态机非法转移(如 cancel 已终态的 run) |
|
|
191
|
-
| `WORKFLOW_COMPLETION_REQUIRED` | 缺可信宿主完成校验或回读未通过;保持暂停 |
|
|
192
190
|
| `WORKFLOW_DRIFTED` | 历史步骤形状与现行定义不同;只读恢复 |
|
|
193
191
|
| `VALIDATION` | 必填 input 缺失 |
|
|
194
192
|
| `RECEIPT_INVALID` / `RECEIPT_SIZE` | envelope 不符合 v1/超过大小边界 |
|
|
@@ -205,7 +205,7 @@ export function createStore(workdir) {
|
|
|
205
205
|
// doctor must report the shipped package version, never a frozen constant (0.2.3 drifted across
|
|
206
206
|
// six releases). The npm tree always ships package.json at the skill root; standalone skill copies
|
|
207
207
|
// fall back to FALLBACK_VERSION, which scripts/release-check.mjs pins to package.json.
|
|
208
|
-
const FALLBACK_VERSION = "0.
|
|
208
|
+
const FALLBACK_VERSION = "0.13.0";
|
|
209
209
|
export let VERSION = FALLBACK_VERSION;
|
|
210
210
|
try {
|
|
211
211
|
VERSION = String(JSON.parse(fs.readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version || FALLBACK_VERSION);
|
|
@@ -390,7 +390,7 @@ export async function main(argv, opts = {}) {
|
|
|
390
390
|
io.out.write(canonicalJson({ ok: true, data: { usage: USAGE_TEXT } }) + "\n");
|
|
391
391
|
return 0;
|
|
392
392
|
}
|
|
393
|
-
const data = await dispatch(argv, { io, workdir, env, fetchImpl: opts.fetchImpl, spawnImpl: opts.spawnImpl, modulePorts: opts.modulePorts, readEvidence: opts.readEvidence
|
|
393
|
+
const data = await dispatch(argv, { io, workdir, env, fetchImpl: opts.fetchImpl, spawnImpl: opts.spawnImpl, modulePorts: opts.modulePorts, readEvidence: opts.readEvidence });
|
|
394
394
|
io.out.write(canonicalJson({ ok: true, data }) + "\n");
|
|
395
395
|
return 0;
|
|
396
396
|
} catch (e) {
|
|
@@ -844,9 +844,8 @@ function validateWorkflow(def, name) {
|
|
|
844
844
|
if (s.executor === "agent" && s.replySchema !== undefined && (typeof s.replySchema !== "object" || s.replySchema === null || Array.isArray(s.replySchema))) {
|
|
845
845
|
issues.push(`step ${s.id}: replySchema must be an object`);
|
|
846
846
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
}
|
|
847
|
+
// Product completion (adoption, confirmation, delivery) belongs to the host, never to a workflow step.
|
|
848
|
+
if (s.completionGuard !== undefined) issues.push(`step ${s.id}: completionGuard was removed; product completion is decided by the host, not by a workflow`);
|
|
850
849
|
if (s.completeWhen !== undefined && (s.executor !== "cli" || typeof s.completeWhen?.path !== "string" || !Object.hasOwn(s.completeWhen, "equals") || !s.completionResult)) issues.push(`step ${s.id}: completeWhen requires a CLI step, path, equals and completionResult`);
|
|
851
850
|
if (s.executor !== "cli" && s.executor !== "agent") issues.push(`step ${s.id}: executor must be cli|agent`);
|
|
852
851
|
}
|
|
@@ -1040,15 +1039,6 @@ export function createWorkflowStore(workdir) {
|
|
|
1040
1039
|
// 引步之前比对定义:cursor 在两份序列上必须指向同一步,否则不落盘、不执行。
|
|
1041
1040
|
assertDefinitionMatches(instance, def);
|
|
1042
1041
|
const step = instance.steps[instance.cursor];
|
|
1043
|
-
const guard = def.steps[instance.cursor].completionGuard;
|
|
1044
|
-
if (guard) {
|
|
1045
|
-
if (typeof deps.verifyCompletion !== "function") throw fail("WORKFLOW_COMPLETION_REQUIRED", "trusted host completion verifier is unavailable", { guard });
|
|
1046
|
-
const original = canonicalJson(instance);
|
|
1047
|
-
const verified = await deps.verifyCompletion({ guard, instance: JSON.parse(original), result: JSON.parse(canonicalJson(result)) });
|
|
1048
|
-
if (verified?.ok !== true) throw fail("WORKFLOW_COMPLETION_REQUIRED", "trusted host did not verify completion", { guard });
|
|
1049
|
-
if (canonicalJson(load(runId)) !== original) throw fail("WORKFLOW_STATE", "workflow changed during completion verification");
|
|
1050
|
-
step.completionReceipt = verified.receipt ?? null;
|
|
1051
|
-
}
|
|
1052
1042
|
step.result = result;
|
|
1053
1043
|
step.status = "done";
|
|
1054
1044
|
step.finishedAt = nowIso();
|
|
@@ -1206,7 +1196,6 @@ async function workflowCmd(pos, flags, deps) {
|
|
|
1206
1196
|
env: deps.env,
|
|
1207
1197
|
fetchImpl: deps.fetchImpl,
|
|
1208
1198
|
readEvidence: deps.readEvidence,
|
|
1209
|
-
verifyCompletion: deps.verifyCompletion,
|
|
1210
1199
|
modulePorts: deps.modulePorts,
|
|
1211
1200
|
});
|
|
1212
1201
|
const wfs = createWorkflowStore(deps.workdir);
|
|
@@ -1271,7 +1260,7 @@ if (isMain) {
|
|
|
1271
1260
|
// 语义零变化;main() 若异常拒绝,未处理拒绝与原 TLA 拒绝同为打印后 exit 1。
|
|
1272
1261
|
main(process.argv.slice(2)).then(async code => {
|
|
1273
1262
|
// process.exit() 不等待管道异步写刷盘:单行 JSON payload 较大时(实测 ~8KB 起截断,
|
|
1274
|
-
//
|
|
1263
|
+
// 如硬件 workflow 的 status/advance 回执),超出首笔写入的部分会被静默丢弃。
|
|
1275
1264
|
// 追加一次空写作为刷盘屏障——流按写入顺序回调,空写回调触发即代表此前全部字节
|
|
1276
1265
|
// 已交给 OS——随后再退出;写错误(如 EPIPE)仍由回调吞掉,保持既有静默语义。
|
|
1277
1266
|
await Promise.all([
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: abelien-workspace
|
|
3
|
-
description: Abelien 作品旅程与业务契约:维护 PRD 定义与逐轮条目登记、A1–C
|
|
3
|
+
description: Abelien 作品旅程与业务契约:维护 PRD 定义与逐轮条目登记、A1–C 推进条件、可选交互细化与收尾成果体验、作品上下文、采用与版本、检查、同版确认及交付恢复规则,提供按意图推进的阶段计划(workspace.journey-next)。用于判断作品如何推进或启动、恢复创作;guide 按本包规则调度,clarify 提供需求建议,实际写入由已接产品服务完成。
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility:
|
|
6
6
|
- claude-code
|
|
@@ -14,9 +14,9 @@ compatibility:
|
|
|
14
14
|
|
|
15
15
|
## 读取入口
|
|
16
16
|
|
|
17
|
-
- 判断阶段、修改回流或确认资格:先核对 OSS CAD 优先和Studio 产品要求与宿主版本边界,再读[完整用户旅程](references/user-journey.md)
|
|
17
|
+
- 判断阶段、修改回流或确认资格:先核对 OSS CAD 优先和Studio 产品要求与宿主版本边界,再读[完整用户旅程](references/user-journey.md),这是产品规则的唯一正文(A1 通过之后到交付首版的逐阶段规则在阶段计划,见下文);交互细化邀请见 §4.1,点击“随时收尾”后的成果体验、仿真/Three.js 行为演示与 PRD 入口见 §4.2。
|
|
18
18
|
- 核对作品上下文、需求建议与采用结果:读[领域模型与职责交接](references/domain-model.md)。
|
|
19
|
-
-
|
|
19
|
+
- 推进作品、恢复、调用领域流程或提交结果:读[调用与能力映射](references/harness-binding.md),领域 workflow 另读其实际定义。
|
|
20
20
|
- 在已采用设计上做 CAD 局部修改或候选返工:按[调用与能力映射](references/harness-binding.md#adopted-design-revision-loop)使用受信 Studio broker;候选和采用版本必须分开。
|
|
21
21
|
- 初次建立或随需求变更维护 PRD:读[PRD 定义与逐轮维护契约](references/prd-contract.md),按[模板快照](assets/prd-template.json)定位字段与稳定行 ID。
|
|
22
22
|
|
|
@@ -26,20 +26,13 @@ compatibility:
|
|
|
26
26
|
|
|
27
27
|
## 产品入口
|
|
28
28
|
|
|
29
|
-
[
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
node <skills目录>/abelien-harness/scripts/ab.mjs --workdir <作品工作目录> workflow run user-journey --input '{"prompt":"..."}'
|
|
33
|
-
# 新创作才启动;已有运行先按调用说明恢复
|
|
34
|
-
# awaiting_agent@drive-product-journey:继续在同一步骤内消费宿主输入与真实任务
|
|
35
|
-
```
|
|
29
|
+
作品推进没有 workflow:宿主持有作品事实、版本与任务,任何宿主(Web、Claude Code、Codex)都按同一套规则推进。A1 之前与 A1–C 的判定读[完整用户旅程](references/user-journey.md);A1 通过之后到交付首版的阶段与逐阶段规则由 `workspace.journey-next` 给出(见下节);领域内的多步流程(如硬件选型 `user-journey-hardware`)各跑自己的 workflow。逐步做法见[调用与能力映射](references/harness-binding.md#host-driven-journey)。
|
|
36
30
|
|
|
37
31
|
- 阶段、采用、版本及确认条件以本包规则为准。clarify 的结构校验和领域阶段成功不替代作品确认条件。
|
|
38
|
-
- 当前引擎是顺序 workflow v1,不解释 `journey` 分支,也不校验 `input.optional`、`replySchema` 或 B3。业务规则由 Agent 按真实证据执行;产品服务仍须提供实际写入与验证契约。
|
|
39
32
|
- 必要问题的工具等待与回答结果见[卡片调用契约](../../docs/architecture/question-card.md):宿主持久原调用后结束执行,独立答卡请求接上原 tool 结果,不构造 User 答案;真实用户原文仍保留 user 来源。
|
|
40
|
-
-
|
|
41
|
-
- 收尾/返回编辑是同一作品的展示模式切换;成果体验中的 machine states 只驱动行为演示,不替代 A1–C
|
|
42
|
-
-
|
|
33
|
+
- 首版展示、普通进度、必要问题和等待任务时停止本轮、说明下一步,等新事实到来再判断;不为“保持位置”启动或挂起任何 run。
|
|
34
|
+
- 收尾/返回编辑是同一作品的展示模式切换;成果体验中的 machine states 只驱动行为演示,不替代 A1–C 或确认条件。
|
|
35
|
+
- 需求、采用方案、版本、决定与任务每轮从宿主/产品回读,缺接口就说明缺口。
|
|
43
36
|
|
|
44
37
|
## 阶段计划与 journey-next
|
|
45
38
|
|
|
@@ -50,7 +43,7 @@ node <skills目录>/abelien-harness/scripts/ab.mjs --workdir <作品工作目录
|
|
|
50
43
|
--input '{"intent":"restore","artifacts":{"reference_final":true}}'
|
|
51
44
|
```
|
|
52
45
|
|
|
53
|
-
它是纯计算,返回下一阶段、目标、`directives` 与 `actions`。宿主先跑完自己的守卫,再把 `directives` 原样附在自己的执行说明(工具名、字段)之后;`actions` 是该阶段允许的动作(取自计划的 `action_catalog
|
|
46
|
+
它是纯计算,返回下一阶段、目标、`directives` 与 `actions`。宿主先跑完自己的守卫,再把 `directives` 原样附在自己的执行说明(工具名、字段)之后;`actions` 是该阶段允许的动作(取自计划的 `action_catalog`),宿主把它们映射成自己的工具,只放行这些。`gate` 是该阶段结束后宿主固定发出的确认卡(问题、选项与各选项对应的动作:继续 / 重新生成 / 改需求),`waits_for` 列出该阶段开始前必须已答的确认卡所属阶段:卡片不等人,只有要用到那个结果的阶段才停下等用户答复。restore 默认锁定外形;锁定时,CAD 阶段的规则会写明不得放大外壳;未锁定时只允许在 appearance 认可的外观容差内调整外壳,超出容差换件或交用户取舍。意图的划分见 [意图路由](../../docs/architecture/intent-routing.md)。阶段计划是 A1 通过之后到交付首版这一段(及其旁路)阶段顺序、目标、规则与允许动作的唯一来源;「[阶段计划一览](references/journey-stage-plan.md)」由 `npm run render:journey-plan` 从它生成;[完整用户旅程](references/user-journey.md#journey-stage-plan)维护 A1–C 判定与产品规则,两者不得冲突。本能力在[能力映射](references/harness-binding.md#当前能力映射)中登记。
|
|
54
47
|
|
|
55
48
|
本包定义作品事实之间的关系,不表示已实现完整的产品后台。现有可用能力和未接入项集中维护在[能力映射](references/harness-binding.md),不使用旧 `workspace.*` mock 顶替。
|
|
56
49
|
|
|
@@ -60,4 +53,4 @@ node <skills目录>/abelien-harness/scripts/ab.mjs --workdir <作品工作目录
|
|
|
60
53
|
|
|
61
54
|
## 已移除的旧样例
|
|
62
55
|
|
|
63
|
-
`user-journey-mock`、其 8 个 `workspace.*` mock 能力和 draft/quote 样例 FSM
|
|
56
|
+
`user-journey-mock`、其 8 个 `workspace.*` mock 能力和 draft/quote 样例 FSM 已移除;作品推进 workflow `user-journey`(单步 `drive-product-journey` 与其 `journey` 判定契约)和跨领域的 `user-journey-hardware-algorithm` 也已退役,规则只在旅程正文与阶段计划中维护。保留真实硬件阶段使用的 draft 资源类型。现行产品入口不生成假 PRD、报价或确认。旧运行及评估报告原样保留,仅查看原始 status;不要对旧实例取 HTTL 或 advance。处理方法见调用说明。
|