# Entry 设备事件重构修复报告

## 1. 背景

本次检查对照 `sc/260817/beta-HDLKiosk` 的 Entry/Flow 设备事件实现，复核无头
Application Runtime 重构后的扫码与称重链路。目标行为为：

- Entry 首码先创建并发布 Catalog Session，再执行 `barcode.consume`；
- Session 启动期间的后续扫码归属于该 pending Session，不丢码、不另起流程；
- Scanner 在 Entry、Session 启动中和 Flow 内由同一个无头路由持有；称重由 Entry
  Trigger 与共享 Scale Runtime 分别负责边沿触发和连续状态；
- inactive、支付完成和 reset 阶段不得由设备事件启动新 Session。

## 2. 发现的问题

### 2.1 Runtime 在 Kiosk inactive 时仍激活

Session Controller 无条件激活 Application Runtime，导致隐藏状态仍订阅 Scanner/Scale。
除了可能抢占其他页面的扫码焦点，设备事件还可能先执行 `session.ensure`，再被 UI 层销毁。

### 2.2 Session 启动窗口内扫码丢失

重构后 Entry Trigger 和 Flow Scanner Controller 分开。Session 尚未发布时，Flow
Controller 因没有 `sessionRef` 忽略扫码；Application Runtime 又因已有 pending start
拒绝新的自动入口，导致 Barcode 连扫以及 Quick/Scale 启动期间扫码丢失。

### 2.3 finished Session 不再占用准入名额

Runtime 原先只统计 `session.isActive()`。PaymentCompleted 会让 Actor 进入 `finished`，但
UI 仍展示支付结果并持有该 Session。在自动 reset 前，扫码或称重可能启动新流程。

### 2.4 finished Session 无法显式释放

`disposeSession` 对 `isActive() === false` 直接返回，导致 PaymentCompleted reset 后 Actor
和 Registry 记录没有按调用方预期释放。

### 2.5 Entry 与 Flow 同时消费同一设备 Port

Application Trigger Adapter 和 Flow Scanner Controller 使用两套 Ingress/支付策略接收
同一事件。虽然 Operation Coordinator 能避免直接并发写购物车，但会产生重复准入、
遥测缺口和后续策略漂移风险。

### 2.6 Barcode-only 配置不激活 Scale Runtime

Barcode Workflow 可以进入 Customization，但 BigSale Scale Runtime 只在 Quick/Scale
入口启用。仅开启 Barcode 时，称重详情无法取得有效秤快照。

## 3. 修复方案

### 3.1 可逆 Runtime 生命周期

Application Runtime 新增 `deactivate()`：释放 Trigger Adapter cleanup，但保留现有
Session。Session Controller 根据 `isActive` 激活或停用 Runtime。每次 activate 同时推进
`activationEpoch`；旧激活周期创建的 Session 即使在重新 active 后才完成，也会被识别为
迟到结果并 dispose，不会跨生命周期发布。

### 3.2 显式 Session 所有权准入

Runtime 使用 owned Session 集合控制 `maxActiveSessions`。Session 即使自然 finished，
在显式 `disposeSession` 前仍占用名额。`disposeSession` 现在允许释放 finished Session。

### 3.3 单一 ScannerSessionRouter

移除组合层 `PendingStartupScan` 和 React `useHDLKioskScannerController`。无头
`ScannerSessionRouter` 唯一订阅 Scanner，并用一条 FIFO 覆盖三种阶段：

- `idle`：首码入队后同步进入 `starting`，再发出 `onScan` 启动请求；
- `starting`：Quick/Scale/Barcode 启动期间的扫码继续追加到同一队列；
- `active`：Flow 新扫码仍追加到同一队列，并由当前 Session 串行消费。

因此启动窗口的 B 码一定早于 Session 发布后到达的 C 码，不再存在两条队列交叉导致
`A → C → B` 的可能。

每次启动以 `cause.correlationId` 作为 `startId`。`open/attach/close` 使用相同 startId，
旧启动的迟到 rejected 无权清理新启动队列。

队列最多容纳 5 个未完成任务（running 也计入）；超限、启动失败、inactive、Session
dispose 或前一任务结果不确定时，剩余任务都会收到结构化拒绝遥测，且不记录原始条码。
当 Host 返回 `requiresSelection` 时，拒绝选择态建立前已经排队的旧任务并结束本轮
drain。Router 不为尚不存在的 Host 完成协议维护永久冻结状态；选择弹层由 Scanner
focus stack 临时接管物理扫码，关闭后新扫码重新按正常准入处理。

### 3.4 Session Attachment 生命周期

Session `attach` 连接 `ScannerSessionPort` 并返回 Attachment。Kernel 在 Registry 发布
`started` 后调用 `Attachment.activate()` 开始 drain，保持“先进入 Flow，再消费 code”的
业务时序。Session dispose 调用 `Attachment.dispose()`，Router 回到 `idle`。

### 3.5 Router 与业务 Command 解耦

