fix: harden real history replay evidence
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user