fix: preserve required shadow comparison evidence
This commit is contained in:
@@ -22,6 +22,12 @@ import java.util.function.Supplier;
|
||||
public class ShadowPlanningEngine {
|
||||
private static final int INSTANCE_STALE_SECONDS = 120;
|
||||
private static final int DECISION_REASON_MAX_LENGTH = 4000;
|
||||
private static final int IDENTIFIER_MAX_LENGTH = 80;
|
||||
private static final int WIDE_IDENTIFIER_MAX_LENGTH = 255;
|
||||
private static final int SNAPSHOT_SYNC_MAX_LENGTH = 40;
|
||||
private static final int SOURCE_SIGNATURE_MAX_LENGTH = 64;
|
||||
private static final int RESULT_REASON_MAX_LENGTH = 600;
|
||||
private static final int NUMBER_MAX_LENGTH = 64;
|
||||
private static final String PUBLISHED = "PUBLISHED";
|
||||
|
||||
private final SchedulingFacade schedulingFacade;
|
||||
@@ -124,13 +130,7 @@ public class ShadowPlanningEngine {
|
||||
observation.snapshotGeneration(), observation.snapshotSynchronizedAt(),
|
||||
observation.sourceSignature(),
|
||||
comparisonCode(observation.proposedSpaceId(), legacySpaceId),
|
||||
abbreviate("snapshotGeneration=" + observation.snapshotGeneration()
|
||||
+ "; request=" + observation.requestId()
|
||||
+ "; shadow=" + observation.resultCode()
|
||||
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
||||
+ "; legacy=" + nullSafe(legacyResultCode)
|
||||
+ "; legacySpace=" + nullSafe(legacySpaceId)
|
||||
+ "; shadowReason=" + nullSafe(observation.reason()), DECISION_REASON_MAX_LENGTH));
|
||||
inboundComparisonReason(observation, legacyResultCode, legacySpaceId));
|
||||
requireComparisonInserted(inserted);
|
||||
});
|
||||
}
|
||||
@@ -144,13 +144,7 @@ public class ShadowPlanningEngine {
|
||||
UUID.randomUUID().toString(), observation.warehouseId(), "OUTBOUND_COMPARE",
|
||||
observation.snapshotGeneration(), observation.snapshotSynchronizedAt(),
|
||||
observation.sourceSignature(), "LEGACY_" + nullSafe(legacyResultCode),
|
||||
abbreviate("snapshotGeneration=" + observation.snapshotGeneration()
|
||||
+ "; request=" + observation.requestId()
|
||||
+ "; shadow=" + observation.resultCode()
|
||||
+ "; proposedStock=" + nullSafe(observation.proposedStockId())
|
||||
+ "; proposedSpace=" + nullSafe(observation.proposedSpaceId())
|
||||
+ "; legacy=" + nullSafe(legacyResultCode)
|
||||
+ "; shadowReason=" + nullSafe(observation.reason()), DECISION_REASON_MAX_LENGTH));
|
||||
outboundComparisonReason(observation, legacyResultCode));
|
||||
requireComparisonInserted(inserted);
|
||||
});
|
||||
}
|
||||
@@ -299,42 +293,98 @@ public class ShadowPlanningEngine {
|
||||
|
||||
private String outboundDiagnostic(PublishedShadowSnapshot snapshot, String requestId,
|
||||
ShadowStockReference reference, String resultReason) {
|
||||
return snapshotDiagnostic(snapshot)
|
||||
+ "; detail=" + nullSafe(requestId)
|
||||
+ "; rawWarehouse=" + nullSafe(reference == null ? null : reference.getWarehouseId())
|
||||
+ "; project=" + nullSafe(reference == null ? null : reference.getProjectNo())
|
||||
+ "; product=" + nullSafe(reference == null ? null : reference.getProductId())
|
||||
+ "; tray=" + nullSafe(reference == null ? null : reference.getTrayNo())
|
||||
+ "; stock=" + nullSafe(reference == null ? null : reference.getStockId())
|
||||
+ "; space=" + nullSafe(reference == null ? null : reference.getSpaceId())
|
||||
+ "; requestedQuantity=" + objectOrEmpty(
|
||||
reference == null ? null : reference.getRequestedQuantity())
|
||||
+ "; inputSnapshotSync=" + nullSafe(
|
||||
reference == null ? null : reference.getSnapshotSynchronizedAt())
|
||||
+ "; shadowMatches=" + numberOrUnknown(
|
||||
reference == null ? null : reference.getShadowMatchCount())
|
||||
+ "; reason=" + abbreviate(nullSafe(resultReason), 600);
|
||||
StringBuilder diagnostic = snapshotDiagnostic(snapshot);
|
||||
appendField(diagnostic, "detail", requestId, IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "rawWarehouse", reference == null ? null : reference.getWarehouseId(),
|
||||
IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "project", reference == null ? null : reference.getProjectNo(),
|
||||
WIDE_IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "product", reference == null ? null : reference.getProductId(),
|
||||
WIDE_IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "tray", reference == null ? null : reference.getTrayNo(),
|
||||
WIDE_IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "stock", reference == null ? null : reference.getStockId(),
|
||||
IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "space", reference == null ? null : reference.getSpaceId(),
|
||||
IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "requestedQuantity", objectOrEmpty(
|
||||
reference == null ? null : reference.getRequestedQuantity()), NUMBER_MAX_LENGTH);
|
||||
appendField(diagnostic, "inputSnapshotSync",
|
||||
reference == null ? null : reference.getSnapshotSynchronizedAt(), SNAPSHOT_SYNC_MAX_LENGTH);
|
||||
appendField(diagnostic, "shadowMatches", numberOrUnknown(
|
||||
reference == null ? null : reference.getShadowMatchCount()), NUMBER_MAX_LENGTH);
|
||||
appendField(diagnostic, "reason", resultReason, RESULT_REASON_MAX_LENGTH);
|
||||
return diagnostic.toString();
|
||||
}
|
||||
|
||||
private String inboundDiagnostic(PublishedShadowSnapshot snapshot, String taskNo, String trayNo,
|
||||
ShadowInboundReference reference, String resultReason) {
|
||||
return snapshotDiagnostic(snapshot)
|
||||
+ "; task=" + nullSafe(taskNo)
|
||||
+ "; tray=" + nullSafe(trayNo)
|
||||
+ "; request=" + nullSafe(reference == null ? null : reference.getRequestId())
|
||||
+ "; rawWarehouse=" + nullSafe(reference == null ? null : reference.getWarehouseId())
|
||||
+ "; project=" + nullSafe(reference == null ? null : reference.getProjectNo())
|
||||
+ "; product=" + nullSafe(reference == null ? null : reference.getProductId())
|
||||
+ "; lot=" + nullSafe(reference == null ? null : reference.getLotNo())
|
||||
+ "; shadowCandidates=" + numberOrUnknown(
|
||||
reference == null ? null : reference.getShadowCandidateCount())
|
||||
+ "; reason=" + abbreviate(nullSafe(resultReason), 600);
|
||||
StringBuilder diagnostic = snapshotDiagnostic(snapshot);
|
||||
appendField(diagnostic, "task", taskNo, IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "tray", trayNo, WIDE_IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "request", reference == null ? null : reference.getRequestId(),
|
||||
IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "rawWarehouse", reference == null ? null : reference.getWarehouseId(),
|
||||
IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "project", reference == null ? null : reference.getProjectNo(),
|
||||
WIDE_IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "product", reference == null ? null : reference.getProductId(),
|
||||
WIDE_IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "lot", reference == null ? null : reference.getLotNo(),
|
||||
IDENTIFIER_MAX_LENGTH);
|
||||
appendField(diagnostic, "shadowCandidates", numberOrUnknown(
|
||||
reference == null ? null : reference.getShadowCandidateCount()), NUMBER_MAX_LENGTH);
|
||||
appendField(diagnostic, "reason", resultReason, RESULT_REASON_MAX_LENGTH);
|
||||
return diagnostic.toString();
|
||||
}
|
||||
|
||||
private String snapshotDiagnostic(PublishedShadowSnapshot snapshot) {
|
||||
return "snapshotGeneration=" + snapshot.generation()
|
||||
+ "; snapshotSync=" + instantOrEmpty(snapshot.synchronizedAt())
|
||||
+ "; sourceSignature=" + nullSafe(snapshot.sourceSignature());
|
||||
private String inboundComparisonReason(InboundShadowObservation observation,
|
||||
String legacyResultCode, String legacySpaceId) {
|
||||
StringBuilder reason = new StringBuilder();
|
||||
appendField(reason, "snapshotGeneration", Long.toString(observation.snapshotGeneration()),
|
||||
NUMBER_MAX_LENGTH);
|
||||
appendField(reason, "request", observation.requestId(), IDENTIFIER_MAX_LENGTH);
|
||||
appendField(reason, "shadow", observation.resultCode(), IDENTIFIER_MAX_LENGTH);
|
||||
appendField(reason, "proposedSpace", observation.proposedSpaceId(), IDENTIFIER_MAX_LENGTH);
|
||||
appendField(reason, "legacy", legacyResultCode, IDENTIFIER_MAX_LENGTH);
|
||||
appendField(reason, "legacySpace", legacySpaceId, IDENTIFIER_MAX_LENGTH);
|
||||
appendRemainingField(reason, "shadowReason", observation.reason());
|
||||
return reason.toString();
|
||||
}
|
||||
|
||||
private String outboundComparisonReason(OutboundShadowObservation observation, String legacyResultCode) {
|
||||
StringBuilder reason = new StringBuilder();
|
||||
appendField(reason, "snapshotGeneration", Long.toString(observation.snapshotGeneration()),
|
||||
NUMBER_MAX_LENGTH);
|
||||
appendField(reason, "request", observation.requestId(), IDENTIFIER_MAX_LENGTH);
|
||||
appendField(reason, "shadow", observation.resultCode(), IDENTIFIER_MAX_LENGTH);
|
||||
appendField(reason, "proposedStock", observation.proposedStockId(), IDENTIFIER_MAX_LENGTH);
|
||||
appendField(reason, "proposedSpace", observation.proposedSpaceId(), IDENTIFIER_MAX_LENGTH);
|
||||
appendField(reason, "legacy", legacyResultCode, IDENTIFIER_MAX_LENGTH);
|
||||
appendRemainingField(reason, "shadowReason", observation.reason());
|
||||
return reason.toString();
|
||||
}
|
||||
|
||||
private StringBuilder snapshotDiagnostic(PublishedShadowSnapshot snapshot) {
|
||||
StringBuilder diagnostic = new StringBuilder();
|
||||
appendField(diagnostic, "snapshotGeneration", Long.toString(snapshot.generation()), NUMBER_MAX_LENGTH);
|
||||
appendField(diagnostic, "snapshotSync", instantOrEmpty(snapshot.synchronizedAt()),
|
||||
SNAPSHOT_SYNC_MAX_LENGTH);
|
||||
appendField(diagnostic, "sourceSignature", snapshot.sourceSignature(), SOURCE_SIGNATURE_MAX_LENGTH);
|
||||
return diagnostic;
|
||||
}
|
||||
|
||||
private void appendField(StringBuilder target, String name, String value, int maxValueLength) {
|
||||
if (target.length() > 0) {
|
||||
target.append("; ");
|
||||
}
|
||||
target.append(name).append('=').append(abbreviate(nullSafe(value), maxValueLength));
|
||||
}
|
||||
|
||||
private void appendRemainingField(StringBuilder target, String name, String value) {
|
||||
String prefix = target.length() == 0 ? name + "=" : "; " + name + "=";
|
||||
int remaining = Math.max(0, DECISION_REASON_MAX_LENGTH - target.length() - prefix.length());
|
||||
target.append(prefix).append(abbreviate(nullSafe(value), remaining));
|
||||
}
|
||||
|
||||
private void requireUppercaseHexSignature(String value, String description) {
|
||||
@@ -356,7 +406,15 @@ public class ShadowPlanningEngine {
|
||||
}
|
||||
|
||||
private String abbreviate(String value, int maxLength) {
|
||||
return value.length() <= maxLength ? value : value.substring(0, maxLength);
|
||||
if (value.length() <= maxLength) {
|
||||
return value;
|
||||
}
|
||||
int end = maxLength;
|
||||
if (end > 0 && Character.isHighSurrogate(value.charAt(end - 1))
|
||||
&& Character.isLowSurrogate(value.charAt(end))) {
|
||||
end--;
|
||||
}
|
||||
return value.substring(0, end);
|
||||
}
|
||||
|
||||
private String comparisonCode(String shadowSpaceId, String legacySpaceId) {
|
||||
|
||||
@@ -212,13 +212,22 @@ class ShadowPlanningEngineTest {
|
||||
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);
|
||||
String shadowDiagnostic = "snapshotGeneration=42; snapshotSync=2026-09-03T00:00:00Z"
|
||||
+ "; sourceSignature=ABC123; project=" + "中".repeat(255)
|
||||
+ "; shadowCandidates=17; reason=" + "长".repeat(5000);
|
||||
InboundShadowObservation observation = new InboundShadowObservation(
|
||||
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(),
|
||||
"JOB-1", "READY", "SPACE-9", shadowDiagnostic);
|
||||
SchedulingFacade facade = mock(SchedulingFacade.class);
|
||||
String oversized = "中".repeat(5000);
|
||||
ShadowInboundReference reference = inboundReference(oversized);
|
||||
reference.setRequestId(oversized);
|
||||
reference.setProjectNo(oversized);
|
||||
reference.setProductId(oversized);
|
||||
reference.setLotNo(oversized);
|
||||
reference.setShadowCandidateCount(17);
|
||||
when(mapper.selectInboundInput("01", 42L, "TASK-1", "TRAY-1")).thenReturn(reference);
|
||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
||||
when(facade.planInboundFromPublishedSnapshot(eq("01"), any()))
|
||||
.thenReturn(new InboundPlanningResult(InboundPlan.assigned(inboundAssignment()),
|
||||
VersionToken.numeric(8)));
|
||||
ShadowPlanningEngine engine = engine(facade, mapper);
|
||||
|
||||
InboundShadowObservation observation = engine.planInbound(SNAPSHOT, "TASK-1", "TRAY-1");
|
||||
|
||||
engine.compareInbound(observation, "READY", "LEGACY-SPACE-9");
|
||||
|
||||
@@ -236,13 +245,23 @@ class ShadowPlanningEngineTest {
|
||||
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);
|
||||
String shadowDiagnostic = "snapshotGeneration=42; snapshotSync=2026-09-03T00:00:00Z"
|
||||
+ "; sourceSignature=ABC123; project=" + "中".repeat(255)
|
||||
+ "; shadowMatches=23; reason=" + "长".repeat(5000);
|
||||
OutboundShadowObservation observation = new OutboundShadowObservation(
|
||||
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(),
|
||||
"DETAIL-1", "READY", "STOCK-1", "SPACE-9", shadowDiagnostic);
|
||||
SchedulingFacade facade = mock(SchedulingFacade.class);
|
||||
String oversized = "中".repeat(5000);
|
||||
ShadowStockReference reference = projectReference(oversized);
|
||||
reference.setRequestId(oversized);
|
||||
reference.setProjectNo(oversized);
|
||||
reference.setProductId(oversized);
|
||||
reference.setTrayNo(oversized);
|
||||
reference.setStockId(oversized);
|
||||
reference.setSpaceId(oversized);
|
||||
reference.setShadowMatchCount(23);
|
||||
when(mapper.selectProjectOutboundReference("01", 42L, "DETAIL-1")).thenReturn(reference);
|
||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
||||
when(facade.planOutboundFromPublishedSnapshot(eq("01"), any()))
|
||||
.thenReturn(OutboundPlan.ready(outboundAssignment(), TaskCountEstimate.zero()));
|
||||
ShadowPlanningEngine engine = engine(facade, mapper);
|
||||
|
||||
OutboundShadowObservation observation = engine.planProjectOutbound(SNAPSHOT, "DETAIL-1");
|
||||
|
||||
engine.compareOutbound(observation, "10");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user