Router 只依赖 `ScannerSessionPort`，不再 import `HDLKioskWorkflowSession`，也不包含
`barcode.consume`、支付 Operation ID 或页面 Activity。具体可写策略、导航取消和
Command 映射集中在 `scannerSessionPort.ts` 适配层，并增加架构边界测试防止回流。

### 3.6 执行中任务的取消与代际隔离

Session dispose 现在同时 dispose Operation Coordinator，触发其 AbortController，避免
仍在执行的扫码 Command 泄漏到已销毁 Session。单纯 inactive 会保留 Session，因此不
强杀可能已经写入 Host 的操作；Router 会推进 generation，并把迟到结果标为 stale，
禁止它冻结、清理或继续驱动新激活周期的队列。

### 3.7 选择态的最小处理

`requiresSelection` 只承担当前已知事实：拒绝此前积压任务并停止本轮 drain。不新增
无生产调用方的 resume API，也不维护无法被当前 Host 协议可靠解除的冻结状态。未来若
Host 提供正式完成事件，再按实际契约补充等待语义。

### 3.8 Barcode Scale Runtime

BigSale Scale Runtime 的激活条件补充 Barcode 入口，确保 Barcode Flow 进入
Customization 后仍能获得实时秤快照。

BigSale Scanner focus 保持标准栈语义，不设置永久高优先级：HDLKiosk active 时位于全局
扫码监听之上，后打开的选择或搜索弹层仍可临时接管，关闭后自然恢复。同步修正了遗留
测试对 `priority: 100` 的错误预期。

### 3.9 Scale 提交安全补强

Scale Runtime 增加唯一的 `sampleSequence`，仅由 NativeScale 的真实 `onChange` 帧
推进。断连、停止监听、读取错误、负重或超载会使当前采样失效；重连但未收到新帧时不
恢复 Next。这里没有增加固定读数超时：顾客将商品放稳后长时间选择规格属于正常场景。

Customization 的展示与提交复用同一个纯校验，提交回调不再信任上一次 React render，
而是在点击 Next 时同步读取 Runtime 最新快照并冻结当下稳定重量。SkuDetailModal 的
实时 Auto 模式同步收紧为只有 liveData、listening、connected、stable 且无错误时有效；
员工 Manual 和编辑态尚未 re-read 的历史快照行为保持不变。

## 4. 修复后的关键时序

```text
Entry 首码
  → ScannerIngress
  → ScannerSessionRouter FIFO
  → onScan occurrence
  → Session open/create
  → session.ensure
  → Session attach
  → Registry started（Catalog 已发布）
  → Attachment activate
  → barcode.consume（同一 FIFO 串行 drain）

Flow 内扫码
  → 同一个 ScannerSessionRouter FIFO
  → barcode.consume

Entry 称重
  → ScaleIngress
  → onWeight occurrence
  → Scale Session / Customization

Flow 内称重
  → BigSale Scale owner
  → HDLScaleRuntime snapshot
  → Customization measurement
```

## 5. 验证

新增或补强以下回归覆盖：

- Runtime deactivate/re-activate 与 Adapter cleanup；
- 旧 activation epoch 的迟到 Session 不会在重新激活后发布；
- 旧 startId 的迟到失败不会清理新启动队列；
- finished Session 在 dispose 前继续阻止准入；
- finished Session 可被显式 dispose；
- Barcode 首码先发布 Catalog，再加购；
- Barcode 启动期间连续扫码按顺序消费；
- 启动窗口旧码与 Flow 新码严格保持单一 FIFO 顺序；
- Quick 启动期间扫码绑定 Quick Session；
- Quick-only 配置仍允许 Flow 内扫码，但不允许扫码作为 Entry；
- `requiresSelection` 拒绝旧队列任务，但不会永久冻结 Session 的后续扫码；
- inactive 前已执行的扫码结果按 generation 隔离，不影响重新激活后的队列；
- Session dispose 会取消仍在执行的 Operation；
- owned/finished Session 阶段扫码和称重不能启动新流程；
- inactive Runtime 不持有设备订阅、不执行 `session.ensure`；
- React Session Controller 随 `isActive` 激活/释放 Trigger。

扩展轻量测试结果：HDLKiosk 50 个测试文件、271 项测试全部通过；BigSale HDLKiosk
适配层 11 个测试文件、73 项测试全部通过。

Scale 安全补强后再次合并运行 HDLKiosk、BigSale HDLKiosk Adapter 与
`pro/skuDetailModal`：68 个测试文件、429 项测试全部通过。

补充 `tsc --noEmit` 未能形成有效项目级结论：当前安装的
`node_modules/@types/react/index.d.ts` 在 `HTMLAttributes` 附近存在结构损坏，且仓库内
已有 `.ts` 测试文件包含 JSX，编译在进入本次修改源码前即失败。

未运行 `pnpm`/`npm` webpack build，符合本工作区 Material 项目的验证约束。
