fix: harden real history replay evidence
This commit is contained in:
@@ -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<HistoricalQueueRecord>`.
|
||||
- `HistoricalQueueDataset.getMapSites()` returns an unmodifiable `Set<String>`.
|
||||
|
||||
- [ ] **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<HistoricalQueueRecord>
|
||||
this.mapSites = Collections.unmodifiableSet(new LinkedHashSet<String>(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<String> 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<HistoricalPath> paths, List<HistoricalReplayResult> results)`.
|
||||
- CLI usage: `HistoricalReplayApplication <snapshotDir> <outputDir>`.
|
||||
|
||||
- [ ] **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.
|
||||
|
||||
@@ -11,20 +11,52 @@ public final class HistoricalQueueDataset {
|
||||
private final Set<String> 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<HistoricalQueueRecord> records, Set<String> 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<HistoricalQueueRecord>(records));
|
||||
this.mapSites = Collections.unmodifiableSet(new LinkedHashSet<String>(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<HistoricalQueueRecord> getRecords() { return records; }
|
||||
public Set<String> 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; }
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public final class HistoricalReplayAnalyzer {
|
||||
List<HistoricalPath> ordered = new ArrayList<HistoricalPath>(input);
|
||||
Collections.sort(ordered, stableOrder());
|
||||
List<HistoricalReplayResult> results = new ArrayList<HistoricalReplayResult>();
|
||||
List<HistoricalPath> accepted = new ArrayList<HistoricalPath>();
|
||||
List<HistoricalPath> priorReplayable = new ArrayList<HistoricalPath>();
|
||||
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.<String>emptyList(), "按历史生命周期推导窗口未发现资源冲突"));
|
||||
} else {
|
||||
results.add(conflict);
|
||||
}
|
||||
priorReplayable.add(path);
|
||||
}
|
||||
return Collections.unmodifiableList(results);
|
||||
}
|
||||
|
||||
@@ -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<String> vehicles = new TreeSet<String>();
|
||||
Set<String> taskTypes = new TreeSet<String>();
|
||||
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<String, Integer> issueCounts = new LinkedHashMap<String, Integer>();
|
||||
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<String, Integer> counts = new LinkedHashMap<String, Integer>();
|
||||
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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<HistoricalReplayResult> 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<HistoricalPath> 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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user