# HDLKiosk Host 适配与业务数据

> 主要代码：`packages/private-materials/src/components/bigSale/templates/HDLKiosk`  
> 配套 SalesSDK 扩展：`packages/private-materials/src/plus/salesSdk`  
> 文档目标：说明 BigSale 兼容层具体做了什么、解决了什么旧体系问题、哪些能力仍然缺失。  
> Core 运行机制见 [01-核心运行机制.md](./01-核心运行机制.md)。

## 1. 兼容层的定位

BigSale HDLKiosk Adapter 同时是：

- **防腐层**：只投影 Core 门禁需要的订单事实；页面商品、订单和客户数据直接透传；
- **能力适配层**：把 callback、Context 方法、插件 Hook 统一为 Host Port；
- **兼容层**：处理 Ticket/Retail 商品预载差异、晚到 runtime source 和旧扫码协议；
- **装配层**：创建稳定 Host、Scale Runtime 和显式临时策略。

它不是：

- 第二套 Workflow；
- 第二个 Cart/Product Store；
- BigSale API 的无差别透传；
- 本地定价、优惠或支付引擎；
- 缺失 OS 能力时的静默 mock 集合。

依赖方向必须保持：

```text
BigSale / SalesSDK / Plugins
            ↓ raw context / callback / hook
BigSale HDLKiosk Adapter
            ↓ HDLKioskHost + DTO + OperationResult
Independent HDLKiosk Core
```

Core 不能反向 import BigSale；Adapter 不能决定下一个 Workflow 节点。

## 2. 解决的问题总表

| 旧体系/接入痛点                            | 采用的形式                            | 兼容层做法                                                                                  | 得到的结果                                                |
| ------------------------------------------ | ------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| BigSale Context 对象大、字段易变           | Anti-corruption DTO + 标准商品透传    | 订单/金额只投影 Core 必需字段；指定商品保留 ProductData 原格式                              | Core 不依赖 raw Context/tempOrder，也不维护第二套商品模型 |
| callback 返回成功但订单事实未必收敛        | Command + Writer Receipt               | Core 等 consistency/Summary，并要求变更类 writer 返回窄 receipt                            | 不再用 React 展示投影反向验证已完成写入                   |
| 模板运行能力可能晚于 HDLKiosk 首帧就绪     | 稳定 Host + ref/getter                | `useBigSaleHDLKioskAdapter` 只创建一次 Host，调用时读取最新 context/runtime ref              | 能力变化不要求重建流程；不再经过额外 bridge 对象          |
| 可选方法散落导致运行时 undefined           | Capability Snapshot                   | `createHost.ts` 从当前 Context/runtime source 计算窄能力                                    | Entry/Screen 可提前禁用，Operation 仍可 fail-closed       |
| Ticket 未预载称重商品，Retail 已预载       | Query + scoped hydration              | `queryProducts(includeIds)` 未命中时 `findByIds(loadMissing, refreshCatalog:false)`，再查询 | 不要求先进入 Retail，也不覆盖共享餐牌                     |
| 普通商品列表背后有餐牌筛选和智能报价       | 复用 Product Provider 快照            | Catalog 直接投影 `context.products.products`                                                | 不另起一条“简单拉商品”链路破坏报价上下文                  |
| 主商品读取曾经过 Host Query                | sourceData 直读 + 写时复验            | 页面直接从 catalogProducts 查找；称重写入前再查询并校验最新商品                            | 展示无转发，非法配置仍 fail-closed                        |
| 电子秤能力以 React Hook 提供               | Hook owner + Hub + Runtime            | 顶层 `BigSaleScaleRuntimeOwner` 调 Hook，同时发布设备事件和 Scale Snapshot                  | Entry 与详情页共享一个秤生命周期                          |
| 扫码可能同时来自焦点栈和旧 pubsub          | Device Port + fallback dedupe         | 优先 acquire focus，失败才订阅 pubsub；短窗口生成稳定降级 eventId                           | 降低双重监听与重复加购                                    |
| Checkout callback 形状和状态不统一         | Result normalizer                     | 解析多条已知路径，只有明确 paid + order facts 才成功                                        | 模糊结果统一 `unknown`，阻止重复支付                      |
| OS 尚未标准化履约窄接口                    | Adapter-local writer                  | Adapter 直接从当前 booking runtime 创建窄 writer，不进入 BigSale 公共 Props                | 缺口局限在装配点，同时不扩散额外桥接类型                  |
| 宿主异常格式各异                           | Result/Error normalization            | `failed/partialFailure/unknown` 生成安全 code、recovery、traceId                            | Core 不依赖异常字符串，UI 不泄漏原始错误                  |

