# HDLKiosk 核心运行机制

> 代码范围：`packages/private-materials/src/components/HDLKiosk`  
> 文档目标：说明独立物料、无 UI Kernel 与海底捞业务层如何协作，以及修改需求应落在哪一层。
> BigSale 原始字段与兼容逻辑见 [02-Host 适配与业务数据.md](./02-Host适配与业务数据.md)。

## 1. 先建立正确心智模型

HDLKiosk Core 不是 BigSale 的另一套页面，也不是本地订单 Store。它是一个以 `HDLKioskHost` 为边界的业务流程运行时：

```text
外部事实（Host Projection / Device Event）
                  ↓
Runtime Controller 决定是否启动会话或消费设备事件
                  ↓
Screen 内部动作走 Command；离开步骤产生 Step Resolution，返回产生 Navigation
                  ↓
Workflow Actor 根据 Resolution 选择静态 Transition，并按页面历史处理 Back Navigation
                  ↓
HDLKiosk Transition Runner 求值本地 Predicate、执行 Exit Command 并解释交易结果
                  ↓
Operation Coordinator 安全执行 Screen Command 或 Exit Command
                  ↓
Operation 通过 Host Port 发命令，用 writer receipt 验证、Projection 兼容回退
                  ↓
Actor 根据 Runner 的 advance/stay/blocked 决策提交节点；Screen Command 不改变节点
                  ↓
Screen Renderer 根据当前节点与最新 Projection 渲染
```

其中有三类状态，不能混在一起：

| 状态类型 | 例子                                               | 所有者           | 为什么不能混用                               |
| -------- | -------------------------------------------------- | ---------------- | -------------------------------------------- |
| 业务事实 | Cart、Customer、Summary、Payment                   | Host/OS          | 前端推测会产生重复支付、假优惠或脏订单       |
| 流程状态 | entry、nodeId、fulfillment 本地选择、session epoch | Workflow Runtime | 只回答“顾客处于哪一步”，不能证明订单已写成功 |
| 展示输入 | options 选中项、搜索词、Modal 内选择               | Screen           | 可以随页面销毁，不得成为结账依据             |

核心规则是“流程、命令与事实分离”：Screen Command/Exit Command 发起业务写入；writer receipt 证明本次写入完成了什么；Projection 提供当前 UI 可观察事实；Workflow Definition 只描述 Step Resolution 到目标节点的关系，Back Navigation 不写入 Definition。

## 2. 目录与运行时角色

| 目录/文件                                                                                     | 运行时角色                         | 输入                                            | 输出                                               | 解决的问题                                           |
| --------------------------------------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------- |
| [`app/contracts/`](../app/contracts)                                                          | Application 与任意 Host 的最小协议 | 无宿主实现                                      | DTO、Port、Capability、Result、Workflow schema     | 防止 BigSale/SalesSDK 类型渗入应用层                 |
| [`kernel/`](../kernel)                                                                        | 无 UI 运行内核                     | 注入的 Registry、Policy、Runner、Lifecycle      | Application API、Compiler、Actor、通用 Coordinator | 复用执行机制，同时阻断商家业务和 Host 反向依赖       |
| [`slices/`](../slices)                                                                        | HDLKiosk 业务切片                  | Slice 定义、View、Screen/Dialog                 | 可复用业务能力与其本地界面                         | 一个业务能力的规则、私有写入和界面同置               |
| [`app/manifest.ts`](../app/manifest.ts)                                                       | 应用静态定义                       | Slices、Workflows、Operations、Predicates、Navigation Guards、Trigger Contracts | Application Definition 与只读 Workflow Environment | 固定协议由 Definition 在完整生命周期内持有          |
| [`app/workflow.ts`](../app/workflow.ts)                                                       | 内建 Workflow 查询                 | Application Definition                          | Definition、readiness 与 Activity 查询             | 业务查询共享同一 Workflow Environment               |
| [`runtime/application.ts`](../runtime/application.ts)                                       | 应用运行实现与组合根               | Application Definition、Host Binding、Session Lifecycle | 可通过 `bind()` 创建 Runtime 的 Application | 固定实现与实例环境分离                               |
| [`runtime/session/createSession.ts`](../runtime/session/createSession.ts)                     | HDLKiosk Session Binding           | entry、Host、locale、可选 Lifecycle 扩展        | 通用 Session 与 Payment Projection 桥接            | 只保留订单完成、员工支付和 Screen Command 等业务能力      |
| [`kernel/workflow/compiler.ts`](../kernel/workflow/compiler.ts)                               | Definition 结构校验                | Workflow Definition + 注入的 Registry           | 冻结后的可执行 Definition 或 issues                | 防止未知 Resolution、非法跳转和无界循环进入 Actor        |
| [`kernel/application/session.ts`](../kernel/application/session.ts)                           | Application Session 执行骨架       | Application、Workflow、Runtime Binding          | Compiled Definition、Actor、Acceptance Runner      | 同类应用不重复实现编译、Runner 和 Actor 装配              |
| [`kernel/workflow/actor.ts`](../kernel/workflow/actor.ts)                                     | 单会话流程执行器                   | Step Resolution、Navigation、Definition、Runner | WorkflowSnapshot、Transition                       | 解决双击、乱序导航和 Exit Command 完成前跳页         |
| [`runtime/session/transitionRunner.ts`](../runtime/session/transitionRunner.ts)               | HDLKiosk Acceptance Binding        | OperationResult、Host Projection、Coordinator   | 支付结果策略、回执与遥测                            | 通用 Runner 不理解购物车和支付语义                    |
| [`runtime/session/workflowLifecycle.ts`](../runtime/session/workflowLifecycle.ts)             | 已提交状态的业务生命周期出口       | Actor 提交后的 Snapshot                         | telemetry                                          | 诊断不进入状态机内核                                 |
| [`kernel/operations/coordinator.ts`](../kernel/operations/coordinator.ts)                     | 通用副作用调度器                   | Operation request + 应用 Policy                 | 规范化 OperationResult                             | 解决重复 intent、排队、超时和旧 Promise；不认识支付  |
| [`runtime/index.ts`](../runtime/index.ts)                                                     | 共享 Runtime 公共入口              | Application、Operation Handler 与 Workflow Session | 稳定的统一导出面                                | 避免消费者穿透私有实现目录                           |
| [`runtime/operations/coordinator.ts`](../runtime/operations/coordinator.ts)                   | HDLKiosk Operation Policy          | Host、支付门禁、冲突域、遥测                    | 绑定业务语义的 Coordinator                         | 把交易规则留在应用层而不是通用 Kernel                |
| [`runtime/operations/checkoutRequirements.ts`](../runtime/operations/checkoutRequirements.ts) | 支付前业务要求注册                 | Requirement ID、Workflow Context、Projection    | 当前适用 ID 与校验结果                             | 新增采集事实时不改 Checkout 主流程                   |
| [`ui/controllers/`](../ui/controllers)                                                        | React 生命周期协调                 | Host、Config、Projection、Actor                 | session/device/customer 状态                       | 将根组件中的生命周期职责拆开                         |
| [`ui/HDLKioskWorkflowView.tsx`](../ui/HDLKioskWorkflowView.tsx)                               | 活跃会话 UI 编排                   | Controllers、Projection、Actor snapshot         | Renderer、Shell、全局 Dialog props                 | 防止根组件和具体 Screen 混入运行时协调逻辑           |
| [`devices/`](../devices)                                                                      | 设备入口治理                       | 标准化扫码/称重事件                             | accept/reject、entry claim                         | 解决首页多入口竞争、重复事件和错误页面消费           |
| [`devices/scale/`](../devices/scale)                                                          | 顶层称重快照                       | Adapter 发布的秤状态                            | 全生命周期统一 Scale Snapshot                      | 让 Entry 和详情页共享同一秤连接，不反复挂卸原生 Hook |
| [`ui/ScreenRenderer.tsx`](../ui/ScreenRenderer.tsx)                                           | Workflow 到 Slice View 装配        | Definition、Snapshot、View Context              | 当前 View 与 modal base                            | 页面不参与中央流程判断                               |
| [`components/`](../components)                                                                | 共享 Shell、Overlay 与控件         | 只读 ViewModel、语义 callback                   | 跨 Slice React UI                                  | 共享视觉能力不反向拥有商家流程                       |
| [`testing/`](../testing)                                                                      | 可替换 Host                        | fixture、可控异步结果                           | Fake Host、调用记录                                | 不启动 BigSale 也能确定性验证 Core                   |

