docs: plan standard WCS implementation

This commit is contained in:
zgw
2026-08-04 15:31:38 +08:00
parent 24f1340dbc
commit 9d4ca83a70
6 changed files with 2624 additions and 0 deletions

View File

@@ -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<String, TopologyNode> nodes = uniqueNodes(definition.getNodes());
Map<String, TopologyEdge> 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<DeviceDescriptor>`.
- 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<DeviceCapability>`.
- [ ] **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<String> 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.<String>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<VehicleSnapshot>,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<String> 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<GrantedDispatch>`; 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"
```