## 3. 代码地图：每个文件具体负责什么

| 文件                                                                                                              | 具体职责                                                             | 不应放入的逻辑                         |
| ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------- |
| [`index.tsx`](../../bigSale/templates/HDLKiosk/index.tsx)                                                         | 合并 config、读取 Engine plugin、挂载 Scale Owner、装配 Host 和 Core | Workflow 节点、购物车操作              |
| [`primaryWeightedProduct.ts`](../../bigSale/templates/HDLKiosk/primaryWeightedProduct.ts)                         | 从 catalog list 的首条 data variant 解析称重主商品                   | 商品查询、隐式 fallback                |
| [`types.ts`](../../bigSale/templates/HDLKiosk/types.ts)                                                           | 声明模板支付控制器和认证客户窄类型                                  | BigSale 公共 Props、通用 SalesSDK 类型 |
| [`adapter/useBigSaleHDLKioskAdapter.ts`](../../bigSale/templates/HDLKiosk/adapter/useBigSaleHDLKioskAdapter.ts)   | 用 ref 持有最新 Context/runtime/scan handle，稳定 Host 引用          | DTO 字段转换                           |
| [`adapter/createHost.ts`](../../bigSale/templates/HDLKiosk/adapter/createHost.ts)                                 | 组合 Sales/Device Port、动态 capabilities 和履约 writer             | React 页面逻辑                         |
| [`adapter/transactionState.ts`](../../bigSale/templates/HDLKiosk/adapter/transactionState.ts)                    | 派发瞬间需要的 Cart/Summary/Payment/fingerprint 安全事实              | UI Store、主动修订单                   |
| [`adapter/sales/`](../../bigSale/templates/HDLKiosk/adapter/sales)                                                | 按能力实现 `HDLSalesPort` query/command，并在 `index.ts` 组合        | 决定 target Screen                     |
| [`adapter/sales/weightedProduct.ts`](../../bigSale/templates/HDLKiosk/adapter/sales/weightedProduct.ts)           | 解析称重商品、单位价格换算、验证重量、生成 prepared product          | 原生秤监听、页面状态                   |
| [`adapter/devicePort.ts`](../../bigSale/templates/HDLKiosk/adapter/devicePort.ts)                                 | 扫码原始事件与 ScaleBridge 标准事件汇入 `HDLDevicePort`              | 判断当前 Workflow 是否消费             |
| [`runtime/scaleBridge.ts`](../../bigSale/templates/HDLKiosk/runtime/scaleBridge.ts)                               | 统一 Scale Snapshot、owner 身份与标准设备事件流                      | React Hook、称重商品价格计算           |
| [`runtime/BigSaleScaleRuntimeOwner.tsx`](../../bigSale/templates/HDLKiosk/runtime/BigSaleScaleRuntimeOwner.tsx)   | 隔离 `useNativeScale`，校验实时帧并发布给 ScaleBridge                | Workflow 跳转、购物车写入              |
| [`adapter/normalize.ts`](../../bigSale/templates/HDLKiosk/adapter/normalize.ts)                                   | Host 错误与未知结果标准化                                            | 业务重试策略                           |

兼容层还扩展了 SalesSDK Products Context：

| 文件                                                                                                            | 扩展                                                                           |
| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| [`plus/salesSdk/types.ts`](../../../plus/salesSdk/types.ts)                                                     | 暴露 `ProductListStoreQueryParams`、`queryProducts`、带 options 的 `findByIds` |
| [`plus/salesSdk/context/SalesSdkProductContext.tsx`](../../../plus/salesSdk/context/SalesSdkProductContext.tsx) | 转发 OS `queryProducts`；支持 `findByIds({loadMissing, refreshCatalog})`       |

这两处是对现有 SalesSDK 的窄能力补充，不包含 HDLKiosk Workflow。

## 4. 装配时序