## 3. Root 与 Session 如何装配

[`index.tsx`](../index.tsx) 只做物料组合和 Entry，不定义订单流程：

1. 创建或消费顶层 `HDLScaleRuntime`；
2. 直接消费 Template 每次 render 传入的 `sourceData` 原始字段，并在 ViewModel 就近派生展示值；
3. 使用 Session Controller 管理 Entry 与活跃 session；
4. Application Runtime 的 Trigger Adapter 把扫码/称重事件转成会话启动请求；
5. 把活跃 session 交给 `HDLKioskWorkflowView`；
6. Workflow View 组合 Customer 与 Screen Data Controller；
7. Shell 提供跨页面会员入口、购物车计数和操作反馈；
8. Screen Renderer 将节点映射为 Screen，并按 `presentation` 叠加 Dialog。

一次 session 的创建过程为：

```text
SessionController.start(request) / automatic Trigger occurrence
→ hdlKioskApplication.bind({ host, config }) 创建 Runtime Environment
→ Runtime Admission（enabledEntries / capability / readiness / 单 Session 容量）
→ Session open（Scanner 启动作用域）
→ Session create
   → createHDLKioskWorkflowSession(entry, host, options)
   → hdlKioskDefinition
   → createHDLKioskOperationRegistry()（从 Runtime Binding 收集 Handler）
   → compileWorkflowStructure(builtInDefinition)
   → createOperationCoordinator(host, registry)
   → createHDLKioskTransitionRunner(host, coordinator)
   → createWorkflowActor(definition, runner, lifecycle)
→ Session prepare（session.ensure）
→ Session attach（ScannerSessionPort）
→ Runtime Registry 原子发布 active Session
→ Attachment activate（开始消费 Scanner FIFO）
```

重要细节：

- 同一个入口的并发 `start` 由 HDLKiosk Runtime 共享一个 Promise，跨入口竞争由 Runtime Admission 拒绝；
- Runtime snapshot 是 Session、starting 和 start failure 的唯一状态源，React Controller 只做生命周期适配；
- Barcode/Scale 是会话的外部入口，不是 Workflow Activity。Barcode Workflow 从 Catalog 开始，Scale Workflow 从 Customization 开始；首个业务页 Back 因此落入会话级清单并回 Entry，无需“入口节点不可回访”或 history skip 特例；
- Host 引用变化会 dispose 旧 Runtime/Actor，并重置 ingress；
- capability 变化不会要求 Host 引用变化，因此不应销毁订单；
- session reset 只有在 Host 写入器确认空订单且 consistency barrier 完成后，才 dispose Actor、释放入口并回到 Entry；React Projection 只负责随后收敛展示，不反向否定 Host receipt。

## 4. Contracts 与 Host Ports

Core 只接收：

```ts
interface HDLKioskHost {
  sales: HDLSalesPort;
  devices: HDLDevicePort;
  telemetry: HDLTelemetryPort;
  capabilities: HDLKioskCapabilities;
}
```

### 4.1 Sales Port

`HDLSalesPort` 同时包含三类能力：

