# FT546 Real-Time Read-Only Shadow 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:** Build an independently deployable FT546 real-time shadow program that reads approved production facts, runs the same dispatch and FT546 PLC-intent code intended for `STANDARD`, and persists comparison evidence without starting legacy threads, connecting to PLC/WMS, or writing any production table. **Architecture:** Keep the deployed stable WCS as the only production scheduler and PLC/WMS writer. Add a separate `ruoyi-wcs-shadow` Spring Boot process with two explicit data sources: a production account restricted to approved read-only views and a physically separate evidence database. Extract FT546 register encoding into a pure protocol component shared by the real adapter and shadow recorder; run the shared dispatch kernel through a shadow-specific cycle that persists virtual commands and comparison evidence instead of dispatching Outbox entries. **Tech Stack:** Java 8, Maven multi-module, Spring Boot 2.5.15, Spring JDBC transactions, SQL Server JDBC, H2 file database for local evidence tests, JUnit 5, ArchUnit, PowerShell deployment scripts. ## Global Constraints - Work only in the `optimize/wcs` worktree; do not merge, push, or modify the deployed stable branch. - The deployed stable artifact remains the only scheduler and only PLC/WMS writer throughout shadow collection. - `LEGACY` remains the default `ft546.wcs.runtime-mode`; no task changes that default. - `SHADOW` must have zero PLC connections, zero PLC writes, zero WMS callbacks, zero writes to production business/core tables, and zero legacy business or PLC-reader threads. - The real-time shadow is a separate process, service port, log directory, deployment directory, configuration file, and preferably a separate host. - The shadow production account may read only `shadow_ro.*` views; it must not receive `INSERT`, `UPDATE`, `DELETE`, `EXECUTE`, `ALTER`, `CONTROL`, or ownership permissions in the production database. - Shadow evidence writes use a physically separate database or server. A different schema in the production database is insufficient for the first site run. - Do not use `DataSourceType`, `DynamicDataSourceContextHolder`, or the shared RuoYi `SqlSessionFactory` for the two shadow data sources. Use explicit bean qualifiers and explicit transaction managers so a production connection can never be reused for evidence writes. - Production reads use a maximum pool size of 2, zero minimum idle connections, a 2-second connect timeout, a 3-second query timeout, fixed-delay polling, exponential backoff, and a one-cycle in-flight guard. - Missing, stale, partial, or contradictory facts produce `INSUFFICIENT_FACTS`; they do not generate a comparable virtual command. - The FT546 protocol contract remains exactly: main `4004 -> 4000 -> 4002 -> 4021`; cross-floor adds `4020=1 -> 4022=`; two-stage one `4404 -> 4422=1 -> 4402= -> 4400`; two-stage two `4404 -> 4422=2 -> 4423=99 -> 4402=9 -> 4400`. - A shadow protocol difference and an algorithm decision difference are separate categories. Never call a different selected vehicle/path a protocol failure when both encodings are internally correct. - No history field (`agv_id`, `path_info`, completion status, or `finish_date`) may force the new kernel to repeat the legacy choice. - This plan delivers the real-time read-only shadow and its deployment/rollback evidence. It does not authorize `STANDARD` PLC control or a production cutover. - All feature changes follow red-green-refactor. Each task runs its named test while red and again while green. - Preserve the four pre-existing untracked documentation HTML files and the generated `javaTest/outputs/wcs-history-business-shadow-20260812-f66585f/` evidence directory. --- ## File Structure New shared protocol module: - `ruoyi-ft546-protocol/`: pure FT546 profile, capability, command-intent, register-write, and encoder types. It has no Spring, JDBC, PLC, RuoYi Admin, or network dependency. - `Ft546SrgvCommandIntent`: strong input containing command identity, phase, route version, task number/type, route points, optional cross-floor target, and optional two-stage number. - `Ft546RegisterWrite`: immutable `address/dataType/values/writeSequence/trigger` value. - `Ft546EncodedCommand`: immutable ordered write list and command evidence. - `Ft546SrgvCommandEncoder`: the only FT546 shuttle register encoder used by both real and shadow paths. New independent shadow process: - `ruoyi-wcs-shadow/`: Spring Boot command-line service with no dependency on `ruoyi-admin` or `ruoyi-framework`. - `Ft546ShadowApplication`: imports only explicit shadow configuration and pure core/protocol beans; it does not component-scan `com.ruoyi.wcs` or `com.ruoyi.project`, so it cannot discover `WcsCoreConfiguration`, `ScheduleThread`, `ThreadOperationController`, PLC readers, WMS controllers, or production adapters. - `ShadowSourceConfiguration`: explicit read-only SQL Server `DataSource`, `JdbcTemplate`, and transaction manager. - `ShadowEvidenceConfiguration`: explicit evidence `DataSource`, `JdbcTemplate`, and transaction manager. - `ShadowStartupGate`: fail-closed verification of process mode, JDBC users/databases, source permissions, evidence location, dependency graph, and local output writability. - `SqlServerShadowFactReader`: bounded reads from `shadow_ro.v_*` views and assembly of one immutable `ShadowFactBatch`. - `Ft546ShadowCycle`: source fact collection -> fact validation -> same-kernel dispatch -> virtual command intent -> protocol encoding -> comparison -> evidence commit. It never invokes `CommandOutboxDispatcher`. - `RecordingFt546CommandGateway`: records encoded commands to the evidence repository; it has no `send` or PLC runtime reference. - `ShadowEvidenceRepository`: stores run, input batch, legacy result, new decision, encoded writes, differences, and cycle health in the evidence database. Existing production files remain owners of production behavior: - `ScheduleThread` continues to own legacy readers/business threads in `LEGACY` and standard PLC fact readers in `STANDARD`; `SHADOW` constructs neither. - `Ft546LegacySrgvAdapter` remains the real PLC gateway in `STANDARD`, but delegates encoding to the shared pure protocol module before iterating the encoded writes. - `Ft546AdapterConfiguration` remains `STANDARD`-only. The shadow process does not import it. --- ### Task 1: Make Runtime Modes Fail-Closed Before Any Legacy or PLC Object Exists **Files:** - Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/ThreadOperationController.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/schedule/scheduleTask/srgv/BaseSRgvThread.java` - Create: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/runtime/Ft546AdminRuntimeModeEnvironmentPostProcessor.java` - Modify: `ruoyi-admin/src/main/resources/META-INF/spring.factories` - Modify: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/runtime/Ft546StandardRuntimeTest.java` - Create: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/runtime/Ft546ShadowIsolationTest.java` **Interfaces:** - Produces: `Ft546WcsProperties.RuntimeMode.isLegacy/isShadow/isStandard`. - Produces: `ScheduleThread.RuntimeCapabilities` with `plcFactReadersAllowed`, `legacyBusinessThreadsAllowed`, and `automaticFactReaderStartAllowed`. - Preserves: `LEGACY` initialization/start/stop behavior and `STANDARD` application-ready PLC fact-reader start. - Enforces: every thread operation method rejects `SHADOW` before dereferencing a thread field. - Enforces: the `ruoyi-admin` artifact refuses to start in `SHADOW` during environment preparation, before Spring constructs `ScheduleThread`; operators must use the dedicated `ruoyi-wcs-shadow` artifact. - [ ] **Step 1: Write failing mode-isolation tests** ```java @Test void shadowPostConstructDoesNotQueryDevicesOrConstructAnyThread() { ScheduleThread runtime = runtime(SHADOW); ReflectionTestUtils.setField(runtime, "deviceSrgvService", failOnAnyInvocation(IDeviceSrgvService.class)); runtime.init(); assertTrue(runtime.plcSrgvReadCustomThreads.isEmpty()); assertNull(runtime.plcSsjReadCustomThread); assertNull(runtime.plcTsjReadCustomThread); assertNull(runtime.plcCdzReadCustomThread); assertNull(runtime.sRgvThreadCustomThread); } @Test void everyManualThreadOperationFailsClosedInShadow() { ScheduleThread runtime = runtime(SHADOW); assertThrows(IllegalStateException.class, runtime::startAllThread); assertThrows(IllegalStateException.class, runtime::startAllThread2); assertThrows(IllegalStateException.class, runtime::stopAllThread); assertThrows(IllegalStateException.class, runtime::stopAllThread2); } ``` Add a MockMvc test proving all four mutation endpoints return an error in `SHADOW`. Preserve the deployed stable authorization surface exactly: the stable controller currently leaves its `@PreAuthorize` lines commented, so this task must not enable them and risk blocking existing operator accounts. Add a `SpringApplication` test proving `Ft546AdminRuntimeModeEnvironmentPostProcessor` throws `ADMIN_SHADOW_MODE_FORBIDDEN_USE_RUOYI_WCS_SHADOW` during environment preparation, after Config Data is loaded but before any test bean's constructor or `@PostConstruct`, when the admin artifact is configured with `runtime-mode=SHADOW`. - [ ] **Step 2: Run the isolation test and verify current `@PostConstruct` behavior fails** Run: ```bash mvn -pl ruoyi-admin -am -Dtest=Ft546ShadowIsolationTest,Ft546StandardRuntimeTest test ``` Expected: `shadowPostConstructDoesNotQueryDevicesOrConstructAnyThread` fails because current `init()` unconditionally calls `plcReadInit()` and `scheduleInit()`; manual PLC-reader start also does not reject `SHADOW`. - [ ] **Step 3: Centralize mode capabilities and guard before construction** ```java public void init() { RuntimeCapabilities capabilities = capabilities(); if (capabilities.plcFactReadersAllowed()) { plcReadInit(); } if (capabilities.legacyBusinessThreadsAllowed()) { scheduleInit(); } } private RuntimeCapabilities capabilities() { RuntimeMode mode = ft546WcsProperties.getRuntimeMode(); return new RuntimeCapabilities( mode == RuntimeMode.LEGACY || mode == RuntimeMode.STANDARD, mode == RuntimeMode.LEGACY, mode == RuntimeMode.STANDARD); } ``` Call `requirePlcFactReadersAllowed()` at the first line of `startAllThread/stopAllThread`, and `requireLegacyBusinessThreadsAllowed()` at the first line of `startAllThread2/stopAllThread2`. Keep `BaseSRgvThread.run()` fail-closed for every non-`LEGACY` mode. Preserve the four endpoints' stable authorization behavior. Preserve the existing `ScheduleThread.getScheduleStatus()` Java signature and add a detailed status view for the controller so operators can see the current mode and allowed operations without breaking existing callers. Register `Ft546AdminRuntimeModeEnvironmentPostProcessor` in `META-INF/spring.factories`; it reads the raw environment property, accepts missing/`LEGACY`/`STANDARD`, and throws after Config Data loading but before bean creation for `SHADOW` or an unknown value. The exception names the dedicated shadow artifact and does not silently fall back to `LEGACY`. Lower-level thread guards remain as defense in depth and are unit-tested directly. - [ ] **Step 4: Run focused and regression tests** Run: ```bash mvn -pl ruoyi-admin -am -Dtest=Ft546ShadowIsolationTest,Ft546StandardRuntimeTest,Ft546DeviceRuntimeRegistryTest test ``` Expected: `SHADOW` constructs no thread/runtime object; `LEGACY` existing initialization remains intact; `STANDARD` starts readers only after `ApplicationReadyEvent`; endpoint authorization and mode rejection pass. - [ ] **Step 5: Commit the hard runtime boundary** ```bash git add ruoyi-admin/src/main/java/com/ruoyi/schedule \ ruoyi-admin/src/main/java/com/ruoyi/project/ft546/runtime/Ft546AdminRuntimeModeEnvironmentPostProcessor.java \ ruoyi-admin/src/main/resources/META-INF/spring.factories \ ruoyi-admin/src/test/java/com/ruoyi/project/ft546/runtime git commit -m "fix(ft546): isolate shadow from legacy and PLC threads" ``` --- ### Task 2: Extract One Pure FT546 Protocol Encoder For Real And Shadow Paths **Files:** - Create: `ruoyi-ft546-protocol/pom.xml` - Modify: `pom.xml` - Create: `ruoyi-ft546-protocol/src/main/java/com/ruoyi/ft546/protocol/Ft546DeviceProfile.java` - Create: `ruoyi-ft546-protocol/src/main/java/com/ruoyi/ft546/protocol/Ft546SrgvCommandIntent.java` - Create: `ruoyi-ft546-protocol/src/main/java/com/ruoyi/ft546/protocol/Ft546RegisterWrite.java` - Create: `ruoyi-ft546-protocol/src/main/java/com/ruoyi/ft546/protocol/Ft546EncodedCommand.java` - Create: `ruoyi-ft546-protocol/src/main/java/com/ruoyi/ft546/protocol/Ft546SrgvCommandEncoder.java` - Delete after import migration: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546DeviceProfile.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546ConfigurationValidator.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/config/Ft546CapabilityGate.java` - Modify: `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/Ft546CommandIntentMapper.java` - Delete after callers migrate: `ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvCommandEncoder.java` - Create: `ruoyi-ft546-protocol/src/test/java/com/ruoyi/ft546/protocol/Ft546SrgvCommandEncoderTest.java` - Modify: `ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapterTest.java` - Move/copy fixtures: `ruoyi-ft546-protocol/src/test/resources/ft546/plc/srgv-point-to-point.json` and `srgv-two-stage-gate.json` **Interfaces:** - Produces: `Ft546SrgvCommandEncoder.encode(Ft546SrgvCommandIntent, Ft546DeviceProfile, Set) -> Ft546EncodedCommand`. - Produces: `Ft546CommandIntentMapper.from(DeviceCommandEnvelope) -> Ft546SrgvCommandIntent` in `ruoyi-admin`; the pure module never imports WCS envelope types. - Produces: ordered `Ft546RegisterWrite` values with public getters for `address`, `dataType`, `values`, `writeSequence`, and `trigger`. - Preserves: current `SendReceipt` and `UNKNOWN` behavior in the real adapter. - [ ] **Step 1: Write protocol golden-master tests independent of PLC classes** ```java @Test void crossFloorIntentEncodesTheExactLegacySequence() { Ft546EncodedCommand encoded = encoder.encode( intent("PARENT-1", "LOADED_TO_PLACE", 30001, (short) 8, points(70501, 70502), (short) 8, null), fullProfile(), set("POINT_TO_POINT_COMMAND")); assertWrites(encoded, write(1, "4004", INT32, 70501, 70502, 0, 0, 0, 0, 0, 0), write(2, "4000", INT32, 30001), write(3, "4002", INT16, 8), trigger(4, "4021", INT16, 1), write(5, "4020", INT16, 1), write(6, "4022", INT16, 8)); } ``` Add exact tests for ordinary main task, two-stage stage one, two-stage stage two, missing registers, route length 2/8 boundaries, route length 1/9 rejection, six-digit point overflow, capability gates, and immutable profile/write lists. - [ ] **Step 2: Run the new module test and verify it fails to compile** Run: ```bash mvn -pl ruoyi-ft546-protocol -am -Dtest=Ft546SrgvCommandEncoderTest test ``` Expected: Maven initially cannot find the module/types. - [ ] **Step 3: Implement the pure encoder and make the real adapter consume it** ```java Ft546EncodedCommand encoded = encoder.encode( intentMapper.from(envelope), profile, capabilities.getEnabledCapabilities()); boolean wroteAny = false; for (Ft546RegisterWrite write : encoded.getWrites()) { if (!plcRegisterWriter.write(runtime, write)) { return markUnknown(envelope.getDeviceId(), receiptId, wroteAny ? "PLC rejected a partial register sequence" : "first register write result is ambiguous"); } wroteAny = true; } ``` Keep `PlcRegisterWriter` inside `ruoyi-admin`; the pure module never imports `PlcSrgvRead`. The encoder includes evidence fields `deviceId`, `parentTaskId`, `phase`, `commandKey`, `routeVersion`, `taskNo`, `taskType`, `pathPoints`, `writeSequence`, and `triggerValue`. - [ ] **Step 4: Run protocol and adapter regression tests** Run: ```bash mvn -pl ruoyi-ft546-protocol,ruoyi-admin -am \ -Dtest=Ft546SrgvCommandEncoderTest,Ft546LegacySrgvAdapterTest,Ft546StandardWcsEndToEndTest test ``` Expected: golden protocol fixtures and all current real-adapter behaviors pass; partial writes still become `ACCEPTED_UNKNOWN`; no address/type/value/order changes occur. - [ ] **Step 5: Commit the shared protocol seam** ```bash git add pom.xml ruoyi-ft546-protocol ruoyi-admin/pom.xml \ ruoyi-admin/src/main/java/com/ruoyi/project/ft546 \ ruoyi-admin/src/test/java/com/ruoyi/project/ft546 git commit -m "refactor(ft546): share pure PLC command encoding" ``` --- ### Task 3: Add An Independent Shadow Launcher With A Dependency Deny-List **Files:** - Create: `ruoyi-wcs-shadow/pom.xml` - Modify: `pom.xml` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/Ft546ShadowApplication.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/config/ShadowProperties.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/config/ShadowRuntimeConfiguration.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/startup/ShadowStartupGate.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/startup/ShadowStartupReport.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/web/ShadowStatusController.java` - Create: `ruoyi-wcs-shadow/src/main/resources/application-shadow.yml` - Create: `ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/Ft546ShadowApplicationTest.java` - Create: `ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/ShadowDependencyBoundaryTest.java` - Create: `ruoyi-wcs-shadow/src/test/resources/application-shadow-test.yml` **Interfaces:** - Launch: `java -jar ruoyi-wcs-shadow.jar --spring.profiles.active=shadow --shadow.enabled=true`. - Status: `GET /shadow/status` returns `STARTING`, `READY`, `DEGRADED`, or `STOPPED`, last completed cycle, source lag, evidence backlog, and startup checks. - Produces: `ShadowStartupGate.verify(ApplicationContext, ShadowProperties)`. - Enforces: `server.address=127.0.0.1` by default and `server.port=18091`; no actuator mutation endpoint. - [ ] **Step 1: Write application and architecture tests** ```java @Test void shadowContextContainsNoProductionSideEffectType() { contextRunner.run(context -> { assertThat(context).doesNotHaveBean(DeviceCommandGateway.class); assertThat(context).doesNotHaveBean(WmsNotificationGateway.class); assertFalse(ClassUtils.isPresent( "com.ruoyi.schedule.ScheduleThread", context.getClassLoader())); assertThat(context.getBeanDefinitionNames()) .noneMatch(name -> name.toLowerCase().contains("plc")); }); } @ArchTest static final ArchRule shadowMustNotDependOnAdminOrPlc = noClasses() .that().resideInAPackage("com.ruoyi.shadow..") .should().dependOnClassesThat() .resideInAnyPackage("com.ruoyi.schedule..", "com.ruoyi.api..", "HslCommunication.."); ``` Also assert startup fails unless `shadow.enabled=true`, `spring.profiles.active=shadow`, and both source/evidence URLs are explicitly supplied outside tests. - [ ] **Step 2: Run the new module test and verify the launcher is missing** Run: ```bash mvn -pl ruoyi-wcs-shadow -am -Dtest=Ft546ShadowApplicationTest,ShadowDependencyBoundaryTest test ``` Expected: module/application classes are missing. - [ ] **Step 3: Implement a narrow component graph** ```java @SpringBootConfiguration @EnableAutoConfiguration @Import({ShadowRuntimeConfiguration.class, ShadowKernelConfiguration.class}) @EnableScheduling public class Ft546ShadowApplication { public static void main(String[] args) { SpringApplication.run(Ft546ShadowApplication.class, args); } } ``` Exclude RuoYi security, Redis, Quartz, Swagger, TCP servers, MyBatis auto-scans, `WcsCoreConfiguration`, and all `ruoyi-admin` dependencies. `ShadowKernelConfiguration` constructs `DefaultVehicleAssignmentPolicy`, `AStarRoutePlanner`, `DeadlockGuard`, and `DispatchCoordinator` explicitly. The module depends only on `ruoyi-wcs`, `ruoyi-ft546-protocol`, Spring Web/JDBC, SQL Server driver, Jackson, and tests. `ShadowStartupGate` inspects bean types and package names at runtime and stops scheduling if a forbidden side-effect type appears. - [ ] **Step 4: Run context and package-boundary tests** Run: ```bash mvn -pl ruoyi-wcs-shadow -am -Dtest=Ft546ShadowApplicationTest,ShadowDependencyBoundaryTest test ``` Expected: the context starts with in-memory test adapters only, status binds to loopback, and no forbidden type is reachable. - [ ] **Step 5: Commit the isolated process** ```bash git add pom.xml ruoyi-wcs-shadow git commit -m "feat(ft546): add isolated shadow process" ``` --- ### Task 4: Enforce Two Physical Data Sources And Read-Only Production Permissions **Files:** - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/config/ShadowSourceConfiguration.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/config/ShadowEvidenceConfiguration.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/startup/SqlServerPermissionProbe.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/startup/PermissionCheckResult.java` - Modify: `ruoyi-wcs-shadow/src/main/resources/application-shadow.yml` - Create: `ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/config/ShadowDataSourceIsolationTest.java` - Create: `ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/startup/SqlServerPermissionProbeTest.java` - Create: `sql/shadow/V001__ft546_shadow_read_views.sql` - Create: `sql/shadow/V002__ft546_shadow_read_login.sql` - Create: `sql/shadow/V003__ft546_shadow_evidence.sql` - Create: `sql/shadow/verify_ft546_shadow_permissions.sql` **Interfaces:** - Beans: `@Qualifier("shadowSourceDataSource")`, `shadowSourceJdbc`, `shadowSourceTx`; `@Qualifier("shadowEvidenceDataSource")`, `shadowEvidenceJdbc`, `shadowEvidenceTx`. - Produces: `SqlServerPermissionProbe.verifySource(Connection)` and `verifyEvidence(Connection)`. - Production views: `shadow_ro.v_wcs_srgv_queue`, `v_wcs_srgv_queue_finish`, `v_device_srgv`, `v_wcs_srgv_map`, `v_bas_cargo_space`, `v_tsj_execution`, and `v_current_database_clock`. - Evidence tables: `shadow_run`, `shadow_cycle`, `shadow_input_batch`, `shadow_legacy_queue`, `shadow_decision`, `shadow_encoded_write`, `shadow_difference`, `shadow_traffic_state`, `shadow_hold_history`, and `shadow_startup_check`. - [ ] **Step 1: Write dual-source and permission-probe tests** ```java @Test void sourceAndEvidenceConnectionsCannotResolveToTheSameDatabase() { StartupFailure failure = assertThrows(StartupFailure.class, () -> gate.verify(sourceMeta("prod-host", "FT546-wcs", "shadow_ro"), evidenceMeta("prod-host", "FT546-wcs", "shadow_evidence"))); assertEquals("SOURCE_AND_EVIDENCE_DATABASE_MUST_DIFFER", failure.getReasonCode()); } @Test void sourceProbeRejectsAnyProductionWritePermission() { PermissionCheckResult result = probe.verifySource(connectionWith( "SELECT=1;INSERT=0;UPDATE=1;DELETE=0;CONTROL=0")); assertFalse(result.isAccepted()); assertEquals("SOURCE_HAS_UPDATE_PERMISSION", result.getReasonCode()); } ``` The source connection test executes `SET TRANSACTION ISOLATION LEVEL READ COMMITTED`, calls `Connection.setReadOnly(true)`, and verifies permissions only through `HAS_PERMS_BY_NAME`, `fn_my_permissions`, database role membership, and JDBC metadata. The startup probe performs no DML of any kind, including temporary or probe-table writes. - [ ] **Step 2: Run tests and verify current dynamic data-source path cannot satisfy them** Run: ```bash mvn -pl ruoyi-wcs-shadow -am -Dtest=ShadowDataSourceIsolationTest,SqlServerPermissionProbeTest test ``` Expected: missing explicit data-source beans/probe classes. - [ ] **Step 3: Implement explicit pools and fail-closed startup checks** ```java @Bean("shadowSourceDataSource") DataSource source(ShadowProperties properties) { HikariConfig config = base(properties.getSource()); config.setReadOnly(true); config.setMaximumPoolSize(2); config.setMinimumIdle(0); config.setConnectionTimeout(2000L); return new HikariDataSource(config); } @Bean("shadowSourceTx") DataSourceTransactionManager sourceTx( @Qualifier("shadowSourceDataSource") DataSource source) { DataSourceTransactionManager manager = new DataSourceTransactionManager(source); manager.setEnforceReadOnly(true); return manager; } ``` Do not modify `DataSourceType`, `DruidConfig`, or `DynamicDataSourceContextHolder`; the shadow process does not load them. `ShadowStartupGate` compares JDBC URL host/database and login names, persists every check to the evidence database, and keeps the scheduler disabled unless all checks pass. - [ ] **Step 4: Add additive SQL objects and least-privilege verification** `V001` creates only `shadow_ro` views selecting the exact existing columns needed from `wcs_srgv_queue`, `wcs_srgv_queue_finish`, `device_srgv`, `wcs_srgv_map`, `bas_cargo_space`, and `tsj_device_execution_queue`. `V002` creates a login/user template, denies write/control permissions, and grants `SELECT` only on `shadow_ro`; site credentials remain deployment secrets and are not committed. `V003` creates evidence tables in a separate database. The verify script returns one row per expected grant/deny and fails if the source user can write any approved base table. - [ ] **Step 5: Run automated tests; run SQL verification only against an approved isolated SQL Server** Run locally: ```bash mvn -pl ruoyi-wcs-shadow -am -Dtest=ShadowDataSourceIsolationTest,SqlServerPermissionProbeTest test ``` Run in the approved SQL Server integration environment: ```powershell sqlcmd -S $env:SHADOW_SQLSERVER -d FT546-wcs -E -b -i sql/shadow/verify_ft546_shadow_permissions.sql ``` Expected: local tests pass. SQL verification reports source `SELECT` on views, zero source business-table write permissions, and evidence write permission only in the distinct evidence database. If no isolated SQL Server is available, record this gate as `NOT_RUN`; do not claim site readiness. - [ ] **Step 6: Commit data isolation and additive SQL** ```bash git add ruoyi-wcs-shadow sql/shadow git commit -m "feat(ft546): enforce shadow database isolation" ``` --- ### Task 5: Collect One Immutable, Bounded Production Fact Batch **Files:** - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/facts/ShadowFactBatch.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/facts/ShadowQueueFact.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/facts/ShadowVehicleFact.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/facts/ShadowMapFact.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/facts/ShadowLegacyQueueResult.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/facts/LegacyQueueProtocolProjector.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/facts/ShadowFactValidation.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/facts/SqlServerShadowFactReader.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/facts/ShadowFactBatchValidator.java` - Create: `ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/facts/SqlServerShadowFactReaderTest.java` - Create: `ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/facts/ShadowFactBatchValidatorTest.java` **Interfaces:** - Produces: `ShadowFactBatch SqlServerShadowFactReader.read(ShadowReadCursor cursor, Instant deadline)`. - Produces: `ShadowFactValidation validate(ShadowFactBatch, Instant now)`. - Produces: `LegacyQueueProtocolProjector.project(ShadowQueueFact) -> ShadowLegacyQueueResult` using captured legacy queue fields only; it does not call the shared new encoder. - Cursor: `(lastCreateDate, lastGuid)` with a 5-second overlap; duplicate rows are retained as source evidence but deduplicated by `(tableName, guid, contentHash)` for kernel input. - Hard limits: 500 queue rows, 100 vehicle rows, 10,000 map rows on startup refresh, 3-second JDBC query timeout, and 4-second whole-batch deadline. - [ ] **Step 1: Write immutable batch, ordering, and stale-fact tests** ```java @Test void readsQueueVehicleLegacyResultAndDatabaseClockInOneReadOnlyTransaction() { ShadowFactBatch batch = reader.read(cursor, deadline); assertEquals(Arrays.asList("Q-1", "Q-2"), guids(batch.getQueues())); assertEquals("070502", batch.vehicle("1").getCurrentSite()); assertEquals("4004,4000,4002,4021", projector.project(batch.queue("Q-1")).getWriteAddresses()); assertTrue(batch.getDatabaseCapturedAt().isBefore(deadline)); } @Test void staleOrContradictoryFactsAreNotComparable() { ShadowFactValidation validation = validator.validate( batchWithVehicleAtTwoSites(), NOW); assertEquals(INSUFFICIENT_FACTS, validation.getStatus()); assertEquals("CONTRADICTORY_VEHICLE_POSITION:1", validation.getReasonCode()); } ``` Add tests for query timeout, row-limit overflow, non-monotonic database clock, missing map site, missing vehicle for legacy assignment, queue mutation during the overlap window, source outage backoff, ordinary/cross-floor/two-stage legacy projection, and `LEGACY_RESULT_UNAVAILABLE` when the captured queue lacks the fields needed to reconstruct old register values. - [ ] **Step 2: Run fact tests and verify the reader/validator are missing** Run: ```bash mvn -pl ruoyi-wcs-shadow -am -Dtest=SqlServerShadowFactReaderTest,ShadowFactBatchValidatorTest test ``` Expected: missing fact types/readers. - [ ] **Step 3: Implement bounded read-only SQL with deterministic ordering** ```sql SELECT TOP (?) guid, task_no, task_type, priority, cargo_pick_site, cargo_place_site, start_site, end_site, create_date, agv_id, path_info, default02, default03, default04, default05, default06, default07, default08 FROM shadow_ro.v_wcs_srgv_queue WHERE create_date > ? OR (create_date = ? AND guid > ?) ORDER BY create_date, guid; ``` Read all component queries inside `shadowSourceTx` with `readOnly=true`. Capture the database clock before and after; accept a batch only when the span is within the configured freshness window and every required object is present. `LegacyQueueProtocolProjector` independently reconstructs the old expected queue/PLC result from captured `agv_id/path_info/task_type/default*` fields and the frozen legacy contract; it must not invoke `Ft546SrgvCommandEncoder`. Keep all legacy result fields out of new-kernel candidate selection inputs. When legacy values are not reconstructable, persist `LEGACY_RESULT_UNAVAILABLE` and skip protocol equality rather than declaring a match. - [ ] **Step 4: Add load-shedding and outage behavior** One `AtomicBoolean` prevents overlapping reads. On timeout/row overflow, persist a failed cycle, discard the partial batch, and back off `1s -> 2s -> 4s -> 8s -> 30s`. A successful complete batch resets the backoff. The reader never retries inside the same cycle. - [ ] **Step 5: Run focused tests** Run: ```bash mvn -pl ruoyi-wcs-shadow -am -Dtest=SqlServerShadowFactReaderTest,ShadowFactBatchValidatorTest test ``` Expected: deterministic ordering, immutable data, stale/contradictory rejection, query limits, and backoff pass. - [ ] **Step 6: Commit the fact boundary** ```bash git add ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/facts \ ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/facts git commit -m "feat(ft546): collect bounded read-only shadow facts" ``` --- ### Task 6: Run The Shared Dispatch Kernel Without Command Outbox Or Side Effects **Files:** - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/ShadowDispatchResult.java` - Create: `ruoyi-wcs/src/main/java/com/ruoyi/wcs/application/ReadOnlyDispatchCycle.java` - Create: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/ReadOnlyDispatchCycleTest.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/kernel/ShadowDispatchSnapshotAssembler.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/kernel/ShadowCommandIntentFactory.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/kernel/RecordingFt546CommandGateway.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/kernel/JdbcShadowTrafficStateRepository.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/kernel/Ft546ShadowCycle.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/kernel/ShadowCycleResult.java` - Create: `ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/kernel/Ft546ShadowCycleTest.java` - Modify: `ruoyi-wcs/src/test/java/com/ruoyi/wcs/application/WcsRuntimeCoordinatorTest.java` **Interfaces:** - Produces: `ReadOnlyDispatchCycle.run(DispatchSnapshot) -> ShadowDispatchResult`. - Produces: `ShadowDispatchSnapshotAssembler.assemble(ShadowFactBatch) -> DispatchSnapshot`. - Produces: `ShadowCommandIntentFactory.create(GrantedDispatch) -> Ft546SrgvCommandIntent`. - Produces: `RecordingFt546CommandGateway.record(Ft546SrgvCommandIntent) -> Ft546EncodedCommand`. - Produces: `JdbcShadowTrafficStateRepository.commit(DispatchCommit)` in the evidence database with optimistic `snapshotVersion`; reservations, wait-for edges, and HOLD history survive process restart. - Preserves: `WcsRuntimeCoordinator.tick()` production order and Outbox behavior unchanged. - [ ] **Step 1: Write a same-kernel/no-Outbox test** ```java @Test void readOnlyCycleUsesDispatchCoordinatorButNeverCreatesOrSendsACommand() { List calls = new ArrayList(); ReadOnlyDispatchCycle cycle = new ReadOnlyDispatchCycle( snapshot -> { calls.add("dispatch"); return committedGrant(); }, grant -> calls.add("intent:" + grant.getVehicleId())); ShadowDispatchResult result = cycle.run(snapshot()); assertEquals(Arrays.asList("dispatch", "intent:1"), calls); assertEquals(1, result.getIntents().size()); } ``` The integration test injects throwing fakes for every `DeviceCommandRepository`, `CommandOutboxRepository`, `DeviceCommandGateway`, and `WmsNotificationGateway`; a successful shadow cycle proves none is called. - [ ] **Step 2: Run the tests and verify the read-only cycle does not exist** Run: ```bash mvn -pl ruoyi-wcs,ruoyi-wcs-shadow -am \ -Dtest=ReadOnlyDispatchCycleTest,Ft546ShadowCycleTest,WcsRuntimeCoordinatorTest test ``` Expected: missing cycle/assembler/recorder types. - [ ] **Step 3: Extract the pure dispatch use without changing production tick** ```java public ShadowDispatchResult run(DispatchSnapshot snapshot) { DispatchCycleResult decisions = dispatcher.apply(snapshot); List intents = new ArrayList<>(); for (GrantedDispatch grant : decisions.getGranted()) { intents.add(intentFactory.apply(grant)); } return new ShadowDispatchResult(decisions, intents); } ``` `Ft546ShadowCycle` validates facts first. For valid facts it assembles the same `DispatchSnapshot`/`DispatchTask`/`VehicleSnapshot`/`EffectiveTopology` values used by `DispatchCoordinator`, invokes `ReadOnlyDispatchCycle`, encodes every intent through `Ft546SrgvCommandEncoder`, and sends all evidence to one evidence-database transaction. It does not instantiate `DeviceExecutionService`, `CommandOutboxDispatcher`, `WmsNotificationOutboxDispatcher`, or a production lease/fencing writer. `JdbcShadowTrafficStateRepository` stores the resulting ledger, wait-for graph, consecutive HOLD counters, and snapshot version only in the evidence database; restart reload tests must prove avoidance/HOLD state is not reset. - [ ] **Step 4: Prove phase parameters include ordinary, cross-floor, and two-stage semantics** Add fixture cases whose `GrantedDispatch.commandParameters` include: ```text parentTaskId=PARENT-1 phase=LOADED_TO_PLACE routeVersion=3 taskNo=30001 taskType=1|8 crossFloorTarget=8 (only when taskType=8) stage=1|2 (only for the matching two-stage phase) ``` Assert the encoder output equals the legacy sequence fixture and the vehicle/path may differ without changing protocol classification. - [ ] **Step 5: Run kernel, protocol, and shadow cycle tests** Run: ```bash mvn -pl ruoyi-wcs,ruoyi-ft546-protocol,ruoyi-wcs-shadow -am \ -Dtest=ReadOnlyDispatchCycleTest,WcsRuntimeCoordinatorTest,Ft546SrgvCommandEncoderTest,Ft546ShadowCycleTest test ``` Expected: shared dispatch policy and encoder run; all side-effect fakes remain uncalled; invalid facts produce no intent. - [ ] **Step 6: Commit the same-kernel shadow cycle** ```bash git add ruoyi-wcs ruoyi-wcs-shadow git commit -m "feat(ft546): run shared dispatch kernel in shadow" ``` --- ### Task 7: Persist Auditable Differences And Prove Idempotent Collection **Files:** - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/evidence/ShadowEvidenceRepository.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/evidence/JdbcShadowEvidenceRepository.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/evidence/ShadowDifference.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/evidence/ShadowDifferenceCategory.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/evidence/ShadowComparisonService.java` - Create: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/evidence/ShadowEvidenceDigest.java` - Create: `ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/evidence/ShadowComparisonServiceTest.java` - Create: `ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/evidence/JdbcShadowEvidenceRepositoryTest.java` - Modify: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/kernel/Ft546ShadowCycle.java` - Modify: `ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/web/ShadowStatusController.java` **Interfaces:** - Categories: `MATCH`, `PROTOCOL_ADDRESS`, `PROTOCOL_DATA_TYPE`, `PROTOCOL_VALUE`, `PROTOCOL_ORDER`, `QUEUE_FIELD`, `ALGORITHM_VEHICLE`, `ALGORITHM_ROUTE`, `ALGORITHM_HOLD`, `INSUFFICIENT_FACTS`, and `SOURCE_GAP`. - Idempotency: unique `(run_id, input_batch_hash, parent_task_id, phase, route_version)`. - Produces: `ShadowComparisonService.compare(ShadowLegacyQueueResult, ShadowDecision, Ft546EncodedCommand)`. - Produces: daily digest counts and reproducible evidence hash. - [ ] **Step 1: Write classification and idempotency tests** ```java @Test void differentVehicleWithMatchingWritesIsAlgorithmOnly() { List differences = comparison.compare( legacy("1", path(70501, 70502), mainWrites()), decision("2", path(70501, 70502)), encoded(mainWrites())); assertEquals(singletonList(ALGORITHM_VEHICLE), categories(differences)); } @Test void changedWriteOrderIsProtocolOrderEvenWhenValuesMatch() { assertEquals(PROTOCOL_ORDER, singleDifference( legacy(mainWrites()), encoded(swappedTaskNoAndType())).getCategory()); } @Test void replayingTheSameBatchDoesNotDuplicateEvidence() { repository.save(cycle); repository.save(cycle); assertEquals(1, repository.countCycles()); assertEquals(cycle.getEvidenceHash(), repository.findCycle( cycle.getRunId(), cycle.getInputBatchHash(), cycle.getParentTaskId(), cycle.getPhase(), cycle.getRouteVersion()).getEvidenceHash()); } ``` - [ ] **Step 2: Run evidence tests and verify repository/comparison types are missing** Run: ```bash mvn -pl ruoyi-wcs-shadow -am \ -Dtest=ShadowComparisonServiceTest,JdbcShadowEvidenceRepositoryTest test ``` Expected: missing evidence types. - [ ] **Step 3: Implement one evidence transaction per complete cycle** Persist immutable JSON plus indexed columns. Hash canonical UTF-8 JSON for the input batch, legacy result, decision, encoded writes, and differences. If the unique key already exists, verify the stored hash matches and return the existing cycle; a hash mismatch is `EVIDENCE_KEY_COLLISION` and stops collection. Never partially commit a cycle. - [ ] **Step 4: Add storage-pressure fail-safe** Before every cycle check configured minimum free bytes and evidence database health. When the threshold is crossed, change status to `DEGRADED:EVIDENCE_STORAGE_UNAVAILABLE`, stop new reads/calculations, retain the last complete evidence, and keep the stable WCS unaffected. Do not discard differences and continue reporting healthy. - [ ] **Step 5: Run comparison, repository, and cycle tests** Run: ```bash mvn -pl ruoyi-wcs-shadow -am \ -Dtest=ShadowComparisonServiceTest,JdbcShadowEvidenceRepositoryTest,Ft546ShadowCycleTest test ``` Expected: exact classification, idempotent replay, hash verification, atomic commit, and disk/db fail-safe pass. - [ ] **Step 6: Commit the evidence chain** ```bash git add ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/evidence \ ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/evidence \ ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/kernel \ ruoyi-wcs-shadow/src/main/java/com/ruoyi/shadow/web git commit -m "feat(ft546): persist shadow comparison evidence" ``` --- ### Task 8: Verify End-To-End Shadow Operation And Prepare Immediate Stable Rollback **Files:** - Create: `ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/Ft546ShadowEndToEndTest.java` - Create: `ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/Ft546ShadowFaultIsolationTest.java` - Create: `ruoyi-wcs-shadow/src/test/resources/fixtures/ordinary-cycle.json` - Create: `ruoyi-wcs-shadow/src/test/resources/fixtures/cross-floor-cycle.json` - Create: `ruoyi-wcs-shadow/src/test/resources/fixtures/two-stage-cycle.json` - Create: `ruoyi-wcs-shadow/src/test/resources/fixtures/hold-avoidance-cycle.json` - Create: `ruoyi-wcs-shadow/src/test/resources/application-shadow-local.yml` - Create: `tools/shadow/Start-Ft546Shadow.ps1` - Create: `tools/shadow/Stop-Ft546Shadow.ps1` - Create: `tools/shadow/Test-Ft546ShadowPreflight.ps1` - Create: `tools/shadow/Export-Ft546ShadowEvidence.ps1` - Create: `tools/shadow/Capture-StableWcsManifest.ps1` - Create: `docs/runbooks/ft546-realtime-shadow-runbook.md` - Modify: `README.md` **Interfaces:** - Preflight output: stable PID/command line/artifact SHA-256/config SHA-256/listening ports; shadow artifact/config SHA-256; source/evidence JDBC identities; permission checks; PLC/WMS network-deny checks; output free space. - Start command refuses to run without `-ConfirmReadOnlyShadow`, a successful preflight JSON, and a distinct shadow deployment directory. - Stop command stops only the recorded shadow PID and never finds/kills a process by broad Java name. - Evidence export emits `manifest.json`, `manifest.sha256`, cycle CSV/JSON, protocol-difference CSV, algorithm-difference CSV, insufficient-facts CSV, and startup checks. - [ ] **Step 1: Write end-to-end and fault-isolation tests first** ```java @Test void ordinaryCrossFloorAndTwoStageFixturesReachVirtualPlcEvidence() { ShadowRunResult result = harness.run(fixtures()); assertEquals(3, result.getComparableCycles()); assertWrites(result.forTask("ORDINARY"), "4004", "4000", "4002", "4021"); assertWrites(result.forTask("CROSS_FLOOR"), "4004", "4000", "4002", "4021", "4020", "4022"); assertWrites(result.forTask("STAGE_2"), "4404", "4422", "4423", "4402", "4400"); assertEquals("HOLD", result.forTask("AVOIDANCE").getDecision()); assertTrue(result.forTask("AVOIDANCE").getEncodedWrites().isEmpty()); assertEquals(0, result.getSideEffectCalls()); } @Test void sourceOutageEvidenceFailureAndShadowCrashNeverAffectStableProbe() { faultHarness.inject(SOURCE_TIMEOUT, EVIDENCE_DISK_FULL, SHADOW_CRASH); assertTrue(stableProbe.isContinuouslyHealthy()); assertEquals(0, sideEffectProbe.totalCalls()); } ``` Add a package/socket test proving the shadow process has no configured PLC IP/port or WMS URL, a process test proving stop/restart changes only the shadow PID, and a restart test proving persisted wait-for edges/consecutive HOLD counters are reloaded so the third identical HOLD follows the configured reroute rule instead of resetting to the first HOLD. - [ ] **Step 2: Run end-to-end tests and verify scripts/runbook are missing** Run: ```bash mvn -pl ruoyi-ft546-protocol,ruoyi-wcs,ruoyi-wcs-shadow,ruoyi-admin -am \ -Dtest=Ft546ShadowEndToEndTest,Ft546ShadowFaultIsolationTest,Ft546StandardRuntimeTest,Ft546LegacySrgvAdapterTest test ``` Expected: missing fixtures/harness or end-to-end program. - [ ] **Step 3: Implement safe operational scripts** `Capture-StableWcsManifest.ps1` records the actual deployed stable artifact path, SHA-256, command line, configuration paths/hashes, service name/PID, and listening ports; it does not assume `origin/main` is the deployed version. `Test-Ft546ShadowPreflight.ps1` verifies the shadow source login, separate evidence database, permissions, distinct PID/ports/directories, loopback status endpoint, PLC/WMS network denial, disk threshold, and stable health before start. `Start-Ft546Shadow.ps1` starts only the shadow jar with a PID file in its own directory. `Stop-Ft546Shadow.ps1` resolves and validates that exact PID and executable path before stopping it. - [ ] **Step 4: Write the site runbook with explicit stop conditions** The runbook sequence is: ```text capture stable manifest and verify stable health -> create/read-only source login and separate evidence database -> deny shadow host PLC/WMS egress at network layer -> run preflight and archive JSON -> start shadow only -> verify stable PID/health/PLC writer unchanged -> collect 15 minutes at low polling rate -> inspect source query latency and evidence growth -> extend collection window only when stable metrics are unchanged -> stop shadow immediately on any stable degradation -> export evidence and verify manifest hashes ``` Immediate stop conditions include production DB latency increase above the agreed threshold, permission-probe change, evidence disk threshold, source lag beyond freshness, any PLC/WMS connection attempt, any production-table write audit, shadow CPU/memory limit breach, or stable WCS health change. Stopping/removing the shadow service is the rollback for this phase; the stable WCS is never stopped or switched. - [ ] **Step 5: Run all automated verification from a clean build** Run: ```bash mvn clean test ``` Expected: all modules pass with zero failures/errors; record exact test count and duration. - [ ] **Step 6: Run local process smoke without production endpoints** Run: ```powershell .\tools\shadow\Test-Ft546ShadowPreflight.ps1 ` -Config .\ruoyi-wcs-shadow\src\test\resources\application-shadow-local.yml .\tools\shadow\Start-Ft546Shadow.ps1 ` -ConfirmReadOnlyShadow ` -PreflightJson .\outputs\shadow-preflight-local.json Invoke-RestMethod http://127.0.0.1:18091/shadow/status .\tools\shadow\Stop-Ft546Shadow.ps1 ``` Expected: local H2/test-view mode reaches `READY`, generates evidence, makes zero external calls, and stops only its PID. - [ ] **Step 7: Run SQL Server/HIL/site gates only in approved environments** Required evidence before site collection: - SQL Server integration: exact transaction/permission/query-timeout behavior passes against an isolated SQL Server. - Network: shadow host cannot reach PLC write endpoints or production WMS callback endpoints. - Site preflight: stable manifest captured, stable process healthy, source login read-only, evidence database distinct, and shadow directories/ports distinct. - Site observation: stable process metrics and PLC ownership stay unchanged while shadow is started/stopped repeatedly. HIL remains a later gate for real PLC timing and partial-write behavior; this shadow program does not connect to HIL or production PLC. If any gate is missing, the conclusion is limited to local or SQL integration readiness. - [ ] **Step 8: Run repository checks and request review** Run: ```bash git diff --check git status --short git log --oneline -15 ``` Reviewers receive the confirmed design, this plan, base `48b0b154b6319de05df3b71b4457d12110bdb095`, stable tracking reference `652607ce15653f12a682248ad3f335861ebd9e3f`, final HEAD, Maven evidence, preflight output, SQL permission output, process start/stop evidence, and exported manifest. Resolve all Critical/Important findings and rerun focused plus full tests. - [ ] **Step 9: Commit the runbook and acceptance harness** ```bash git add ruoyi-wcs-shadow/src/test tools/shadow \ docs/runbooks/ft546-realtime-shadow-runbook.md README.md git commit -m "test(ft546): verify read-only shadow operation" ``` --- ## End-To-End Acceptance Matrix | Layer | Required proof | Permitted conclusion | | --- | --- | --- | | Unit/domain | Runtime guards, fact validation, same-kernel decision, pure PLC encoding, difference classification | Local implementation behavior only | | Spring context | No side-effect beans/types, separate data sources, fail-closed startup | Process wiring is isolated | | Local E2E | Ordinary/cross-floor/two-stage reach virtual PLC evidence; zero side-effect calls | Local shadow flow works | | SQL Server integration | Read-only permissions, transactions, timeouts, separate evidence writes, restart/idempotency | Database integration works | | Site preflight | Stable artifact/PID/config captured; network deny; accounts/ports/directories distinct | Safe to start collection window | | Live shadow | Stable health unchanged, source query load bounded, evidence continuous, zero production side effects | Shadow observations are valid | | HIL | Real isolated PLC register sequence, disconnect, partial write, restart | Protocol adapter hardware gate only | | Cutover drill | Single-writer lease/fencing, recovery, reconciliation, rollback | Future `STANDARD` cutover gate; outside this plan | ## Plan Self-Review Checklist - Design coverage: mode isolation (Task 1), pure PLC intent (Task 2), independent launcher (Task 3), two-data-source permission gate (Task 4), read-only facts (Task 5), same-kernel cycle (Task 6), differences/evidence (Task 7), E2E/site protection (Task 8). - Stable WCS protection is independent at code, process, database, network, operations, and test layers; one configuration mistake is not the sole barrier. - `LEGACY` remains default and unchanged; no task authorizes production `STANDARD`. - `SHADOW` never uses current PLC readers as its source because those readers can connect/write and mutate production device tables. - The real adapter and shadow recorder share one pure encoder, so address/type/value/order comparisons do not rely on duplicated protocol code. - Production source and evidence data sources are explicit and physically distinct; no ThreadLocal routing or transaction-time switching is used. - Every partial/invalid fact path closes as `INSUFFICIENT_FACTS`; no fabricated comparison is emitted. - Protocol and algorithm differences have distinct enums and tests. - Ordinary, cross-floor, two-stage, HOLD/avoidance inputs, source outage, evidence failure, restart, and stable-process isolation all have named automated tests. - SQL Server, site, network, and HIL evidence boundaries are explicit; local tests cannot be reported as production acceptance. - Every task contains concrete files, interfaces, red/green commands, expected results, and explicit failure behavior; no deferred implementation placeholder remains.