Files
FT546MWS/docs/superpowers/plans/2026-07-23-wms-native-tidy-recovery.md

1035 lines
50 KiB
Markdown

# 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<TidyRecoveryEvidence> 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-<workflowId>-<stepId>` 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<TidyRecoveryEvidence> 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
<resultMap id="tidyRecoveryEvidenceMap" type="com.ruoyi.algorithm.wms.tidy.TidyRecoveryEvidence">
<constructor>
<arg column="workflowId" javaType="java.lang.String" />
<arg column="origin" javaType="com.ruoyi.algorithm.wms.tidy.TidyOrigin" />
<arg column="workflowStatus" javaType="java.lang.String" />
<arg column="activeLockCount" javaType="_int" />
<arg column="stepId" javaType="java.lang.String" />
<arg column="stepNo" javaType="java.lang.Integer" />
<arg column="stepStatus" javaType="java.lang.String" />
<arg column="sourceSpaceId" javaType="java.lang.String" />
<arg column="targetSpaceId" javaType="java.lang.String" />
<arg column="trayCode" javaType="java.lang.String" />
<arg column="activeTidyJobCount" javaType="_int" />
<arg column="activeJobId" javaType="java.lang.String" />
<arg column="activeJobStatus" javaType="java.lang.String" />
<arg column="activeJobInOrOut" javaType="java.lang.String" />
<arg column="activeJobTrayCode" javaType="java.lang.String" />
<arg column="activeJobSourceSpaceId" javaType="java.lang.String" />
<arg column="activeJobTargetSpaceId" javaType="java.lang.String" />
<arg column="finishedJobId" javaType="java.lang.String" />
<arg column="finishedJobStatus" javaType="java.lang.String" />
<arg column="finishedJobInOrOut" javaType="java.lang.String" />
<arg column="finishedJobTrayCode" javaType="java.lang.String" />
<arg column="finishedJobSourceSpaceId" javaType="java.lang.String" />
<arg column="finishedJobTargetSpaceId" javaType="java.lang.String" />
<arg column="sourceStockRows" javaType="_int" />
<arg column="targetStockRows" javaType="_int" />
<arg column="sourceSpaceStatus" javaType="java.lang.String" />
<arg column="sourceSpaceTrayCode" javaType="java.lang.String" />
<arg column="targetSpaceStatus" javaType="java.lang.String" />
<arg column="targetSpaceTrayCode" javaType="java.lang.String" />
</constructor>
</resultMap>
<select id="selectTidyRecoveryEvidence" resultMap="tidyRecoveryEvidenceMap">
select workflow.workflow_id as workflowId,
workflow.origin as origin,
workflow.status as workflowStatus,
(select count(*) from alg.tidy_space_lock held
where held.workflow_id = workflow.workflow_id and held.status = 'ACTIVE') as activeLockCount,
active_step.step_id as stepId,
active_step.step_no as stepNo,
active_step.status as stepStatus,
active_step.source_space_id as sourceSpaceId,
active_step.target_space_id as targetSpaceId,
active_step.tray_code as trayCode,
(select count(*) from dbo.mydo_job workflow_job
where workflow_job.job_id like concat('TDY-', workflow.workflow_id, '-%')) as activeTidyJobCount,
active_job.job_id as activeJobId,
active_job.status as activeJobStatus,
active_job.in_or_out as activeJobInOrOut,
active_job.tray_code as activeJobTrayCode,
active_job.cargo_space_id as activeJobSourceSpaceId,
active_job.target_cargo_space_id as activeJobTargetSpaceId,
finished_job.job_id as finishedJobId,
finished_job.status as finishedJobStatus,
finished_job.in_or_out as finishedJobInOrOut,
finished_job.tray_code as finishedJobTrayCode,
finished_job.cargo_space_id as finishedJobSourceSpaceId,
finished_job.target_cargo_space_id as finishedJobTargetSpaceId,
(select count(*) from dbo.bas_stock source_stock
where source_stock.cargo_space_id = active_step.source_space_id
and source_stock.tray_code = active_step.tray_code) as sourceStockRows,
(select count(*) from dbo.bas_stock target_stock
where target_stock.cargo_space_id = active_step.target_space_id
and target_stock.tray_code = active_step.tray_code) as targetStockRows,
source_space.status as sourceSpaceStatus,
source_space.tray_code as sourceSpaceTrayCode,
target_space.status as targetSpaceStatus,
target_space.tray_code as targetSpaceTrayCode
from alg.tidy_workflow workflow
outer apply (
select top (1) step.step_id, step.step_no, step.status,
step.source_space_id, step.target_space_id, step.tray_code
from alg.tidy_step step
where step.workflow_id = workflow.workflow_id
and step.status in ('PENDING', 'DISPATCHED', 'RUNNING', 'PAUSED_RECOVERY')
order by step.step_no
) active_step
left join dbo.mydo_job active_job
on active_job.job_id = concat('TDY-', workflow.workflow_id, '-', active_step.step_id)
left join dbo.mydo_job_finish finished_job
on finished_job.job_id = concat('TDY-', workflow.workflow_id, '-', active_step.step_id)
left join dbo.bas_cargo_space source_space
on source_space.cargo_space_id = active_step.source_space_id
left join dbo.bas_cargo_space target_space
on target_space.cargo_space_id = active_step.target_space_id
where workflow.warehouse_id = #{warehouseId}
and workflow.status in (
'CLAIMED', 'WAITING_DISPATCH', 'DISPATCHED', 'RUNNING', 'PAUSED_RECOVERY'
)
</select>
```
- [ ] **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<TidyRecoveryEvidence> 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<Arguments> 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<String> 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-<new-run-id>/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.