- 交易安全读取：`readTransactionState` 只供写操作/支付派发即时复验；UI 数据由 Template 通过 `sourceData` 直接传入；
- Query：按 ID 加载称重主商品，原样返回系统标准 ProductData；
- Command：session、identity、cart、checkout、reset。

Port 返回 `OperationResult<T>`，不抛出宿主原始异常给 Core。Host 不转发 `tempOrder`、商品、客户等展示对象；Template 通过只读 `sourceData` 把原对象直接交给页面。设备插件仍不得进入页面，指定主商品查询则原样返回系统 ProductData。

### 4.2 Device Port

`HDLDevicePort.subscribe` 只发布标准化 `barcode / weightChanged / connectionChanged`。Core 不知道事件来自扫码枪焦点栈、pubsub、NativeScale Hook 还是 Web 模拟器。

### 4.3 Capability

Capability 回答“当前 Host 能否安全完成某类动作”，不是 UI 猜测：

- Entry 用它决定入口是否可用；
- Controller 用它决定是否订阅设备；
- Screen 用它决定是否展示正式操作；
- Operation 仍要处理调用时能力失效，因为 bridge 可能动态变化。

## 5. Observed Projection

`ObservedSalesSnapshot` 是 Workflow 判断与写后确认使用的最小事实视图；页面展示继续读取 `sourceData`。它至少区分：

- host ready/loading/error；
- cart lines、lineCount、consistencyPending；
- customer 是否为有效 member；
- summary missing/calculating/ready/error；
- payment idle/pending/succeeded/failed/unknown；
- observationId 与 fingerprint。

Projection 的职责是“读取和标准化”，不是新 Store：

- 不主动修改 OS；
- 不缓存一份可独立演进的订单；
- 不把 callback 成功直接变成支付成功；
- 不把本地 fingerprint 当成服务端 revision。

[`ui/snapshots.ts`](../ui/snapshots.ts) 只订阅 Workflow Actor。订单、商品、客户和金额随父级 Context rerender 直接进入 `sourceData`，不再建立 Host 订阅 Store。

## 6. Slice、Workflow 与 Actor

### 6.1 Workflow 表达什么

Workflow 用有序 Slice 数组描述静态编排。数组顺序是默认主路径，`on` 只写分支、合流和跳跃：

```ts
{
  id: 'hdl.barcode',
  steps: [
    catalogSlice,
    {
      slice: primaryProductPromptSlice,
      presentation: { mode: 'modal' },
      resolutionRouting: { complete: 'bypass' },
      on: { omit: 'fulfillment' },
    },
    customizationSlice,
    fulfillmentSlice,
    paymentMethodSlice,
    { slice: paymentCompletedSlice, isTerminal: true },
  ],
}
```

Workflow 作者不会填写 Screen、Predicate、Operation 或 failureTarget；这些由 Slice 引用带入本地可信 Manifest。Compiler 使用的规范化结构只保留 Activity ID、presentation 与目标，不携带 React 组件、Host 方法或订单对象。Quick、Barcode、Scale 的查询集中在 [`app/workflow.ts`](../app/workflow.ts)。

四个概念必须分开：

- **Slice**：开发者定义的完整业务能力，包含 Screen、Resolution、Command 和 Resolution Acceptance；
- **Activity**：Slice 暴露给 Actor 的无 UI 运行契约，例如 `catalog`；
- **Step**：Activity 在一条流程中的一次出现，是编排和运行时定位的稳定位置；
- **Screen**：Activity 的本地 UI 实现，不属于 Workflow Definition。

Slice 在一条流程只出现一次时，Step ID 默认就是 Slice ID；同一 Slice 多次出现时，每次必须显式使用 `slice:用途`，例如 `catalog:initial`、`catalog:final`。这样既保持常规流程易读，也不会把复用能力误认为同一个运行位置。

Slice 声明可提交的 Resolution 及其 Acceptance，不描述导航。`accept()` 直接接受；`require(predicate)` 在 Predicate 满足后接受；`perform(operation, { require })` 在可选 Predicate 满足且 Operation 成功后接受。Workflow 数组为每个已接受的 Resolution 提供相邻 Step 的默认目标；Step `on` 只覆盖需要分支、合流或跳跃的 Resolution。Back 是独立 Navigation，由 Actor 的实际页面历史解析。没有下一项的 Step 不会被自动推测为合法终点，必须声明 `isTerminal: true`。`isTerminal` 只表达 Workflow 图结构，不表达具体业务的成功或失败结果。

Predicate 是本地可信的纯真假判断，例如 `cart.notEmpty`。Acceptance 使用 `require(predicateId)` 表达单纯准入，或使用 `perform(operation, { require: predicateId })` 将准入与写操作组合；编译结果存入 Transition 的 `acceptance.requirement` 与 `acceptance.operation`。Predicate 只返回 boolean；Navigation Guard 返回 `allow | bypass | cancel | redirect | block`，只处理 Step 导航。

业务 Workflow 当前不启用 Slice `beforeEnter` 守卫。Actor 保留 `beforeEach` 管线处理 Step 进入、后退和重定向，不承载 Session 生命周期决策。

Actor 记录每个 Step 最近一次接受的 `complete | omit` Resolution，以及用户实际访问过的页面历史。Workflow occurrence 使用 `resolutionRouting` 决定已解决 Step 后续被导航命中时的行为：`reenter` 进入该 Step 并保留 Back 路径，`bypass` 沿已保存 Resolution 的 transition 继续且不写入 Back 历史。缺省映射为 `complete: reenter` 和 `omit: bypass`，Workflow 只声明与缺省语义不同的 occurrence。Modal 不写入页面历史；历史为空时 `actor.back()` 返回 `boundary`。用户通过 Back 退出一个已解决页面时，其旧 Resolution 失效。目标导航先完成 Guard 解析，随后执行 Acceptance；被 cancel 的导航不会执行 Operation 或写入 Host。

