From 9d4ca83a70b385f51ed7c13e6e2e059413219468 Mon Sep 17 00:00:00 2001
From: zgw <13623675548@163.com>
Date: Tue, 4 Aug 2026 15:31:38 +0800
Subject: [PATCH] docs: plan standard WCS implementation
---
...4-standard-four-way-shuttle-wcs-roadmap.md | 168 ++++++
...2026-08-04-standard-wcs-core-foundation.md | 534 ++++++++++++++++++
...6-08-04-standard-wcs-execution-recovery.md | 498 ++++++++++++++++
.../2026-08-04-standard-wcs-ft546-adapters.md | 515 +++++++++++++++++
...8-04-standard-wcs-simulation-acceptance.md | 465 +++++++++++++++
...-standard-wcs-topology-dispatch-traffic.md | 444 +++++++++++++++
6 files changed, 2624 insertions(+)
create mode 100644 docs/superpowers/plans/2026-08-04-standard-four-way-shuttle-wcs-roadmap.md
create mode 100644 docs/superpowers/plans/2026-08-04-standard-wcs-core-foundation.md
create mode 100644 docs/superpowers/plans/2026-08-04-standard-wcs-execution-recovery.md
create mode 100644 docs/superpowers/plans/2026-08-04-standard-wcs-ft546-adapters.md
create mode 100644 docs/superpowers/plans/2026-08-04-standard-wcs-simulation-acceptance.md
create mode 100644 docs/superpowers/plans/2026-08-04-standard-wcs-topology-dispatch-traffic.md
diff --git a/docs/superpowers/plans/2026-08-04-standard-four-way-shuttle-wcs-roadmap.md b/docs/superpowers/plans/2026-08-04-standard-four-way-shuttle-wcs-roadmap.md
new file mode 100644
index 0000000..510bf1d
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-04-standard-four-way-shuttle-wcs-roadmap.md
@@ -0,0 +1,168 @@
+# Standard Four-Way Shuttle WCS Roadmap Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 在 FT546 的 `optimize/wcs` 工作树中,以 RuoYi 为基础逐步交付可独立运行的标准四向车 WCS,并最终由标准内核承担 FT546 的任务、调度、设备协同和恢复。
+
+**Architecture:** 实施拆成五份可独立验收的子计划,依赖顺序固定为“内核基础 → 拓扑与交通 → 执行与恢复 → FT546 适配 → 产品验收”。每份计划结束时都产生可运行代码、自动化测试和独立提交;后续计划只依赖前一计划声明的稳定接口。
+
+**Tech Stack:** Java 8, Spring Boot 2.5.15, RuoYi 3.8.9, Maven Reactor, MyBatis/MyBatis-Plus, SQL Server, JUnit 5, ArchUnit, Vue 2, Element UI.
+
+## Global Constraints
+
+- 继续使用 RuoYi;新增 `ruoyi-wcs` Maven 模块,不另建 FT546 专用中间模块。
+- Maven 依赖只能是 `ruoyi-admin -> ruoyi-wcs -> ruoyi-common`;`ruoyi-wcs` 不依赖 `ruoyi-admin`。
+- 一个部署只运行一个项目和一个仓库,不建设多租户平台。
+- Java 版本固定为 1.8,Spring Boot 固定为 2.5.15。
+- 首版不修改 FT546 PLC 控制流程、安全联锁和既有寄存器语义。
+- 项目 PLC 地址、IP、站点编码、WMS 字段和业务 URL 只能存在于 Admin 适配器或项目配置中。
+- Admin 不得直接写 WCS 核心表,设备适配器不得推进任务状态或释放资源。
+- `UNKNOWN` 命令禁止自动重发;未知资源禁止按超时释放。
+- 业务订单、执行步骤、设备命令和资源预约保持独立状态机。
+- 现有 FT546 代码、表和 `javaTest` 只作为证据与验收语料,不作为新内核包结构模板。
+- 本计划只描述目标版本实现和验收,不设计旧表迁移、灰度切换或双写流程。
+- 每个行为先写失败测试,确认 RED 后再写最小实现。
+- 每个任务完成后运行聚焦测试、相关模块测试和 `git diff --check`,然后独立提交。
+- 保留用户现有未跟踪 HTML 文件,不暂存、不修改、不删除。
+
+---
+
+## Plan Sequence
+
+### Plan 1: WCS Core Foundation and Task Workflow
+
+文件:`docs/superpowers/plans/2026-08-04-standard-wcs-core-foundation.md`
+
+交付:
+
+- `ruoyi-wcs` Maven 模块和测试基础。
+- 架构依赖门禁。
+- `TransportOrder`、`WorkflowInstance`、`TaskStep` 状态机。
+- WMS 标准入站用例和出站事件端口。
+- 第一版核心表脚本和持久化边界。
+
+验收命令:
+
+```bash
+mvn -pl ruoyi-wcs -am test
+mvn -pl ruoyi-admin -am test
+```
+
+### Plan 2: Topology, Dispatch, and Traffic Control
+
+文件:`docs/superpowers/plans/2026-08-04-standard-wcs-topology-dispatch-traffic.md`
+
+依赖:Plan 1 的订单、步骤、仓储端口和模块边界。
+
+交付:
+
+- 版本化拓扑和设备能力模型。
+- 路径规划、车辆选择和稳定评分。
+- 资源账本、预约、死锁预防、HOLD/YIELD。
+- 调度应用服务和决策审计。
+
+验收命令:
+
+```bash
+mvn -pl ruoyi-wcs -am -Dtest='*Topology*,*Assignment*,*Traffic*,*Dispatch*' test
+```
+
+### Plan 3: Execution, Equipment Coordination, and Recovery
+
+文件:`docs/superpowers/plans/2026-08-04-standard-wcs-execution-recovery.md`
+
+依赖:Plan 2 的车辆绑定、路线和资源预约。
+
+交付:
+
+- `DeviceCommand`、Outbox、设备事件和快照对账。
+- 四向车、提升机、输送线和充电协同状态机。
+- `RecoveryCase`、启动恢复、局部隔离和人工接管审计。
+- 核心查询 DTO、指标和关联日志。
+
+验收命令:
+
+```bash
+mvn -pl ruoyi-wcs -am -Dtest='*Command*,*Coordinator*,*Recovery*,*Outbox*' test
+```
+
+### Plan 4: FT546 Project Adapters and Operations UI
+
+文件:`docs/superpowers/plans/2026-08-04-standard-wcs-ft546-adapters.md`
+
+依赖:Plan 3 的设备端口、恢复端口和 QueryService。
+
+交付:
+
+- FT546 WMS 适配器,保留既有接口 URL 和回调语义。
+- 四向车、提升机、输送线和充电桩旧 PLC Profile 与适配器。
+- 项目配置表、协议样本测试和启动 wiring。
+- 标准任务、设备、资源、命令和恢复页面。
+- 新调度入口接管,旧 `BaseSRgvThread` 不再创建、调度或完成标准任务。
+
+验收命令:
+
+```bash
+mvn -pl ruoyi-admin -am test
+cd ruoyi-ui && npm run build:prod
+```
+
+### Plan 5: Simulation, Historical Replay, and Product Acceptance
+
+文件:`docs/superpowers/plans/2026-08-04-standard-wcs-simulation-acceptance.md`
+
+依赖:Plan 4 的完整 FT546 适配链和真实核心构建产物。
+
+交付:
+
+- 使用真实 Adapter 契约的确定性模拟器与故障注入。
+- 第二套合成仓库配置,证明不修改核心即可运行。
+- FT546 历史任务与 PLC 报文回放。
+- 真实 PLC/HIL 的受控协议门禁与能力证据。
+- 重启恢复、性能基线和 72 小时/100000 任务验收入口。
+- 人工接管审计、可观测性证据、可审计报告、manifest 和最终验收索引。
+
+验收命令:
+
+```bash
+mvn -pl ruoyi-wcs,ruoyi-admin -am test
+cd javaTest && mvn clean test
+```
+
+## Release Gates
+
+1. Plan 1 未通过时,不创建真实设备适配器。
+2. Plan 2 的资源容量和非法状态指标非零时,不进入执行协同。
+3. Plan 3 的 `UNKNOWN`、恢复和 Outbox 测试未通过时,不接 FT546 PLC。
+4. Plan 4 的协议回归未通过时,不允许旧调度入口退出。
+5. Plan 5 的安全不变量、第二仓配置、历史回放、能力证据和 FT546 HIL 门禁未通过时,不宣称标准产品完成或 FT546 可投产。
+
+## Approved Design Coverage Map
+
+| 总体设计章节 | 实施落点 |
+| --- | --- |
+| 1-4 背景、目标、边界、首版范围 | 本路线图全局约束;Plan 1 模块边界;Plan 4 FT546 项目边界 |
+| 5 总体架构与 Maven 依赖 | Plan 1 Task 1、Task 7 |
+| 6 六个核心领域 | Plan 1 任务/流程;Plan 2 拓扑/调度/交通;Plan 3 协同/执行/恢复 |
+| 7 核心安全不变量 | Plan 1 状态机与幂等;Plan 2 资源容量/等待图;Plan 3 UNKNOWN/恢复;Plan 5 全量不变量验收 |
+| 8 端到端任务流 | Plan 1 Task 5;Plan 2 Task 7;Plan 3 Task 3;Plan 4 Task 6、Task 8 |
+| 9 核心状态机 | Plan 1 Task 3-4;Plan 2 Task 5;Plan 3 Task 1、Task 6 |
+| 10 数据所有权 | Plan 1 Task 6-7;Plan 2 Task 7;Plan 3 Task 7;Plan 4 Task 1 |
+| 11 适配器契约 | Plan 1 WMS 端口;Plan 2 ConfigurationUseCase;Plan 3 设备端口;Plan 4 全部项目 Adapter |
+| 12 PLC 能力策略 | Plan 4 Task 1、Task 3;Plan 5 Task 4-5 |
+| 13 调度、交通、提升机、输送线、充电 | Plan 2 Task 2-7;Plan 3 Task 4-5 |
+| 14 恢复、人工接管、可观测性 | Plan 3 Task 6-8;Plan 4 Task 7;Plan 5 Task 6、Task 8 |
+| 15 测试与模拟器 | Plan 5 Task 1-6 |
+| 16 验收门槛与产物 | Plan 5 Task 7-8;本路线图 Final Definition of Done |
+| 17 建议代码结构 | 五份子计划各 Task 的精确 Files 清单 |
+| 18 与现有 FT546 文档关系 | 本路线图“旧代码仅作证据”和“不讨论迁移”约束;Plan 4 Legacy Profile 边界 |
+| 19 设计完成定义 | 本路线图 Release Gates 和 Final Definition of Done |
+
+## Final Definition of Done
+
+- `ruoyi-wcs` 构建产物在 FT546 和第二套合成仓库配置下完全相同。
+- FT546 Admin 只包含项目接口、协议、流程定义、策略、配置和页面。
+- 旧调度线程不再拥有标准任务创建、选车、路径、完成和充电调度权。
+- 所有核心表只由 `ruoyi-wcs` Repository 写入。
+- 所有安全不变量测试为 0 违规。
+- FT546 历史回放、PLC 样本契约、真实 PLC/HIL、重启恢复、人工接管、可观测性和性能报告齐全。
diff --git a/docs/superpowers/plans/2026-08-04-standard-wcs-core-foundation.md b/docs/superpowers/plans/2026-08-04-standard-wcs-core-foundation.md
new file mode 100644
index 0000000..2e2b002
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-04-standard-wcs-core-foundation.md
@@ -0,0 +1,534 @@
+# Standard WCS Core Foundation Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 建立可被 `ruoyi-admin` 依赖的 `ruoyi-wcs` 模块,并交付订单、流程、步骤、幂等和核心持久化边界。
+
+**Architecture:** `ruoyi-wcs` 只暴露入站用例、出站端口和查询 DTO;任务状态迁移保存在领域对象中,应用服务负责事务和幂等。Admin 只增加模块依赖,本计划不接 PLC,也不替换现有调度线程。
+
+**Tech Stack:** Java 8, Spring Boot 2.5.15, Maven, JUnit 5, ArchUnit 1.2.1, MyBatis, SQL Server.
+
+## Global Constraints
+
+- 根 Reactor 新增且只新增 `ruoyi-wcs` 产品模块。
+- `ruoyi-wcs` 只依赖 `ruoyi-common`、Spring 事务、MyBatis 和测试依赖;不依赖 `ruoyi-admin`、PLC 库或项目包。
+- 所有公共接口使用 Java 8 类型,不使用 record、sealed class 或高版本 API。
+- 订单、步骤和事件 ID 使用非空 `String`,时间使用 `java.time.Instant`。
+- 外部请求以 `externalRequestId` 幂等;重复请求返回同一 `orderId`。
+- 终态订单和步骤不可反向迁移。
+- 本计划不修改 `BaseSRgvThread`、`PlcSrgvRead`、现有 WMS Controller 或旧业务表。
+
+---
+
+### Task 1: Add the ruoyi-wcs Maven module and architecture test harness
+
+**Files:**
+- Modify: `pom.xml`
+- Modify: `ruoyi-admin/pom.xml`
+- Create: `ruoyi-wcs/pom.xml`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/WcsModule.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/architecture/WcsModuleArchitectureTest.java`
+
+**Interfaces:**
+- Produces: Maven artifact `com.ruoyi:ruoyi-wcs:${ruoyi.version}`.
+- Produces: marker type `com.ruoyi.wcs.WcsModule`.
+- Enforces: no class under `com.ruoyi.wcs..` depends on `com.ruoyi.schedule..`, `com.ruoyi.api..`, `HslCommunication..`, or `org.apache.plc4x..`.
+
+- [ ] **Step 1: Add the module skeleton and a deliberately failing boundary test**
+
+Add `ruoyi-wcs` after `ruoyi-common`, add the managed dependency, add the Admin dependency, and create this test before `WcsModule` exists:
+
+```java
+@Test
+void exposesAStableModuleMarker() throws Exception {
+ assertEquals("com.ruoyi.wcs.WcsModule",
+ Class.forName("com.ruoyi.wcs.WcsModule").getName());
+}
+```
+
+Configure `spring-boot-starter-test` and `archunit-junit5:1.2.1` as test dependencies in `ruoyi-wcs/pom.xml`; configure Surefire `3.2.5` in the root build.
+
+- [ ] **Step 2: Run the module test and observe RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=WcsModuleArchitectureTest test`
+
+Expected: FAIL because `com.ruoyi.wcs.WcsModule` does not exist.
+
+- [ ] **Step 3: Add the marker and dependency rules**
+
+```java
+package com.ruoyi.wcs;
+
+public final class WcsModule {
+ private WcsModule() {
+ }
+}
+```
+
+Add ArchUnit rules:
+
+```java
+@Test
+void coreDoesNotDependOnProjectOrPlcPackages() {
+ noClasses().that().resideInAPackage("com.ruoyi.wcs..")
+ .should().dependOnClassesThat()
+ .resideInAnyPackage("com.ruoyi.schedule..", "com.ruoyi.api..",
+ "HslCommunication..", "org.apache.plc4x..")
+ .check(new ClassFileImporter().importPackages("com.ruoyi.wcs"));
+}
+```
+
+- [ ] **Step 4: Run module and Admin reactor tests GREEN**
+
+Run: `mvn -pl ruoyi-wcs,ruoyi-admin -am test`
+
+Expected: Reactor succeeds and Surefire reports at least one `ruoyi-wcs` test.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add pom.xml ruoyi-admin/pom.xml ruoyi-wcs
+git commit -m "build: add standard WCS core module"
+```
+
+### Task 2: Define shared time, ID, event, and failure contracts
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared/WcsClock.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared/IdGenerator.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared/DomainEvent.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared/DomainRuleViolation.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/shared/SharedContractsTest.java`
+
+**Interfaces:**
+- Produces: `WcsClock.now(): Instant`.
+- Produces: `IdGenerator.nextId(): String`.
+- Produces: `DomainEvent.getEventId/getAggregateId/getSequence/getOccurredAt/getType`.
+
+- [ ] **Step 1: Write failing immutable event tests**
+
+```java
+@Test
+void rejectsBlankEventIdentity() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new DomainEvent("", "ORDER-1", 1L, Instant.EPOCH, "OrderAccepted"));
+}
+
+@Test
+void exposesDeterministicEventFields() {
+ DomainEvent event = new DomainEvent("E-1", "ORDER-1", 2L,
+ Instant.parse("2026-08-04T00:00:00Z"), "OrderPlanned");
+ assertEquals(2L, event.getSequence());
+ assertEquals("OrderPlanned", event.getType());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=SharedContractsTest test`
+
+Expected: compilation fails because the shared contracts do not exist.
+
+- [ ] **Step 3: Implement the minimal contracts**
+
+```java
+public interface WcsClock {
+ Instant now();
+}
+
+public interface IdGenerator {
+ String nextId();
+}
+
+public final class DomainRuleViolation extends RuntimeException {
+ public DomainRuleViolation(String message) {
+ super(message);
+ }
+}
+```
+
+Implement `DomainEvent` as a final immutable class with constructor validation and getters.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=SharedContractsTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/shared ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/shared
+git commit -m "feat(wcs): add shared domain contracts"
+```
+
+### Task 3: Implement the TransportOrder state machine
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TransportOrderState.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TransportOrderType.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TransportOrder.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/task/TransportOrderTest.java`
+
+**Interfaces:**
+- Produces: `TransportOrder.accept(...)` factory.
+- Produces: `plan(String workflowId)`, `reject(String code,String reason)`, `start()`, `suspend(String reason)`, `resumeToPlanned()`, `resumeToExecuting()`, `requestCancel()`, `cancel()`, `complete()`, `fail(String reason)`.
+- Produces: `getUncommittedEvents(): List` and `clearUncommittedEvents()`.
+
+- [ ] **Step 1: Write failing legal and illegal transition tests**
+
+```java
+@Test
+void completesOnlyThroughPlannedAndExecuting() {
+ TransportOrder order = order();
+ order.plan("WF-1");
+ order.start();
+ order.complete();
+ assertEquals(TransportOrderState.COMPLETED, order.getState());
+}
+
+@Test
+void terminalOrderCannotResume() {
+ TransportOrder order = order();
+ order.plan("WF-1");
+ order.cancel();
+ assertThrows(DomainRuleViolation.class, order::resumeToPlanned);
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=TransportOrderTest test`
+
+- [ ] **Step 3: Implement explicit transition guards**
+
+```java
+public void start() {
+ requireState(TransportOrderState.PLANNED);
+ transitionTo(TransportOrderState.EXECUTING, "OrderStarted");
+}
+
+public void requestCancel() {
+ if (state != TransportOrderState.EXECUTING
+ && state != TransportOrderState.SUSPENDED) {
+ throw new DomainRuleViolation("only executing or suspended order can request cancellation");
+ }
+ transitionTo(TransportOrderState.CANCEL_REQUESTED, "OrderCancelRequested");
+}
+```
+
+Implement this exact transition set and increment event sequence exactly once per successful transition:
+
+- `ACCEPTED -> PLANNED/REJECTED/SUSPENDED/CANCELLED/FAILED`.
+- `PLANNED -> EXECUTING/SUSPENDED/CANCELLED/FAILED`.
+- `EXECUTING -> COMPLETED/SUSPENDED/CANCEL_REQUESTED/FAILED`.
+- `SUSPENDED -> PLANNED/EXECUTING/CANCEL_REQUESTED/FAILED`.
+- `CANCEL_REQUESTED -> CANCELLED/FAILED` only after application-level command and resource convergence checks.
+- `REJECTED/COMPLETED/CANCELLED/FAILED` are mutually exclusive terminal states and reject every further transition.
+
+- [ ] **Step 4: Run state tests GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=TransportOrderTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/task
+git commit -m "feat(wcs): add transport order state machine"
+```
+
+### Task 4: Implement WorkflowInstance and TaskStep dependencies
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TaskStepState.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TaskStepType.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/TaskStep.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/WorkflowInstance.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/WorkflowDefinition.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task/WorkflowStepDefinition.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WorkflowDefinitionProvider.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/task/WorkflowInstanceTest.java`
+
+**Interfaces:**
+- Produces: `WorkflowDefinitionProvider.definitionFor(TransportOrderType): WorkflowDefinition`.
+- Produces: `WorkflowInstance.create(String workflowId, String orderId, WorkflowDefinition)`.
+- Produces: `readySteps(): List` and `onStepSucceeded(String stepId)`.
+- `TaskStep` exposes `markReady`, `block(reasonCode)`, `unblock`, `assign(vehicleId,reservationIds)`, `start`, `suspend(reason)`, `resumeAssigned`, `resumeRunning`, `succeed`, `skip`, `cancel`, and `fail(reason)`.
+
+- [ ] **Step 1: Write failing dependency and optional-step tests**
+
+```java
+@Test
+void exposesOnlyStepsWhoseMandatoryDependenciesSucceeded() {
+ WorkflowInstance workflow = workflow("MOVE", "DROP");
+ assertEquals(Collections.singletonList("MOVE"), ids(workflow.readySteps()));
+ workflow.step("MOVE").assign("VEH-1");
+ workflow.step("MOVE").start();
+ workflow.step("MOVE").succeed();
+ workflow.onStepSucceeded("MOVE");
+ assertEquals(Collections.singletonList("DROP"), ids(workflow.readySteps()));
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=WorkflowInstanceTest test`
+
+- [ ] **Step 3: Implement dependency evaluation and transition methods**
+
+```java
+public List readySteps() {
+ List result = new ArrayList();
+ for (TaskStep step : steps.values()) {
+ if (step.isWaiting() && mandatoryDependenciesSucceeded(step)) {
+ step.markReady();
+ result.add(step);
+ }
+ }
+ return Collections.unmodifiableList(result);
+}
+```
+
+Implement mandatory dependency failure as `FAILED`, optional branch exclusion as `SKIPPED`, and these exact transitions: `WAITING -> READY/BLOCKED/SKIPPED/CANCELLED/FAILED`; `BLOCKED -> READY/CANCELLED/FAILED`; `READY -> ASSIGNED/BLOCKED/CANCELLED/FAILED`; `ASSIGNED -> RUNNING/SUSPENDED/CANCELLED/FAILED`; `RUNNING -> SUCCEEDED/SUSPENDED/CANCELLED/FAILED`; `SUSPENDED -> ASSIGNED/RUNNING/CANCELLED/FAILED`. `SKIPPED/CANCELLED/FAILED/SUCCEEDED` are terminal.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=WorkflowInstanceTest,TransportOrderTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/task ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WorkflowDefinitionProvider.java ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/task
+git commit -m "feat(wcs): add workflow and task step model"
+```
+
+### Task 5: Add standard order use cases and idempotent application service
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/in/TransportOrderUseCase.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/task/CreateTransportOrderCommand.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/task/TransportOrderResult.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/task/TransportOrderApplicationService.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/TransportOrderRepository.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DomainEventPublisher.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WmsNotificationGateway.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WmsNotification.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WmsNotificationReceipt.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/task/TransportOrderApplicationServiceTest.java`
+
+**Interfaces:**
+- Produces: `create(CreateTransportOrderCommand)`, `cancel(String externalRequestId)`, `suspend(String orderId,String reason)`, `resume(String orderId)`.
+- Consumes: `TransportOrderRepository.findByExternalRequestId/save`.
+- Consumes: `WorkflowDefinitionProvider` and `DomainEventPublisher.publish(List)`.
+- Produces outbound port `WmsNotificationGateway.notify(WmsNotification): WmsNotificationReceipt`; later plans deliver it through a core-owned Outbox.
+- `WmsNotification` contains `eventId`, `externalRequestId`, `orderId`, `occurredAt`, `eventType`, `resultCode`, `resultMessage`, and typed context fields for `OrderAccepted`, `OrderStarted`, `OrderCompleted`, `OrderFailed`, `OrderCancelled`, and `OrderSuspended`.
+
+- [ ] **Step 1: Write failing duplicate-request test**
+
+```java
+@Test
+void returnsTheExistingOrderForTheSameExternalRequest() {
+ TransportOrderResult first = service.create(command("REQ-1"));
+ TransportOrderResult second = service.create(command("REQ-1"));
+ assertEquals(first.getOrderId(), second.getOrderId());
+ assertEquals(1, repository.count());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=TransportOrderApplicationServiceTest test`
+
+- [ ] **Step 3: Implement transactional orchestration**
+
+```java
+@Transactional
+public TransportOrderResult create(CreateTransportOrderCommand command) {
+ TransportOrder existing = repository.findByExternalRequestId(command.getExternalRequestId());
+ if (existing != null) {
+ return TransportOrderResult.from(existing);
+ }
+ TransportOrder order = TransportOrder.accept(command, idGenerator.nextId(), clock.now());
+ WorkflowDefinition definition = workflowDefinitions.definitionFor(order.getType());
+ WorkflowInstance workflow = WorkflowInstance.create(idGenerator.nextId(), order.getOrderId(), definition);
+ order.plan(workflow.getWorkflowId());
+ repository.save(order, workflow);
+ eventPublisher.publish(order.getUncommittedEvents());
+ order.clearUncommittedEvents();
+ return TransportOrderResult.from(order);
+}
+```
+
+Implement cancel/suspend/resume using domain methods; do not expose repository entities in results.
+
+Map order domain events to immutable `WmsNotification` values without calling the gateway in the order transaction. `WmsNotificationReceipt` exposes `DELIVERED`, `RETRYABLE_FAILURE`, and `PERMANENT_FAILURE`; the notification `eventId` is always the external idempotency key.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest='TransportOrder*Test,WorkflowInstanceTest' test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/task ruoyi-wcs/src/main/java/com/ruoyi/wcs/port ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/task
+git commit -m "feat(wcs): add idempotent transport order use cases"
+```
+
+### Task 6: Add the foundation SQL schema and MyBatis repository
+
+**Files:**
+- Create: `sql/wcs/V001__wcs_task_foundation.sql`
+- Create: `sql/wcs/verify_V001__wcs_task_foundation.sql`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRow.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/WorkflowRow.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TaskStepRow.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderMapper.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task/MyBatisTransportOrderRepository.java`
+- Create: `ruoyi-wcs/src/main/resources/mapper/wcs/task/TransportOrderMapper.xml`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task/TransportOrderRowMapperTest.java`
+
+**Interfaces:**
+- Implements: `TransportOrderRepository` from Task 5.
+- Produces: unique constraint `uk_wcs_transport_order_external_request`.
+- Produces: optimistic `state_version` on order, workflow, and step rows.
+
+- [ ] **Step 1: Write a failing domain-row round-trip test**
+
+```java
+@Test
+void preservesOrderStateAndVersionAcrossPersistenceMapping() {
+ TransportOrder source = executingOrder("ORDER-1", 4L);
+ TransportOrderRow row = TransportOrderRow.fromDomain(source);
+ TransportOrder restored = row.toDomain();
+ assertEquals(source.getState(), restored.getState());
+ assertEquals(4L, restored.getStateVersion());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=TransportOrderRowMapperTest test`
+
+- [ ] **Step 3: Create idempotent SQL Server DDL**
+
+The first table must include this exact invariant:
+
+```sql
+IF OBJECT_ID('dbo.wcs_transport_order', 'U') IS NULL
+BEGIN
+ CREATE TABLE dbo.wcs_transport_order (
+ order_id varchar(64) NOT NULL PRIMARY KEY,
+ external_request_id varchar(128) NOT NULL,
+ order_type varchar(32) NOT NULL,
+ load_id varchar(128) NULL,
+ source_code varchar(128) NOT NULL,
+ destination_code varchar(128) NOT NULL,
+ priority int NOT NULL,
+ order_state varchar(32) NOT NULL,
+ workflow_id varchar(64) NULL,
+ state_version bigint NOT NULL,
+ created_time datetime2(3) NOT NULL,
+ updated_time datetime2(3) NOT NULL,
+ CONSTRAINT uk_wcs_transport_order_external_request UNIQUE (external_request_id)
+ );
+END;
+```
+
+Add `wcs_workflow_instance`, `wcs_task_step`, `wcs_task_dependency`, and `wcs_domain_event` with named constraints and no `default02-default08` columns.
+
+- [ ] **Step 4: Implement mapping and optimistic updates**
+
+`TransportOrderMapper.updateState` must include `WHERE order_id = #{orderId} AND state_version = #{expectedVersion}` and increment `state_version`; repository throws `OptimisticLockingFailureException` when update count is 0.
+
+- [ ] **Step 5: Run mapping and schema text tests GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=TransportOrderRowMapperTest test`
+
+Run the read-only verification script against the isolated WCS test database and expect one row per required table and unique constraint.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add sql/wcs ruoyi-wcs/src/main/java/com/ruoyi/wcs/infrastructure/persistence/task ruoyi-wcs/src/main/resources/mapper/wcs/task ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/task
+git commit -m "feat(wcs): persist transport orders and workflows"
+```
+
+### Task 7: Wire the core module into RuoYi without changing legacy behavior
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/WcsCoreConfiguration.java`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/wcs/WcsCoreWiringTest.java`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/wcs/TestPersistenceConfiguration.java`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/wcs/AdminWcsBoundaryArchitectureTest.java`
+- Verify: `ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java`
+- Verify: `ruoyi-admin/src/main/resources/application.yml`
+
+**Interfaces:**
+- Produces: Spring beans `TransportOrderUseCase`, `TransportOrderRepository`, `WcsClock`, and `IdGenerator`.
+- Does not change: `ScheduleThread`, `BaseSRgvThread`, `Wcs2WmsController`, or PLC writes.
+- Enforces: project/Admin packages may use `com.ruoyi.wcs.port.in..`, `com.ruoyi.wcs.port.out..`, and `com.ruoyi.wcs.query..`, but may not depend on `com.ruoyi.wcs.infrastructure.persistence..` or concrete Repository implementations.
+
+- [ ] **Step 1: Write a failing Spring wiring test**
+
+```java
+@Test
+void exposesTheTransportOrderUseCaseFromTheWcsModule() {
+ try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
+ context.register(WcsCoreConfiguration.class, TestPersistenceConfiguration.class);
+ context.refresh();
+ assertNotNull(context.getBean(TransportOrderUseCase.class));
+ }
+}
+```
+
+Create the test-only dependency configuration explicitly:
+
+```java
+@TestConfiguration
+public class TestPersistenceConfiguration {
+ @Bean
+ public TransportOrderRepository transportOrderRepository() {
+ return Mockito.mock(TransportOrderRepository.class);
+ }
+
+ @Bean
+ public WorkflowDefinitionProvider workflowDefinitionProvider() {
+ return Mockito.mock(WorkflowDefinitionProvider.class);
+ }
+
+ @Bean
+ public DomainEventPublisher domainEventPublisher() {
+ return Mockito.mock(DomainEventPublisher.class);
+ }
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=WcsCoreWiringTest test`
+
+- [ ] **Step 3: Add explicit core configuration**
+
+```java
+@Configuration
+@ComponentScan(basePackages = "com.ruoyi.wcs")
+public class WcsCoreConfiguration {
+}
+```
+
+Do not alter `@MapperScan("com.ruoyi.**.mapper")`; add a test assertion that `TransportOrderMapper` is found under the existing wildcard scan.
+
+Add an ArchUnit rule importing `com.ruoyi.project..` and `com.ruoyi.api..` that fails on dependencies to `com.ruoyi.wcs.infrastructure.persistence..`; allow only the public ports, application use cases, domain value types required by those ports, and query DTOs.
+
+- [ ] **Step 4: Run the full foundation verification**
+
+Run: `mvn -pl ruoyi-wcs,ruoyi-admin -am test`
+
+Run: `git diff --check`
+
+Expected: tests pass, no PLC or scheduler source file is modified, and the module boundary test passes.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/WcsCoreConfiguration.java ruoyi-admin/src/test/java/com/ruoyi/wcs
+git commit -m "feat(wcs): wire core module into RuoYi"
+```
diff --git a/docs/superpowers/plans/2026-08-04-standard-wcs-execution-recovery.md b/docs/superpowers/plans/2026-08-04-standard-wcs-execution-recovery.md
new file mode 100644
index 0000000..bc51511
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-04-standard-wcs-execution-recovery.md
@@ -0,0 +1,498 @@
+# Standard WCS Execution Recovery Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 把已授权的调度结果转换为可靠设备命令,并实现设备协同、事实对账、Outbox、异常隔离和重启恢复。
+
+**Architecture:** 业务事务先保存 `DeviceCommand` 和 Outbox,再由发送器调用项目 Adapter。设备事件进入标准快照后由 `FeedbackReconciler` 推进命令、步骤和资源;结果不确定时创建 `RecoveryCase`,而不是由 Adapter 重试或改状态。
+
+**Tech Stack:** Java 8, Spring transactions, MyBatis, SQL Server, JUnit 5, scheduled Outbox polling, SLF4J MDC, Micrometer.
+
+## Global Constraints
+
+- 依赖 Plan 2 已交付的车辆绑定、路线、资源预约和 `DispatchCycleResult`。
+- 网络 I/O 不在业务数据库事务中执行。
+- `send` 成功只表示通信层受理,不表示设备完成。
+- `UNKNOWN` 期间只允许快照读取和诊断,不允许并行运动命令。
+- 适配器不得访问 WCS Mapper/Repository。
+- 设备事件允许重复和乱序,核心必须幂等处理。
+- 资源释放必须引用 `releaseEvidenceId`。
+- 恢复和人工动作必须保留操作人、原因、前后状态和时间。
+
+---
+
+### Task 1: Implement DeviceCommand and transactional Outbox
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/execution/DeviceCommandState.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/execution/DeviceCommandType.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/execution/DeviceCommand.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/execution/CommandOutboxEntry.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/execution/WmsNotificationOutboxEntry.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DeviceCommandRepository.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/CommandOutboxRepository.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/WmsNotificationOutboxRepository.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/execution/DeviceCommandTest.java`
+
+**Interfaces:**
+- Produces: `DeviceCommand.pending(commandKey,deviceId,type,payload,attempt,createdAt)`.
+- Produces: `markSent`, `acknowledge`, `succeed`, `reject`, `failBeforeSend`, `markUnknown`.
+- Produces: `CommandOutboxEntry.pending(commandId,commandKey,payload,nextAttemptAt)`.
+- Produces: `WmsNotificationOutboxEntry.pending(eventId,notification,nextAttemptAt)`; `eventId` is the callback idempotency key.
+
+- [ ] **Step 1: Write failing UNKNOWN and terminal-state tests**
+
+```java
+@Test
+void sentCommandBecomesUnknownInsteadOfRetryableFailure() {
+ DeviceCommand command = pendingCommand();
+ command.markSent("SEND-1", Instant.EPOCH);
+ command.markUnknown("ACK_TIMEOUT", Instant.EPOCH.plusSeconds(5));
+ assertEquals(DeviceCommandState.UNKNOWN, command.getState());
+ assertFalse(command.canSendMotionCommandAfterIt());
+}
+
+@Test
+void terminalCommandCannotBeAcknowledgedAgain() {
+ DeviceCommand command = succeededCommand();
+ assertThrows(DomainRuleViolation.class, () -> command.acknowledge("ACK-2", Instant.now()));
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=DeviceCommandTest test`
+
+- [ ] **Step 3: Implement explicit command transitions**
+
+```java
+public void markUnknown(String reason, Instant at) {
+ requireAnyState(DeviceCommandState.SENT, DeviceCommandState.ACKNOWLEDGED);
+ this.state = DeviceCommandState.UNKNOWN;
+ this.statusReason = requireText(reason, "reason");
+ this.updatedAt = at;
+ this.stateVersion++;
+}
+```
+
+Implement only these transitions: `PENDING -> SENT/FAILED`; `SENT -> ACKNOWLEDGED/SUCCEEDED/REJECTED/UNKNOWN`; `ACKNOWLEDGED -> SUCCEEDED/UNKNOWN`; `UNKNOWN -> SUCCEEDED/FAILED` after matching physical evidence or controlled recovery. `failBeforeSend` is legal only from `PENDING`; `SUCCEEDED/REJECTED/FAILED` are terminal. No transition from `UNKNOWN` may create a new motion command or release an unproven resource.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=DeviceCommandTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/execution ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DeviceCommandRepository.java ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/CommandOutboxRepository.java ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/execution/DeviceCommandTest.java
+git commit -m "feat(wcs): add device command and outbox lifecycle"
+```
+
+### Task 2: Define device gateway, events, snapshots, and reconciliation
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DeviceCommandGateway.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DeviceSnapshotGateway.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/RecoveryCaseOpener.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/in/DeviceEventUseCase.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/DeviceCommandEnvelope.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/SendReceipt.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/EventAcceptanceResult.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/ReconcileResult.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/ReconcileStatus.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/execution/DeviceEvent.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/execution/DeviceSnapshot.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/FeedbackReconciler.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/device/FeedbackReconcilerTest.java`
+
+**Interfaces:**
+- `DeviceCommandGateway.send(DeviceCommandEnvelope): SendReceipt` returns exactly `ACCEPTED`, `ACCEPTED_UNKNOWN`, `NOT_SENT`, or `REJECTED`.
+- `SendReceipt.Status.NOT_SENT` proves no device-side effect and is the only send outcome eligible for automatic retry.
+- `SendReceipt.Status.ACCEPTED_UNKNOWN` means a device-side effect may have occurred; the command enters `UNKNOWN` and opens recovery without automatic resend.
+- `DeviceSnapshotGateway.readSnapshot(String deviceId): DeviceSnapshot` performs read-only diagnostics.
+- `DeviceEventUseCase.accept(DeviceEvent): EventAcceptanceResult` is idempotent by `eventId`; the enum values are `APPLIED`, `DUPLICATE`, `STALE`, and `RECOVERY_REQUIRED`.
+- `FeedbackReconciler` implements `DeviceEventUseCase.accept(DeviceEvent)` and also exposes `reconcile(DeviceSnapshot): ReconcileResult`; `ReconcileStatus` values are `CONSISTENT`, `ADVANCED`, `IGNORED_STALE`, and `RECOVERY_REQUIRED`.
+- `RecoveryCaseOpener.open(deviceId,commandKey,reason,evidenceIds)` is the only dependency used by reconciliation and Outbox delivery to request isolation; Task 6 supplies its production implementation.
+
+- [ ] **Step 1: Write failing duplicate, stale, and release-evidence tests**
+
+```java
+@Test
+void duplicateEventDoesNotAdvanceTheCommandTwice() {
+ EventAcceptanceResult first = reconciler.accept(completedEvent("E-1", "CMD-1"));
+ EventAcceptanceResult duplicate = reconciler.accept(completedEvent("E-1", "CMD-1"));
+ assertEquals(EventAcceptanceResult.APPLIED, first);
+ assertEquals(EventAcceptanceResult.DUPLICATE, duplicate);
+ assertEquals(1, repository.updateCount("CMD-1"));
+}
+
+@Test
+void taskNumberMismatchCreatesRecoveryInsteadOfReleasing() {
+ ReconcileResult result = reconciler.reconcile(snapshot("VEH-1", "WRONG-TASK", "B"));
+ assertEquals(ReconcileStatus.RECOVERY_REQUIRED, result.getStatus());
+ assertEquals(0, reservationRepository.releaseCount());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=FeedbackReconcilerTest test`
+
+- [ ] **Step 3: Implement standard event envelopes and release predicates**
+
+Each event contains `eventId`, `deviceId`, `commandKey`, `deviceSequence`, `deviceTime`, `receivedTime`, `eventType`, and typed facts. Reconcile movement only when the node is a legal non-regressing suffix of the committed route; release each passed resource with `releaseEvidenceId=eventId`.
+
+Implement `SendReceipt` as an immutable class with nested `Status` enum, `receiptId`, and `detail`. `DeviceCommandEnvelope` carries `commandId`, stable `commandKey`, `deviceId`, typed command payload, attempt, topology version, and fencing token. `ReconcileResult` carries `ReconcileStatus`, affected command key, released resource keys, and optional recovery reason.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=FeedbackReconcilerTest,DeviceCommandTest,ResourceLedgerTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/port ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/execution ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/device
+git commit -m "feat(wcs): reconcile device facts with execution state"
+```
+
+### Task 3: Implement command creation and Outbox delivery service
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/DeviceExecutionService.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/CommandOutboxDispatcher.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/OutboxDispatchResult.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/WmsNotificationOutboxProjector.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/WmsNotificationOutboxDispatcher.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/device/CommandOutboxDispatcherTest.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/device/WmsNotificationOutboxDispatcherTest.java`
+
+**Interfaces:**
+- Consumes: granted assignment and protected route from Plan 2.
+- Produces: `createCommand(GrantedDispatch): DeviceCommand` and one Outbox entry in the same transaction.
+- Produces: `dispatchDue(int limit,Instant now): OutboxDispatchResult`.
+- Produces: one `WmsNotificationOutboxEntry` for each terminal or suspension business event; `WmsNotificationOutboxDispatcher.dispatchDue` calls `WmsNotificationGateway` outside the business transaction.
+
+- [ ] **Step 1: Write failing transaction and retry-boundary tests**
+
+```java
+@Test
+void persistsCommandAndOutboxBeforeCallingTheGateway() {
+ service.createCommand(grantedDispatch());
+ assertTrue(commandRepository.exists("CMD-1"));
+ assertTrue(outboxRepository.existsForCommand("CMD-1"));
+ assertEquals(0, gateway.sendCount());
+}
+
+@Test
+void acceptedSendIsNotMarkedCompleted() {
+ dispatcher.dispatchDue(10, Instant.EPOCH);
+ assertEquals(DeviceCommandState.SENT, commandRepository.get("CMD-1").getState());
+}
+
+@Test
+void acceptedUnknownIsIsolatedAndNeverAutomaticallyRetried() {
+ gateway.returnReceipt(SendReceipt.acceptedUnknown("WRITE_RESULT_AMBIGUOUS"));
+ dispatcher.dispatchDue(10, Instant.EPOCH);
+ assertEquals(DeviceCommandState.UNKNOWN, commandRepository.get("CMD-1").getState());
+ assertEquals(0, outboxRepository.retryableCount("CMD-1"));
+}
+
+@Test
+void duplicateBusinessEventCreatesOneWmsOutboxEntry() {
+ projector.on(orderCompletedEvent("EVENT-1"));
+ projector.on(orderCompletedEvent("EVENT-1"));
+ assertEquals(1, wmsOutboxRepository.countByEventId("EVENT-1"));
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=CommandOutboxDispatcherTest test`
+
+- [ ] **Step 3: Implement delivery outcomes**
+
+`NOT_SENT` increments attempt and schedules exponential backoff; `ACCEPTED` marks SENT and completes the device Outbox row; `ACCEPTED_UNKNOWN` marks `UNKNOWN`, isolates the command, and opens recovery; `REJECTED` marks the command rejected. A WMS notification transport failure remains pending with backoff, while a successful callback records the external receipt against the original `eventId` and never changes the already-completed physical task.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=CommandOutboxDispatcherTest,WmsNotificationOutboxDispatcherTest,FeedbackReconcilerTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/device/CommandOutboxDispatcherTest.java
+git commit -m "feat(wcs): deliver device commands through outbox"
+```
+
+### Task 4: Coordinate lift transfers as joint resources
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/LiftTransferState.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/LiftTransfer.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/LiftDecision.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/LiftDecisionType.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/LiftCoordinator.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/device/LiftCoordinatorTest.java`
+
+**Interfaces:**
+- Produces: `prepare(String stepId,String vehicleId,String liftId,String sourcePort,String targetPort)`.
+- Produces: `onVehicleLeftTarget(LiftTransfer,DeviceSnapshot): LiftDecision` after validating combined vehicle, lift, door, and port facts.
+- `LiftDecisionType` values are `READY`, `BLOCKED`, `ADVANCED`, `COMPLETED`, and `RECOVERY_REQUIRED`; every decision carries one stable reason code.
+- Produces states `WAITING_SOURCE`, `SOURCE_ENTRY_COMMITTED`, `VEHICLE_IN_LIFT`, `LIFT_MOVING`, `AT_TARGET`, `TARGET_EXIT_COMMITTED`, `VEHICLE_LEFT_LIFT`, `COMPLETED`.
+
+- [ ] **Step 1: Write failing joint-reservation tests**
+
+```java
+@Test
+void doesNotEnterLiftWhenTargetExitCannotBeReserved() {
+ ledger.occupyResource("LIFT_PORT:TARGET", "OTHER");
+ LiftDecision decision = coordinator.prepare("STEP-1", "V1", "L1", "P1", "P2");
+ assertEquals(LiftDecisionType.BLOCKED, decision.getType());
+ assertEquals(0, commandRepository.count());
+}
+
+@Test
+void releasesLiftOnlyAfterVehicleLeavesTargetPort() {
+ LiftTransfer transfer = transferAtTarget();
+ coordinator.onVehicleLeftTarget(transfer, matchingFacts());
+ assertEquals(LiftTransferState.COMPLETED, transfer.getState());
+ assertTrue(ledger.isAvailable("LIFT:L1"));
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=LiftCoordinatorTest test`
+
+- [ ] **Step 3: Implement the state machine and UNKNOWN branch**
+
+Reserve source port, lift body, and target port atomically. Any unknown entry/movement/exit fact suspends the step and isolates all already committed lift resources until combined vehicle, floor, door, and port facts reconcile.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=LiftCoordinatorTest,ResourceLedgerTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/LiftCoordinator.java ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/device/LiftCoordinatorTest.java
+git commit -m "feat(wcs): coordinate lift joint resources"
+```
+
+### Task 5: Coordinate conveyor ports and charging
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/PortMode.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/ConveyorPortState.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/ConveyorCoordinator.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/ChargingPolicyDecision.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/PendingWorkload.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/AvailableChargers.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/ChargingCoordinator.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/PortSelectionPolicy.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/ChargingPolicy.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/device/ConveyorAndChargingCoordinatorTest.java`
+
+**Interfaces:**
+- Produces: `requestModeSwitch(portId,targetMode)` and `startTransfer(stepId,portId,direction)`.
+- Produces: `ChargingPolicy.decide(VehicleSnapshot,PendingWorkload,AvailableChargers)`.
+- `PendingWorkload` is an immutable summary of ready urgent/ordinary work and oldest wait time; `AvailableChargers` contains only healthy, reachable, unreserved charger descriptors and topology distance.
+
+- [ ] **Step 1: Write failing mode-lock and charging-priority tests**
+
+```java
+@Test
+void locksPortAgainstNewOutboundAssignmentDuringInboundSwitchPermission() {
+ coordinator.grantModeSwitch("PORT-1", PortMode.INBOUND);
+ assertFalse(coordinator.canAssignOutbound("PORT-1"));
+}
+
+@Test
+void forcedChargeWinsOverOrdinaryReadyTask() {
+ ChargingPolicyDecision decision = charging.decide(vehicleWithBattery(15), ordinaryWork(), chargers());
+ assertEquals(ChargingPolicyDecision.Type.FORCED_CHARGE, decision.getType());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=ConveyorAndChargingCoordinatorTest test`
+
+- [ ] **Step 3: Implement port and charging state**
+
+Port mode changes require zero remaining bound jobs and a switch lock that blocks new opposite-direction assignments. Charging reserves one charger resource and creates a normal workflow step; it never interrupts an active physical task solely because battery crossed a threshold.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=ConveyorAndChargingCoordinatorTest,LiftCoordinatorTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/PortSelectionPolicy.java ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/ChargingPolicy.java ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/device/ConveyorAndChargingCoordinatorTest.java
+git commit -m "feat(wcs): coordinate conveyor ports and charging"
+```
+
+### Task 6: Implement RecoveryCase and startup reconciliation
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/recovery/RecoveryCaseState.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/recovery/RecoveryCase.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/recovery/RecoveryEvidence.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/recovery/ControlStatus.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/recovery/StartupRecoveryResult.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/in/RecoveryUseCase.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/RecoveryCaseRepository.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/recovery/StartupRecoveryService.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/recovery/RecoveryApplicationService.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/recovery/StartupRecoveryServiceTest.java`
+
+**Interfaces:**
+- Produces: `recover(): StartupRecoveryResult`.
+- Produces: `confirmFacts(caseId,operatorId,RecoveryEvidence)` and `abort(caseId,operatorId,reason)`.
+- Consumes: active orders, reservations, commands, Outbox entries, last snapshots, and fresh `DeviceSnapshotGateway` reads.
+- `ControlStatus` values are `PAUSED`, `PARTIALLY_ENABLED`, and `ENABLED`; `StartupRecoveryResult` returns control status, isolated device IDs, isolated resource keys, resumed order IDs, and unresolved recovery case IDs.
+- `RecoveryEvidence` is a typed immutable DTO containing observed site, load state, task number, device mode, evidence time, source, and operator reason; it does not accept arbitrary state overrides.
+- `RecoveryApplicationService` implements both `RecoveryUseCase` and `RecoveryCaseOpener`; automatic callers may open/isolate a case but only `RecoveryUseCase` can confirm facts or abort it with an authenticated operator.
+
+- [ ] **Step 1: Write failing restart and operator-evidence tests**
+
+```java
+@Test
+void startupKeepsDispatchPausedUntilAllActiveDevicesAreReconciled() {
+ StartupRecoveryResult result = recovery.recover();
+ assertEquals(ControlStatus.PAUSED, result.getControlStatus());
+ assertEquals(Collections.singleton("VEH-2"), result.getIsolatedDeviceIds());
+}
+
+@Test
+void operatorCannotResolveCaseWithoutMatchingEvidence() {
+ assertThrows(DomainRuleViolation.class, () ->
+ service.confirmFacts("RC-1", "OP-1", mismatchingEvidence()));
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=StartupRecoveryServiceTest test`
+
+- [ ] **Step 3: Implement ordered startup recovery**
+
+Acquire an instance lease and new fencing token, load all active state, query devices, reconcile, resume matched work, isolate mismatches, restore callback Outbox, and enable dispatch only after no unexplained global resource remains.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest='*Recovery*Test,*FeedbackReconcilerTest,*CommandOutboxDispatcherTest' test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/recovery ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/recovery ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/in/RecoveryUseCase.java ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/RecoveryCaseRepository.java ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/recovery
+git commit -m "feat(wcs): recover execution state after restart"
+```
+
+### Task 7: Persist execution state and expose stable operations queries
+
+**Files:**
+- Create: `sql/wcs/V003__wcs_execution_and_recovery.sql`
+- Create: `sql/wcs/verify_V003__wcs_execution_and_recovery.sql`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/query/WcsOperationsQueryService.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/query/OrderExecutionView.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/query/DeviceOperationsView.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/query/ResourceOperationsView.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/query/RecoveryCaseView.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/query/WcsOperationsQueryServiceTest.java`
+
+**Interfaces:**
+- Persists: `wcs_device_command`, `wcs_command_outbox`, `wcs_wms_notification_outbox`, `wcs_device_snapshot`, `wcs_alarm`, `wcs_recovery_case`, `wcs_manual_action_audit`.
+- Produces read-only page queries by order, device, resource, command, alarm, and recovery status.
+- `wcs_wms_notification_outbox.event_id` has a unique constraint and stores notification type, canonical payload, delivery state, attempt, next attempt time, external receipt, last error, created time, and delivered time.
+
+- [ ] **Step 1: Write failing query-projection tests**
+
+```java
+@Test
+void returnsCommandAndRecoveryCorrelationForAnOrder() {
+ OrderExecutionView view = queries.order("ORDER-1");
+ assertEquals("CMD-1", view.getCurrentCommandKey());
+ assertEquals("RC-1", view.getRecoveryCaseId());
+ assertEquals("UNKNOWN", view.getCommandState());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=WcsOperationsQueryServiceTest test`
+
+- [ ] **Step 3: Add tables, indexes, and read models**
+
+Index `command_key`, `device_id + command_state`, `order_id + step_id`, `resource_key + reservation_state`, and `recovery_state + created_time`. Query DTOs contain no Mapper rows and expose explicit strings/timestamps only.
+
+- [ ] **Step 4: Run full Plan 3 verification**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest='*Command*,*Reconciler*,*Lift*,*Conveyor*,*Charging*,*Recovery*,*OperationsQuery*' test`
+
+Run: `git diff --check`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add sql/wcs ruoyi-wcs/src/main/java/com/ruoyi/wcs/query ruoyi-wcs/src/test/java/com/ruoyi/wcs/query
+git commit -m "feat(wcs): persist and query execution recovery state"
+```
+
+### Task 8: Add correlated logs and product metrics
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/observability/CorrelationContext.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/observability/WcsMetricNames.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/observability/WcsTelemetry.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/observability/MicrometerWcsTelemetry.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/observability/WcsObservabilityTest.java`
+
+**Interfaces:**
+- `CorrelationContext` carries `traceId`, `externalRequestId`, `orderId`, `workflowId`, `stepId`, `deviceId`, `commandKey`, `resourceId`, `eventId`, and `recoveryCaseId` into SLF4J MDC for one application operation and clears it in `close()`.
+- `WcsTelemetry` records gauges/counters/timers named `wcs.task.backlog`, `wcs.dispatch.duration`, `wcs.traffic.wait.duration`, `wcs.command.ack.duration`, `wcs.command.complete.duration`, `wcs.device.utilization`, `wcs.device.battery`, `wcs.lift.wait.duration`, `wcs.outbox.retry.count`, `wcs.device.fault.count`, and `wcs.recovery.duration`.
+- Metric labels are bounded to warehouse, device type, command type, order type, outcome, and recovery level; IDs stay in logs and never become metric labels.
+
+- [ ] **Step 1: Write failing correlation and metric tests**
+
+```java
+@Test
+void writesAllCorrelationFieldsAndClearsMdcAfterTheOperation() {
+ try (CorrelationContext ignored = CorrelationContext.open(fullCorrelation())) {
+ assertEquals("ORDER-1", MDC.get("orderId"));
+ assertEquals("RC-1", MDC.get("recoveryCaseId"));
+ }
+ assertNull(MDC.get("orderId"));
+}
+
+@Test
+void recordsTheRequiredBoundedProductMetrics() {
+ telemetry.recordDispatch(Duration.ofMillis(80), "INBOUND", "GRANTED");
+ telemetry.incrementOutboxRetry("DEVICE_COMMAND", "NOT_SENT");
+ assertEquals(1L, registry.counter("wcs.outbox.retry.count",
+ "outboxType", "DEVICE_COMMAND", "outcome", "NOT_SENT").count(), 0.0D);
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=WcsObservabilityTest test`
+
+- [ ] **Step 3: Instrument application boundaries**
+
+Open one `CorrelationContext` at WMS command acceptance, dispatch, device event acceptance, Outbox delivery, recovery, and operator action boundaries. Record timers only around the owned operation; publish backlog, device utilization/battery, and isolated recovery counts from read-only repository snapshots. Never log PLC credentials, WMS signatures, or complete raw payloads.
+
+- [ ] **Step 4: Run full Plan 3 verification**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest='*Command*,*Reconciler*,*Lift*,*Conveyor*,*Charging*,*Recovery*,*OperationsQuery*,*Observability*' test`
+
+Run: `git diff --check`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/observability ruoyi-wcs/src/test/java/com/ruoyi/wcs/observability
+git commit -m "feat(wcs): add correlated logs and product metrics"
+```
diff --git a/docs/superpowers/plans/2026-08-04-standard-wcs-ft546-adapters.md b/docs/superpowers/plans/2026-08-04-standard-wcs-ft546-adapters.md
new file mode 100644
index 0000000..a91cbb5
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-04-standard-wcs-ft546-adapters.md
@@ -0,0 +1,515 @@
+# Standard WCS FT546 Adapters Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 用 FT546 Admin 适配器把现有 WMS URL、旧 PLC 寄存器、设备清单和 RuoYi 页面接入标准 `ruoyi-wcs`,并让标准运行入口取代旧调度线程的业务所有权。
+
+**Architecture:** `ruoyi-admin` 下新增 `com.ruoyi.project.ft546`,保存 WMS、设备协议、配置、运行 wiring 和页面 Controller。现有 `PlcSrgvRead/PlcTsjRead/PlcSsjRead/PlcCdzRead` 只保留通信与事实采集职责;核心端口实现调用它们,但状态裁决全部回到 `ruoyi-wcs`。
+
+**Tech Stack:** Java 8, RuoYi, Spring MVC, Spring scheduling, HslCommunication Modbus, PLC4J, MyBatis, Vue 2, Element UI.
+
+## Global Constraints
+
+- 保留 `/wcs2Wms/api/receiveOutMydoJob`、`receiveInMydoJob`、`cancelMydoJob` 和 `tidyVehicleSnapshot` URL。
+- 保留 FT546 现有 PLC 地址和寄存器语义,不在核心出现 `4000/4400/4068` 等地址。
+- `Ft546LegacySrgvAdapter` 必须按能力 Profile 限制命令;旧 PLC 不支持命令幂等和 Prepare/Commit。
+- 旧队列、`default02-default08` 和 `mydo_job_wcs` 只作为旧接口证据,不成为新核心状态。
+- 一个设备同一时刻只允许一个运动命令;`UNKNOWN` 时只读快照。
+- 项目配置在启动时校验;有活动命令时禁止切换 Profile。
+- `BaseSRgvThread`、`TsjThread` 和 `CdzThread` 不再创建、调度或完成标准任务。
+- UI 只调用 QueryService 和 RecoveryUseCase,不直接编辑核心表。
+
+---
+
+### Task 1: Add FT546 project configuration and capability profiles
+
+**Files:**
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546WcsProperties.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546DeviceProfile.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546ConfigurationValidator.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546CapabilityGate.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546CapabilityEvidence.java`
+- Create: `ruoyi-admin/src/main/resources/ft546/wcs-device-profiles.yml`
+- Create: `sql/wcs/V004__ft546_project_adapter_config.sql`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/config/Ft546ConfigurationValidatorTest.java`
+
+**Interfaces:**
+- Produces: `Ft546WcsProperties.getRuntimeMode/getDispatchIntervalMs/getCommandPollIntervalMs`.
+- Produces: device profile fields `deviceId`, `deviceType`, `adapterType`, `ip`, `port`, `station`, `capabilities`, `registers`, `profileVersion`.
+- Persists Admin-owned `project_device_endpoint`, `project_plc_address`, `project_protocol_profile`, `project_protocol_capability_evidence`, `project_wms_mapping`.
+- `Ft546CapabilityGate.evaluate(profile,evidence): CapabilityGateResult` defaults `TWO_STAGE_ATOMIC_TASK` to disabled and permits it only when evidence names the exact profile version, corpus manifest hash, passing test run ID, Git commit, and approval time.
+
+- [ ] **Step 1: Write failing profile-validation tests**
+
+```java
+@Test
+void rejectsLegacyProfileThatClaimsPrepareCommit() {
+ Ft546DeviceProfile profile = legacySrgvProfile();
+ profile.getCapabilities().add("PREPARE_COMMIT_SEGMENT");
+ assertThrows(IllegalArgumentException.class, () -> validator.validate(profile));
+}
+
+@Test
+void rejectsDuplicateRegisterOwnershipWithinOneDevice() {
+ assertThrows(IllegalArgumentException.class, () ->
+ validator.validate(profileWithDuplicateAddress("4000")));
+}
+
+@Test
+void rejectsTwoStageCapabilityWithoutPassingEvidenceForTheExactProfile() {
+ Ft546DeviceProfile profile = legacySrgvProfileWith("TWO_STAGE_ATOMIC_TASK");
+ assertFalse(capabilityGate.evaluate(profile, noEvidence()).isPublishable());
+ assertEquals("TWO_STAGE_EVIDENCE_REQUIRED",
+ capabilityGate.evaluate(profile, noEvidence()).getReasonCode());
+}
+
+@Test
+void legacyProfileKeepsUnsupportedCapabilitiesDisabled() {
+ CapabilityGateResult result = capabilityGate.evaluate(legacySrgvProfile(), noEvidence());
+ assertFalse(result.isEnabled("COMMAND_RESULT_QUERY"));
+ assertFalse(result.isEnabled("ORDERED_DEVICE_SNAPSHOT"));
+ assertFalse(result.isEnabled("CANDIDATE_BUFFER"));
+ assertFalse(result.isEnabled("PREPARE_COMMIT_SEGMENT"));
+ assertFalse(result.isEnabled("IDEMPOTENT_COMMAND_KEY"));
+ assertFalse(result.isEnabled("BOOT_SESSION_ID"));
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546ConfigurationValidatorTest test`
+
+- [ ] **Step 3: Implement strongly typed configuration**
+
+The checked-in profile contains no credentials and declares:
+
+```yaml
+devices:
+ - deviceId: "1"
+ deviceType: SHUTTLE
+ adapterType: FT546_LEGACY_SRGV
+ profileVersion: 1
+ capabilities: [POINT_TO_POINT_COMMAND]
+ registers:
+ taskNo: "4000"
+ taskType: "4002"
+ path: "4004"
+ trigger: "4021"
+ status: "4068"
+ execution: "4060"
+```
+
+IP, port, station and WMS URLs come from database/environment configuration, not the checked-in YAML.
+
+Persist capability evidence separately from the profile. Publishing or activating a profile fails closed when evidence is absent, expired, belongs to another profile version/hash, or reports any protocol regression failure. Active-command devices cannot change profile or evidence binding.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546ConfigurationValidatorTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config ruoyi-admin/src/main/resources/ft546 sql/wcs/V004__ft546_project_adapter_config.sql ruoyi-admin/src/test/java/com/ruoyi/project/ft546/config
+git commit -m "feat(ft546): add WCS adapter configuration profiles"
+```
+
+### Task 2: Replace fixed shuttle readers with a device runtime registry
+
+**Files:**
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/Ft546DeviceRuntimeRegistry.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvRuntimeFactory.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/srgv/PlcSrgvRead.java`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546DeviceRuntimeRegistryTest.java`
+
+**Interfaces:**
+- Produces: `register(String deviceId, PlcSrgvRead runtime)`, `requireSrgv(String deviceId)`, `allSrgv()`.
+- Produces: factory `create(DeviceSrgv,Ft546DeviceProfile): PlcSrgvRead`.
+- Removes new-code dependence on fixed `srgv1Read/srgv2Read/srgv3Read` switches.
+
+- [ ] **Step 1: Write a failing configurable-count test**
+
+```java
+@Test
+void registersAnyConfiguredNumberOfShuttleRuntimes() {
+ registry.register("1", runtime("1"));
+ registry.register("2", runtime("2"));
+ registry.register("9", runtime("9"));
+ assertEquals(3, registry.allSrgv().size());
+ assertSame(runtime("9").getClass(), registry.requireSrgv("9").getClass());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546DeviceRuntimeRegistryTest test`
+
+- [ ] **Step 3: Implement registry-backed startup**
+
+`ScheduleThread.startAllThread()` loads enabled devices, creates readers through the factory, registers them, and starts only fact-collection threads. It must not start `SRgv1Thread` or any task-creation thread in `STANDARD` mode.
+
+- [ ] **Step 4: Run GREEN and verify no three-vehicle switch remains in new paths**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546DeviceRuntimeRegistryTest test`
+
+Run: `rg "case 1|case 2|case 3|srgv1Read|srgv2Read|srgv3Read" ruoyi-admin/src/main/java/com/ruoyi/project/ft546 ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java`
+
+Expected: no fixed vehicle selection in the new project package or Standard startup branch.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/srgv/PlcSrgvRead.java ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device
+git commit -m "refactor(ft546): register shuttle runtimes from configuration"
+```
+
+### Task 3: Implement the legacy shuttle Adapter and snapshot mapper
+
+**Files:**
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapter.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvCommandEncoder.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvSnapshotMapper.java`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapterTest.java`
+- Create: `ruoyi-admin/src/test/resources/ft546/plc/srgv-point-to-point.json`
+- Create: `ruoyi-admin/src/test/resources/ft546/plc/srgv-two-stage-gate.json`
+
+**Interfaces:**
+- Implements: `DeviceCommandGateway` and `DeviceSnapshotGateway` for `SHUTTLE` devices.
+- Consumes: `Ft546DeviceRuntimeRegistry` and validated profile.
+- Produces: existing write sequence `4004 -> 4000 -> 4002 -> 4021`, plus configured optional fields.
+
+- [ ] **Step 1: Write failing protocol sequence and UNKNOWN tests**
+
+```java
+@Test
+void encodesPointToPointWritesInTheExistingOrder() {
+ SendReceipt receipt = adapter.send(pointToPoint("CMD-1", "1", route("A", "B")));
+ assertEquals(SendReceipt.Status.ACCEPTED, receipt.getStatus());
+ assertEquals(Arrays.asList("4004", "4000", "4002", "4021"), fakeRuntime.writtenAddresses());
+}
+
+@Test
+void writeAmbiguityReturnsAcceptedUnknownInsteadOfRetryableNotSent() {
+ fakeRuntime.throwAfterWriting("4000");
+ assertEquals(SendReceipt.Status.ACCEPTED_UNKNOWN,
+ adapter.send(pointToPoint("CMD-1", "1", route("A", "B"))).getStatus());
+}
+
+@Test
+void twoStageCommandIsRejectedUntilTheProfileGateEvidencePasses() {
+ assertThrows(IllegalStateException.class, () ->
+ adapterFor(twoStageProfile(), noEvidence()));
+ assertEquals(0, fakeRuntime.writtenAddresses().size());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546LegacySrgvAdapterTest test`
+
+- [ ] **Step 3: Implement profile-limited command encoding**
+
+Reject commands other than enabled `POINT_TO_POINT_COMMAND` and evidence-gated `TWO_STAGE_ATOMIC_TASK`. The two-stage gate fixture records the exact legacy write order, trigger semantics, disconnect boundaries, expected certainty, profile version, and manifest hash; it does not claim Prepare/Commit semantics. Never send a new command when the core reports an active `UNKNOWN` command for the device. Map `DeviceSrgv` connection, mode, status, battery, current site, task number, source, target, floor and load facts into the standard snapshot without inventing device sequence or boot ID.
+
+- [ ] **Step 4: Run protocol regression GREEN**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546LegacySrgvAdapterTest test`
+
+Expected: JSON sample and register sequence match, and no command completes merely because writes returned success.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/shuttle ruoyi-admin/src/test/resources/ft546/plc/srgv-point-to-point.json
+git commit -m "feat(ft546): adapt legacy shuttle PLC protocol"
+```
+
+### Task 4: Implement lift, conveyor, and charger Adapters
+
+**Files:**
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/lift/Ft546LiftAdapter.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/conveyor/Ft546ConveyorAdapter.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/charger/Ft546ChargerAdapter.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/tsj/PlcTsjRead.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/ssj/PlcSsjRead.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead/cdz/PlcCdzRead.java`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546EquipmentAdaptersTest.java`
+
+**Interfaces:**
+- Implements standard `DeviceCommandGateway`/`DeviceSnapshotGateway` dispatch by device type.
+- Maps lift floor/door/port facts, conveyor goods/mode/ready facts, and charger occupancy/charging/fault facts.
+
+- [ ] **Step 1: Write failing fact-mapping and no-business-decision tests**
+
+```java
+@Test
+void liftAdapterReportsFactsWithoutReleasingCoreResources() {
+ DeviceSnapshot snapshot = liftAdapter.readSnapshot("TSJ-1");
+ assertEquals("3", snapshot.fact("floor"));
+ assertEquals("OPEN", snapshot.fact("doorState"));
+ assertFalse(snapshot.getFacts().containsKey("releaseLiftResource"));
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546EquipmentAdaptersTest test`
+
+- [ ] **Step 3: Implement typed protocol translation**
+
+Each adapter validates device type and enabled command, performs only configured writes/reads, and emits standard facts. Remove business entry/exit decisions from new calls to `TsjThread`, `SsjThread`, and `CdzThread`; keep those classes unused in Standard mode.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546EquipmentAdaptersTest,Ft546LegacySrgvAdapterTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device ruoyi-admin/src/main/java/com/ruoyi/schedule/plcRead ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device
+git commit -m "feat(ft546): adapt lift conveyor and charger protocols"
+```
+
+### Task 5: Adapt existing WMS endpoints to standard order use cases
+
+**Files:**
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsOrderMapper.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterService.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms/Ft546WmsNotificationGateway.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/api/controller/Wcs2WmsController.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ApiServiceImpl.java`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/wms/Ft546WmsAdapterServiceTest.java`
+
+**Interfaces:**
+- Maps existing `ReceiveJob` to `CreateTransportOrderCommand`.
+- Keeps existing response envelope expected by WMS.
+- Implements `WmsNotificationGateway.notify(WmsNotification): WmsNotificationReceipt` by calling the existing `RequestLogService` callback transport.
+
+- [ ] **Step 1: Write failing endpoint mapping and idempotency tests**
+
+```java
+@Test
+void mapsExistingInboundRequestToCanonicalOrder() {
+ adapter.receiveInMydoJob(receiveJob("TASK-1", "PORT-1", "SPACE-9"));
+ CreateTransportOrderCommand command = useCase.lastCreateCommand();
+ assertEquals("TASK-1", command.getExternalRequestId());
+ assertEquals(TransportOrderType.INBOUND, command.getOrderType());
+ assertEquals("PORT-1", command.getSource());
+ assertEquals("SPACE-9", command.getDestination());
+}
+
+@Test
+void duplicateWmsRequestReturnsTheSameOrder() {
+ assertEquals(adapter.receiveOutMydoJob(job).getOrderId(),
+ adapter.receiveOutMydoJob(job).getOrderId());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546WmsAdapterServiceTest test`
+
+- [ ] **Step 3: Delegate the four preserved endpoints**
+
+`Wcs2WmsController` routes `receiveOutMydoJob`, `receiveInMydoJob`, `cancelMydoJob`, and `tidyVehicleSnapshot` to `Ft546WmsAdapterService`. The new path must not insert `mydo_job_wcs` or `wcs_srgv_queue`; unrelated legacy endpoints remain untouched.
+
+- [ ] **Step 4: Implement idempotent completion callback**
+
+Map `OrderCompleted/Failed/Cancelled` to existing callback payloads and use WCS `eventId` as the callback idempotency key. A callback transport failure leaves Outbox pending and never recreates an order.
+
+- [ ] **Step 5: Run GREEN**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546WmsAdapterServiceTest test`
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/wms ruoyi-admin/src/main/java/com/ruoyi/api/controller/Wcs2WmsController.java ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ApiServiceImpl.java ruoyi-admin/src/test/java/com/ruoyi/project/ft546/wms
+git commit -m "feat(ft546): adapt existing WMS contract to WCS core"
+```
+
+### Task 6: Wire the Standard runtime and remove legacy scheduling ownership
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/WcsRuntimeCoordinator.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/RuntimeTickResult.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/device/SnapshotCollector.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/recovery/RecoveryGate.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/runtime/Ft546WcsScheduler.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/runtime/Ft546AdapterConfiguration.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java`
+- Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/scheduleTask/srgv/BaseSRgvThread.java`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/runtime/Ft546StandardRuntimeTest.java`
+
+**Interfaces:**
+- Produces: `WcsRuntimeCoordinator.tick(): RuntimeTickResult`.
+- Scheduler calls snapshot ingest, recovery gate, dispatch, command creation, and Outbox delivery in that order.
+- Standard mode has exactly one business scheduler and one Outbox sender.
+- Consumes: `DispatchSnapshotProvider.load()` and `DispatchCoordinator.dispatch(DispatchSnapshot)` from Plan 2.
+
+- [ ] **Step 1: Write a failing single-writer test**
+
+```java
+@Test
+void standardModeStartsCoreRuntimeButNotLegacyBusinessThreads() {
+ runtime.start();
+ assertTrue(runtime.isCoreSchedulerStarted());
+ assertFalse(runtime.isBaseSrgvThreadStarted());
+ assertFalse(runtime.isTsjBusinessThreadStarted());
+ assertFalse(runtime.isCdzBusinessThreadStarted());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546StandardRuntimeTest test`
+
+- [ ] **Step 3: Implement the runtime tick**
+
+```java
+public RuntimeTickResult tick() {
+ snapshotCollector.collectAndAccept();
+ if (!recoveryGate.allowsDispatch()) {
+ return RuntimeTickResult.recoveryBlocked();
+ }
+ DispatchCycleResult dispatch = dispatchCoordinator.dispatch(dispatchSnapshotProvider.load());
+ for (GrantedDispatch granted : dispatch.getGranted()) {
+ executionService.createCommand(granted);
+ }
+ OutboxDispatchResult outbox = outboxDispatcher.dispatchDue(100, clock.now());
+ return RuntimeTickResult.of(dispatch, outbox);
+}
+```
+
+`ScheduleThread` in Standard mode starts PLC fact readers only. Remove standard-mode calls that instantiate or start `SRgv1Thread`, `TsjThread`, `SsjThread`, or `CdzThread`. Keep legacy code compilable but unreachable from Standard startup.
+
+- [ ] **Step 4: Run single-writer and reactor tests GREEN**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546StandardRuntimeTest,Ft546WmsAdapterServiceTest,Ft546LegacySrgvAdapterTest test`
+
+Run: `rg "new SRgv1Thread|new TsjThread|new CdzThread" ruoyi-admin/src/main/java/com/ruoyi/project/ft546 ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java`
+
+Expected: Standard branch contains no legacy business thread construction.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/WcsRuntimeCoordinator.java ruoyi-admin/src/main/java/com/ruoyi/project/ft546/runtime ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java ruoyi-admin/src/main/java/com/ruoyi/schedule/scheduleTask/srgv/BaseSRgvThread.java ruoyi-admin/src/test/java/com/ruoyi/project/ft546/runtime
+git commit -m "feat(ft546): run standard WCS as the single scheduler"
+```
+
+### Task 7: Add operations controllers and Vue monitoring pages
+
+**Files:**
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web/WcsOrderController.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web/WcsDeviceController.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web/WcsTrafficController.java`
+- Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web/WcsRecoveryController.java`
+- Create: `ruoyi-ui/src/api/wcs/order.js`
+- Create: `ruoyi-ui/src/api/wcs/device.js`
+- Create: `ruoyi-ui/src/api/wcs/traffic.js`
+- Create: `ruoyi-ui/src/api/wcs/recovery.js`
+- Create: `ruoyi-ui/src/views/wcs/order/index.vue`
+- Create: `ruoyi-ui/src/views/wcs/device/index.vue`
+- Create: `ruoyi-ui/src/views/wcs/traffic/index.vue`
+- Create: `ruoyi-ui/src/views/wcs/recovery/index.vue`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/web/WcsOperationsControllerTest.java`
+
+**Interfaces:**
+- GET `/wcs/orders`, `/wcs/devices`, `/wcs/traffic/resources`, `/wcs/recovery/cases`.
+- POST `/wcs/orders/{id}/suspend`, `/resume`, `/cancel`.
+- POST `/wcs/recovery/cases/{id}/confirm` and `/abort` with operator reason/evidence.
+
+- [ ] **Step 1: Write failing controller authorization and DTO tests**
+
+```java
+@Test
+@WithMockUser(authorities = "wcs:recovery:confirm")
+void confirmsRecoveryThroughTheUseCase() throws Exception {
+ mockMvc.perform(post("/wcs/recovery/cases/RC-1/confirm")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"reason\":\"现场确认车辆位于070501\",\"currentSite\":\"070501\",\"loaded\":false}"))
+ .andExpect(status().isOk());
+ verify(recoveryUseCase).confirmFacts(eq("RC-1"), anyString(), any(RecoveryEvidence.class));
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=WcsOperationsControllerTest test`
+
+- [ ] **Step 3: Implement controllers and API clients**
+
+Controllers return stable QueryService DTOs and use RuoYi permissions. Vue pages provide filters, tables, state tags, detail drawers, pagination and explicit recovery dialogs; they do not include generic CRUD edit/delete for core records.
+
+- [ ] **Step 4: Implement the traffic view without editing core state**
+
+Show resource key, type, capacity, protected count, holder, reservation state and evidence ID. Only Recovery page exposes controlled confirm/abort commands.
+
+- [ ] **Step 5: Run backend and frontend verification**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=WcsOperationsControllerTest test`
+
+Run: `cd ruoyi-ui && npm run build:prod`
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add ruoyi-admin/src/main/java/com/ruoyi/project/ft546/web ruoyi-admin/src/test/java/com/ruoyi/project/ft546/web ruoyi-ui/src/api/wcs ruoyi-ui/src/views/wcs
+git commit -m "feat(ft546): add WCS operations console"
+```
+
+### Task 8: Add menus, permissions, and the FT546 end-to-end contract test
+
+**Files:**
+- Create: `sql/wcs/V005__ft546_wcs_menus_and_permissions.sql`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/Ft546StandardWcsEndToEndTest.java`
+- Create: `ruoyi-admin/src/test/resources/ft546/scenarios/inbound-same-floor.json`
+- Create: `ruoyi-admin/src/test/resources/ft546/scenarios/outbound-cross-floor.json`
+
+**Interfaces:**
+- Permissions: `wcs:order:list`, `wcs:order:control`, `wcs:device:list`, `wcs:traffic:list`, `wcs:recovery:list`, `wcs:recovery:confirm`.
+- End-to-end test uses fake PLC runtimes and fake WMS callback transport through real Spring wiring.
+
+- [ ] **Step 1: Write the failing same-floor scenario**
+
+```java
+@Test
+void completesInboundThroughExistingWmsUrlAndLegacyShuttleAdapter() throws Exception {
+ String orderId = postInboundScenario("inbound-same-floor.json");
+ runTicksUntilTerminal(orderId, 200);
+ assertEquals("COMPLETED", operations.order(orderId).getOrderState());
+ assertEquals(1, callbackTransport.businessEffectCount(orderId));
+ assertEquals(0, invariantMonitor.totalViolations());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546StandardWcsEndToEndTest test`
+
+- [ ] **Step 3: Add permission SQL and scenario fixtures**
+
+Insert one WCS menu group and four child pages using existing RuoYi `sys_menu` conventions. Use idempotent `IF NOT EXISTS` checks keyed by permission string.
+
+- [ ] **Step 4: Make same-floor and cross-floor scenarios GREEN**
+
+The cross-floor test must prove source port, lift and target port joint reservation; WMS receives one completion event; no legacy queue row is created.
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546StandardWcsEndToEndTest test`
+
+- [ ] **Step 5: Run full Plan 4 verification**
+
+Run: `mvn -pl ruoyi-admin -am test`
+
+Run: `cd ruoyi-ui && npm run build:prod`
+
+Run: `git diff --check`
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add sql/wcs/V005__ft546_wcs_menus_and_permissions.sql ruoyi-admin/src/test/java/com/ruoyi/project/ft546/Ft546StandardWcsEndToEndTest.java ruoyi-admin/src/test/resources/ft546/scenarios
+git commit -m "test(ft546): verify standard WCS end to end"
+```
diff --git a/docs/superpowers/plans/2026-08-04-standard-wcs-simulation-acceptance.md b/docs/superpowers/plans/2026-08-04-standard-wcs-simulation-acceptance.md
new file mode 100644
index 0000000..1be2859
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-04-standard-wcs-simulation-acceptance.md
@@ -0,0 +1,465 @@
+# Standard WCS Simulation Acceptance Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 用真实标准内核和 Adapter 契约完成确定性模拟、第二仓配置、FT546 历史回放、故障恢复、性能和长期稳定性验收。
+
+**Architecture:** 快速领域测试保留在 `ruoyi-wcs`,真实历史语料继续由独立 `javaTest` 工程加载,但它改为调用标准 `ruoyi-wcs` 构建产物,不再用自身的演示调度器证明产品完成。所有长测试输出统一报告、JSON 摘要和 SHA-256 manifest。
+
+**Tech Stack:** Java 8, Maven, JUnit 5, existing `javaTest` historical fixtures, SQL Server read-only replay input, deterministic simulated adapters.
+
+## Global Constraints
+
+- 依赖 Plan 4 已完成的标准运行 wiring 和 FT546 Adapter。
+- 模拟器必须实现与真实设备相同的 `DeviceCommandGateway` 和 `DeviceSnapshotGateway`。
+- 历史车辆、路径、完成时间和状态只用于比较,不作为新调度输入。
+- FT546 历史回放允许车辆和路径不同,但业务终点、PLC 语义和安全不变量必须正确。
+- 第二套仓库配置不得修改 `ruoyi-wcs` 代码或构建产物。
+- 72 小时/100000 任务测试使用固定随机种子并可从失败种子复现。
+- 长测试不进入普通 `mvn test`;使用显式 Maven profile 和 JUnit tag。
+- 验收报告不得包含数据库密码、PLC IP 密码或 WMS 签名密钥。
+
+---
+
+### Task 1: Build deterministic simulated adapters and fault injection
+
+**Files:**
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/SimulationClock.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/SimulatedDeviceGateway.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/SimulatedDeviceRuntime.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/DeterministicFaultPlan.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/FaultType.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/SimulationResult.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation/StandardWcsSimulationTest.java`
+
+**Interfaces:**
+- `SimulationClock.advance(Duration)` controls all event time.
+- `DeterministicFaultPlan.atTick(long,FaultType,String deviceId)` returns one immutable plan and injects `DISCONNECT`, `COMMAND_ACK_LOST`, `DUPLICATE_EVENT`, `OUT_OF_ORDER_EVENT`, `COMMAND_REJECTED`, `POSITION_MISMATCH`, `LOAD_MISMATCH`, `MANUAL_MODE`, or `WCS_RESTART`.
+- `SimulationResult` exposes event digest, metrics, command counts, callback counts, final command/order states, recovery cases, and invariant violations.
+- Simulated gateway records standard command envelopes and emits standard events only.
+
+- [ ] **Step 1: Write failing deterministic replay tests**
+
+```java
+@Test
+void sameSeedProducesTheSameEventStream() {
+ SimulationResult first = run(20260804L, 500);
+ SimulationResult second = run(20260804L, 500);
+ assertEquals(first.getEventDigest(), second.getEventDigest());
+ assertEquals(first.getMetrics(), second.getMetrics());
+}
+
+@Test
+void commitTimeoutCreatesUnknownWithoutDuplicateMotion() {
+ SimulationResult result = run(DeterministicFaultPlan.atTick(
+ 12, FaultType.COMMAND_ACK_LOST, "V1"));
+ assertEquals(1, result.motionCommandCount("CMD-1"));
+ assertEquals("UNKNOWN", result.commandState("CMD-1"));
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=StandardWcsSimulationTest test`
+
+- [ ] **Step 3: Implement the event-driven simulator**
+
+Process each tick in fixed order: due fault → device physics → event delivery → snapshot ingest → recovery gate → dispatch → command creation → Outbox send → invariant scan. Sort devices, events, tasks and resources by stable ID before each phase.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=StandardWcsSimulationTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/test/java/com/ruoyi/wcs/simulation
+git commit -m "test(wcs): add deterministic device simulation"
+```
+
+### Task 2: Prove core portability with a second warehouse configuration
+
+**Files:**
+- Create: `ruoyi-wcs/src/test/resources/acceptance/warehouse-b/topology.json`
+- Create: `ruoyi-wcs/src/test/resources/acceptance/warehouse-b/devices.json`
+- Create: `ruoyi-wcs/src/test/resources/acceptance/warehouse-b/workflows.json`
+- Create: `ruoyi-wcs/src/test/resources/acceptance/warehouse-b/policies.json`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/SecondWarehouseConfigurationTest.java`
+
+**Interfaces:**
+- Warehouse B has 4 floors, 6 shuttles, 2 lifts, 8 conveyor ports, and 3 chargers.
+- Uses the same `TopologyDefinitionProvider`, `WorkflowDefinitionProvider`, `DispatchPolicy`, `PortSelectionPolicy`, and `ChargingPolicy` ports.
+
+- [ ] **Step 1: Write a failing no-core-change acceptance test**
+
+```java
+@Test
+void runsDifferentWarehouseThroughTheSameCoreArtifact() {
+ AcceptanceResult result = runner.run("acceptance/warehouse-b", 5000);
+ assertEquals(200, result.getCompletedOrders());
+ assertEquals(0, result.getFailedOrders());
+ assertEquals(0, result.getInvariantViolations());
+ assertEquals("ruoyi-wcs", result.getCoreArtifactId());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=SecondWarehouseConfigurationTest test`
+
+- [ ] **Step 3: Add the complete synthetic configuration**
+
+Topology includes two independent traffic zones, both lifts, a shared source-floor intersection, capacity-1 chargers, safe wait points, inbound/outbound/relocation/inventory workflows, and policy weights different from FT546.
+
+- [ ] **Step 4: Run GREEN and record the core JAR hash**
+
+Run: `mvn -pl ruoyi-wcs -am package`
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=SecondWarehouseConfigurationTest test`
+
+Record SHA-256 of `ruoyi-wcs/target/ruoyi-wcs-*.jar` in the test report; the same hash is later recorded for FT546.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/test/resources/acceptance/warehouse-b ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/SecondWarehouseConfigurationTest.java
+git commit -m "test(wcs): prove second warehouse portability"
+```
+
+### Task 3: Make javaTest replay the real standard WCS core
+
+**Files:**
+- Modify: `javaTest/pom.xml`
+- Create: `javaTest/src/main/java/com/ft546/wcs/standard/StandardWcsHistoricalReplayAdapter.java`
+- Create: `javaTest/src/main/java/com/ft546/wcs/standard/StandardWcsReplayResult.java`
+- Create: `javaTest/src/test/java/com/ft546/wcs/standard/StandardWcsHistoricalReplayTest.java`
+
+**Interfaces:**
+- Adds dependency `com.ruoyi:ruoyi-wcs:3.8.9` to `javaTest`.
+- Consumes existing `HistoricalQueueLoader` and frozen manifest.
+- Maps each record to canonical `CreateTransportOrderCommand`; historical `agv_id/path_info/finish_date` remain comparison metadata only.
+
+- [ ] **Step 1: Install the core and write a failing replay test**
+
+Run: `mvn -pl ruoyi-wcs -am install -DskipTests`
+
+Add:
+
+```java
+@Test
+void completesFrozenHistoryThroughTheStandardCore() {
+ StandardWcsReplayResult result = adapter.replay(loader.load(classLoader, "shadow/history"));
+ assertEquals(result.getInputOrders(), result.getCompletedOrders());
+ assertEquals(0, result.getSafetyViolations());
+ assertEquals(0, result.getDuplicateBusinessEffects());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `cd javaTest && mvn -Dtest=StandardWcsHistoricalReplayTest test`
+
+Expected: compilation fails because the standard replay adapter does not exist.
+
+- [ ] **Step 3: Implement canonical replay mapping**
+
+Use record task number as `externalRequestId`, map task type to standard order type, resolve physical source/destination from frozen map data, submit through `TransportOrderUseCase`, and run the real `WcsRuntimeCoordinator` against deterministic simulated adapters until terminal or watchdog failure.
+
+- [ ] **Step 4: Produce semantic comparison fields**
+
+For each task record input task number, historical vehicle/path/duration, selected standard vehicle/path/duration, final order state, callback effect count, and violation list. Do not assert identical vehicle or path.
+
+- [ ] **Step 5: Run GREEN with existing history tests**
+
+Run: `cd javaTest && mvn -Dtest=StandardWcsHistoricalReplayTest,HistoricalQueueLoaderTest,HistoricalQueueDatabaseReplayTest test`
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add javaTest/pom.xml javaTest/src/main/java/com/ft546/wcs/standard javaTest/src/test/java/com/ft546/wcs/standard
+git commit -m "test(javaTest): replay history through standard WCS core"
+```
+
+### Task 4: Add PLC sample corpus regression
+
+**Files:**
+- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/manifest.json`
+- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/srgv-command-cases.json`
+- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/srgv-two-stage-cases.json`
+- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/srgv-snapshot-cases.json`
+- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/lift-cases.json`
+- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/conveyor-cases.json`
+- Create: `ruoyi-admin/src/test/resources/ft546/plc/corpus/charger-cases.json`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546PlcCorpusRegressionTest.java`
+
+**Interfaces:**
+- Each corpus case contains sanitized input facts, expected standard command/event, expected addresses, and expected certainty `NOT_SENT/ACCEPTED/ACCEPTED_UNKNOWN/REJECTED`.
+- Manifest stores row counts and SHA-256 for every sample file.
+- The manifest emits `Ft546CapabilityEvidence` only when every required point-to-point case passes and, when `TWO_STAGE_ATOMIC_TASK` is requested, every two-stage case passes for the exact profile version.
+
+- [ ] **Step 1: Write a failing manifest and mapping test**
+
+```java
+@ParameterizedTest
+@MethodSource("commandCases")
+void mapsFrozenCommandCases(PlcCommandCase sample) {
+ EncodedWrites writes = encoder.encode(sample.getCommand(), sample.getProfile());
+ assertEquals(sample.getExpectedWrites(), writes.asAddressValuePairs());
+}
+
+@Test
+void refusesTwoStageCapabilityWhenItsCorpusIsMissingOrFails() {
+ CapabilityGateResult result = gate.evaluate(twoStageProfile(), corpusWithoutPassingTwoStage());
+ assertFalse(result.isPublishable());
+ assertEquals("TWO_STAGE_PROTOCOL_GATE_FAILED", result.getReasonCode());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546PlcCorpusRegressionTest test`
+
+- [ ] **Step 3: Freeze sanitized production samples**
+
+Export only bounded register values and logical facts; remove IPs, credentials and unrelated data. Calculate canonical UTF-8 hashes and reject corpus load when any hash or row count differs.
+
+- [ ] **Step 4: Run all Adapter contract tests GREEN**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest='Ft546*AdapterTest,Ft546PlcCorpusRegressionTest' test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-admin/src/test/resources/ft546/plc/corpus ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/Ft546PlcCorpusRegressionTest.java
+git commit -m "test(ft546): freeze PLC protocol regression corpus"
+```
+
+### Task 5: Add the real PLC and hardware-in-the-loop contract gate
+
+**Files:**
+- Modify: `ruoyi-admin/pom.xml`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/hil/Ft546PlcHilContractTest.java`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/hil/HilSafetyGuard.java`
+- Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/hil/HilContractReportWriter.java`
+- Create: `docs/wcs/acceptance/ft546-plc-hil-runbook.md`
+- Create at runtime: `outputs/wcs-acceptance/ft546-plc-hil-report.json`
+
+**Interfaces:**
+- Maven profile `ft546-plc-hil` excludes the suite unless `ft546.hil.enabled=true` and requires an isolated test PLC endpoint, expected profile version, device ID, safe start site, safe target site, and operator approval ID.
+- `HilSafetyGuard.verify()` performs read-only connection, AUTO/healthy/idle state, no load, no active command, expected start site, E-stop-chain-ready fact, and exclusive test lease checks before any write.
+- The suite always verifies snapshot mapping and point-to-point write/ack/final-fact semantics; it verifies the legacy two-stage path only when the published profile requests `TWO_STAGE_ATOMIC_TASK`.
+
+- [ ] **Step 1: Write failing guard and report tests with a fake PLC runtime**
+
+```java
+@Test
+void refusesMotionWhenAnySafetyPreconditionIsMissing() {
+ fakeRuntime.setEmergencyChainReady(false);
+ assertThrows(HilSafetyException.class, () -> guard.verify());
+ assertEquals(0, fakeRuntime.writeCount());
+}
+
+@Test
+void sanitizedReportContainsNoEndpointOrCredential() throws Exception {
+ Path report = writer.write(passingHilResult());
+ String json = new String(Files.readAllBytes(report), StandardCharsets.UTF_8);
+ assertFalse(json.contains("192.168."));
+ assertFalse(json.toLowerCase(Locale.ROOT).contains("password"));
+}
+```
+
+- [ ] **Step 2: Run RED on the fake runtime**
+
+Run: `mvn -pl ruoyi-admin -am -Dtest=Ft546PlcHilContractTest test`
+
+- [ ] **Step 3: Implement the guarded HIL profile**
+
+The test acquires an exclusive lease, records the initial snapshot, sends exactly one enabled command through `Ft546LegacySrgvAdapter`, waits for the standard completion predicate, returns the rig to its approved safe site, records the final snapshot, and releases the lease. Any timeout becomes `ACCEPTED_UNKNOWN`, stops further motion writes, records the isolated state, and requires the operator runbook before the suite can be rerun.
+
+- [ ] **Step 4: Run against the isolated FT546 test PLC**
+
+```bash
+mvn -pl ruoyi-admin -Pft546-plc-hil \
+ -Dft546.hil.enabled=true \
+ -Dft546.hil.endpointRef=FT546_HIL_ENDPOINT \
+ -Dft546.hil.profileVersion=1 \
+ -Dft546.hil.deviceId=1 \
+ -Dft546.hil.startSite=HIL_SAFE_A \
+ -Dft546.hil.targetSite=HIL_SAFE_B \
+ -Dft546.hil.operatorApprovalId=${HIL_APPROVAL_ID} \
+ -Dtest=Ft546PlcHilContractTest test
+```
+
+Expected: all enabled command contracts pass, the device returns to the approved safe site, and `ft546-plc-hil-report.json` contains the profile hash, test cases, sanitized facts, result, Git commit, operator approval ID, and run time.
+
+- [ ] **Step 5: Commit the suite and runbook**
+
+```bash
+git add ruoyi-admin/pom.xml ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/hil docs/wcs/acceptance/ft546-plc-hil-runbook.md
+git commit -m "test(ft546): add guarded PLC hardware contract gate"
+```
+
+### Task 6: Verify restart, fault recovery, and manual takeover matrix
+
+**Files:**
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/RecoveryFaultMatrixTest.java`
+- Create: `ruoyi-wcs/src/test/resources/acceptance/fault-matrix.json`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/RecoveryMatrixReportWriter.java`
+
+**Interfaces:**
+- Matrix includes send-before failure, ACK loss, device offline, PLC restart, position mismatch, load mismatch, lift fault, conveyor mode conflict, WMS callback failure, WCS restart, duplicate restart, operator-confirmed resume, and operator-aborted task.
+- Produces one `RecoveryScenarioResult` per scenario with terminal state, isolation scope, manual requirement, released resources, duplicate command count, and duplicate callback count.
+- Produces `manual-recovery-audit.json` with operator ID, reason, submitted evidence, pre-state, post-state, affected resources, permission result, and timestamp for the two manual scenarios.
+
+- [ ] **Step 1: Write failing matrix-completeness test**
+
+```java
+@Test
+void executesEveryRequiredRecoveryScenario() {
+ RecoveryMatrixResult result = runner.run("acceptance/fault-matrix.json");
+ assertEquals(13, result.getScenarioCount());
+ assertEquals(0, result.getBlindRetries());
+ assertEquals(0, result.getUnsafeReleases());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=RecoveryFaultMatrixTest test`
+
+- [ ] **Step 3: Implement restart checkpoints**
+
+For each scenario stop the runtime after the configured persisted checkpoint, discard all in-memory services, construct a new runtime with the same database snapshot and a new instance token, then run `StartupRecoveryService.recover()` before any dispatch.
+
+- [ ] **Step 4: Run GREEN and write report**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=RecoveryFaultMatrixTest test`
+
+Expected reports: `outputs/wcs-acceptance/recovery-matrix.json` with 13 PASS rows and zero blind retry/unsafe release, plus `outputs/wcs-acceptance/manual-recovery-audit.json` with two fully audited manual actions.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance ruoyi-wcs/src/test/resources/acceptance/fault-matrix.json
+git commit -m "test(wcs): verify recovery fault matrix"
+```
+
+### Task 7: Add performance and long-run profiles
+
+**Files:**
+- Modify: `ruoyi-wcs/pom.xml`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/PerformanceBaselineTest.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/LongRunStabilityTest.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/AcceptanceMetrics.java`
+
+**Interfaces:**
+- Maven profile `wcs-performance` includes JUnit tag `performance`.
+- Maven profile `wcs-long-run` includes JUnit tag `long-run` and properties `wcs.longRun.hours`, `wcs.longRun.minimumTasks`, `wcs.seed`.
+
+- [ ] **Step 1: Write failing baseline assertions**
+
+```java
+@Test
+@Tag("performance")
+void meetsTheApprovedStandardScale() {
+ AcceptanceMetrics metrics = benchmark.run(scale(50, 10, 300, 20, 2000));
+ assertTrue(metrics.getDispatchP95Millis() <= 500);
+ assertTrue(metrics.getEventCommitP95Millis() <= 200);
+ assertTrue(metrics.getAutomaticRecoveryMillis() <= 60000);
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -Pwcs-performance -Dtest=PerformanceBaselineTest test`
+
+- [ ] **Step 3: Implement measurement without weakening assertions**
+
+Warm up 1000 cycles, measure at least 10000 cycles, use `System.nanoTime()` only for elapsed time, publish P50/P95/P99 and throughput, and fail when task backlog grows for 100 consecutive cycles.
+
+- [ ] **Step 4: Add explicit long-run entry**
+
+```java
+@Test
+@Tag("long-run")
+void completesTheConfiguredLongRunWithoutInvariantViolations() {
+ AcceptanceMetrics metrics = longRun.run(hours(), minimumTasks(), seed());
+ assertTrue(metrics.getCompletedTasks() >= minimumTasks());
+ assertEquals(0, metrics.getSafetyViolations());
+ assertEquals(0, metrics.getDuplicateBusinessEffects());
+}
+```
+
+- [ ] **Step 5: Run short CI and full acceptance modes**
+
+CI smoke: `mvn -pl ruoyi-wcs -Pwcs-long-run -Dwcs.longRun.hours=0.05 -Dwcs.longRun.minimumTasks=1000 -Dwcs.seed=20260804 test`
+
+Full acceptance: `mvn -pl ruoyi-wcs -Pwcs-long-run -Dwcs.longRun.hours=72 -Dwcs.longRun.minimumTasks=100000 -Dwcs.seed=20260804 test`
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add ruoyi-wcs/pom.xml ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance
+git commit -m "test(wcs): add performance and long run acceptance"
+```
+
+### Task 8: Generate the final acceptance evidence bundle
+
+**Files:**
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/AcceptanceReportWriter.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance/AcceptanceReportWriterTest.java`
+- Create: `docs/wcs/acceptance/README.md`
+- Create: `docs/wcs/acceptance/standard-wcs-acceptance-index.md`
+- Create at runtime: `outputs/wcs-standard-acceptance-{runId}/summary.json`
+- Create at runtime: `outputs/wcs-standard-acceptance-{runId}/report.md`
+- Create at runtime: `outputs/wcs-standard-acceptance-{runId}/manifest.sha256`
+
+**Interfaces:**
+- `summary.json` contains Git commit, core JAR hash, FT546 config hash, Warehouse B config hash, test counts, replay counts, performance metrics, recovery matrix, safety counts, start/end time, and verdict.
+- `report.md` contains dedicated sections for structured-log field coverage, required metric coverage, manual takeover audit, PLC corpus capability evidence, and the latest guarded HIL report.
+- Manifest covers every file except itself.
+
+- [ ] **Step 1: Write a failing evidence-completeness test**
+
+```java
+@Test
+void writesACompleteTamperEvidentEvidenceBundle() throws Exception {
+ Path output = writer.write(passingAcceptanceResult());
+ assertTrue(Files.exists(output.resolve("summary.json")));
+ assertTrue(Files.exists(output.resolve("report.md")));
+ assertTrue(Files.exists(output.resolve("manifest.sha256")));
+ assertTrue(manifestVerifier.verify(output));
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=AcceptanceReportWriterTest test`
+
+- [ ] **Step 3: Implement canonical reports and verdict rules**
+
+Verdict is PASS only when both warehouse configurations use the same core JAR hash, all required tests ran, historical replay has no safety violations, fault/manual matrix passes, required logs and metrics are observed, PLC capability evidence matches the published profile, the guarded HIL report passes every enabled FT546 command, performance thresholds pass, long-run minimum task count is reached, and every absolute safety count is zero.
+
+- [ ] **Step 4: Run the complete standard verification**
+
+```bash
+mvn -pl ruoyi-wcs,ruoyi-admin -am test
+mvn -pl ruoyi-wcs -Pwcs-performance test
+cd javaTest && mvn clean test
+cd ../ruoyi-ui && npm run build:prod
+```
+
+Then run the approved full long-run command and generate the evidence bundle.
+
+- [ ] **Step 5: Verify repository and evidence integrity**
+
+Run: `git diff --check`
+
+Run the manifest verifier against the generated directory. Expected: all hashes match and the acceptance index links the exact run ID and Git commit.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add ruoyi-wcs/src/test/java/com/ruoyi/wcs/acceptance docs/wcs/acceptance
+git commit -m "docs(wcs): record standard product acceptance"
+```
diff --git a/docs/superpowers/plans/2026-08-04-standard-wcs-topology-dispatch-traffic.md b/docs/superpowers/plans/2026-08-04-standard-wcs-topology-dispatch-traffic.md
new file mode 100644
index 0000000..780b7ac
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-04-standard-wcs-topology-dispatch-traffic.md
@@ -0,0 +1,444 @@
+# Standard WCS Topology Dispatch Traffic Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 在标准订单与步骤模型之上实现版本化仓库拓扑、设备能力、选车、路径规划、资源预约和无环交通裁决。
+
+**Architecture:** 运行时只读取编译后的不可变 `EffectiveTopology`;车辆选择和路径规划产生候选,`ResourceLedger` 与 `DeadlockGuard` 在同一调度快照中决定 GRANT/HOLD/YIELD。项目只提供定义和评分权重,不直接持有资源或修改步骤状态。
+
+**Tech Stack:** Java 8, JUnit 5, MyBatis, SQL Server, deterministic graph search.
+
+## Global Constraints
+
+- 依赖 `2026-08-04-standard-wcs-core-foundation.md` 已交付的 `TransportOrder`、`TaskStep`、共享事件和 Repository 边界。
+- 交通区域是版本与协调边界,不默认是容量 1 的资源。
+- 节点和无向边默认容量为 1;容量大于 1 必须显式配置。
+- 地图加载后不可变;运行循环不得临时查询多张地图表或解释 `remark`。
+- 车辆筛选先执行硬条件,再执行可配置评分。
+- 已占资源只有匹配物理事实或恢复裁定才能释放。
+- 等待图新增授权后必须无环。
+- 本计划只生成设备无关的调度决定,不调用 PLC。
+
+---
+
+### Task 1: Define, validate, publish, and compile versioned warehouse configuration
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology/TopologyDefinition.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology/TopologyNode.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology/TopologyEdge.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology/ResourceDefinition.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology/EffectiveTopology.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology/TopologyCompiler.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/in/ConfigurationUseCase.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/TopologyDefinitionProvider.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/ConfigurationRepository.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/configuration/PublishConfigurationCommand.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/configuration/ConfigurationPublicationResult.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/configuration/PublishedConfigurationView.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/configuration/ConfigurationApplicationService.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/topology/TopologyCompilerTest.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/configuration/ConfigurationApplicationServiceTest.java`
+
+**Interfaces:**
+- Consumes: `TopologyDefinitionProvider.load(String warehouseCode, long version)`.
+- Produces: `TopologyCompiler.compile(TopologyDefinition): EffectiveTopology`.
+- Produces: `EffectiveTopology.neighbors(String nodeId)`, `edge(String sourceNodeId,String targetNodeId)`, `resource(String resourceKey)`, `containsNode(String nodeId)`, `version()`.
+- Produces: `ConfigurationUseCase.validate(PublishConfigurationCommand)`, `publish(PublishConfigurationCommand): ConfigurationPublicationResult`, `read(String warehouseCode,long version): PublishedConfigurationView`, and `readActive(String warehouseCode): PublishedConfigurationView`.
+- `publish` atomically stores the immutable warehouse, floor, topology, traffic-zone, resource, device, capability, port, charger, and workflow version after all cross-reference checks pass.
+- Rejects publication while an active order references the version being replaced; running orders remain pinned to their original topology/configuration version.
+
+- [ ] **Step 1: Write failing topology invariant tests**
+
+```java
+@Test
+void canonicalizesUndirectedEdgeResources() {
+ EffectiveTopology topology = compiler.compile(definition(
+ node("A", 1), node("B", 1), edge("A", "B")));
+ assertEquals("EDGE:A:B", topology.edge("A", "B").getResourceKey());
+ assertEquals("EDGE:A:B", topology.edge("B", "A").getResourceKey());
+}
+
+@Test
+void rejectsDisconnectedLiftPortReference() {
+ assertThrows(DomainRuleViolation.class, () ->
+ compiler.compile(definitionWithLiftPort("MISSING")));
+}
+
+@Test
+void publishesOneImmutableConfigurationVersionAtomically() {
+ ConfigurationPublicationResult result = service.publish(validConfiguration(7L));
+ assertEquals(7L, result.getPublishedVersion());
+ assertTrue(repository.isPublished("FT546", 7L));
+ assertEquals(0, repository.partialWriteCount());
+}
+
+@Test
+void readsOneInternallyConsistentPublishedVersion() {
+ service.publish(validConfiguration(7L));
+ PublishedConfigurationView view = service.read("FT546", 7L);
+ assertEquals(7L, view.getTopologyVersion());
+ assertEquals(7L, view.getDeviceConfigurationVersion());
+ assertEquals(7L, view.getWorkflowConfigurationVersion());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=TopologyCompilerTest,ConfigurationApplicationServiceTest test`
+
+- [ ] **Step 3: Implement immutable compilation**
+
+```java
+public EffectiveTopology compile(TopologyDefinition definition) {
+ requirePublishedVersion(definition);
+ Map nodes = uniqueNodes(definition.getNodes());
+ Map edges = canonicalEdges(definition.getEdges(), nodes);
+ validatePortsAndResources(definition, nodes);
+ return new EffectiveTopology(definition.getWarehouseCode(),
+ definition.getVersion(), nodes, edges,
+ immutableResources(definition, nodes, edges));
+}
+```
+
+Reject duplicate IDs, duplicate coordinates on one floor, missing endpoints, nonpositive capacity, unconfirmed traffic zones, and lift/conveyor ports without nodes.
+
+`ConfigurationApplicationService.publish` first compiles topology, validates every device floor/capability/port/charger reference and every workflow step type, then calls one optimistic `ConfigurationRepository.publish(command, effectiveTopology)` transaction. It returns stable validation codes and never exposes partially published versions.
+
+`read` and `readActive` return immutable DTOs through the inbound use case. Admin configuration pages may call these methods but may not query the core configuration Mapper or tables directly.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=TopologyCompilerTest,ConfigurationApplicationServiceTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/topology ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/configuration ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/in/ConfigurationUseCase.java ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/TopologyDefinitionProvider.java ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/ConfigurationRepository.java ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/topology ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/configuration
+git commit -m "feat(wcs): publish immutable warehouse configuration"
+```
+
+### Task 2: Add device registry and capability matching
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceType.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceCapability.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceDescriptor.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/VehicleSnapshot.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DeviceRegistry.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/equipment/DeviceCapabilityTest.java`
+
+**Interfaces:**
+- Produces: `DeviceRegistry.vehicles(): List`.
+- Produces: `DeviceDescriptor.supports(DeviceCapability): boolean`.
+- Produces: `VehicleSnapshot.isDispatchable(int minimumBattery)`.
+
+- [ ] **Step 1: Write failing dispatchability tests**
+
+```java
+@ParameterizedTest
+@CsvSource({
+ "ONLINE,AUTO,NORMAL,IDLE,80,true",
+ "OFFLINE,AUTO,NORMAL,IDLE,80,false",
+ "ONLINE,MANUAL,NORMAL,IDLE,80,false",
+ "ONLINE,AUTO,FAULT,IDLE,80,false",
+ "ONLINE,AUTO,NORMAL,BUSY,80,false",
+ "ONLINE,AUTO,NORMAL,IDLE,20,false"
+})
+void dispatchabilityUsesOrthogonalStateDimensions(
+ String connection, String mode, String health,
+ String occupation, int battery, boolean expected) {
+ assertEquals(expected, snapshot(connection, mode, health, occupation, battery)
+ .isDispatchable(30));
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=DeviceCapabilityTest test`
+
+- [ ] **Step 3: Implement descriptors and snapshots**
+
+Keep connection, mode, health, occupation, location, load, battery, current command, floor, and snapshot time as separate immutable fields. `supports` checks an immutable `EnumSet`.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=DeviceCapabilityTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DeviceRegistry.java ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/equipment
+git commit -m "feat(wcs): add device capabilities and snapshots"
+```
+
+### Task 3: Implement deterministic route planning
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/RoutePlan.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/RoutePlanner.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/AStarRoutePlanner.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/traffic/AStarRoutePlannerTest.java`
+
+**Interfaces:**
+- Produces: `RoutePlanner.plan(EffectiveTopology,String source,String destination,Set blockedResources): RoutePlan`.
+- Produces: ordered node IDs, ordered resource keys, distance cost, turn count, and topology version.
+
+- [ ] **Step 1: Write failing shortest-safe-route tests**
+
+```java
+@Test
+void avoidsBlockedEdgeAndUsesStableNodeIdTieBreak() {
+ RoutePlan route = planner.plan(grid(), "A", "D",
+ Collections.singleton("EDGE:B:D"));
+ assertEquals(Arrays.asList("A", "C", "D"), route.getNodeIds());
+}
+
+@Test
+void rejectsUnknownDestination() {
+ assertThrows(DomainRuleViolation.class, () ->
+ planner.plan(grid(), "A", "UNKNOWN", Collections.emptySet()));
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=AStarRoutePlannerTest test`
+
+- [ ] **Step 3: Implement A* with deterministic ordering**
+
+Use Manhattan distance only when coordinates exist; otherwise use zero heuristic. Sort equal-cost open nodes by total cost, path cost, turn count, then node ID. Never read historical `path_info`.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=AStarRoutePlannerTest,TopologyCompilerTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/traffic
+git commit -m "feat(wcs): add deterministic route planning"
+```
+
+### Task 4: Implement vehicle assignment policy
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/dispatch/DispatchTask.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/dispatch/VehicleAssignmentDecision.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/dispatch/VehicleAssignmentPolicy.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/dispatch/DefaultVehicleAssignmentPolicy.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DispatchPolicy.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/dispatch/DefaultVehicleAssignmentPolicyTest.java`
+
+**Interfaces:**
+- Produces: `assign(DispatchTask,List,EffectiveTopology): VehicleAssignmentDecision`.
+- Produces: selected vehicle ID, route, total score, and ordered reason codes.
+- Consumes: optional `DispatchPolicy.adjustment(DispatchTask,VehicleSnapshot): int` with no side effects.
+
+- [ ] **Step 1: Write failing hard-filter and stable-score tests**
+
+```java
+@Test
+void rejectsCloserVehicleWithoutRequiredCapability() {
+ VehicleAssignmentDecision decision = policy.assign(taskRequiringLoad(),
+ Arrays.asList(vehicle("V1", 1, 90, false), vehicle("V2", 2, 70, true)), topology);
+ assertEquals("V2", decision.getVehicleId());
+ assertTrue(decision.getReasonCodes().contains("CAPABILITY_MATCH"));
+}
+
+@Test
+void breaksEqualScoresByVehicleId() {
+ assertEquals("V1", policy.assign(task(), Arrays.asList(vehicle("V2"), vehicle("V1")), topology)
+ .getVehicleId());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=DefaultVehicleAssignmentPolicyTest test`
+
+- [ ] **Step 3: Implement filtering and scoring**
+
+Apply online/auto/healthy/idle/battery/capability/reachability filters before scoring. Score business priority, waiting age, same floor, path cost, battery margin, current workload, policy adjustment, then stable vehicle ID.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest='*VehicleAssignment*Test,*RoutePlannerTest' test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/dispatch ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DispatchPolicy.java ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/dispatch
+git commit -m "feat(wcs): add deterministic vehicle assignment"
+```
+
+### Task 5: Implement the resource ledger and reservation lifecycle
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/ReservationState.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/ResourceReservation.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/ReservationRequest.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/ReservationDecision.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/ResourceLedger.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/traffic/ResourceLedgerTest.java`
+
+**Interfaces:**
+- Produces: `reserve(ReservationRequest): ReservationDecision`.
+- Produces: `occupy(String reservationId,long fencingToken)`.
+- Produces: `release(String reservationId,String releaseEvidenceId,long fencingToken)`.
+- Produces: `abortUnoccupied(String reservationId,String reason)`.
+- `ReservationState` values are `REQUESTED`, `GRANTED`, `OCCUPIED`, `RELEASED`, `REJECTED`, `EXPIRED`, and `ABORTED`.
+
+- [ ] **Step 1: Write failing capacity and UNKNOWN tests**
+
+```java
+@Test
+void grantsOnlyOneReservationForCapacityOneResource() {
+ assertTrue(ledger.reserve(request("R1", "NODE:A")).isGranted());
+ assertFalse(ledger.reserve(request("R2", "NODE:A")).isGranted());
+}
+
+@Test
+void occupiedReservationCannotExpireByTime() {
+ ResourceReservation reservation = occupiedReservation();
+ assertThrows(DomainRuleViolation.class, reservation::expire);
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=ResourceLedgerTest test`
+
+- [ ] **Step 3: Implement atomic in-memory ledger semantics**
+
+Sort requested resource keys lexicographically, verify all capacities before changing any count, assign one fencing token to the decision, and roll back the whole request on any conflict. Permit only `REQUESTED -> GRANTED/REJECTED/EXPIRED/ABORTED`, `GRANTED -> OCCUPIED/EXPIRED/ABORTED`, and `OCCUPIED -> RELEASED`. `EXPIRED` is legal only before physical occupation; `OCCUPIED` can be released only with matching evidence/fencing token and must remain protected when a linked command is `UNKNOWN` or a RecoveryCase is unresolved.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=ResourceLedgerTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/traffic/ResourceLedgerTest.java
+git commit -m "feat(wcs): add resource reservation ledger"
+```
+
+### Task 6: Add deadlock prevention, HOLD, and YIELD decisions
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/WaitForGraph.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/TrafficDecisionType.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/TrafficDecision.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic/DeadlockGuard.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/traffic/DeadlockGuardTest.java`
+
+**Interfaces:**
+- Produces: `wouldCreateCycle(String waiter,Set holders): boolean`.
+- Produces: `decide(candidate,ledger,topology): TrafficDecision` with `GRANT/HOLD/YIELD` and reason codes.
+
+- [ ] **Step 1: Write failing cycle and safe-yield tests**
+
+```java
+@Test
+void rejectsGrantThatClosesAWaitCycle() {
+ graph.addWait("V1", "V2");
+ graph.addWait("V2", "V3");
+ assertTrue(graph.wouldCreateCycle("V3", Collections.singleton("V1")));
+}
+
+@Test
+void doesNotInventYieldPointWhenTopologyHasNone() {
+ TrafficDecision decision = guard.decide(blockedCandidate(), ledger, topologyWithoutSafeWait());
+ assertEquals(TrafficDecisionType.HOLD, decision.getType());
+ assertEquals("NO_SAFE_YIELD_POINT", decision.getReasonCode());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=DeadlockGuardTest test`
+
+- [ ] **Step 3: Implement deterministic wait-graph traversal**
+
+Use stable vehicle-ID traversal. YIELD may target only topology nodes flagged `safeWait=true`, must not cross occupied resources, and must never alter the business destination.
+
+- [ ] **Step 4: Run GREEN**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=DeadlockGuardTest,ResourceLedgerTest test`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/traffic ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/traffic/DeadlockGuardTest.java
+git commit -m "feat(wcs): prevent traffic deadlocks"
+```
+
+### Task 7: Orchestrate dispatch decisions and persist traffic state
+
+**Files:**
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/dispatch/DispatchCoordinator.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/dispatch/DispatchCycleResult.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/dispatch/DispatchSnapshot.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/dispatch/GrantedDispatch.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DispatchSnapshotProvider.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/TrafficStateRepository.java`
+- Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DecisionEventRepository.java`
+- Create: `sql/wcs/V002__wcs_topology_and_traffic.sql`
+- Create: `sql/wcs/verify_V002__wcs_topology_and_traffic.sql`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/dispatch/DispatchCoordinatorTest.java`
+- Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/traffic/TrafficConcurrencyIT.java`
+
+**Interfaces:**
+- Produces: `DispatchSnapshotProvider.load(): DispatchSnapshot`; the immutable snapshot contains ready steps, current vehicle snapshots, effective topology, and one optimistic `snapshotVersion`.
+- Produces: `dispatch(DispatchSnapshot): DispatchCycleResult`.
+- `DispatchCycleResult.getGranted()` returns `List`; each item contains order ID, step ID, vehicle ID, protected route, reservation IDs, topology version, and fencing token.
+- Persists: vehicle assignment, route plan, reservations, resource counts, and decision event in one transaction.
+- Produces: no device command; Plan 3 consumes granted assignments.
+
+- [ ] **Step 1: Write a failing two-vehicle arbitration test**
+
+```java
+@Test
+void grantsOneVehicleAndHoldsTheOtherForTheSameIntersection() {
+ DispatchCycleResult result = coordinator.dispatch(dispatchSnapshot(
+ Arrays.asList(step("S1", 1), step("S2", 2)),
+ Arrays.asList(vehicle("V1"), vehicle("V2")), topology));
+ assertEquals(1, result.getGranted().size());
+ assertEquals(1, result.getHeld().size());
+ assertEquals(0, result.getInvariantViolations().size());
+}
+```
+
+- [ ] **Step 2: Run RED**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest=DispatchCoordinatorTest test`
+
+- [ ] **Step 3: Implement the coordinator transaction boundary**
+
+Load one immutable `DispatchSnapshot`, sort ready steps by priority/age/order ID, assign vehicles, plan routes, call `DeadlockGuard`, then call one repository method:
+
+```java
+trafficStateRepository.commit(new DispatchCommit(
+ topology.version(), snapshotVersion, assignments,
+ routePlans, reservations, decisionEvents));
+```
+
+If optimistic versions changed, return `RETRY_SNAPSHOT_CHANGED` and persist no partial assignment.
+
+- [ ] **Step 4: Add SQL Server tables and locking test**
+
+Create `wcs_warehouse`, `wcs_floor`, `wcs_topology_version`, `wcs_topology_node`, `wcs_topology_edge`, `wcs_traffic_zone`, `wcs_resource_definition`, `wcs_device`, `wcs_device_capability`, `wcs_device_port`, `wcs_charger`, `wcs_vehicle_assignment`, `wcs_route_plan`, `wcs_resource_state`, `wcs_resource_reservation`, and `wcs_traffic_decision_event`. The integration test runs two concurrent capacity-1 reservations and asserts exactly one commit succeeds.
+
+- [ ] **Step 5: Run full Plan 2 verification**
+
+Run: `mvn -pl ruoyi-wcs -am -Dtest='*Topology*,*Capability*,*RoutePlanner*,*VehicleAssignment*,*ResourceLedger*,*DeadlockGuard*,*DispatchCoordinator*' test`
+
+Run SQL-tagged tests against the isolated SQL Server: `mvn -pl ruoyi-wcs -Dgroups=sqlserver -Dtest=TrafficConcurrencyIT test`.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/dispatch ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/dispatch ruoyi-wcs/src/test/java/com/ruoyi/wcs/infrastructure/persistence/traffic sql/wcs
+git commit -m "feat(wcs): coordinate and persist traffic decisions"
+```