```mermaid
flowchart LR
  CTX["BigSaleContext\nsales / products / cart / customer / summary"]
  PLUGIN["Engine nativeScaleTools"]
  REF["useBigSaleHDLKioskAdapter refs\ncontext / payment / member / fulfillment / scan"]
  PROJECTION["projection.ts\nObservedSalesSnapshot"]
  SALES["sales/index.ts\nHDLSalesPort"]
  DEVICE["devicePort.ts + ScaleBridge + ScaleOwner\nHDLDevicePort / ScaleRuntime"]
  CAP["createHost.ts\ndynamic capability getter"]
  HOST["Stable HDLKioskHost"]
  CORE["Independent HDLKiosk Core"]

  CTX --> REF
  PLUGIN --> DEVICE
  REF --> PROJECTION
  REF --> SALES
  REF --> DEVICE
  REF --> CAP
  PROJECTION --> HOST
  SALES --> HOST
  DEVICE --> HOST
  CAP --> HOST
  HOST --> CORE
```

[`HDLKioskBigSaleTemplate`](../../bigSale/templates/HDLKiosk/index.tsx) 的装配顺序：

```text
BigSaleTemplateProps
├─ context（sales/products/customer/cart/summary/booking）
└─ businessCode / locale / isActive
        ↓
resolveHDLKioskBigSaleConfig
  └─ catalog list 首条 data variant 的称重主商品标记
        ↓
读取 Engine nativeScaleTools plugin
        ↓
创建 ScaleBridge（内部统一 ownerId + HDLScaleRuntime + device events）
        ↓
useBigSaleHDLKioskAdapter → stable HDLKioskHost
  ├─ late runtime refs（payment / member / fulfillment）
  └─ subscribeScaleEvents 来自 ScaleBridge
        ↓
BigSaleScaleRuntimeOwner + <HDLKiosk host=... />
```

为什么 Host 必须稳定：

- Actor、Coordinator、设备订阅和 session 都持有 Host；
- 每次 BigSale Context rerender 都重建 Host，会销毁流程或让旧 Promise 写回；
- 所以 Hook 用 `contextRef` 和各能力 ref 保存最新值，仅首次 `createHost`，不创建额外 bridge 对象；
- Host 的 `capabilities` 是 getter，每次读取当前真实能力，不冻结首帧结果。

Host 真正替换只应发生在订单宿主/物料实例发生语义变化时，而不是普通 Context rerender。

## 5. BigSale Context 到 Host 的映射

```text
context.sales      → session ensure/reset、checkout、payment 流程事实
context.products   → sourceData 原样直传、queryProducts、主商品 hydration
context.customer   → Guest/current member 选择
context.cart       → add/update/remove、prepared product、consistency、summary recalc
context.summary    → sourceData 原样直传 + checkout 门禁所需 total/due
bookingTicket      → order customer、ProductList query
Internal Bridge    → 当前只接 Scale Hub；预留 member/fulfillment 的模板内适配缝
Engine plugin      → useNativeScale
```

Template 把展示对象和当前 `orderState` 作为只读 `sourceData` 直接交给页面；Host 只承载写 Command、设备、支付和派发瞬间的安全读取。

## 6. Observed Projection 具体做了什么

### 6.1 Cart 行

`projection.ts` 同时兼容普通商品行和 booking display line：

- booking 行从 `_extend.product` 取真实商品；
- lineId 优先使用 `unique_identification_number/identity_key`；
- 其次使用 `order_detail_id`、booking id；
- 最后才使用 product/variant 组合降级 ID；
- 称重/合并行优先使用 `displayQuantity`；
- 只有普通正整数、无 customization 的行标记 `quantityEditable=true`；
- 仅称重行保留写后确认所需的标准 `skuValue`。

Projection 不再复制标题、图片、价格、定制摘要等展示字段；生产购物车继续复用读取原始订单的 SaleDetail。

### 6.2 Customer

Customer 只投影流程判断所需的 `customerId/member`；姓名、头像等展示字段直接读取 `sourceData.customer`。ID `0/1/空` 被视为非正式会员，防止 Guest 占位客户触发会员 UI。

### 6.3 Summary

- 有 OS summary → `ready(total,due,currency)`；
- sales loading → `calculating`；
- 其他缺失 → `missing`。