Runtime 在收到 `boundary` 后发起 Exit Protocol。Workflow 引用固定 `exitPolicy`，策略返回 `allow | challenge | block`；`challenge` 由 Runtime 分配唯一 `exitId`。Shell 只投影 `pendingExit`，确认时 Runtime 重新评估策略并在同一命令中执行 reset、Session dispose 与 Entry 状态发布，旧 `exitId` 不能确认后来的退出请求。

Step ID 只标识“一次出现”。Runtime 判断加载哪类数据、是否消费扫码/称重、是否等待现金支付时，只读取由 Slice 派生的当前 `activity`，不得比较 `nodeId === 'customization'` 之类的固定位置。这样重复 Slice 使用 `slice:用途` 后仍执行同一套本地能力。

### 6.2 Slice 表达什么

[`slices/<name>/slice.ts`](../slices) 是本地可信 Slice 定义，每个 Slice 声明：

- 业务 Activity 使用哪个 `surface.id`，以及该 Screen 的 `view`；
- 可以接受哪些 Resolution；
- 支持 page 还是 modal；
- 某个 Resolution 是否需要纯 Predicate 作为 Precondition；
- Resolution 被接受前是否需要执行 Operation；
- Screen 内有哪些不会离开节点的 Command；
- 使用该 Slice 的流程在支付前需要哪些业务事实。

例如 `identity.complete` / `identity.omit` 可在携带输入时执行 `identity.sync`，`customization.complete` 执行主商品同步，`paymentMethod.complete` 检查 Host 已确认支付成功。EFTPOS 派发和店员支付授权是 Payment 页面 Command，不作为流程 Resolution。`defineApplication` 从 Slice 自动生成 Activity Contract 与 Operation 注册信息；远端定义只看到 `Slice ID + 标准 Resolution → target`，不能替换这些安全规则。

Slice 的业务定义与 React View 文件分离但按 feature 并置：Slice `id` 是 Workflow 和 Actor 使用的业务 Activity；`surface.id` 会被 Compiler 复制为 Workflow Node 的 `surfaceId`，供 Shell、Controller 和数据加载策略使用；`ui/views.ts` 按 Activity 注册本地可信 View。各 Slice 的 `view.tsx` 仍负责 ViewModel 派生与组件装配，但不会进入 `defineSlice` 或 Workflow Definition。

| Slice 定义                       | 编译/运行期形态  | 唯一用途                           |
| -------------------------------- | ---------------- | ---------------------------------- |
| `id`                             | Node `activity`  | 业务状态、Resolution 和流程编排        |
| `surface.id`                     | Node `surfaceId` | Shell 变体、数据加载和页面类别判断 |
| `ui/views.ts` 中的 Activity 映射 | 仅应用展示层使用 | React UI 渲染                      |

因此 `id`、`screen` 和 `view` 不互相替代：`catalog` 是商品浏览页面，`primaryProductPrompt` 是覆盖在 Catalog 上的独立 Modal Slice。店员支付授权不改变当前页面，因此是 `paymentMethod` 的 Screen Command，而不是独立 Activity。Application Manifest 是业务运行 Contract 的唯一索引，`ui/views.ts` 是展示实现的唯一索引。View 使用同步本地组件引用，不产生运行期 UI chunk；纯 Compiler/Actor 依赖链也不会加载该注册表。Renderer 若发现 View 缺失，仍使用 `BlockingNotice` fail closed，且不会改变 Workflow。

`slices/index.ts` 对整套 Slice 清单执行一次类型约束：`surface.id` 只能取 HDLKiosk 的非 Entry `ScreenId`。`ui/views.ts` 使用完整的 `Record<ActivityId, HDLKioskSliceView>`，因此新增 Activity 若没有注册 View 会产生类型错误。Kernel 的 `defineSlice` 同时拒绝空 `surface.id`。

Compiler 从实际使用的 Slice 汇总 Checkout Requirement ID，但不把判断函数或 Host 字段写入 Workflow。当前 HDLKiosk 默认注册表为空；未来增加其他业务要求时仍由 [`checkoutRequirements.ts`](../runtime/operations/checkoutRequirements.ts) 集中判断。

`WorkflowEvent` 同时约束 Activity、Resolution 和必要输入：例如 `identity.omit` 必须携带身份输入，`fulfillment.complete` 必须携带 fulfillment context patch。Back 直接调用 `actor.back(intentId)` 并取得 `navigated | boundary | ignored` 结果，不进入 Resolution Event mailbox。Barcode 入口的条码输入不是 Workflow Event，而是 Session Controller 在发布会话前交给 `barcode.consume` 的 Command 输入。Screen Command 由 `OperationInputMap` 约束 Command ID 与输入类型。

Step 可选声明 Modal 展示关系：

```ts
presentation: {
  mode: 'modal';
}
```

这只控制渲染层级。Actor Surface 保留进入 Modal 前实际展示的 Page，Workflow 不指定背景 Step。Compiler 会拒绝以 Modal 作为首节点或 Modal 直接进入另一个 Modal。

### 6.3 结构 Compiler 与业务 Policy 校验

结构 Compiler 会检查：

- workflow id、Step 数组及数量/transition 上限；
- Activity 是否存在；重复 Activity 是否具有稳定且不冲突的 Step ID；
- Resolution 是否由 Activity Contract 暴露；
- Definition 是否夹带 screen/guard/operation/failureTarget；
- 默认相邻导航和显式 target 是否有效，所有 Step 是否从首项可达；
- 无后继 Step 是否声明结束语义，首项是否能到达成功终点；
- 指向自身或更早 Step 的非 Back 跳转是否声明有限 `repeat`；
- Modal presentation 引用是否合法。

