# WMS-Native Tidy Recovery Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace WMS restart recovery's WCS `tidyTaskState` dependency with transactional decisions based only on WMS workflow, job, inventory, cargo-space, and lock evidence. **Architecture:** `WmsAlgorithmMapper` returns one immutable `TidyRecoveryEvidence` projection for each non-terminal tidy workflow. `TidyWorkflowRecoveryService` applies the approved decision table inside the WMS transaction manager and delegates a locally proven archived completion to `TidyWorkflowTransaction` with callback stage `RECOVERY_FINISHED`; no recovery path performs HTTP or repeats inventory movement. **Tech Stack:** Java 17 records, Spring Boot transactions, MyBatis XML, SQL Server, JUnit 5, AssertJ, Mockito, PowerShell 7/Pester, Node.js built-in HTTP simulator. ## Global Constraints - A tidy move remains a normal WMS job with `in_or_out='28'`; normal completion continues through `alg.usp_complete_move_job` and `jobFinish`. - WMS-local state is the only completion and restart-recovery authority; do not add or retain a WCS task-state API. - Keep `tidy-wcs-snapshot-url` and `WcsVehicleSnapshotClient` unchanged because manual capacity still needs vehicle availability. - Never call `alg.usp_complete_move_job` during recovery of an already archived job. - A workflow in `PAUSED_RECOVERY` is never changed automatically and its active locks are never released automatically. - Database/query exceptions propagate out of the transactional recovery method; they are not translated into recovery reasons. - Preserve normal `MOVE_FINISHED` and `OUTBOUND_FINISHED` callback behavior and duplicate-callback idempotency. - Do not change outbound-relocation recovery or unrelated WMS/WCS endpoints. - The WMS repository has extensive user changes. Stage and commit only the exact files listed by each task; never reset, unstage, or overwrite unrelated work. - `D:/workSpace/PROJECT_SPACE/FT546` has no usable root Git metadata. Replay and handoff files outside `FT546MWS/FT546MWS` are verified but are not committed unless that repository is restored by the user. --- ## File Structure - Create `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyRecoveryEvidence.java`: immutable database-shaped recovery projection only. - Modify `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java`: expose the local-evidence query. - Modify `ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml`: select the first incomplete step and its active/finished job, stock, cargo-space, and lock evidence. - Modify `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryService.java`: implement the approved decision table without HTTP. - Modify `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransaction.java`: remove the special `UNKNOWN` callback branch while retaining the shared idempotent advancement path. - Delete `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskState.java`: obsolete WCS state DTO. - Delete `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateClient.java`: obsolete HTTP adapter. - Delete `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowState.java`: obsolete WCS-oriented recovery projection. - Modify `fortucky-algorithm/src/main/java/com/ruoyi/algorithm/integration/AlgorithmProperties.java`: remove only task-state URL/timeout properties. - Modify `ruoyi-admin/src/main/resources/application.yml`: remove only task-state configuration. - Modify focused JUnit tests under `ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms`: prove SQL shape, every recovery decision, callback idempotency, and dependency removal. - Modify `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/Invoke-Ft546TidyBatchReplay.ps1`: replace the WCS UNKNOWN scenario with repairable-finish and missing-local-job cases. - Modify `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/simulators/ft546-wcs-simulator.mjs`: remove task-state simulator state and endpoints. - Modify `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/tests/Invoke-Ft546TidyBatchReplay.Tests.ps1`: enforce the new replay contract. - Modify historical design/plan notes, the root handoff, and `tools/history-replay/README.md`: explain supersession and point to new evidence. ### Task 1: WMS Local Recovery Evidence Projection **Files:** - Create: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyRecoveryEvidence.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java` - Modify: `ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml` - Test: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapperWorkflowSqlTest.java` **Interfaces:** - Produces: `List selectTidyRecoveryEvidence(String warehouseId)`. - Produces: nullable `stepId`/`stepNo` when every step is complete; all count fields are non-null `int` values. - Produces: job fields from the deterministic ID `TDY--` even when the row disagrees with the step tuple. - [ ] **Step 1: Write failing SQL contract tests** Add these tests to `WmsAlgorithmMapperWorkflowSqlTest`: ```java @Test void tidyRecoveryProjectionMapsWorkflowJobInventoryAndLockEvidence() throws IOException { String mapping = statement("resultmap", "tidyRecoveryEvidenceMap"); assertThat(mapping) .contains("type=\"com.ruoyi.algorithm.wms.tidy.tidyrecoveryevidence\"") .contains("column=\"workflowid\"") .contains("column=\"stepid\"") .contains("column=\"activejobid\"") .contains("column=\"finishedjobid\"") .contains("column=\"sourcestockrows\"") .contains("column=\"targetstockrows\"") .contains("column=\"activelockcount\""); } @Test void tidyRecoveryQueryUsesOnlyWmsLocalAuthoritativeTables() throws IOException { String query = statement("select", "selectTidyRecoveryEvidence"); assertThat(query) .contains("from alg.tidy_workflow workflow") .contains("outer apply") .contains("from alg.tidy_step step") .contains("left join dbo.mydo_job active_job") .contains("left join dbo.mydo_job_finish finished_job") .contains("from dbo.bas_stock") .contains("left join dbo.bas_cargo_space source_space") .contains("left join dbo.bas_cargo_space target_space") .contains("from alg.tidy_space_lock") .contains("concat('tdy-', workflow.workflow_id, '-', active_step.step_id)") .doesNotContain("wcs_task_id"); } ``` - [ ] **Step 2: Run the focused test and confirm the red state** Run from `D:/workSpace/PROJECT_SPACE/FT546/FT546MWS/FT546MWS`: ```powershell mvn -pl ruoyi-admin -am "-Dtest=WmsAlgorithmMapperWorkflowSqlTest" "-Dsurefire.failIfNoSpecifiedTests=false" test ``` Expected: FAIL because `tidyRecoveryEvidenceMap` and `selectTidyRecoveryEvidence` do not exist. - [ ] **Step 3: Add the immutable projection and mapper signature** Create `TidyRecoveryEvidence.java` exactly as a data-only record: ```java package com.ruoyi.algorithm.wms.tidy; public record TidyRecoveryEvidence( String workflowId, TidyOrigin origin, String workflowStatus, int activeLockCount, String stepId, Integer stepNo, String stepStatus, String sourceSpaceId, String targetSpaceId, String trayCode, int activeTidyJobCount, String activeJobId, String activeJobStatus, String activeJobInOrOut, String activeJobTrayCode, String activeJobSourceSpaceId, String activeJobTargetSpaceId, String finishedJobId, String finishedJobStatus, String finishedJobInOrOut, String finishedJobTrayCode, String finishedJobSourceSpaceId, String finishedJobTargetSpaceId, int sourceStockRows, int targetStockRows, String sourceSpaceStatus, String sourceSpaceTrayCode, String targetSpaceStatus, String targetSpaceTrayCode) { } ``` Import it in `WmsAlgorithmMapper` and add: ```java List selectTidyRecoveryEvidence(@Param("warehouseId") String warehouseId); ``` - [ ] **Step 4: Implement the database-shaped result map and local-evidence SQL** Replace the old recovery result map/query only after adding this new query, so the project remains compilable between tasks. The SQL must use `OUTER APPLY` so a workflow whose steps are all complete is still returned: ```xml ``` - [ ] **Step 5: Run the mapper test and commit the projection** ```powershell mvn -pl ruoyi-admin -am "-Dtest=WmsAlgorithmMapperWorkflowSqlTest" "-Dsurefire.failIfNoSpecifiedTests=false" test git add ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyRecoveryEvidence.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapperWorkflowSqlTest.java git commit --only -m "feat: project local tidy recovery evidence" ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyRecoveryEvidence.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapperWorkflowSqlTest.java ``` Expected: the focused test passes and the commit contains only the four listed files. ### Task 2: Transactional Recovery Decision Service **Files:** - Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryService.java` - Test: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryServiceTest.java` **Interfaces:** - Consumes: `List selectTidyRecoveryEvidence(String warehouseId)` from Task 1. - Consumes: `boolean TidyWorkflowTransaction.onJobFinished(String jobId, String callbackStage)` with `callbackStage="RECOVERY_FINISHED"`. - Produces: `int reconcile(String warehouseId)`, counting workflows changed by completion repair, terminal cleanup, or pause; no-op and duplicate repairs return zero. - [ ] **Step 1: Replace WCS-oriented unit tests with the local decision table** Use a test builder that creates a matching active job in the valid pre-move state by default. The test class must include these assertions: ```java private WmsAlgorithmMapper mapper; private TidyWorkflowTransaction transaction; private TidyWorkflowRecoveryService service; @BeforeEach void setUp() { mapper = mock(WmsAlgorithmMapper.class); transaction = mock(TidyWorkflowTransaction.class); service = new TidyWorkflowRecoveryService(mapper, transaction); } @Test void matchingActiveJobWithPreMoveEvidenceRemainsRunning() { TidyRecoveryEvidence evidence = matching().build(); when(mapper.selectTidyRecoveryEvidence("01")).thenReturn(List.of(evidence)); assertThat(service.reconcile("01")).isZero(); verifyNoInteractions(transaction); verify(mapper, never()).pauseTidyWorkflow(anyString(), anyString()); verify(mapper, never()).releaseTidyResources(anyString()); } @Test void matchingArchivedFinishAdvancesOnceWithoutMovingInventoryAgain() { TidyRecoveryEvidence evidence = matching().finished().build(); when(mapper.selectTidyRecoveryEvidence("01")).thenReturn(List.of(evidence)); when(transaction.onJobFinished(evidence.finishedJobId(), "RECOVERY_FINISHED")) .thenReturn(true, false); assertThat(service.reconcile("01")).isEqualTo(1); assertThat(service.reconcile("01")).isZero(); verify(transaction, times(2)).onJobFinished(evidence.finishedJobId(), "RECOVERY_FINISHED"); verify(mapper, never()).releaseTidyResources(evidence.workflowId()); } @Test void completedStepsWithoutAnActiveTidyJobFinishWorkflowAndReleaseLocks() { TidyRecoveryEvidence evidence = matching().allStepsCompleted().build(); when(mapper.selectTidyRecoveryEvidence("01")).thenReturn(List.of(evidence)); assertThat(service.reconcile("01")).isEqualTo(1); verify(mapper).completeTidyWorkflow(evidence.workflowId()); verify(mapper).releaseTidyResources(evidence.workflowId()); } @ParameterizedTest(name = "{0}") @MethodSource("pauseCases") void inconsistentLocalEvidencePausesWithoutReleasingLocks( String label, TidyRecoveryEvidence evidence, String expectedReason) { when(mapper.selectTidyRecoveryEvidence("01")).thenReturn(List.of(evidence)); assertThat(service.reconcile("01")).isEqualTo(1); verify(mapper).pauseTidyWorkflow(evidence.workflowId(), expectedReason); verify(mapper).pauseTidyStep(evidence.workflowId(), evidence.stepNo(), expectedReason); verify(mapper, never()).releaseTidyResources(anyString()); verifyNoInteractions(transaction); } private static Stream pauseCases() { return Stream.of( Arguments.of("invalid active inventory", matching().sourceStockRows(0).build(), "RECOVERY_ACTIVE_EVIDENCE_INVALID"), Arguments.of("missing active and finish jobs", matching().missingJobs().build(), "RECOVERY_JOB_MISSING"), Arguments.of("active job type mismatch", matching().activeJobInOrOut("27").build(), "RECOVERY_JOB_MISMATCH"), Arguments.of("active job tray mismatch", matching().activeJobTrayCode("OTHER").build(), "RECOVERY_JOB_MISMATCH"), Arguments.of("active job source mismatch", matching().activeJobSourceSpaceId("OTHER").build(), "RECOVERY_JOB_MISMATCH"), Arguments.of("finish status is not terminal", matching().finished().finishedJobStatus("3").build(), "RECOVERY_FINISH_EVIDENCE_INVALID"), Arguments.of("finish job target mismatch", matching().finished().finishedJobTargetSpaceId("OTHER").build(), "RECOVERY_JOB_MISMATCH"), Arguments.of("finish inventory is not terminal", matching().finished().sourceStockRows(1).build(), "RECOVERY_FINISH_EVIDENCE_INVALID"), Arguments.of("active and finish rows coexist", matching().withBothJobs().build(), "RECOVERY_FINISH_EVIDENCE_INVALID")); } @Test void alreadyPausedWorkflowIsLeftUntouched() { TidyRecoveryEvidence evidence = matching().workflowStatus("PAUSED_RECOVERY").build(); when(mapper.selectTidyRecoveryEvidence("01")).thenReturn(List.of(evidence)); assertThat(service.reconcile("01")).isZero(); verifyNoInteractions(transaction); verify(mapper, never()).pauseTidyWorkflow(anyString(), anyString()); } @Test void queryFailurePropagatesWithoutWritingRecoveryState() { when(mapper.selectTidyRecoveryEvidence("01")).thenThrow(new IllegalStateException("db unavailable")); assertThatThrownBy(() -> service.reconcile("01")) .isInstanceOf(IllegalStateException.class) .hasMessage("db unavailable"); verify(mapper, never()).pauseTidyWorkflow(anyString(), anyString()); verifyNoInteractions(transaction); } ``` Define the referenced builder in the same test class. Its defaults represent a valid pre-move state, while its named mutations create each exact decision-table row: ```java private static EvidenceBuilder matching() { return new EvidenceBuilder(); } private static final class EvidenceBuilder { private String workflowId = "WF-1"; private TidyOrigin origin = TidyOrigin.MANUAL_TIDY; private String workflowStatus = "RUNNING"; private int activeLockCount = 2; private String stepId = "WF-1-1"; private Integer stepNo = 1; private String stepStatus = "PENDING"; private String sourceSpaceId = "SOURCE"; private String targetSpaceId = "TARGET"; private String trayCode = "TRAY-1"; private int activeTidyJobCount = 1; private String activeJobId = "TDY-WF-1-WF-1-1"; private String activeJobStatus = "1"; private String activeJobInOrOut = "28"; private String activeJobTrayCode = "TRAY-1"; private String activeJobSourceSpaceId = "SOURCE"; private String activeJobTargetSpaceId = "TARGET"; private String finishedJobId; private String finishedJobStatus; private String finishedJobInOrOut; private String finishedJobTrayCode; private String finishedJobSourceSpaceId; private String finishedJobTargetSpaceId; private int sourceStockRows = 1; private int targetStockRows; private String sourceSpaceStatus = "2"; private String sourceSpaceTrayCode = "TRAY-1"; private String targetSpaceStatus = "1"; private String targetSpaceTrayCode = ""; EvidenceBuilder workflowStatus(String value) { workflowStatus = value; return this; } EvidenceBuilder activeJobInOrOut(String value) { activeJobInOrOut = value; return this; } EvidenceBuilder activeJobTrayCode(String value) { activeJobTrayCode = value; return this; } EvidenceBuilder activeJobSourceSpaceId(String value) { activeJobSourceSpaceId = value; return this; } EvidenceBuilder sourceStockRows(int value) { sourceStockRows = value; return this; } EvidenceBuilder finishedJobStatus(String value) { finishedJobStatus = value; return this; } EvidenceBuilder finishedJobTargetSpaceId(String value) { finishedJobTargetSpaceId = value; return this; } EvidenceBuilder finished() { activeTidyJobCount = 0; activeJobId = null; activeJobStatus = null; activeJobInOrOut = null; activeJobTrayCode = null; activeJobSourceSpaceId = null; activeJobTargetSpaceId = null; finishedJobId = "TDY-WF-1-WF-1-1"; finishedJobStatus = "4"; finishedJobInOrOut = "28"; finishedJobTrayCode = trayCode; finishedJobSourceSpaceId = sourceSpaceId; finishedJobTargetSpaceId = targetSpaceId; sourceStockRows = 0; targetStockRows = 1; sourceSpaceStatus = "1"; sourceSpaceTrayCode = ""; targetSpaceStatus = "2"; targetSpaceTrayCode = trayCode; return this; } EvidenceBuilder missingJobs() { activeTidyJobCount = 0; activeJobId = null; activeJobStatus = null; activeJobInOrOut = null; activeJobTrayCode = null; activeJobSourceSpaceId = null; activeJobTargetSpaceId = null; finishedJobId = null; finishedJobStatus = null; finishedJobInOrOut = null; finishedJobTrayCode = null; finishedJobSourceSpaceId = null; finishedJobTargetSpaceId = null; return this; } EvidenceBuilder withBothJobs() { finishedJobId = activeJobId; finishedJobStatus = "4"; finishedJobInOrOut = "28"; finishedJobTrayCode = trayCode; finishedJobSourceSpaceId = sourceSpaceId; finishedJobTargetSpaceId = targetSpaceId; return this; } EvidenceBuilder allStepsCompleted() { missingJobs(); stepId = null; stepNo = null; stepStatus = null; sourceSpaceId = null; targetSpaceId = null; trayCode = null; sourceStockRows = 0; targetStockRows = 0; sourceSpaceStatus = null; sourceSpaceTrayCode = null; targetSpaceStatus = null; targetSpaceTrayCode = null; return this; } TidyRecoveryEvidence build() { return new TidyRecoveryEvidence( workflowId, origin, workflowStatus, activeLockCount, stepId, stepNo, stepStatus, sourceSpaceId, targetSpaceId, trayCode, activeTidyJobCount, activeJobId, activeJobStatus, activeJobInOrOut, activeJobTrayCode, activeJobSourceSpaceId, activeJobTargetSpaceId, finishedJobId, finishedJobStatus, finishedJobInOrOut, finishedJobTrayCode, finishedJobSourceSpaceId, finishedJobTargetSpaceId, sourceStockRows, targetStockRows, sourceSpaceStatus, sourceSpaceTrayCode, targetSpaceStatus, targetSpaceTrayCode); } } ``` - [ ] **Step 2: Run the recovery test and confirm the red state** ```powershell mvn -pl ruoyi-admin -am "-Dtest=TidyWorkflowRecoveryServiceTest" "-Dsurefire.failIfNoSpecifiedTests=false" test ``` Expected: compilation or test failure because the service still requires `WcsTaskStateClient` and calls `selectActiveTidyWorkflows`. - [ ] **Step 3: Implement the minimal transactional decision service** Replace `TidyWorkflowRecoveryService` with this decision structure; keep all comparison helpers in this service, not in the projection: ```java @Service public class TidyWorkflowRecoveryService { private static final Set ACTIVE_JOB_STATUSES = Set.of("1", "2", "3"); private final WmsAlgorithmMapper mapper; private final TidyWorkflowTransaction transaction; public TidyWorkflowRecoveryService(WmsAlgorithmMapper mapper, TidyWorkflowTransaction transaction) { this.mapper = mapper; this.transaction = transaction; } @Transactional(rollbackFor = Exception.class) public int reconcile(String warehouseId) { int changed = 0; for (TidyRecoveryEvidence evidence : mapper.selectTidyRecoveryEvidence(warehouseId)) { if ("PAUSED_RECOVERY".equals(evidence.workflowStatus())) { continue; } if (evidence.stepId() == null) { if (evidence.activeTidyJobCount() == 0) { mapper.completeTidyWorkflow(evidence.workflowId()); mapper.releaseTidyResources(evidence.workflowId()); changed++; } else { changed += pause(evidence, "RECOVERY_JOB_MISMATCH"); } continue; } if (evidence.activeJobId() != null && evidence.finishedJobId() != null) { changed += pause(evidence, "RECOVERY_FINISH_EVIDENCE_INVALID"); continue; } if (evidence.activeJobId() != null) { if (!matchesStep(evidence, false)) { changed += pause(evidence, "RECOVERY_JOB_MISMATCH"); } else if (ACTIVE_JOB_STATUSES.contains(evidence.activeJobStatus()) && isPreMoveState(evidence)) { continue; } else { changed += pause(evidence, "RECOVERY_ACTIVE_EVIDENCE_INVALID"); } continue; } if (evidence.finishedJobId() != null) { if (!matchesStep(evidence, true)) { changed += pause(evidence, "RECOVERY_JOB_MISMATCH"); } else if ("4".equals(evidence.finishedJobStatus()) && isTerminalMoveState(evidence)) { if (transaction.onJobFinished(evidence.finishedJobId(), "RECOVERY_FINISHED")) { changed++; } } else { changed += pause(evidence, "RECOVERY_FINISH_EVIDENCE_INVALID"); } continue; } changed += pause(evidence, "RECOVERY_JOB_MISSING"); } return changed; } private boolean matchesStep(TidyRecoveryEvidence evidence, boolean finished) { return "28".equals(finished ? evidence.finishedJobInOrOut() : evidence.activeJobInOrOut()) && Objects.equals(evidence.trayCode(), finished ? evidence.finishedJobTrayCode() : evidence.activeJobTrayCode()) && Objects.equals(evidence.sourceSpaceId(), finished ? evidence.finishedJobSourceSpaceId() : evidence.activeJobSourceSpaceId()) && Objects.equals(evidence.targetSpaceId(), finished ? evidence.finishedJobTargetSpaceId() : evidence.activeJobTargetSpaceId()); } private boolean isPreMoveState(TidyRecoveryEvidence evidence) { return evidence.sourceStockRows() > 0 && evidence.targetStockRows() == 0 && "2".equals(evidence.sourceSpaceStatus()) && Objects.equals(evidence.trayCode(), trimToNull(evidence.sourceSpaceTrayCode())) && "1".equals(evidence.targetSpaceStatus()) && trimToNull(evidence.targetSpaceTrayCode()) == null; } private boolean isTerminalMoveState(TidyRecoveryEvidence evidence) { return evidence.sourceStockRows() == 0 && evidence.targetStockRows() > 0 && "1".equals(evidence.sourceSpaceStatus()) && trimToNull(evidence.sourceSpaceTrayCode()) == null && "2".equals(evidence.targetSpaceStatus()) && Objects.equals(evidence.trayCode(), trimToNull(evidence.targetSpaceTrayCode())); } private int pause(TidyRecoveryEvidence evidence, String reason) { mapper.pauseTidyWorkflow(evidence.workflowId(), reason); if (evidence.stepNo() != null) { mapper.pauseTidyStep(evidence.workflowId(), evidence.stepNo(), reason); } return 1; } private String trimToNull(String value) { if (value == null || value.trim().isEmpty()) return null; return value.trim(); } } ``` Add imports for `java.util.Objects` and `java.util.Set`. Do not catch mapper exceptions and do not call `releaseTidyResources` from `pause`. - [ ] **Step 4: Run service, transaction, and mapper tests** ```powershell mvn -pl ruoyi-admin -am "-Dtest=TidyWorkflowRecoveryServiceTest,TidyWorkflowTransactionTest,WmsAlgorithmMapperWorkflowSqlTest" "-Dsurefire.failIfNoSpecifiedTests=false" test ``` Expected: PASS; Mockito proves no lock release on every paused path and no inventory procedure is reachable from the service. - [ ] **Step 5: Commit the recovery decision** ```powershell git add ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryService.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryServiceTest.java git commit --only -m "feat: recover tidy workflows from WMS evidence" ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryService.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowRecoveryServiceTest.java ``` ### Task 3: Remove WCS Task-State and UNKNOWN Callback Dependencies **Files:** - Create: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateDependencyRemovalTest.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransaction.java` - Modify: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransactionTest.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java` - Modify: `ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml` - Modify: `fortucky-algorithm/src/main/java/com/ruoyi/algorithm/integration/AlgorithmProperties.java` - Modify: `ruoyi-admin/src/main/resources/application.yml` - Delete: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskState.java` - Delete: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateClient.java` - Delete: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowState.java` **Interfaces:** - Retains: `tidyWcsSnapshotUrl` and its getter/setter. - Removes: task-state URL, timeout, DTO, HTTP client, old mapper projection/query, and the `UNKNOWN` callback branch. - Retains: `RECOVERY_FINISHED` on the same callback-event idempotency key used by normal completion. - [ ] **Step 1: Add a failing absence test and a positive recovery-stage test** Create `WcsTaskStateDependencyRemovalTest`: ```java class WcsTaskStateDependencyRemovalTest { @Test void taskStateClassesPropertiesAndConfigurationAreAbsent() throws Exception { assertThatThrownBy(() -> Class.forName( "com.ruoyi.algorithm.wms.tidy.WcsTaskStateClient")) .isInstanceOf(ClassNotFoundException.class); assertThatThrownBy(() -> Class.forName( "com.ruoyi.algorithm.wms.tidy.WcsTaskState")) .isInstanceOf(ClassNotFoundException.class); assertThat(Arrays.stream(AlgorithmProperties.class.getMethods()) .map(Method::getName)) .noneMatch(name -> name.contains("TidyWcsTaskState")); try (InputStream stream = getClass().getResourceAsStream("/application.yml")) { assertThat(stream).isNotNull(); String yaml = new String(stream.readAllBytes(), StandardCharsets.UTF_8); assertThat(yaml).doesNotContain("tidy-wcs-task-state"); assertThat(yaml).contains("tidy-wcs-snapshot-url"); } } } ``` Replace the old `unknownCallbackPausesWorkflowAndKeepsResources` transaction test with: ```java @Test void recoveryFinishedUsesTheNormalIdempotentAdvancementPath() { WmsAlgorithmMapper mapper = mock(WmsAlgorithmMapper.class); ExecutionAdmissionService admission = mock(ExecutionAdmissionService.class); TidyWorkflowTransaction tx = new TidyWorkflowTransaction(mapper, admission); when(mapper.selectTidyWorkflowIdByJob("JOB-1")).thenReturn("WF-1"); when(mapper.selectTidyStepIdByJob("JOB-1")).thenReturn("STEP-1"); when(mapper.insertTidyCallbackEventIfAbsent(any(), eq("WF-1"), eq("STEP-1"), eq("JOB-1"), eq("RECOVERY_FINISHED"), eq("SUCCESS"))).thenReturn(1); when(mapper.selectNextTidyStepId("WF-1")).thenReturn(null); assertThat(tx.onJobFinished("JOB-1", "RECOVERY_FINISHED")).isTrue(); verify(mapper).completeTidyStep("STEP-1"); verify(mapper).completeTidyWorkflow("WF-1"); verify(mapper).releaseTidyResources("WF-1"); verify(mapper, never()).pauseTidyWorkflow(anyString(), anyString()); } ``` - [ ] **Step 2: Run clean focused tests and confirm the absence test is red** ```powershell mvn -pl ruoyi-admin -am clean "-Dtest=WcsTaskStateDependencyRemovalTest,TidyWorkflowTransactionTest" "-Dsurefire.failIfNoSpecifiedTests=false" test ``` Expected: FAIL because task-state classes, properties, and YAML keys still exist. - [ ] **Step 3: Delete the dependency and obsolete recovery projection** Delete the three listed Java files. Remove these members from `AlgorithmProperties` and nothing related to the snapshot URL: ```java private String tidyWcsTaskStateUrl; private int tidyWcsTaskStateTimeoutMs; public String getTidyWcsTaskStateUrl(); public void setTidyWcsTaskStateUrl(String value); public int getTidyWcsTaskStateTimeoutMs(); public void setTidyWcsTaskStateTimeoutMs(int value); ``` Remove these two YAML entries: ```yaml tidy-wcs-task-state-url: ${FORTUCKY_TIDY_WCS_TASK_STATE_URL:http://localhost:8081/wcs2Wms/api/tidyTaskState} tidy-wcs-task-state-timeout-ms: ${FORTUCKY_TIDY_WCS_TASK_STATE_TIMEOUT_MS:3000} ``` Remove `selectActiveTidyWorkflows`, its `TidyWorkflowState` import/signature, `tidyWorkflowStateMap`, and the old XML query. In `TidyWorkflowTransaction`, remove only: ```java if ("UNKNOWN".equals(callbackStage)) { mapper.pauseTidyWorkflow(workflowId, "PAUSED_RECOVERY"); return true; } ``` - [ ] **Step 4: Verify dependency removal and callback regressions** ```powershell mvn -pl ruoyi-admin -am clean "-Dtest=WcsTaskStateDependencyRemovalTest,TidyWorkflowRecoveryServiceTest,TidyWorkflowTransactionTest,ToWmsServiceImplWorkflowCallbackTest" "-Dsurefire.failIfNoSpecifiedTests=false" test rg -n "WcsTaskState|tidyWcsTaskState|tidy-wcs-task-state|FORTUCKY_TIDY_WCS_TASK_STATE|UNKNOWN.*callbackStage" fortucky-algorithm/src/main ruoyi-admin/src/main ``` Expected: Maven PASS. The search prints no matches. A separate search for `tidy-wcs-snapshot-url` still finds the retained snapshot configuration. - [ ] **Step 5: Commit only WMS dependency-removal files** ```powershell git add fortucky-algorithm/src/main/java/com/ruoyi/algorithm/integration/AlgorithmProperties.java ruoyi-admin/src/main/resources/application.yml ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransaction.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransactionTest.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateDependencyRemovalTest.java git add -u ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskState.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateClient.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowState.java git commit --only -m "refactor: remove WCS tidy task-state recovery" fortucky-algorithm/src/main/java/com/ruoyi/algorithm/integration/AlgorithmProperties.java ruoyi-admin/src/main/resources/application.yml ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/WmsAlgorithmMapper.java ruoyi-admin/src/main/resources/mapper/algorithm/WmsAlgorithmMapper.xml ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransaction.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowTransactionTest.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateDependencyRemovalTest.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskState.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/WcsTaskStateClient.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/wms/tidy/TidyWorkflowState.java ``` ### Task 4: Replace UNKNOWN Replay with WMS-Local Recovery Cases **Files:** - Modify: `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/Invoke-Ft546TidyBatchReplay.ps1` - Modify: `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/simulators/ft546-wcs-simulator.mjs` - Test: `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/tests/Invoke-Ft546TidyBatchReplay.Tests.ps1` **Interfaces:** - Produces replay event `TIDY_RECOVERY_FINISHED` for archived completion repaired after restart. - Produces replay event `TIDY_RECOVERY_MISSING_JOB` for a run-scoped deleted active job paused with retained locks. - Produces two `tidy-reconciliation.csv` rows with phases `RECOVERY_FINISHED` and `RECOVERY_MISSING_JOB`. - Removes simulator endpoints `/sim/task-state` and `/sim/unknown-task` and environment variable `FORTUCKY_TIDY_WCS_TASK_STATE_URL`. - [ ] **Step 1: Change Pester expectations first** In the automatic/manual control test, replace the old positive task-state assertions with: ```powershell $source | Should Match 'TIDY_RECOVERY_FINISHED' $source | Should Match 'TIDY_RECOVERY_MISSING_JOB' $source | Should Match 'RECOVERY_FINISHED' $source | Should Match 'RECOVERY_JOB_MISSING' $source | Should Match 'usp_complete_move_job' $source | Should Match 'DELETE FROM dbo\.mydo_job' $source | Should Not Match 'FORTUCKY_TIDY_WCS_TASK_STATE_URL' $source | Should Not Match "Invoke-Simulator '/sim/unknown-task'" $simulator = Get-Content -LiteralPath $simulatorPath -Raw $simulator | Should Not Match '/sim/task-state' $simulator | Should Not Match '/sim/unknown-task' ``` Add a contract assertion that the runner inspects `/sim/events` and requires zero task-state paths after both recovery cases. - [ ] **Step 2: Run Pester and confirm the red state** Run from `D:/workSpace/PROJECT_SPACE/FT546`: ```powershell Invoke-Pester .\tools\history-replay\tests\Invoke-Ft546TidyBatchReplay.Tests.ps1 -Output Detailed ``` Expected: FAIL on the old environment variable, simulator endpoints, and missing local-recovery events. - [ ] **Step 3: Remove task-state behavior from the simulator and WMS process environment** Delete `unknownNextTask`, both task-state routes, and the reset assignment from `ft546-wcs-simulator.mjs`. Keep vehicle snapshot, receive-job, completion, event, and reset routes unchanged. Remove only this environment entry from `Start-WmsProcess`: ```powershell FORTUCKY_TIDY_WCS_TASK_STATE_URL = "http://127.0.0.1:$SimulatorPort/sim/task-state" ``` - [ ] **Step 4: Add run-scoped SQL helpers for the two crash boundaries** Add these helpers beside `Complete-TidyWorkflow`: ```powershell function Complete-TidyJobWithoutCallback([string]$JobId) { $jobLiteral = ConvertTo-SqlLiteral $JobId $result = @(Invoke-Query @" DECLARE @result int; EXEC alg.usp_complete_move_job @v_job_id=$jobLiteral, @v_result=@result OUTPUT; SELECT @result AS result_code; "@) if ($result.Count -ne 1 -or [int]$result[0].result_code -ne 10) { throw "Crash-boundary tidy completion failed for $JobId" } } function Remove-ActiveTidyJob([string]$JobId) { $deleted = [int](Invoke-NonQuery "DELETE FROM dbo.mydo_job WHERE job_id=$(ConvertTo-SqlLiteral $JobId) AND in_or_out='28'") if ($deleted -ne 1) { throw "Expected one active tidy job to be removed for $JobId; deleted=$deleted" } } ``` These mutations run only after the verified backup. They must be recorded with `Add-Event` and are cleaned by the existing `finally` restore. - [ ] **Step 5: Implement repairable archived-completion recovery** Replace the old UNKNOWN block with this ordered flow: 1. Start WMS with a fast automatic scan and wait for a fresh `AUTO_TIDY` workflow and its active `TDY-%` job. 2. Stop WMS. 3. Capture the tray/source/target and current matching `move_msg` count. 4. Call `Complete-TidyJobWithoutCallback`; assert the active row is gone, one status `4` finish row exists, the tidy step is still non-complete, stock is at target, source is empty, and target holds the tray. 5. Capture the post-procedure `move_msg` count, restart WMS with automatic scan quiet and reconciliation at 1000 ms, then wait for the step/workflow to advance. 6. Assert exactly one `RECOVERY_FINISHED` callback event, no active deterministic job, unchanged post-procedure `move_msg` count, and zero active locks when the workflow is terminal. 7. Write `TIDY_RECOVERY_FINISHED` and a passing `RECOVERY_FINISHED` reconciliation row. Use these acceptance predicates in the runner: ```powershell $repairPass = [string]$repairAfter.status -eq 'COMPLETED' -and [int]$repairAfter.active_jobs -eq 0 -and [int]$repairAfter.finish_jobs -eq 1 -and [int]$repairAfter.recovery_events -eq 1 -and [int]$repairAfter.active_locks -eq 0 -and [int]$repairAfter.move_messages -eq $moveMessagesAfterProcedure ``` - [ ] **Step 6: Implement missing-local-job recovery** 1. Start WMS with a fast automatic scan and wait for another fresh workflow/job, excluding the normal and repaired workflow IDs. 2. Stop WMS and call `Remove-ActiveTidyJob`. 3. Restart WMS with automatic scan quiet and reconciliation at 1000 ms. 4. Wait for workflow status `PAUSED_RECOVERY`, workflow `status_reason='RECOVERY_JOB_MISSING'`, at least one paused step, and active lock count greater than zero. 5. Assert no replacement deterministic job and no `RECOVERY_FINISHED` event. 6. Write `TIDY_RECOVERY_MISSING_JOB` and a passing `RECOVERY_MISSING_JOB` reconciliation row. Use these acceptance predicates: ```powershell $missingPass = [string]$missingAfter.status -eq 'PAUSED_RECOVERY' -and [string]$missingAfter.status_reason -eq 'RECOVERY_JOB_MISSING' -and [int]$missingAfter.paused_steps -ge 1 -and [int]$missingAfter.active_locks -gt 0 -and [int]$missingAfter.active_jobs -eq 0 -and [int]$missingAfter.recovery_events -eq 0 ``` Finally query simulator events and fail the run unless both task-state paths are absent: ```powershell $simulatorEvents = @((Invoke-Simulator '/sim/events').data) $taskStateRequests = @($simulatorEvents | Where-Object { $_.path -in @('/sim/task-state', '/sim/unknown-task') }).Count if ($taskStateRequests -ne 0) { throw "Unexpected WCS task-state requests observed: $taskStateRequests" } ``` - [ ] **Step 7: Run Pester to green** ```powershell Invoke-Pester .\tools\history-replay\tests\Invoke-Ft546TidyBatchReplay.Tests.ps1 -Output Detailed ``` Expected: all contract tests pass; the exact total may increase from the historical 25 as the two new contracts are added. No Git commit is attempted for this task because `D:/workSpace/PROJECT_SPACE/FT546` is not a Git repository. ### Task 5: Documentation, Full Regression, and Final Replay Evidence **Files:** - Modify: `docs/superpowers/specs/2026-07-22-ft546-tidy-and-batch-replay-design.md` - Modify: `docs/superpowers/plans/2026-07-22-ft546-tidy-and-batch-replay.md` - Modify: `D:/workSpace/PROJECT_SPACE/FT546/tools/history-replay/README.md` - Modify: `D:/workSpace/PROJECT_SPACE/FT546/docs/FT546-WMS-history-replay-handoff-2026-07-22.md` - Generate: `D:/workSpace/PROJECT_SPACE/FT546/outputs/ft546-tidy-batch-replay-/report.md` **Interfaces:** - Historical documents remain intact except for a dated supersession note linking to the approved 2026-07-23 design and this implementation plan. - The handoff reports the new run ID, recovery evidence, unit/Pester totals, final JAR hash, and restoration result. - [ ] **Step 1: Add supersession notes before changing historical claims** At the WCS recovery sections in both 2026-07-22 documents, add: ```markdown > Superseded on 2026-07-23: WCS task-state lookup and the UNKNOWN-task replay case are replaced by WMS-native evidence recovery. See `docs/superpowers/specs/2026-07-23-wms-native-tidy-recovery-design.md` and `docs/superpowers/plans/2026-07-23-wms-native-tidy-recovery.md`. Vehicle snapshot behavior is not superseded. ``` Do not rewrite the historical task list or pretend its old acceptance evidence used the new design. - [ ] **Step 2: Run focused and full WMS verification** From `D:/workSpace/PROJECT_SPACE/FT546/FT546MWS/FT546MWS`: ```powershell $env:MAVEN_OPTS='-Xms64m -Xmx384m -XX:MaxMetaspaceSize=192m -XX:ReservedCodeCacheSize=64m -XX:+UseSerialGC' mvn -pl ruoyi-admin -am "-Dtest=TidyWorkflowRecoveryServiceTest,TidyWorkflowTransactionTest,WcsTaskStateDependencyRemovalTest,WmsAlgorithmMapperWorkflowSqlTest,ToWmsServiceImplWorkflowCallbackTest" "-Dsurefire.failIfNoSpecifiedTests=false" test mvn -q "-DargLine=-Xms32m -Xmx128m -XX:MaxMetaspaceSize=96m -XX:ReservedCodeCacheSize=32m -XX:+UseSerialGC" test mvn -DskipTests package ``` Expected: all focused tests pass, the root reactor has zero failure/error/skipped tests, and packaging exits 0. - [ ] **Step 3: Run replay contract tests and read-only preflight** From `D:/workSpace/PROJECT_SPACE/FT546`: ```powershell Invoke-Pester .\tools\history-replay\tests\Invoke-Ft546TidyBatchReplay.Tests.ps1 -Output Detailed pwsh -File .\tools\history-replay\Invoke-Ft546TidyBatchReplay.ps1 -WmsBaseUrl http://127.0.0.1:18082 -SimulatorPort 18081 -PreflightOnly ``` Expected: Pester passes and preflight creates a new output directory without changing the database. - [ ] **Step 4: Run the destructive replay under the existing verified-backup guard** Confirm ports 18081/18082 are unused and do not stop the user's WMS on 18080. Then run: ```powershell pwsh -File .\tools\history-replay\Invoke-Ft546TidyBatchReplay.ps1 -WmsBaseUrl http://127.0.0.1:18082 -SimulatorPort 18081 -ConfirmReplay ``` Do not use `-KeepFailedState`. If backup creation, restore, or baseline verification fails, stop further writes and preserve the run output for DBA review. - [ ] **Step 5: Verify the new run artifacts rather than trusting the exit code** Inspect the newest run directory and require all of the following: ```text summary.completed = true summary.restoration.passed = true RESTORE event mismatches = [] TIDY_RECOVERY_FINISHED pass = true TIDY_RECOVERY_MISSING_JOB pass = true RECOVERY_FINISHED callback event count = 1 for the repair workflow missing-job workflow status_reason = RECOVERY_JOB_MISSING missing-job active lock count > 0 before restore simulator task-state request count = 0 ``` Also confirm `tidy-reconciliation.csv` contains the unchanged automatic/manual rows plus both new recovery rows and that ports 18081/18082 have no listener after cleanup. - [ ] **Step 6: Update the replay README and handoff with measured evidence** Replace the handoff's WCS UNKNOWN claim with the two WMS-local cases. Record exact output paths, test totals, final JAR size/hash, backup path, restoration result, and any remaining limitations. Do not reuse the 2026-07-23 04:29 evidence as proof of the new recovery implementation. - [ ] **Step 7: Commit only WMS repository documentation and perform final checks** From `D:/workSpace/PROJECT_SPACE/FT546/FT546MWS/FT546MWS`: ```powershell git add docs/superpowers/specs/2026-07-22-ft546-tidy-and-batch-replay-design.md docs/superpowers/plans/2026-07-22-ft546-tidy-and-batch-replay.md git commit --only -m "docs: supersede WCS tidy task-state recovery" docs/superpowers/specs/2026-07-22-ft546-tidy-and-batch-replay-design.md docs/superpowers/plans/2026-07-22-ft546-tidy-and-batch-replay.md git diff --check 590eea6..HEAD git status --short ``` Expected: `git diff --check` prints nothing. `git status --short` may still show the user's pre-existing changes, but none were reset, unstaged, or included in these focused commits.