Files
FT546-WCS/docs/superpowers/plans/2026-08-04-standard-wcs-topology-dispatch-traffic.md

445 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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"
```