结构校验通过后，Compiler 还会将所有 Step 对应 Activity 的通用 `requirements` 去重汇总到编译结果。Kernel 不解释这些 ID；HDLKiosk Domain 再把其中的 Checkout Requirement 交给本地注册表校验。该字段是 Activity Contract 的派生值，不是 Workflow Definition 的作者输入。

支付安全不再通过额外 Workflow policy 转发：支付 Operation 自身在 Host dispatch 前统一执行 Checkout Requirement、prepare token 与 Checkout Safety 校验，Workflow 只负责声明结构和支付意图。

数组天然只有一个首项；不可达 Step 会被拒绝。显式业务回路必须声明 `repeat.max`，Actor 按会话计数，超过限制进入阻塞态。普通 Back 属于用户导航，不计入业务回路限制。校验通过后，Compiler 把数组转成 Actor 查询高效的冻结节点 Map；运行中的 session 不替换 Definition。

### 6.4 Actor 如何处理 Step Resolution 与 Navigation

Actor 使用单 mailbox：

1. 忽略 terminal、blocked 或 disposed 后的新事件；
2. 同时校验事件的 `stepId` 和 `activityId`，拒绝旧 Screen 或同类 Activity 旧实例的迟到回调；
3. 校验 Resolution 是否由当前 Activity 暴露，并查找 Transition；Back 则读取页面历史；
4. 通过注入的 Context Reducer 合并受限 contextPatch；
5. 把 Exit Policy 交给 HDLKiosk Transition Runner；
6. Runner 用最新 Sales Projection 求值 Precondition Predicate，并通过 Coordinator 执行 Exit Command；
7. Runner 将结果归一为 `advance / stay / blocked`，支付成功回执也由 Runner 解释；
8. Actor 只负责提交 target、当前 Step 或 blocked，并清理 active operation；
9. 已提交 Snapshot 交给 Workflow Lifecycle 记录 telemetry；外层可组合额外观察器。

结果到节点的映射：

| OperationResult  | Actor 行为                                                                        |
| ---------------- | --------------------------------------------------------------------------------- |
| `succeeded`      | 进入 target                                                                       |
| `cancelled`      | 留在当前 Step                                                                     |
| `failed`         | 留在当前 Step，并展示可恢复错误                                                   |
| `partialFailure` | 保留当前 Step 并展示错误；仅支付派发或 reset 等 P0 副作用进入 blocked             |
| `unknown`        | 保留当前 Step 并展示错误；仅可能重复扣款、跨订单污染或 reset 不完整时进入 blocked |

`blocked` 是运行时 P0 安全状态，不是可编排的 Activity。UI 在 Actor 阻塞时统一覆盖安全提示，因此 Workflow 中不存在为了显示错误而设置的伪业务节点。普通 Cart、Customer、Fulfillment 和支付前置校验的模糊结果停留原页并允许显式重试，不锁死整个 Actor。

## 7. Operation Registry 与 Coordinator

### 7.1 为什么不能从 Screen 直接调用 Host

“点击按钮后连续调用三条 API”会导致双击、卸载、timeout、会员重报价和支付并发都散落在页面。Operation 把一个业务动作封装为可审计单元；Coordinator 为所有动作统一执行安全策略。

Coordinator 现在有两个明确调用来源：

- Screen Command：Catalog 加购、Cart Overlay 数量修改、称重草稿同步等；由 `session.executeCommand` 执行，成功后 Actor 节点不变；
- Activity Exit Command：Guest 同步、Checkout Prepare/Open 等；由 Transition Runner 从本地 Exit Policy 发起，只有 succeeded 才允许 Actor 转移。

partialFailure/unknown 的 Screen Command 会通过 Actor 的 system block 入口 fail closed；它不伪造 Step Resolution。

当前 Operation 组：

| Operation                            | scope    | timeout | 核心职责                                                                             |
| ------------------------------------ | -------- | ------- | ------------------------------------------------------------------------------------ |
| `session.ensure/reset`               | session  | 15s     | 建立订单；主动退出时清理订单并验证 reset                                              |
| `session.finalizeCompletedOrder`     | session  | 15s     | 支付成功后立即清空 Host 工作区，保留 Payment Completed 会话                          |
| `identity.sync`                      | customer | 15s     | 按 `customerId` 有无应用认证客户或清除订单客户，并执行 consistency、重报价和写后观察 |
| `barcode.consume`                    | cart     | —       | 调宿主扫码 Flow；规格选择完成前不启动写后一致性检查                                  |
| `cart.addProduct`                    | cart     | —       | 调宿主商品 Flow，等待规格或其它业务选择后验证写入                                    |
| `cart.update/remove`                 | cart     | 15s     | 数量修改/删除、summary 重算和目标行验证                                              |
| `product.syncPrimaryWeightedProduct` | cart     | —       | 首次写入可能等待 OS 额外业务弹窗；更新或写入后验证重量/options 和写后证据            |
| `product.removePrimaryProduct`       | cart     | 15s     | 将主商品置为不存在；本来不存在时直接成功，存在时验证删除结果                         |
| `checkout.payEftpos`                 | checkout | —       | 内部校验 fulfillment、cart、summary 与 token，再等待 EFTPOS 终态                     |
| `checkout.completeNoPayment`         | checkout | —       | 提交零元订单并等待 Host 返回可验证的订单终态                                          |
| `checkout.requestStaffPaymentApproval` | checkout | —     | 请求主屏店员授权；终态结果由支付订阅单独返回                                           |

### 7.2 冲突矩阵

```text
session  ↔ 所有写操作
checkout ↔ cart / customer / checkout
cart     ↔ cart / customer
customer ↔ customer / cart
none     ↔ 不参与互斥
```

同 scope 按 FIFO 执行；不冲突的动作可并行。Cart 队列默认最多 20 条，防止设备或 UI 异常灌入。

### 7.3 Kernel 与 HDLKiosk Policy 的边界