Adapter 不从 Cart 行合计最终金额。

### 6.4 Payment

| BigSale/OS 原始事实                       | HDLKiosk Payment                   |
| ----------------------------------------- | ---------------------------------- |
| `unpaid`、`pending`、`payment_processing` | `idle`，仍是可编辑草稿             |
| `payment_pending`、通用 `processing`      | `pending`，冻结重付与订单写入；显式 reset 仅作为恢复出口 |
| `failed`、`declined`                      | `failed`                           |
| `paid` + orderId                          | `succeeded`                        |
| `paid` + 无 orderId + 空 cart             | `idle`，识别 OS reset 后的短暂空壳 |
| 其他 `paid` 残留                          | `unknown`                          |

最后一条只处理清理残留，不能扩展为“所有不完整 paid 都放行”。

### 6.6 observationId 与 fingerprint

Projection Reader 对语义快照生成本地 observationId；fingerprint 由 order/cart/summary/customer 拼接。它们只用于本实例变化检测与 checkout token，本质不是服务端 revision。

## 7. 商品数据：为何不能简单重新 load

### 7.1 普通 Catalog

Catalog 直接读取 Template 传入的 `context.products.products` 原始引用，因为这个快照已经包含：

- 当前门店/渠道餐牌；
- 类目/collection 筛选；
- 智能报价和促销标签；
- 库存/上下架；
- 当前 BigSale 会话的商品加载策略。

Adapter 不再调用 `products.load`，也不维护第二份目录。普通加购必须从当前快照找回完整 `ProductData`，再调用 `addProductWithFlow`，这样规格/booking/报价判断仍由现有 SalesSDK 处理。

Catalog DTO 会额外投影 `category/parent_category` 的 ID、名称、父级、排序和“是否直接挂载”标记。它只用于 HDLKiosk 内部一级分类、子分类和搜索，不包含原始 `ProductData`，也不参与价格计算。没有分类的商品统一进入 Other；主称重商品按配置 ID 从补充商品列表排除。

### 7.2 主称重商品为何不同

主商品 ID 固定配置，但 Ticket 页面可能没有预载它；Retail 页面通常已加载，所以相同代码会出现“Retail 可用、Ticket 找不到”。安全查询链路为：

```text
queryProducts({ includeIds: [primaryWeightedProductId] })
→ 命中：使用当前 ProductList 中的商品
→ 未命中：findByIds([primaryWeightedProductId], {
     loadMissing: true,
     refreshCatalog: false
   })
→ 再次 queryProducts(includeIds)
→ 仍未命中：primaryProductNotFound，fail-closed
```

`refreshCatalog:false` 是关键：补拉只把缺失商品加入 OS ProductList 能力范围，不把共享 Retail Catalog 替换成单商品列表。

### 7.3 为什么三入口都依赖 queryProducts

Quick 和 Scale 立即需要主商品；Barcode 虽然先扫码零售商品，后续正式编排仍可能进入主商品称重步骤。因此 capability 缺失时三个入口统一不可用，避免流程走到中途才发现关键商品无法加载。

### 7.4 查询与提交都重新读取

- 进入详情时查询一次，用于展示；
- 首次加车以及每次重量/options 更新时再次查询，用于验证当前售价、单位、available 和 options；
- Adapter 不缓存第二份主商品事实；
- 页面直接从 `sourceData.catalogProducts` 读取当前 ProductData；写入前由 operation 重新查询并校验；
- 查不到或商品结构变化时不使用旧商品提交。

## 8. 称重与 Options 兼容

### 8.1 物料 `product.ts` 与 Adapter `weightedProduct.ts`

两侧共用同一个 `resolveHDLWeightedProduct(ProductData)` 解释称重事实，避免页面和写入器对单位/价格的理解漂移。该函数保留原 ProductData 引用，不生成第二份商品：

1. 只接受根商品称重，或恰好一个称重 variant；
2. 多个称重 variant 视为歧义，要求商品配置侧先消除；
3. 校验 price、unit、unit_value；
4. 使用 `@pisell/utils` 转为每 kg 单价；
5. Screen 只读取派生事实用于展示，Adapter 在最新 ProductData 上再执行同一解析；
6. 提交时校验 auto/stable、gross/tare/net 以及 `gross - tare = net`；
7. 生成 SalesSDK `product_sku/_extend.weighing/unit_data_snapshot` 所需结构；
8. 调 `context.cart.addProductWithFlow`，并将已完成的称重/SKU 数据作为 prepared cache 传入；不复用旧 Weighing UI 组件。

