fix: classify equivalent outbound no-stock results
This commit is contained in:
105
docs/superpowers/plans/2026-09-15-ft546wms-takeover-readiness.md
Normal file
105
docs/superpowers/plans/2026-09-15-ft546wms-takeover-readiness.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# FT546WMS Takeover Readiness 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:** Produce a testable candidate that reports equivalent outbound failures correctly, refreshes reliably, and never dispatches a blocked rear outbound before its relocation completes.
|
||||
|
||||
**Architecture:** Keep Legacy as the production executor while hardening observation and dispatch safety. Separate refresh timeout from request comparison timeout; classify outbound comparisons by business meaning; enforce the relocation dependency both before WMS HTTP dispatch and, where the WCS contract permits, before WCS execution. ACTIVE takeover remains disabled until a dedicated mutation adapter and field evidence exist.
|
||||
|
||||
**Tech Stack:** Java 17, Spring Boot, MyBatis, SQL Server 2016, JUnit 5, Mockito, PowerShell integration fixtures.
|
||||
|
||||
**Spec:** User instruction in this thread: relocation precedes associated outbound; completion and inventory update release outbound; WCS must guard bypasses; unrelated channels continue normally.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- No production switch, restart, synthetic production task, or business-data mutation during implementation.
|
||||
- Existing mixed-pilot lane scope and Legacy allocation behavior remain unchanged.
|
||||
- Every behavior change follows red-green testing and is independently revertible.
|
||||
- SQL Server 2016 syntax only; no online dependency downloads are assumed.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Outbound comparison semantics
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngineTest.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngine.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `OutboundShadowObservation.resultCode()` and Legacy numeric result code.
|
||||
- Produces: stable `MATCH`, `DIFFERENT`, `NOT_COMPARABLE` comparison codes while retaining raw evidence in `reason`.
|
||||
|
||||
- [ ] Add a failing test proving shadow `ERROR_STOCK_NOT_FOUND` plus Legacy `3` is an equivalent no-inventory outcome, and successful/technical/skipped cases are not falsely matched.
|
||||
- [ ] Run only `ShadowPlanningEngineTest` and observe the exact `LEGACY_3` failure.
|
||||
- [ ] Add a private outbound comparison classifier; do not alter planning or Legacy execution.
|
||||
- [ ] Re-run focused and shadow-module tests.
|
||||
- [ ] Commit independently.
|
||||
|
||||
### Task 2: Dedicated refresh timeout
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowConfigurationTest.java`
|
||||
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngineTest.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowObservationProperties.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngine.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowSafetyGuard.java`
|
||||
- Modify: `ruoyi-admin/src/main/resources/application.yml`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `FORTUCKY_SHADOW_REFRESH_TIMEOUT_SECONDS`, default 10 seconds.
|
||||
- Produces: a refresh-only transaction timeout; the 3-second Legacy comparison deadline stays unchanged.
|
||||
|
||||
- [ ] Add failing property/guard tests and a transaction-manager test that distinguishes planning timeout from refresh timeout.
|
||||
- [ ] Run focused tests and observe missing property/incorrect timeout failure.
|
||||
- [ ] Use separate transaction templates or per-refresh timeout without sharing mutable template state.
|
||||
- [ ] Re-run focused and shadow-module tests.
|
||||
- [ ] Commit independently.
|
||||
|
||||
### Task 3: WMS dispatch dependency gate
|
||||
|
||||
**Files:**
|
||||
- Create or modify focused mapper/service files under `ruoyi-admin/src/main/java/com/ruoyi/api` and `ruoyi-admin/src/main/resources/mapper` after locating the existing job mapper contract.
|
||||
- Create: focused JUnit test for `ToWcsServiceImpl.pushOutMydoJob()`.
|
||||
- Modify: `scripts/mixed-pilot/tests/behavior.sql` only if an SQL regression is required by the confirmed race.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: candidate outbound job plus current lane obstruction / unresolved relocation state.
|
||||
- Produces: dispatch decision `READY` or `WAITING_FOR_RELOCATION`; only READY calls WCS and advances status.
|
||||
|
||||
- [ ] Add a failing service test: blocked rear outbound makes zero HTTP calls, unrelated/front outbounds still dispatch, and completing relocation makes the rear job dispatch exactly once.
|
||||
- [ ] Reproduce the task-creation race in the SQL scratch fixture if it bypasses the existing status-8 trigger.
|
||||
- [ ] Add the smallest read-only pre-dispatch guard and atomic claim/update boundary supported by current service patterns.
|
||||
- [ ] Re-run Java and SQL mixed-pilot regression suites.
|
||||
- [ ] Commit independently.
|
||||
|
||||
### Task 4: WCS defense and release candidate
|
||||
|
||||
**Files:**
|
||||
- Modify only the actual FT546-WCS receiving/execution module identified by repository audit.
|
||||
- Add focused WCS tests at its receive or execution seam.
|
||||
- Add deployment and rollback notes under `scripts/` in each affected repository.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: WMS job payload and WCS current/declared dependency evidence.
|
||||
- Produces: explicit accepted/waiting/rejected behavior without treating HTTP receipt as physical completion.
|
||||
|
||||
- [ ] Add a failing WCS test proving a blocked rear task cannot enter executable state, including manual/replayed payloads.
|
||||
- [ ] Implement the minimal defense compatible with the existing WMS/WCS contract; if WCS lacks authoritative obstruction data, reject ambiguous rear tasks rather than inventing state.
|
||||
- [ ] Run both repository test suites and inspect package contents offline.
|
||||
- [ ] Prepare versioned artifacts and rollback scripts; do not deploy or switch without a maintenance decision.
|
||||
- [ ] Request independent code review and address findings.
|
||||
|
||||
### Task 5: ACTIVE takeover gate
|
||||
|
||||
**Files:**
|
||||
- No production code change until Tasks 1-4 and field evidence pass.
|
||||
- Update readiness documentation with exact remaining mutation adapter and rollback requirements.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: verified planner result and explicit operator mode.
|
||||
- Produces: a future single-writer execution path, never simultaneous Legacy and algorithm allocation.
|
||||
|
||||
- [ ] Preserve `ShadowSafetyGuard` rejection of ACTIVE in this candidate.
|
||||
- [ ] Define endpoint-by-endpoint single-writer tests before adding any ACTIVE mutation adapter.
|
||||
- [ ] Require fresh inbound, successful outbound, no-inventory outbound, refresh and relocation field evidence before implementation approval.
|
||||
- [ ] Do not claim formal takeover readiness merely because the candidate builds.
|
||||
@@ -236,7 +236,7 @@ public class ShadowPlanningEngine {
|
||||
int inserted = inputMapper.insertComparison(
|
||||
UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE",
|
||||
observation.snapshotGeneration(), observation.snapshotSynchronizedAt(),
|
||||
observation.sourceSignature(), "LEGACY_" + nullSafe(legacyResultCode),
|
||||
observation.sourceSignature(), outboundComparisonCode(observation, legacyResultCode),
|
||||
outboundComparisonReason(observation, legacyResultCode));
|
||||
requireComparisonInserted(inserted);
|
||||
});
|
||||
@@ -600,6 +600,15 @@ public class ShadowPlanningEngine {
|
||||
? result + "_CONSERVATIVE" : result;
|
||||
}
|
||||
|
||||
private String outboundComparisonCode(OutboundShadowObservation observation, String legacyCode) {
|
||||
if (observation.observed()
|
||||
&& "ERROR_STOCK_NOT_FOUND".equals(observation.resultCode())
|
||||
&& "3".equals(legacyCode)) {
|
||||
return "MATCH_NO_INVENTORY";
|
||||
}
|
||||
return "LEGACY_" + nullSafe(legacyCode);
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
|
||||
@@ -211,6 +211,43 @@ class ShadowPlanningEngineTest {
|
||||
eq(SNAPSHOT.synchronizedAt()), eq("ABC123"), eq("LEGACY_10"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundComparisonMatchesWhenBothPlannersReportNoInventory() {
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
when(mapper.insertComparison(any(), any(), any(), any(Long.class),
|
||||
any(Instant.class), any(), any(), any())).thenReturn(1);
|
||||
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
|
||||
OutboundShadowObservation observation = OutboundShadowObservation.unavailable(
|
||||
SNAPSHOT, "DETAIL-1", "ERROR_STOCK_NOT_FOUND", "no matching inventory");
|
||||
|
||||
engine.compareOutbound(observation, "3");
|
||||
|
||||
verify(mapper).insertComparison(any(), eq("01"), eq("OUTBOUND_COMPARE"), eq(42L),
|
||||
eq(SNAPSHOT.synchronizedAt()), eq("ABC123"), eq("MATCH_NO_INVENTORY"),
|
||||
argThat(reason -> reason.contains("shadow=ERROR_STOCK_NOT_FOUND")
|
||||
&& reason.contains("legacy=3")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundComparisonDoesNotMatchNoInventoryAgainstTechnicalOrDifferentOutcomes() {
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
when(mapper.insertComparison(any(), any(), any(), any(Long.class),
|
||||
any(Instant.class), any(), any(), any())).thenReturn(1);
|
||||
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
|
||||
OutboundShadowObservation technical = OutboundShadowObservation.unavailable(
|
||||
SNAPSHOT, "DETAIL-1", "ERROR_TECHNICAL", "database failure");
|
||||
OutboundShadowObservation noInventory = OutboundShadowObservation.unavailable(
|
||||
SNAPSHOT, "DETAIL-2", "ERROR_STOCK_NOT_FOUND", "no matching inventory");
|
||||
|
||||
engine.compareOutbound(technical, "3");
|
||||
engine.compareOutbound(noInventory, "10");
|
||||
|
||||
verify(mapper).insertComparison(any(), eq("01"), eq("OUTBOUND_COMPARE"), eq(42L),
|
||||
eq(SNAPSHOT.synchronizedAt()), eq("ABC123"), eq("LEGACY_3"), any());
|
||||
verify(mapper).insertComparison(any(), eq("01"), eq("OUTBOUND_COMPARE"), eq(42L),
|
||||
eq(SNAPSHOT.synchronizedAt()), eq("ABC123"), eq("LEGACY_10"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void inboundComparisonRetainsRequiredEvidenceWhenUnicodeReasonIsOversized() {
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
|
||||
Reference in New Issue
Block a user