通用 [`kernel/operations/coordinator.ts`](../kernel/operations/coordinator.ts) 负责：

- `sessionEpoch + operationId + intentId` 校验；
- 相同 intent 复用同一个 pending Promise；
- AbortController 和可选 timeout；未声明时等待 Operation/Host 主动终结；
- session epoch 变化后丢弃旧结果；
- 调用应用注入的冲突、错误、Context 和生命周期 Policy。

[`runtime/operations/coordinator.ts`](../runtime/operations/coordinator.ts) 只负责 HDLKiosk 交易语义：

- 注入 `HDLKioskHost` 和最新 Workflow Context；
- 定义 session/cart/customer/checkout 的冲突矩阵；
- 将写操作超时分类为 `unknown`，且不自动重试；
- checkout opening/pending/unknown 后冻结危险写；
- 识别 `session-reset` 与 `checkout-dispatch` 语义标签；
- reset 成功后清 Checkout Safety，遥测失败不改变交易结果。

因此第二个商家可复用 Kernel 的执行纪律，同时声明自己的 Context、冲突域和支付策略，不需要继承 HDLKioskHost 或称重规则。

### 7.4 Cart consistency 的边界

Cart Mutation 的典型链路：

```text
Host command
→ Host waitForCartConsistency（权威屏障）
→ recalculateSummary
→ 优先验证 writer receipt（目标行/数量/删除/summary）
→ writer receipt 确认目标写入
```

`Projection.cart.consistencyPending` 是展示和结账前复验状态，React 提交可能晚于 Host 屏障 Promise，因此不能在同一调用栈中反向推翻已成功的 Host 屏障。

receipt 是 Result DTO 上的窄字段，不是新 Store、事件源或第二套状态机。缺失关键 receipt 时返回 unknown，不再使用 React 展示状态猜测成功。

## 8. Runtime Controllers

### 8.1 Session Controller

负责：

- Entry capability 与 production-readiness 门禁；
- 三入口仲裁和 start Promise 去重；
- session.ensure；
- Barcode Session 激活后的首码及后续 `barcode.consume`；
- Host 替换清理；
- reset 成功后的完整释放。

它不处理具体 Screen 导航，也不实现订单写入；入口准备只调用已注册的 Operation。

### 8.2 Device Controller

在 Entry 可见且尚无活跃会话时订阅标准化 Scanner/Scale Port，只有以下条件满足才请求启动会话：

- 物料 active；
- capability 可用且对应自动入口 enabled；
- 尚无活跃或启动中的 session；
- 支付状态未冻结。

通过 ScannerIngress/ScaleIngress 完成 eventId 去重、阈值检查和拒绝原因遥测。Device Controller 不向 Actor 发送流程 Resolution；Barcode 把条码作为 start request 的一部分，Scale 只发起 Scale session。

### 8.3 Customer Controller

Identity 节点内的登录是 Step Resolution，不是 Screen 直接写订单：

```text
BigSale Login2/Register2
→ 暂存已认证 customer
→ identity.complete(customerId)
→ identity.sync Acceptance Operation
→ Adapter customer.select + consistency + repricing
→ 成功后进入下一节点
```

认证 UI 通过 HDLKiosk 应用展示层插槽注入，不进入 `defineSlice`、View 注册表或 Kernel。Slice 的 `complete` 在携带输入时执行 `identity.sync`；已登录客户的自动略过使用 `omit` 且不携带输入，因此不会重复写客户。

Shell 登录不是 Workflow 跳转，而是 session-level 插入 Operation：

```text
Shell Login
→ session.executeCommand(identity.sync)
→ Coordinator(customer scope)
→ identity.sync
→ customer + cart consistency + pricing Projection 更新
→ succeeded/failed 保持当前 nodeId；partial/unknown 阻塞 Actor
```

会员入口的可见性和可操作性由本地 Activity 策略决定：已确认会员可在安全页展示身份；未登录时只有注入正式认证 renderer 才开放登录。支付未决、reset、Operation running 或 checkout 后续 Activity 不允许插入身份。`identity.sync` 与 cart mutation 复用同一 Screen Command 边界，已写入客户但一致性/重报价不明时不得继续下单。

### 8.4 Screen Data Controller

只在相关节点加载 Catalog、Weighted Product 或 Customization DTO；使用 AbortController 防止离开页面后的结果写回。Modal 激活时，Screen Data Controller 继续使用 Actor Surface 中保留的 `pageStepId`，底层 Page 保持挂载和原数据，不按 Modal Activity 清空或重新加载。

### 8.5 Session 结束能力

当前物料不维护 idle 计时器或警告弹窗。Payment 成功进入 Payment Completed 后立即执行 `session.finalizeCompletedOrder`，验证 Host 空订单和一致性，但保留成功页 Actor 与订单展示快照。10 秒倒计时或手动返回调用 `completeOrder(source)`；故障恢复调用 `recover(source)`。两者执行验证式 reset，成功后释放 Actor 和 Ingress，失败时保留 Session 与错误证据。

未来应用级生命周期策略可以决定何时调用该接口，但不能直接清 React 状态或只切换页面。支付 pending 继续等待外部终态；unknown 保持重付与订单写入冻结，但允许阻断页显式 reset。reset 失败保留错误证据并允许再次发起恢复，不形成无出口页面。

### 8.6 主动退出订单

Entry 不是 Workflow Step，因此“退出订单并回到 Entry”不属于普通 `back/prev`，而是跨流程的 Session Action：

```text
Back 或 Shell 退出入口
→ actor.back() 返回 boundary，或直接 requestExit()
→ exitPolicy 返回 challenge
→ EndOrderDialog 确认 exitId
→ session.reset
→ Host clearAndReset receipt
→ cart consistency barrier
→ Session Controller dispose Actor / release ingress
→ Entry
```