### 8.2 `customizationProduct.ts`

当前只安全支持单选 options：

- 过滤未启用/已删除 group 和 option；
- 按 sort 排序；
- 任一启用 group 无法表达就整体 fail-closed；
- required group 必须选择；
- selection 的 groupId/optionId 必须能回到最新 ProductData；
- 生成 `product_option_item/skuValue/_skuValue`；
- options 加价并入 prepared product 原始总额，但最终 Summary 仍由 OS 计算。

这不是通用商品详情引擎；多选、bundle、复杂 variant 需要先扩展契约和测试。

当前三条内置 Workflow 都通过 `customization` Activity 处理主商品，主商品写入必须同时携带稳定重量和 options。因此 Sales Port 只提供 `syncPrimaryWeightedProduct`：

- 没有 `existingLineId`：首次加入主商品；
- 有 `existingLineId`：使用 `updateItem` 更新指定订单行；
- 两种情况均校验同一份 `PrimaryWeightedProductInput`，不存在绕过 options 校验的纯称重写入通道。

## 9. Sales Port 命令映射

| HDLSalesPort                     | BigSale/SalesSDK 调用                                            | Adapter 补充的安全语义                                                                                         |
| -------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `ensureSession`                  | 已有 tempOrder 则复用，否则 `sales.createNew`                    | dispatch 后 abort → unknown                                                                                    |
| `selectIdentity({ customerId })` | 消费登录 UI 暂存客户并调用 `customer.select`                     | 找不到对应认证客户时明确失败，不再回退到旧会员选择器；返回最小 customer receipt，再由 Core 等待一致性与重报价  |
| `selectIdentity({})`             | 调用 `customer.select(null)`                                     | 明确清除当前订单客户，以未登录身份继续                                                                         |
| `setFulfillment`                 | 当前 booking runtime 的窄 writer                                 | writer 缺失时明确失败，不模拟成功                                                                              |
| `syncPrimaryWeightedProduct`     | 无 `existingLineId` 时 `addProductWithFlow`；有值时 `updateItem` | 新增必须经 OS 统一加购编排；提交前重查商品并重验称重/options；更新必须命中原行                                 |
| `consumeBarcode`                 | `useSalesSdkGlobalScanHandler`                                   | cancelled/failed/pending/added 标准化；仅 success 且 count > 0 作为 cart change receipt                        |
| `addProduct`                     | 当前 Catalog ProductData + `addProductWithFlow`                  | requiresSelection 当前映射为不支持，不伪装 added                                                               |
| `updateQuantity/removeLine`      | 从最新 displayLines 反查 line 后调用 cart API                    | 将 API 返回行投影为 `confirmedLine/removedLineId`；line 不存在时可恢复失败                                     |
| `waitForCartConsistency`         | SalesSDK consistency barrier                                     | 成功视为 Host 权威屏障                                                                                         |
| `recalculateSummary`             | `cart.recalcSummary`                                             | 返回 `confirmedSummary`；summary 缺失/异常显式失败                                                             |
| `payEftpos`                      | 模板内 `PaymentMethodRuntime`                                    | 只接受明确终态，并把结果存入模板 React state                                                                   |
| `requestStaffPaymentApproval`    | 主屏授权 command                                                 | 终态 `payment_result` 通过订阅返回，不从 tempOrder 推断                                                         |
| `clearAndReset`                  | `sales.clearCartAndReset`                                        | 将 OS 返回的空 `products + bookings` 投影为 `cartEmpty` receipt；Core 不用滞后的 React Projection 否定写入结果 |

Adapter 命令返回成功不等于 Workflow 一定跳页；Core Operation 还会检查 writer receipt 和一致性屏障。Screen Command 成功保持当前 Activity，只有 Actor 执行的 Activity Exit Command 成功才会提交目标节点。

