50 KiB
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 throughalg.usp_complete_move_jobandjobFinish. - 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-urlandWcsVehicleSnapshotClientunchanged because manual capacity still needs vehicle availability. - Never call
alg.usp_complete_move_jobduring recovery of an already archived job. - A workflow in
PAUSED_RECOVERYis 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_FINISHEDandOUTBOUND_FINISHEDcallback 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/FT546has no usable root Git metadata. Replay and handoff files outsideFT546MWS/FT546MWSare 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 specialUNKNOWNcallback 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/stepNowhen every step is complete; all count fields are non-nullintvalues. -
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:
@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:
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:
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:
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:
<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
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)withcallbackStage="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:
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:
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
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:
@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
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
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:
tidyWcsSnapshotUrland its getter/setter. -
Removes: task-state URL, timeout, DTO, HTTP client, old mapper projection/query, and the
UNKNOWNcallback branch. -
Retains:
RECOVERY_FINISHEDon 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:
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:
@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
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:
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:
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:
if ("UNKNOWN".equals(callbackStage)) {
mapper.pauseTidyWorkflow(workflowId, "PAUSED_RECOVERY");
return true;
}
- Step 4: Verify dependency removal and callback regressions
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
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_FINISHEDfor archived completion repaired after restart. -
Produces replay event
TIDY_RECOVERY_MISSING_JOBfor a run-scoped deleted active job paused with retained locks. -
Produces two
tidy-reconciliation.csvrows with phasesRECOVERY_FINISHEDandRECOVERY_MISSING_JOB. -
Removes simulator endpoints
/sim/task-stateand/sim/unknown-taskand environment variableFORTUCKY_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:
$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:
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:
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:
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:
- Start WMS with a fast automatic scan and wait for a fresh
AUTO_TIDYworkflow and its activeTDY-%job. - Stop WMS.
- Capture the tray/source/target and current matching
move_msgcount. - Call
Complete-TidyJobWithoutCallback; assert the active row is gone, one status4finish row exists, the tidy step is still non-complete, stock is at target, source is empty, and target holds the tray. - Capture the post-procedure
move_msgcount, restart WMS with automatic scan quiet and reconciliation at 1000 ms, then wait for the step/workflow to advance. - Assert exactly one
RECOVERY_FINISHEDcallback event, no active deterministic job, unchanged post-proceduremove_msgcount, and zero active locks when the workflow is terminal. - Write
TIDY_RECOVERY_FINISHEDand a passingRECOVERY_FINISHEDreconciliation row.
Use these acceptance predicates in the runner:
$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
- Start WMS with a fast automatic scan and wait for another fresh workflow/job, excluding the normal and repaired workflow IDs.
- Stop WMS and call
Remove-ActiveTidyJob. - Restart WMS with automatic scan quiet and reconciliation at 1000 ms.
- Wait for workflow status
PAUSED_RECOVERY, workflowstatus_reason='RECOVERY_JOB_MISSING', at least one paused step, and active lock count greater than zero. - Assert no replacement deterministic job and no
RECOVERY_FINISHEDevent. - Write
TIDY_RECOVERY_MISSING_JOBand a passingRECOVERY_MISSING_JOBreconciliation row.
Use these acceptance predicates:
$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:
$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
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:
> 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:
$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:
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:
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:
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:
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.