只有 Host 确认空订单后才可返回 Entry。操作执行中、payment pending/unknown 或 Actor blocked 时不允许主动退出；否则会把“页面回到首页”误当成“OS 订单已清理”。所有 Back 入口统一调用 `runtime.back()`：有历史时返回上一 Step，历史为空时进入 Exit Protocol。重复点击复用同一个 Challenge；退出确认展示时底栏保留在 Modal 遮罩下。

## 9. Device Ingress 与 Scale Runtime

设备分为两层：

```text
Adapter：把原始插件/Hook 转成 HDLDeviceEvent 或 Scale Snapshot
Core：Entry Ingress 负责去重、仲裁和启动会话；活跃 Screen 只消费自身需要的设备快照
```

`HDLScaleRuntime` 在物料顶层只有一个 owner：

- Entry 用 weightChanged 边沿自动启动 Scale 流程；
- Customization/Weighted Detail 订阅同一实时 Snapshot；
- Screen 切换不卸载原生秤 Hook；
- 是否消费重量由节点决定，不由设备模块决定业务流程。

详细设备协议见 [04-设备与称重.md](./04-设备与称重.md)。

## 10. Slice View 与组件组织

表现层的调用方向固定为：

```text
HDLKiosk root
→ HDLKioskWorkflowView（运行时状态转 props）
→ ui/ScreenRenderer（按 Activity 查询 ui/views.ts 中的本地 View）
→ slices/<name>/view + Screen/Dialog
→ components/shell + components/overlays + components/common
→ Activity Action callback
→ HDLKioskWorkflowView
├─ Resolution → Workflow Actor
├─ Back → Workflow Navigation
│             → HDLKiosk Transition Runner（Predicate / Exit Command）
├─ Command → session.executeCommand → Coordinator
└─ Session Action → reset controller
```

界面按所有权分为：

- `slices/<name>/Screen|Dialog`：只属于一个业务能力的页面、局部 Dialog 和样式；
- `slices/<name>/view.tsx`：派生本 Slice 的 ViewModel，并把统一 View Context 装配成 UI props；
- `components/shell/`：跨页面骨架、Header、BottomBar 与错误边界；
- `components/overlays/`：会话级 Modal、CartDrawer 与统一弹窗壳；
- `components/common/`：规格选择等跨 Slice 的共享视觉控件；
- 根 `utils.ts`：金额格式化、购物车摘要、通用 Activity 模型和错误呈现等跨 Slice 纯方法；
- 根 `types.ts`：公共 Props、展示 DTO、Slice View Context 与语义 Action 类型。

每个 Screen/Dialog/Layout 拥有自己的目录与样式文件。根 `index.less` 只保留物料级作用域、基础 token 和按确定顺序聚合组件样式；这样既能避免单个巨型样式文件，也不会因为 React 条件挂载改变 CSS 注入顺序。

除 CartDrawer 保留 Drawer 语义外，所有阻断式业务弹窗都通过 `components/overlays/HDLKioskModal` 间接使用 `pro/compensatedPisellContainer` 的 modal 模式。该共享壳层统一遮罩、层级、Kiosk 关闭策略、内容表面和按钮规格；具体 Dialog 只保留业务内容与 tone 差异。因为容器挂载到 body portal，Modal 样式必须以 `.pisell-lowcode-modal-root .hdl-kiosk-modal` 作为边界，不能依赖 `.pisell-hdl-kiosk` 祖先。

Screen 只做三件事：

1. 渲染 View DTO；
2. 维护局部输入；
3. 内部业务写入发送 Screen Command，离开步骤发送 Step Resolution，返回发送 Navigation。

Screen 不得：

- import BigSale/SalesSDK；
- 直接调用 Host；
- 本地计算最终金额/折扣；
- 把 loading 当成功；
- 自己决定 `unknown` 后去哪。

Shell 负责跨页面 UI：共享 Header、会员入口、唯一悬浮栏挂载点、购物车 Drawer 挂载点和全局阻断反馈。`HDLKioskBottomBarHost` 通过内部 Context 接收当前 Screen 的声明，并只渲染一个 `HDLKioskFloatingBar`；Screen 仍负责计算 `leading / summary / trailing` 的内容和禁用状态，但金额、数量和优惠只能来自 Host Projection。其中 `HDLKioskOrderSummary` 是可跨 Screen 组合的 `summary` slot；Fulfillment 始终展示该摘要，空车时为禁用态，扫码加购并获得 OS 确认投影后自动启用。切换 Screen 时声明随组件生命周期替换或清理，没有声明就不展示底栏，不存在旧 `hdl-shell__footer` 回退。BottomBar 有三种明确展示状态：`visible` 正常交互，`masked` 在普通业务 Modal 下保留可见但收起 Drawer、由遮罩阻止交互，`hidden` 仅用于支付未决、等待员工和 blocked 等不可导航状态。Screen 内部 Dialog 通过 `overlayActive` 请求 `masked`，不通过注销底栏制造视觉跳变。

CartDrawer 也遵循“Screen 声明、Shell 挂载”：Screen 只请求打开并提供 `CartDrawerModel`、Screen Command 和当前步骤允许的底部动作；Drawer 基于 `CompensatedPisellContainer` 处理 Kiosk 缩放补偿及浮层定位。抽屉行、数量、定制摘要和价格只来自 Host Projection，数量修改和删除发送 `cart.updateQuantity / cart.removeLine` Command，经 Coordinator 写后验证但不触发 Workflow self-loop；不得从 BigSale 商品对象或页面 DOM 反向读取订单事实。

Customization 的规格 UI 通过 `components/common/HDLKioskSkuOptionsSelection` 接入共享 `skuOptionsSelection`。查询所得 ProductData 本身直接作为 `dataSource`，兼容层不再复制或重命名 option group/item，只在选择器 `SkuValue` 与业务命令所需 `HDLProductOptionSelection[]` 之间转换“用户选中结果”。临时独立运行缺少 Engine 上下文时，退化为直接读取同一 `option_group/option_item` 的单选控件。

