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,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"
```