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

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 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:

@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) 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:

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: 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:

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_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:

$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:

  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:

$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:

$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.