称重主商品在 Customization 中先保持页面本地草稿。重量或 options 变化不写 Host；点击 Next 时才将当下完整草稿交给 `customization.complete` 的 Acceptance Operation `product.syncPrimaryWeightedProduct`。OS 完成其内部业务判断并返回加购成功，且 writer receipt/Projection 验证通过后，该 Resolution 才被接受并进入下一节点。失败或取消留在当前页面，`partial/unknown` 阻断。底栏和 Drawer 仍只消费 Host `summary.due/cart.lines`，不在本地构造未确认的购物车。

Workflow View 负责控制器组合、自动推进和会话级 Dialog；Renderer 只负责选择要展示的组件。普通 `cart.add/update/remove` 和防抖的 `product.syncPrimaryWeightedProduct` 是 Screen Command，保持当前 Activity 挂载，只禁用冲突操作，不产生 transition；会员重报价和 Checkout 等 Activity Exit Command 才阻断整个 surface。

OS 加购过程中的内部弹窗和业务判断不属于 HDLKiosk Activity、Host Contract 或 Checkout Requirement。HDLKiosk 不读写相关业务事实，只根据主商品加购操作的最终结果决定是否跳转。

Barcode 的主商品询问是独立 `primaryProductPrompt` Modal Slice：Yes 发出 `complete`，No 发出 `omit`。Prompt 的两种 Resolution 均使用 `bypass`，因此选择成功提交后不再询问；关闭 Modal 只执行 Back，没有 Resolution，下次仍会展示。Customization 的 `complete` 使用 `reenter`。Quick 和 Scale 的 `omit` 也使用 `reenter`，删除已有主商品后仍可通过 Back 或编辑入口返回；Barcode 的 `omit` 使用 `bypass`，后续导航会绕过该 Step。

## 11. 四条端到端调用链

### 11.1 Quick 启动

```text
Entry click
→ SessionController.start('quick')
→ capability + arbiter
→ compile Quick Definition
→ create Coordinator + Transition Runner + Actor + Lifecycle
→ session.ensure
→ Identity Screen
```

### 11.2 Catalog 加购

```text
CatalogScreen command(cart.addProduct)
→ useHDLKioskActionDispatcher
→ session.executeCommand
→ Coordinator: cart.addProduct
→ Operation: Host addProduct
→ Host consistency + Summary
→ Projection 更新 cart count/amount
→ Actor nodeId 保持 catalog
```

### 11.3 Scale 自动入口

```text
Native Scale Adapter
→ HDLDeviceEvent(weightChanged)
→ DeviceController + ScaleIngress
→ SessionController.start({ entry: 'scale' })
→ 发布以 fulfillment 为首节点的 active session
→ 顾客确认堂食/外带后进入 Customization
→ CustomizationScreen 读取同一 ScaleRuntime 快照
→ Next Acceptance Operation(product.syncPrimaryWeightedProduct) 提交点击时的本地草稿
```

Scale 与 Quick 复用同一套称重商品原子操作和页面，差异只保留在入口与前置编排；Renderer 只保留一个称重详情页分支。

### 11.4 Checkout

```text
用户选择支付方式
→ payment dispatch 内部 ensureCheckoutPrepared
→ consistency + fulfillment + stable Projection
→ Prepare Token
→ Payment Method
→ payEftpos / staff authorization consumes token
→ Checkout Safety freezes dangerous writes
→ Host result + Payment Projection
→ Session Payment Projection bridge
→ paymentMethod.complete 本地门禁
→ paymentCompleted / failed / blocked
```

## 12. 修改需求应该落在哪里

| 需求                  | 首选位置                                           | 同步检查                                  |
| --------------------- | -------------------------------------------------- | ----------------------------------------- |
| 调整节点顺序          | `app/manifest.ts`                                  | 03 文档、definition/actor 测试            |
| 新增跨 Slice 副作用   | `app/contracts` + `runtime/operations` + Host Port | conflict scope、timeout、unknown、adapter |
| 新增 BigSale 字段映射 | BigSale Adapter                                    | Core DTO 是否真的需要扩展                 |
| 新增设备来源          | Adapter Device Port                                | Core Ingress 是否已有相同语义             |
| 修改入口消费条件      | Device/Session Controller                          | capability、payment freeze、dedupe        |
| 修改页面局部交互      | `slices/<name>/Screen` + `view.tsx`                | 不得直接调用 Host；样式留在组件目录       |
| 新增业务 Dialog       | Slice `presentation` + `slices/<name>/Dialog`      | 不新增第二套导航状态                      |
| 修改会话级 UI 协调    | `ui/HDLKioskWorkflowView.tsx`                      | 不把 Controller/Actor 传入具体组件        |
| 新增审计或持久化观察  | 外层 `WorkflowLifecycle` 扩展                      | 不得让 Kernel 依赖存储或恢复策略          |
| 新增跨刷新恢复        | Kernel 外初始化决策模块 + Actor 初始状态           | OS 事实、隐私、支付优先级、人工处置       |
| 调整支付安全          | Kernel `checkoutSafety` + 业务 `checkout`          | 必须更新 05 文档与集成测试                |

## 13. 当前刻意不做的抽象

- 不引入 XState/BPMN；
- 不开放远端函数或动态组件；
- 不新增全局事件总线；
- 不做本地 pricing/discount engine；
- 不把 Projection 变成第二个 Store；
- 不把 writer receipt 扩展为事件日志、完整 OS 对象或第二套订单状态；
- 不做通用插件发现系统；
- 不做前端事务补偿框架。

只有出现多个稳定复用场景，并且当前结构已造成可观测维护成本时才扩展。