Quick、Barcode 的主商品分支和 Scale 都复用同一 Customization 写入链路。Quick/Scale 入口需要 `addProductWithFlow`、`updateItem`、`transformDetailToProductAndBooking` 和 `queryProducts`均可用；Barcode 首次扫码只要求 scanner/session，不因后续可选的称重分支未就绪而阻断 Catalog。

## 10. 支付结果归一化

支付 Runtime 分别处理两条真实来源：EFTPOS 直接返回值，以及主屏 `payment_result` command。Adapter 只把它们规范成 `PaymentConfirmationResult` 和页面可读的 `HDLKioskPaymentResult`，不再调用通用 `openCheckout`，也不要求从结果对象内转换整份订单。

规则：明确 success 才跳转结果页；failed/cancelled 留在支付页；不识别的形状保持 unknown。success 后先浅拷贝 OS 本地已完成订单作为稳定展示快照；本地取餐码为空时按该订单 ID 发起只读详情请求，然后立即通过 Core Operation 清空 Host 工作区。结果页从 `sourceData.completedOrder.shop_full_order_number` 读取取餐码，不经支付结果转发；请求结果不 hydrate 新建的 OS 当前订单，支付结果状态清除后的迟到响应会被忽略。

不能把“弹窗关闭”“Promise resolve”或空对象当支付成功。

## 11. 设备兼容层

### 11.1 Scanner

`devices.ts` 优先调用扫码焦点栈：

```text
acquireScanFocus(handler, { priority: 100, id: 'hdl-kiosk' })
```

只有不可用/异常时才回退 `nativeScanResult` pubsub。原始 code 只用于执行扫码，不写 telemetry 或长期状态；降级 eventId 用非明文 fingerprint + 350ms 窗口去重，不把它声明为宿主永久事件 ID。

### 11.2 Scale Hook 到普通订阅

`useNativeScale` 是 Hook，不能直接塞进普通 Adapter 函数。`BigSaleScaleRuntimeOwner` 常驻模板顶层：

- Hook `autoStart` 随物料 active；
- 通过 `ScaleBridge` 发布完整 Snapshot 给 Core `HDLScaleRuntime`；
- 通过同一个 ScaleBridge 发布连接状态和首次有效正重标准事件；
- 重量回落到阈值下后才允许下一次入口触发；
- Error Boundary 将 Hook 异常转成 scale error，不炸毁 HDLKiosk；
- ownerId 防止多个发布者同时写同一 Runtime。

Adapter 直接把 `ScaleBridge.subscribeDeviceEvents` 作为标准事件源；详情连续读数直接读取同一 `ScaleBridge.runtime`。Capability 仍要求 `nativeScaleTools.useNativeScale`，不能把一个没有真实 Hook 来源的订阅函数冒充完整称重能力。

## 13. 模板内部运行能力

支付控制器、Identity H5 认证客户、履约 writer 和 ScaleBridge 都由 `useBigSaleHDLKioskAdapter` 直接持有。它们使用独立 ref/getter 消费最新实例，但不再先组合成一个中间 Bridge 类型，也不通过 `BigSaleProps → BigSaleBaseProps → BigSaleTemplateRendererProps` 透传。

新增能力前先判断它是否应成为 SalesSDK 正式能力；只有 React 或设备生命周期要求的窄接口才留在模板 Adapter 内，不要把订单、商品或客户对象复制进额外中间模型。

边界规则：

- 当前配置由 catalog list 加载完成后筛选
  `data_variants[0].data.client_meta.is_main_weigh_product === 1` 的商品，再交给
  `resolveHDLKioskBigSaleConfig()` 生成，不接受 `hdlKioskConfig` 上层参数；
- 当前运行能力由 `HDLKioskBigSaleTemplate` 内部装配，不接受额外 Host bridge 上层参数；
- 真正出现调用方之前，不为“未来可能需要”修改 BigSale 公共 Props；
- 后续 OS 提供正式能力时，优先在该目录转换为 Host Port；只有多个模板都需要时，才评估上移到 SalesSDK。

## 14. Capability 是如何计算的

Capability 不是固定配置，而是当前 Context 与真实 runtime source 的交集，并只保留 Runtime/Entry 实际读取的字段：

- session ensure：检查 `createNew/clearCartAndReset`；
- scanner：bookingTicket 有 global scan；
- primary product：Products Context 有 queryProducts；
- scale/customization：原生称重源、`addProductWithFlow`、queryProducts 同时存在；

