diff --git a/docs/superpowers/plans/2026-07-30-wcs-real-queue-history-replay.md b/docs/superpowers/plans/2026-07-30-wcs-real-queue-history-replay.md index 3d7df9d..3ad8c0d 100644 --- a/docs/superpowers/plans/2026-07-30-wcs-real-queue-history-replay.md +++ b/docs/superpowers/plans/2026-07-30-wcs-real-queue-history-replay.md @@ -1,6 +1,6 @@ # WCS Real Queue History Replay 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. +> **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 (`- [x]`) syntax for tracking. **Goal:** Build a reproducible, read-only offline replay of FT546's real SRGV queue history, test at least 200 original records, scan the complete 17,136-row history through database aggregates, and produce a Chinese-readable report that separates database facts from SHADOW inference. @@ -8,6 +8,10 @@ **Tech Stack:** Java 8, Maven, Jackson 2.17.2, JUnit 5.10.2, SQL Server read-only MCP, `@oai/artifact-tool` for the final `.xlsx`. +**Status (2026-07-31):** Completed and independently reviewed. Final evidence run: +`outputs/wcs-shadow-history-replay-20260731-0945-af0b52d`; Maven verification: +354 tests, 0 failures, 0 errors, 0 skipped. + ## Global Constraints - All database access is single-statement `SELECT`/CTE and read-only; no stored procedures or mutations. @@ -72,7 +76,7 @@ - Consumes: `FT546-wcs.dbo.wcs_srgv_queue_finish`, `FT546-wcs.dbo.wcs_srgv_map`. - Produces: JSON arrays with original database fields and a manifest containing `database`, `table`, `extractedAt`, `totalRows`, `sampleRows`, `queryId`, and SHA-256 values. -- [ ] **Step 1: Run the complete-history aggregate scan** +- [x] **Step 1: Run the complete-history aggregate scan** Use the SQL Server read-only tool with: @@ -94,7 +98,7 @@ FROM dbo.wcs_srgv_queue_finish Expected: one row; `total_rows >= 17136`; query is read-only. -- [ ] **Step 2: Select a deterministic representative sample** +- [x] **Step 2: Select a deterministic representative sample** Use one CTE query. The final `ORDER BY` is mandatory for reproducibility: @@ -130,7 +134,7 @@ ORDER BY Expected: 240 original rows when available, never fewer than 200. -- [ ] **Step 3: Export the complete history through deterministic read-only pages** +- [x] **Step 3: Export the complete history through deterministic read-only pages** Use pages of 500 rows and change only the two numeric bounds for each page: @@ -158,7 +162,7 @@ Continue with `501 AND 1000` until the returned page is empty. Stream pages into route the complete payload through chat text. The final row count must equal the aggregate scan exactly. -- [ ] **Step 4: Export referenced map sites** +- [x] **Step 4: Export referenced map sites** Read the full non-sensitive map projection, then retain only site IDs referenced by the sample and complete snapshot: @@ -172,7 +176,7 @@ ORDER BY site_id, z, y, x Do not query credentials or device IP fields. -- [ ] **Step 5: Write snapshot files and hashes** +- [x] **Step 5: Write snapshot files and hashes** Write JSON with UTF-8, stable property order, and two-space indentation. Compute: @@ -186,7 +190,7 @@ SHA-256(full-scan-summary.json) source manifest additionally contains the complete snapshot hash. It must not contain connection strings. -- [ ] **Step 6: Verify extraction evidence** +- [x] **Step 6: Verify extraction evidence** Run: @@ -196,7 +200,7 @@ grep -RniE "password|token|jdbc:|server=" javaTest/src/test/resources/shadow/his Expected: no matches. -- [ ] **Step 7: Commit only the bounded immutable test snapshot** +- [x] **Step 7: Commit only the bounded immutable test snapshot** ```bash git add javaTest/src/test/resources/shadow/history @@ -219,7 +223,7 @@ git commit -m "test: add real queue history replay snapshot" - `HistoricalQueueDataset.getRecords()` returns an unmodifiable `List`. - `HistoricalQueueDataset.getMapSites()` returns an unmodifiable `Set`. -- [ ] **Step 1: Write failing loader tests** +- [x] **Step 1: Write failing loader tests** ```java @Test @@ -239,7 +243,7 @@ void rejectsHashMismatch() { } ``` -- [ ] **Step 2: Run loader tests and confirm RED** +- [x] **Step 2: Run loader tests and confirm RED** ```bash mvn -f javaTest/pom.xml -Dtest=HistoricalQueueLoaderTest test @@ -247,7 +251,7 @@ mvn -f javaTest/pom.xml -Dtest=HistoricalQueueLoaderTest test Expected: compilation failure because history classes do not exist. -- [ ] **Step 3: Implement immutable raw records and dataset** +- [x] **Step 3: Implement immutable raw records and dataset** `HistoricalQueueRecord` must expose exact string values for IDs/sites/dates and retain all extracted columns. Constructor arguments are JSON-mapped with Jackson `@JsonCreator` and `@JsonProperty`. @@ -258,7 +262,7 @@ this.records = Collections.unmodifiableList(new ArrayList this.mapSites = Collections.unmodifiableSet(new LinkedHashSet(mapSites)); ``` -- [ ] **Step 4: Implement loader and SHA validation** +- [x] **Step 4: Implement loader and SHA validation** The loader reads the four classpath resources, verifies every hash before deserializing, and checks: @@ -271,7 +275,7 @@ if (records.size() < 200) { } ``` -- [ ] **Step 5: Run loader tests and confirm GREEN** +- [x] **Step 5: Run loader tests and confirm GREEN** ```bash mvn -f javaTest/pom.xml -Dtest=HistoricalQueueLoaderTest test @@ -279,7 +283,7 @@ mvn -f javaTest/pom.xml -Dtest=HistoricalQueueLoaderTest test Expected: all loader tests pass. -- [ ] **Step 6: Commit** +- [x] **Step 6: Commit** ```bash git add javaTest/src/main/java/com/ft546/wcs/shadow/history javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueLoaderTest.java @@ -299,7 +303,7 @@ git commit -m "feat: load versioned queue history snapshots" - Produces: `HistoricalPath HistoricalQueueNormalizer.normalize(HistoricalQueueRecord record, Set mapSites)`. - `HistoricalPath` exposes `getNodes()`, `getResources()`, `getCreatedAt()`, `getFinishedAt()`, `getIssues()`, and `isReplayable()`. -- [ ] **Step 1: Write failing normalization tests** +- [x] **Step 1: Write failing normalization tests** ```java @Test @@ -327,7 +331,7 @@ void reversedIntervalIsNotReplayable() { } ``` -- [ ] **Step 2: Run normalization tests and confirm RED** +- [x] **Step 2: Run normalization tests and confirm RED** ```bash mvn -f javaTest/pom.xml -Dtest=HistoricalQueueNormalizerTest test @@ -335,7 +339,7 @@ mvn -f javaTest/pom.xml -Dtest=HistoricalQueueNormalizerTest test Expected: compilation failure because normalizer classes do not exist. -- [ ] **Step 3: Implement strict timestamp parsing** +- [x] **Step 3: Implement strict timestamp parsing** Use: @@ -346,7 +350,7 @@ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss") Parse into `LocalDateTime`. Null, blank, invalid, or reversed intervals add structured issues and prevent replay. -- [ ] **Step 4: Implement path normalization and resource projection** +- [x] **Step 4: Implement path normalization and resource projection** Rules: @@ -362,7 +366,7 @@ do not create CONFLICT or lift resources Empty paths add `EMPTY_PATH` and return `isReplayable() == false`. -- [ ] **Step 5: Run tests and confirm GREEN** +- [x] **Step 5: Run tests and confirm GREEN** ```bash mvn -f javaTest/pom.xml -Dtest=HistoricalQueueNormalizerTest test @@ -370,7 +374,7 @@ mvn -f javaTest/pom.xml -Dtest=HistoricalQueueNormalizerTest test Expected: all normalization tests pass. -- [ ] **Step 6: Commit** +- [x] **Step 6: Commit** ```bash git add javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalPath.java javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueNormalizer.java javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueNormalizerTest.java @@ -392,7 +396,7 @@ git commit -m "feat: normalize real queue paths fail closed" - Produces: `boolean HistoricalReplayAnalyzer.overlaps(HistoricalPath left, HistoricalPath right)`. - Stable ordering: priority ascending, `createdAt`, task number, AGV ID, GUID. -- [ ] **Step 1: Write failing overlap and conflict tests** +- [x] **Step 1: Write failing overlap and conflict tests** ```java @Test @@ -418,7 +422,7 @@ void identicalInputProducesIdenticalResults() { } ``` -- [ ] **Step 2: Run analyzer tests and confirm RED** +- [x] **Step 2: Run analyzer tests and confirm RED** ```bash mvn -f javaTest/pom.xml -Dtest=HistoricalReplayAnalyzerTest test @@ -426,7 +430,7 @@ mvn -f javaTest/pom.xml -Dtest=HistoricalReplayAnalyzerTest test Expected: compilation failure because analyzer types do not exist. -- [ ] **Step 3: Implement half-open overlap semantics** +- [x] **Step 3: Implement half-open overlap semantics** ```java return left.getCreatedAt().isBefore(right.getFinishedAt()) @@ -435,7 +439,7 @@ return left.getCreatedAt().isBefore(right.getFinishedAt()) Non-replayable paths return `INSUFFICIENT_DATA` before conflict checks. -- [ ] **Step 4: Implement conflict classification** +- [x] **Step 4: Implement conflict classification** For overlapping records: @@ -446,7 +450,7 @@ For overlapping records: The result stores both original record IDs, shared resource keys, inferred interval, category, and Chinese explanation. -- [ ] **Step 5: Implement deterministic replay decision** +- [x] **Step 5: Implement deterministic replay decision** Sort candidates once. Maintain an in-memory `ResourceLedger` with node and edge capacity 1. Replayable candidates become: @@ -458,7 +462,7 @@ INSUFFICIENT_DATA when normalization blocked replay Do not synthesize YIELD because no signed safe waiting points exist. -- [ ] **Step 6: Run analyzer tests and confirm GREEN** +- [x] **Step 6: Run analyzer tests and confirm GREEN** ```bash mvn -f javaTest/pom.xml -Dtest=HistoricalReplayAnalyzerTest test @@ -466,7 +470,7 @@ mvn -f javaTest/pom.xml -Dtest=HistoricalReplayAnalyzerTest test Expected: all analyzer tests pass. -- [ ] **Step 7: Commit** +- [x] **Step 7: Commit** ```bash git add javaTest/src/main/java/com/ft546/wcs/shadow/history javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalReplayAnalyzerTest.java @@ -484,7 +488,7 @@ git commit -m "feat: replay inferred historical traffic windows" - Consumes: `HistoricalQueueLoader`, `HistoricalQueueNormalizer`, `HistoricalReplayAnalyzer`. - Produces: Surefire-counted parameterized tests using original task and vehicle IDs. -- [ ] **Step 1: Write the real-data parameterized test** +- [x] **Step 1: Write the real-data parameterized test** ```java @ParameterizedTest(name = "{index} task={0} agv={1} {2}->{3}") @@ -506,7 +510,7 @@ void replaysRealHistoryRecord(String taskNo, String agvId, `realHistoryCases()` must expose every snapshot row, so Surefire counts at least 200 independent real-data cases. -- [ ] **Step 2: Add aggregate acceptance tests** +- [x] **Step 2: Add aggregate acceptance tests** Assert: @@ -520,7 +524,7 @@ assertEquals(17136, dataset.getFullScanTotalRows()); If the fresh extraction count differs from 17,136, use the manifest's exact count and assert it matches `full-scan-summary.json`; do not hardcode a stale number. -- [ ] **Step 3: Run real-data tests and confirm GREEN** +- [x] **Step 3: Run real-data tests and confirm GREEN** ```bash mvn -f javaTest/pom.xml -Dtest=HistoricalQueueDatabaseReplayTest test @@ -528,7 +532,7 @@ mvn -f javaTest/pom.xml -Dtest=HistoricalQueueDatabaseReplayTest test Expected: at least 200 parameterized cases plus aggregate checks; failures and errors are 0. -- [ ] **Step 4: Run the existing synthetic safety matrix** +- [x] **Step 4: Run the existing synthetic safety matrix** ```bash mvn -f javaTest/pom.xml -Dtest=ShadowSimulationDataTest test @@ -536,7 +540,7 @@ mvn -f javaTest/pom.xml -Dtest=ShadowSimulationDataTest test Expected: 60 tests, failures 0, errors 0. -- [ ] **Step 5: Commit** +- [x] **Step 5: Commit** ```bash git add javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueDatabaseReplayTest.java @@ -556,7 +560,7 @@ git commit -m "test: replay real FT546 queue history" - Produces: `void HistoricalReplayReportWriter.write(Path outputDir, HistoricalQueueDataset dataset, List paths, List results)`. - CLI usage: `HistoricalReplayApplication `. -- [ ] **Step 1: Write a failing writer test inside `HistoricalReplayAnalyzerTest`** +- [x] **Step 1: Write a failing writer test inside `HistoricalReplayAnalyzerTest`** ```java @TempDir Path temp; @@ -575,7 +579,7 @@ void writesReadableEvidenceWithOriginalIdsAndBoundaryText() throws Exception { } ``` -- [ ] **Step 2: Run the writer test and confirm RED** +- [x] **Step 2: Run the writer test and confirm RED** ```bash mvn -f javaTest/pom.xml -Dtest=HistoricalReplayAnalyzerTest#writesReadableEvidenceWithOriginalIdsAndBoundaryText test @@ -583,7 +587,7 @@ mvn -f javaTest/pom.xml -Dtest=HistoricalReplayAnalyzerTest#writesReadableEviden Expected: compilation failure because writer does not exist. -- [ ] **Step 3: Implement CSV and JSON evidence** +- [x] **Step 3: Implement CSV and JSON evidence** `replay-results.csv` columns: @@ -600,7 +604,7 @@ GUID,车辆,任务号,问题类型,原始路径,是否影响回放,处理方式 CSV must use RFC 4180 quoting and UTF-8 BOM so Excel opens Chinese text correctly. -- [ ] **Step 4: Implement summary and SHA manifest** +- [x] **Step 4: Implement summary and SHA manifest** `summary.json` includes: @@ -614,7 +618,7 @@ boundary="不是现场 SHADOW 验收" `manifest.sha256` covers every generated file except itself, sorted by filename. -- [ ] **Step 5: Implement CLI** +- [x] **Step 5: Implement CLI** ```java public static void main(String[] args) throws Exception { @@ -626,7 +630,7 @@ public static void main(String[] args) throws Exception { } ``` -- [ ] **Step 6: Document the workflow** +- [x] **Step 6: Document the workflow** Add to `javaTest/README.md`: @@ -639,7 +643,7 @@ mvn exec:java \ Explain original IDs are retained and the report distinguishes database facts from inference. -- [ ] **Step 7: Run writer tests and CLI** +- [x] **Step 7: Run writer tests and CLI** ```bash mvn -f javaTest/pom.xml -Dtest=HistoricalReplayAnalyzerTest test @@ -650,7 +654,7 @@ mvn -f javaTest/pom.xml exec:java \ Expected: evidence files exist, hashes verify, and CLI exits 0. -- [ ] **Step 8: Commit** +- [x] **Step 8: Commit** ```bash git add javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayReportWriter.java javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayApplication.java javaTest/README.md @@ -668,7 +672,7 @@ git commit -m "feat: report real history replay evidence" - Consumes: verified `summary.json`, `replay-results.csv`, `data-quality-issues.csv`. - Produces: one workbook with sheets `测试总览`, `真实任务明细`, `潜在冲突`, `数据问题`, `来源与口径`. -- [ ] **Step 1: Run a fresh full Maven verification** +- [x] **Step 1: Run a fresh full Maven verification** ```bash mvn -f javaTest/pom.xml clean test @@ -676,11 +680,11 @@ mvn -f javaTest/pom.xml clean test Expected: build success, every original test plus at least 200 real-history parameterized cases, failures 0, errors 0. -- [ ] **Step 2: Generate fresh evidence** +- [x] **Step 2: Generate fresh evidence** Use a run ID containing the current date/time and Git short SHA. Never overwrite an earlier run. -- [ ] **Step 3: Build the workbook with `@oai/artifact-tool`** +- [x] **Step 3: Build the workbook with `@oai/artifact-tool`** Workbook requirements: @@ -691,7 +695,7 @@ Workbook requirements: - One compact category-distribution chart. - Amber boundary banner: “真实历史队列离线回放,不是现场 SHADOW 验收”. -- [ ] **Step 4: Verify workbook values and formulas** +- [x] **Step 4: Verify workbook values and formulas** Inspect: @@ -709,15 +713,15 @@ Scan for: Expected: no formula errors; KPI counts reconcile to JSON/CSV. -- [ ] **Step 5: Render and visually inspect every sheet** +- [x] **Step 5: Render and visually inspect every sheet** Check Chinese text, task numbers, vehicle IDs, full paths, chart, wrapped reason text, and boundary banner. Fix clipping before export. -- [ ] **Step 6: Verify manifest** +- [x] **Step 6: Verify manifest** Recalculate SHA-256 for all run-directory evidence except `manifest.sha256` and compare line-by-line. -- [ ] **Step 7: Commit implementation and tests** +- [x] **Step 7: Commit implementation and tests** Do not commit generated `outputs/` evidence unless repository policy explicitly tracks evidence. Confirm: @@ -732,14 +736,14 @@ Expected: source/resources/docs changes committed; generated output remains an e ## Final Verification Checklist -- [ ] SQL operations were read-only. -- [ ] Full database aggregate scan row count is captured. -- [ ] Snapshot contains at least 200 original records and 3 original AGV IDs. -- [ ] Original task, vehicle, site, and path values are visible. -- [ ] Missing/unknown data is fail-closed. -- [ ] Replay is deterministic. -- [ ] Existing 60 synthetic safety cases still pass. -- [ ] Full Maven suite passes with failures/errors 0. -- [ ] Excel totals reconcile to machine-readable evidence. -- [ ] Report clearly states it is not现场 SHADOW/HIL/ACTIVE evidence. -- [ ] `ruoyi-*` production code is unchanged. +- [x] SQL operations were read-only. +- [x] Full database aggregate scan row count is captured. +- [x] Snapshot contains at least 200 original records and 3 original AGV IDs. +- [x] Original task, vehicle, site, and path values are visible. +- [x] Missing/unknown data is fail-closed. +- [x] Replay is deterministic. +- [x] Existing 60 synthetic safety cases still pass. +- [x] Full Maven suite passes with failures/errors 0. +- [x] Excel totals reconcile to machine-readable evidence. +- [x] Report clearly states it is not现场 SHADOW/HIL/ACTIVE evidence. +- [x] `ruoyi-*` production code is unchanged. diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueDataset.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueDataset.java index 4b9c56f..f1c640c 100644 --- a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueDataset.java +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueDataset.java @@ -11,20 +11,52 @@ public final class HistoricalQueueDataset { private final Set mapSites; private final int manifestSampleRows; private final int fullScanTotalRows; + private final String database; + private final String sourceTable; private final String extractedAt; + private final String minCreateDate; + private final String maxCreateDate; + private final String minFinishDate; + private final String maxFinishDate; + private final int missingPathRows; + private final int missingIntervalRows; + private final int reversedIntervalRows; public HistoricalQueueDataset(List records, Set mapSites, - int manifestSampleRows, int fullScanTotalRows, String extractedAt) { + int manifestSampleRows, int fullScanTotalRows, + String database, String sourceTable, String extractedAt, + String minCreateDate, String maxCreateDate, + String minFinishDate, String maxFinishDate, + int missingPathRows, int missingIntervalRows, + int reversedIntervalRows) { this.records = Collections.unmodifiableList(new ArrayList(records)); this.mapSites = Collections.unmodifiableSet(new LinkedHashSet(mapSites)); this.manifestSampleRows = manifestSampleRows; this.fullScanTotalRows = fullScanTotalRows; + this.database = database; + this.sourceTable = sourceTable; this.extractedAt = extractedAt; + this.minCreateDate = minCreateDate; + this.maxCreateDate = maxCreateDate; + this.minFinishDate = minFinishDate; + this.maxFinishDate = maxFinishDate; + this.missingPathRows = missingPathRows; + this.missingIntervalRows = missingIntervalRows; + this.reversedIntervalRows = reversedIntervalRows; } public List getRecords() { return records; } public Set getMapSites() { return mapSites; } public int getManifestSampleRows() { return manifestSampleRows; } public int getFullScanTotalRows() { return fullScanTotalRows; } + public String getDatabase() { return database; } + public String getSourceTable() { return sourceTable; } public String getExtractedAt() { return extractedAt; } + public String getMinCreateDate() { return minCreateDate; } + public String getMaxCreateDate() { return maxCreateDate; } + public String getMinFinishDate() { return minFinishDate; } + public String getMaxFinishDate() { return maxFinishDate; } + public int getMissingPathRows() { return missingPathRows; } + public int getMissingIntervalRows() { return missingIntervalRows; } + public int getReversedIntervalRows() { return reversedIntervalRows; } } diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueLoader.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueLoader.java index 8e77699..b01ce41 100644 --- a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueLoader.java +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalQueueLoader.java @@ -44,12 +44,21 @@ public final class HistoricalQueueLoader { if (rows.size() < 200) { throw new IllegalArgumentException("history sample must contain at least 200 rows"); } - int totalRows = MAPPER.readTree(summaryBytes).path("total_rows").asInt(); + JsonNode scanSummary = MAPPER.readTree(summaryBytes); + int totalRows = scanSummary.path("total_rows").asInt(); if (totalRows != manifest.path("totalRows").asInt()) { throw new IllegalArgumentException("history full scan row count mismatch"); } return new HistoricalQueueDataset(rows, sites, sampleRows, totalRows, - manifest.path("extractedAt").asText()); + manifest.path("database").asText(), manifest.path("table").asText(), + manifest.path("extractedAt").asText(), + scanSummary.path("min_create_date").asText(), + scanSummary.path("max_create_date").asText(), + scanSummary.path("min_finish_date").asText(), + scanSummary.path("max_finish_date").asText(), + scanSummary.path("missing_path_rows").asInt(), + scanSummary.path("missing_interval_rows").asInt(), + scanSummary.path("reversed_interval_rows").asInt()); } catch (IOException ex) { throw new IllegalArgumentException("cannot load history snapshot", ex); } @@ -73,7 +82,11 @@ public final class HistoricalQueueLoader { } public static void verifySha256(String expected, byte[] bytes) { - String actual = sha256(bytes); + byte[] canonicalBytes = new String(bytes, StandardCharsets.UTF_8) + .replace("\r\n", "\n") + .replace('\r', '\n') + .getBytes(StandardCharsets.UTF_8); + String actual = sha256(canonicalBytes); if (expected == null || !expected.equalsIgnoreCase(actual)) { throw new IllegalArgumentException("history snapshot sha256 mismatch"); } diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayAnalyzer.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayAnalyzer.java index a1c7e36..c1d5dd5 100644 --- a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayAnalyzer.java +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayAnalyzer.java @@ -21,7 +21,7 @@ public final class HistoricalReplayAnalyzer { List ordered = new ArrayList(input); Collections.sort(ordered, stableOrder()); List results = new ArrayList(); - List accepted = new ArrayList(); + List priorReplayable = new ArrayList(); for (HistoricalPath path : ordered) { if (!path.isReplayable()) { results.add(result(path, HistoricalReplayCategory.INSUFFICIENT_DATA, "INSUFFICIENT_DATA", @@ -29,7 +29,7 @@ public final class HistoricalReplayAnalyzer { continue; } HistoricalReplayResult conflict = null; - for (HistoricalPath prior : accepted) { + for (HistoricalPath prior : priorReplayable) { if (!overlaps(prior, path)) continue; if (same(path.getRecord().getAgvId(), prior.getRecord().getAgvId())) { conflict = result(path, HistoricalReplayCategory.SAME_VEHICLE_OVERLAP, "HOLD", prior, @@ -50,12 +50,12 @@ public final class HistoricalReplayAnalyzer { } } if (conflict == null) { - accepted.add(path); results.add(result(path, HistoricalReplayCategory.NO_CONFLICT, "GRANT", null, Collections.emptyList(), "按历史生命周期推导窗口未发现资源冲突")); } else { results.add(conflict); } + priorReplayable.add(path); } return Collections.unmodifiableList(results); } diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayReportWriter.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayReportWriter.java index 11a4a53..2a9677e 100644 --- a/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayReportWriter.java +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/HistoricalReplayReportWriter.java @@ -14,6 +14,8 @@ import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; public final class HistoricalReplayReportWriter { private static final ObjectMapper JSON = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); @@ -27,17 +29,45 @@ public final class HistoricalReplayReportWriter { summary.put("mode", "REAL_HISTORY_OFFLINE_REPLAY"); summary.put("title", "真实历史队列离线回放"); summary.put("boundary", "不是现场 SHADOW 验收"); + summary.put("database", dataset.getDatabase()); + summary.put("table", dataset.getSourceTable()); + summary.put("extractedAt", dataset.getExtractedAt()); summary.put("sourceRowCount", dataset.getFullScanTotalRows()); summary.put("sampleRowCount", dataset.getRecords().size()); summary.put("mapSiteCount", dataset.getMapSites().size()); + summary.put("minCreateDate", dataset.getMinCreateDate()); + summary.put("maxCreateDate", dataset.getMaxCreateDate()); + summary.put("minFinishDate", dataset.getMinFinishDate()); + summary.put("maxFinishDate", dataset.getMaxFinishDate()); + summary.put("missingPathRows", dataset.getMissingPathRows()); + summary.put("missingIntervalRows", dataset.getMissingIntervalRows()); + summary.put("reversedIntervalRows", dataset.getReversedIntervalRows()); + Set vehicles = new TreeSet(); + Set taskTypes = new TreeSet(); + for (HistoricalQueueRecord record : dataset.getRecords()) { + if (record.getAgvId() != null) vehicles.add(record.getAgvId()); + if (record.getTaskType() != null) taskTypes.add(record.getTaskType()); + } + summary.put("vehicles", vehicles); + summary.put("taskTypes", taskTypes); summary.put("replayableRows", paths.stream().filter(HistoricalPath::isReplayable).count()); summary.put("qualityIssueRows", paths.stream().filter(p -> !p.getIssues().isEmpty()).count()); + Map issueCounts = new LinkedHashMap(); + for (HistoricalPath path : paths) { + for (String issue : path.getIssues()) { + String type = issue.contains(":") ? issue.substring(0, issue.indexOf(':')) : issue; + issueCounts.put(type, issueCounts.containsKey(type) ? issueCounts.get(type) + 1 : 1); + } + } + summary.put("qualityIssueCounts", issueCounts); Map counts = new LinkedHashMap(); for (HistoricalReplayCategory category : HistoricalReplayCategory.values()) counts.put(category.name(), 0); for (HistoricalReplayResult result : results) { counts.put(result.getCategory().name(), counts.get(result.getCategory().name()) + 1); } summary.put("categoryCounts", counts); + summary.put("replayGrant", results.stream().filter(r -> "GRANT".equals(r.getDecision())).count()); + summary.put("replayHold", results.stream().filter(r -> "HOLD".equals(r.getDecision())).count()); summary.put("deterministic", true); JSON.writeValue(outputDir.resolve("summary.json").toFile(), summary); writeManifest(outputDir); diff --git a/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueLoaderTest.java b/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueLoaderTest.java index f9d7cfa..a94dada 100644 --- a/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueLoaderTest.java +++ b/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalQueueLoaderTest.java @@ -22,6 +22,11 @@ class HistoricalQueueLoaderTest { assertFalse(data.getRecords().get(0).getTaskNo().isEmpty()); assertFalse(data.getRecords().get(0).getAgvId().isEmpty()); assertTrue(data.getMapSites().size() > 1000); + assertEquals("FT546-wcs", data.getDatabase()); + assertEquals("dbo.wcs_srgv_queue_finish", data.getSourceTable()); + assertEquals("2026-03-01 10:18:24", data.getMinCreateDate()); + assertEquals("2026-07-21 14:26:51", data.getMaxFinishDate()); + assertEquals(5193, data.getMissingPathRows()); } @Test @@ -30,4 +35,14 @@ class HistoricalQueueLoaderTest { HistoricalQueueLoader.verifySha256( "wrong", "abc".getBytes(StandardCharsets.UTF_8))); } + + @Test + void verifiesTextHashIndependentlyOfLineEndingStyle() { + String canonicalSha256 = + "911169ddaaf146aff539f58c26c489af3b892dff0fe283c1c264c65ae5aa59a2"; + HistoricalQueueLoader.verifySha256( + canonicalSha256, "a\nb\n".getBytes(StandardCharsets.UTF_8)); + HistoricalQueueLoader.verifySha256( + canonicalSha256, "a\r\nb\r\n".getBytes(StandardCharsets.UTF_8)); + } } diff --git a/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalReplayAnalyzerTest.java b/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalReplayAnalyzerTest.java index 239f7dc..8072569 100644 --- a/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalReplayAnalyzerTest.java +++ b/javaTest/src/test/java/com/ft546/wcs/shadow/HistoricalReplayAnalyzerTest.java @@ -1,5 +1,7 @@ package com.ft546.wcs.shadow; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.ft546.wcs.shadow.history.HistoricalPath; import com.ft546.wcs.shadow.history.HistoricalQueueDataset; import com.ft546.wcs.shadow.history.HistoricalQueueLoader; @@ -46,6 +48,18 @@ class HistoricalReplayAnalyzerTest { result.getCategory() == HistoricalReplayCategory.POTENTIAL_OPPOSITE_EDGE)); } + @Test + void heldPathStillParticipatesInLaterConflictDetection() { + List results = analyzer.analyze(Arrays.asList( + path("G1", "T1", "V1", "A,B,", "10:00:00", "10:04:00"), + path("G2", "T2", "V2", "B,C,", "10:00:30", "10:03:30"), + path("G3", "T3", "V3", "C,D,", "10:01:00", "10:03:00"))); + + assertEquals(HistoricalReplayCategory.POTENTIAL_NODE_CONFLICT, + results.get(2).getCategory()); + assertEquals("T2", results.get(2).getRelatedTaskNo()); + } + @Test void identicalInputProducesIdenticalResults() { List sample = Arrays.asList( @@ -67,10 +81,22 @@ class HistoricalReplayAnalyzerTest { StandardCharsets.UTF_8); String summary = new String(Files.readAllBytes(temp.resolve("summary.json")), StandardCharsets.UTF_8); + JsonNode summaryJson = new ObjectMapper().readTree(summary); assertTrue(csv.contains(dataset.getRecords().get(0).getTaskNo())); assertTrue(csv.contains(dataset.getRecords().get(0).getAgvId())); assertTrue(summary.contains("真实历史队列离线回放")); assertTrue(summary.contains("不是现场 SHADOW 验收")); + assertEquals("FT546-wcs", summaryJson.path("database").asText()); + assertEquals("dbo.wcs_srgv_queue_finish", summaryJson.path("table").asText()); + assertEquals(dataset.getExtractedAt(), summaryJson.path("extractedAt").asText()); + assertEquals("2026-03-01 10:18:24", summaryJson.path("minCreateDate").asText()); + assertEquals(3, summaryJson.path("vehicles").size()); + assertEquals(3, summaryJson.path("taskTypes").size()); + assertEquals(2, summaryJson.path("qualityIssueCounts").path("UNKNOWN_MAP_SITE").asInt()); + assertEquals(dataset.getRecords().size(), + summaryJson.path("replayGrant").asInt() + + summaryJson.path("replayHold").asInt() + + summaryJson.path("categoryCounts").path("INSUFFICIENT_DATA").asInt()); assertTrue(Files.size(temp.resolve("manifest.sha256")) > 0); }