当 pisellos/Products Context 不提供 `queryProducts` 时，三个入口会同时 unavailable。Hook 仍保留内部能力源变化时不重建 Host 的机制，但不提供额外 bridge 注入入口。

## 15. Error/Result 转换规则

Adapter 统一生成：

```text
failed         明确未完成，可按 recovery back/retry/staff/fatal 处理
partialFailure 多步骤可能只完成一部分，要求员工核对
unknown        已派发但最终事实不明确，禁止自动重试危险操作
cancelled      未完成或用户主动退出，不等同业务失败
```

每个 Host Error 带：

- 稳定 code；
- `source: host`；
- recovery；
- safeMessageKey；
- traceId；
- 可选 cause（只供内部诊断，不直接展示）。

Abort 的时机很重要：dispatch 前可 cancelled；dispatch 后再 abort 通常必须 unknown，因为 OS 可能已写入。

## 16. 当前临时策略与真实缺口

| 开关/现状                         | 当前行为                       | 风险                   | 退出条件                         |
| --------------------------------- | ------------------------------ | ---------------------- | -------------------------------- |
| 无跨刷新恢复                      | 刷新后不推断原 Workflow 节点   | 支付派发窗口刷新风险   | OS 支付查询/幂等和明确的恢复需求 |
| 无支付主动查询                    | 依赖设备返回或主屏 command     | 重启后无法主动对账     | OS 支付查询能力                  |
| telemetry no-op                   | Core 事件被安全丢弃            | 线上不可观测           | 正式 telemetry sink 与隐私规则   |

任何新增临时策略必须：

1. 只出现在 BigSale 装配层；
2. 使用显式命名开关；
3. 记录影响、风险和退出条件；
4. 不在 Core 内降低默认安全语义。

## 17. 排查问题时如何定位层级

| 现象                               | 首查                                      | 判断依据                                                     |
| ---------------------------------- | ----------------------------------------- | ------------------------------------------------------------ |
| Entry unavailable                  | `createHost.ts` capabilities + config     | 缺哪一项真实方法/runtime source                              |
| Ticket 找不到、Retail 找得到主商品 | Products Context/query hydration          | Ticket ProductList 是否预载、补拉是否关闭 catalog refresh    |
| 商品能显示但加购失败               | `sales.addProduct` / `addProductWithFlow` | ProductData 是否来自当前 Catalog、结果是否 requiresSelection |
| 连续加购进入 checking              | Core cart consistency + SalesSDK barrier  | Host 屏障与 React pending 展示态是否混淆                     |
| Catalog 加购时闪动                 | Shell/Screen busy presentation            | 是否全屏遮罩或全部卡片透明度变化，不先怀疑数据丢失           |
| Scale 入口 unavailable             | plugin + capability gating                | `useNativeScale`、prepared product、queryProducts 是否齐全   |
| Scale 有读数但不自动进入           | `ScaleBridge` + Core ScaleIngress         | 阈值、connection、重复边沿、当前 session lock                |
| 登录成功但金额不变                 | identity Operation + Summary Projection   | customer 是否写入、consistency/重报价是否完成                |
| Checkout 后 blocked                | callback normalizer + Payment Projection  | 是明确 failed 还是 unknown，禁止直接放行                     |
| 刷新后存在旧订单或支付状态         | Host/OS 订单与支付处置                    | 当前 OS payment/cart 事实与员工处置入口                      |

## 18. 接入与变更验收清单

- Core 目录没有 BigSale/SalesSDK/Engine import；
- Adapter 没有 Workflow target 或页面导航；
- Catalog 继续复用 Product Provider 快照；
- 主商品补拉不覆盖共享 Catalog；
- 称重/options 提交前重新读取并验证最新 ProductData；
- callback 模糊结果保持 unknown；
- dispatch 后 Abort 不误判 cancelled；
- runtime source 晚到不重建 Host/session；
- 设备订阅可释放，单 listener 异常不破坏宿主分发；
- 临时策略集中登记；
- Adapter contract、weighted product、device、host stability 测试通过；
- 真实 Ticket 与 Retail、Web 模拟秤与 Kiosk 设备都完成验收。
