feat: replay guarded shadow decisions
This commit is contained in:
@@ -14,9 +14,15 @@
|
|||||||
<maven.compiler.source>1.8</maven.compiler.source>
|
<maven.compiler.source>1.8</maven.compiler.source>
|
||||||
<maven.compiler.target>1.8</maven.compiler.target>
|
<maven.compiler.target>1.8</maven.compiler.target>
|
||||||
<junit.version>5.10.2</junit.version>
|
<junit.version>5.10.2</junit.version>
|
||||||
|
<jackson.version>2.17.2</jackson.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
|||||||
@@ -13,9 +13,23 @@ import com.ft546.wcs.demo.simulation.SimulatedProtocolAdapter;
|
|||||||
import com.ft546.wcs.demo.traffic.GridMap;
|
import com.ft546.wcs.demo.traffic.GridMap;
|
||||||
import com.ft546.wcs.demo.traffic.GridPathPlanner;
|
import com.ft546.wcs.demo.traffic.GridPathPlanner;
|
||||||
import com.ft546.wcs.demo.traffic.TrafficController;
|
import com.ft546.wcs.demo.traffic.TrafficController;
|
||||||
|
import com.ft546.wcs.shadow.application.ShadowTrafficCoordinator;
|
||||||
|
import com.ft546.wcs.shadow.decision.DecisionType;
|
||||||
|
import com.ft546.wcs.shadow.decision.RollingDispatchPolicy;
|
||||||
|
import com.ft546.wcs.shadow.decision.ShadowCandidate;
|
||||||
|
import com.ft546.wcs.shadow.map.EffectiveTrafficMap;
|
||||||
|
import com.ft546.wcs.shadow.map.TrafficMapCompiler;
|
||||||
|
import com.ft546.wcs.shadow.replay.DecisionEvent;
|
||||||
|
import com.ft546.wcs.shadow.resource.ResourceKey;
|
||||||
|
import com.ft546.wcs.shadow.resource.ResourceLedger;
|
||||||
|
import com.ft546.wcs.shadow.snapshot.TrafficSnapshot;
|
||||||
|
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
|
||||||
public final class DemoApplication {
|
public final class DemoApplication {
|
||||||
|
|
||||||
@@ -82,6 +96,7 @@ public final class DemoApplication {
|
|||||||
+ " history=" + state.getHistory().size()
|
+ " history=" + state.getHistory().size()
|
||||||
+ " protocolAttempts=" + protocol.getAttemptedQueueIds().size()
|
+ " protocolAttempts=" + protocol.getAttemptedQueueIds().size()
|
||||||
);
|
);
|
||||||
|
printOfflineShadowSummary(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void printCycle(PrintStream out, CycleResult result) {
|
private static void printCycle(PrintStream out, CycleResult result) {
|
||||||
@@ -135,4 +150,39 @@ public final class DemoApplication {
|
|||||||
private static VehicleSnapshot vehicle(String id, String site) {
|
private static VehicleSnapshot vehicle(String id, String site) {
|
||||||
return new VehicleSnapshot(id, 1, site, 5, 1, 80, true, true, false);
|
return new VehicleSnapshot(id, 1, site, 5, 1, 80, true, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void printOfflineShadowSummary(PrintStream out) {
|
||||||
|
LinkedHashSet<String> nodes = new LinkedHashSet<String>(Arrays.asList("A", "B", "C"));
|
||||||
|
LinkedHashSet<TrafficMapCompiler.EdgeDefinition> edges =
|
||||||
|
new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>();
|
||||||
|
edges.add(new TrafficMapCompiler.EdgeDefinition("A", "B"));
|
||||||
|
edges.add(new TrafficMapCompiler.EdgeDefinition("B", "C"));
|
||||||
|
EffectiveTrafficMap map = new TrafficMapCompiler().compile(
|
||||||
|
new TrafficMapCompiler.MapDefinition(
|
||||||
|
"FT546", 2L, "ZONE-DEMO", true, "demo-sha", "demo-sha",
|
||||||
|
nodes, edges, new LinkedHashMap<ResourceKey, Integer>(),
|
||||||
|
Collections.singleton("A"), Collections.singleton("C")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
ResourceLedger ledger = new ResourceLedger(map.getCapacities());
|
||||||
|
ledger.evaluate("BLOCKER", Collections.singleton(ResourceKey.node("B")));
|
||||||
|
DecisionEvent event = new ShadowTrafficCoordinator(
|
||||||
|
map, ledger, new RollingDispatchPolicy()
|
||||||
|
).run(
|
||||||
|
new TrafficSnapshot(1L, Instant.parse("2026-07-30T05:00:00Z"),
|
||||||
|
"FT546", 2L, "POLICY-DEMO", Collections.emptyList()),
|
||||||
|
Arrays.asList(
|
||||||
|
ShadowCandidate.waiting("SHADOW-1", "V1", false, 2, 5, 1,
|
||||||
|
Collections.singleton(ResourceKey.node("B")), null),
|
||||||
|
ShadowCandidate.waiting("SHADOW-2", "V2", false, 1, 1, 1,
|
||||||
|
Collections.singleton(ResourceKey.node("C")), null)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
long grants = event.getDecisions().stream()
|
||||||
|
.filter(decision -> decision.getType() == DecisionType.GRANT).count();
|
||||||
|
long holds = event.getDecisions().stream()
|
||||||
|
.filter(decision -> decision.getType() == DecisionType.HOLD).count();
|
||||||
|
out.println("SHADOW-SUMMARY mode=OFFLINE_SHADOW decisions="
|
||||||
|
+ event.getDecisions().size() + " grants=" + grants + " holds=" + holds + " writes=0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.ft546.wcs.shadow.application;
|
||||||
|
|
||||||
|
import com.ft546.wcs.shadow.decision.RollingDispatchPolicy;
|
||||||
|
import com.ft546.wcs.shadow.decision.ShadowCandidate;
|
||||||
|
import com.ft546.wcs.shadow.decision.ShadowDecision;
|
||||||
|
import com.ft546.wcs.shadow.map.EffectiveTrafficMap;
|
||||||
|
import com.ft546.wcs.shadow.replay.DecisionEvent;
|
||||||
|
import com.ft546.wcs.shadow.resource.ResourceKey;
|
||||||
|
import com.ft546.wcs.shadow.resource.ResourceLedger;
|
||||||
|
import com.ft546.wcs.shadow.snapshot.TrafficSnapshot;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class ShadowTrafficCoordinator {
|
||||||
|
private final EffectiveTrafficMap map;
|
||||||
|
private final ResourceLedger authoritativeLedger;
|
||||||
|
private final RollingDispatchPolicy policy;
|
||||||
|
|
||||||
|
public ShadowTrafficCoordinator(EffectiveTrafficMap map, ResourceLedger authoritativeLedger,
|
||||||
|
RollingDispatchPolicy policy) {
|
||||||
|
this.map = map;
|
||||||
|
this.authoritativeLedger = authoritativeLedger;
|
||||||
|
this.policy = policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecisionEvent run(TrafficSnapshot snapshot, List<ShadowCandidate> candidates) {
|
||||||
|
if (!map.getMapCode().equals(snapshot.getMapCode())
|
||||||
|
|| map.getVersionId() != snapshot.getMapVersion()) {
|
||||||
|
throw new IllegalArgumentException("snapshot map version mismatch");
|
||||||
|
}
|
||||||
|
String hash = hash(snapshot, candidates);
|
||||||
|
List<ShadowDecision> decisions = policy.decide(candidates, authoritativeLedger, map);
|
||||||
|
return new DecisionEvent("shadow-" + snapshot.getTickSeq() + "-" + hash.substring(0, 12),
|
||||||
|
snapshot.getTickSeq(), snapshot.getMapCode(), snapshot.getMapVersion(),
|
||||||
|
snapshot.getPolicyVersion(), hash, decisions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String hash(TrafficSnapshot snapshot, List<ShadowCandidate> candidates) {
|
||||||
|
StringBuilder canonical = new StringBuilder()
|
||||||
|
.append(snapshot.getTickSeq()).append('|')
|
||||||
|
.append(snapshot.getMapCode()).append('|')
|
||||||
|
.append(snapshot.getMapVersion()).append('|')
|
||||||
|
.append(snapshot.getPolicyVersion());
|
||||||
|
for (ShadowCandidate candidate : candidates) {
|
||||||
|
canonical.append('|').append(candidate.getTaskId()).append(':')
|
||||||
|
.append(candidate.getVehicleId()).append(':')
|
||||||
|
.append(candidate.isCommitted()).append(':')
|
||||||
|
.append(candidate.getBusinessPriority()).append(':')
|
||||||
|
.append(candidate.getWaitAge());
|
||||||
|
for (ResourceKey resource : candidate.getResources()) {
|
||||||
|
canonical.append(':').append(resource.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
byte[] digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(canonical.toString().getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder hex = new StringBuilder();
|
||||||
|
for (byte value : digest) hex.append(String.format("%02x", value & 0xff));
|
||||||
|
return hex.toString();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("SHA-256 unavailable", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ft546.wcs.shadow.application;
|
||||||
|
|
||||||
|
public final class ShadowWriteForbiddenException extends IllegalStateException {
|
||||||
|
public ShadowWriteForbiddenException(String operation) {
|
||||||
|
super("SHADOW forbids authoritative write: " + operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ft546.wcs.shadow.application;
|
||||||
|
|
||||||
|
public final class ShadowWritePort {
|
||||||
|
public void sendPlcCommand(String vehicleId) {
|
||||||
|
throw new ShadowWriteForbiddenException("PLC " + vehicleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateBusinessState(String taskId) {
|
||||||
|
throw new ShadowWriteForbiddenException("BUSINESS " + taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeReservation(String segmentId) {
|
||||||
|
throw new ShadowWriteForbiddenException("RESERVATION " + segmentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.ft546.wcs.shadow.replay;
|
||||||
|
|
||||||
|
import com.ft546.wcs.shadow.decision.ShadowDecision;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class DecisionEvent {
|
||||||
|
private final String decisionId;
|
||||||
|
private final long tickSeq;
|
||||||
|
private final String mapCode;
|
||||||
|
private final long mapVersion;
|
||||||
|
private final String policyVersion;
|
||||||
|
private final String inputHash;
|
||||||
|
private final List<ShadowDecision> decisions;
|
||||||
|
|
||||||
|
public DecisionEvent(String decisionId, long tickSeq, String mapCode, long mapVersion,
|
||||||
|
String policyVersion, String inputHash, List<ShadowDecision> decisions) {
|
||||||
|
this.decisionId = decisionId;
|
||||||
|
this.tickSeq = tickSeq;
|
||||||
|
this.mapCode = mapCode;
|
||||||
|
this.mapVersion = mapVersion;
|
||||||
|
this.policyVersion = policyVersion;
|
||||||
|
this.inputHash = inputHash;
|
||||||
|
this.decisions = Collections.unmodifiableList(new ArrayList<ShadowDecision>(decisions));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDecisionId() { return decisionId; }
|
||||||
|
public long getTickSeq() { return tickSeq; }
|
||||||
|
public String getMapCode() { return mapCode; }
|
||||||
|
public long getMapVersion() { return mapVersion; }
|
||||||
|
public String getPolicyVersion() { return policyVersion; }
|
||||||
|
public String getInputHash() { return inputHash; }
|
||||||
|
public List<ShadowDecision> getDecisions() { return decisions; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.ft546.wcs.shadow.replay;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.ft546.wcs.shadow.decision.DecisionType;
|
||||||
|
import com.ft546.wcs.shadow.decision.ShadowDecision;
|
||||||
|
import com.ft546.wcs.shadow.resource.ResourceKey;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class DecisionReplayService {
|
||||||
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
public DecisionEvent read(InputStream input) throws IOException {
|
||||||
|
if (input == null) throw new IllegalArgumentException("decision event input is required");
|
||||||
|
JsonNode root = mapper.readTree(input);
|
||||||
|
List<ShadowDecision> decisions = new ArrayList<ShadowDecision>();
|
||||||
|
for (JsonNode node : root.withArray("decisions")) {
|
||||||
|
Set<ResourceKey> conflicts = new LinkedHashSet<ResourceKey>();
|
||||||
|
for (JsonNode resource : node.withArray("conflictingResources")) {
|
||||||
|
conflicts.add(ResourceKey.fromValue(resource.asText()));
|
||||||
|
}
|
||||||
|
Set<String> blockers = new LinkedHashSet<String>();
|
||||||
|
for (JsonNode blocker : node.withArray("blockingVehicles")) {
|
||||||
|
blockers.add(blocker.asText());
|
||||||
|
}
|
||||||
|
decisions.add(new ShadowDecision(
|
||||||
|
node.path("taskId").asText(), node.path("vehicleId").asText(),
|
||||||
|
DecisionType.valueOf(node.path("type").asText()),
|
||||||
|
node.path("reasonCode").asText(), conflicts, blockers,
|
||||||
|
node.path("yieldSite").isNull() ? null : node.path("yieldSite").asText()));
|
||||||
|
}
|
||||||
|
return new DecisionEvent(root.path("decisionId").asText(), root.path("tickSeq").asLong(),
|
||||||
|
root.path("mapCode").asText(), root.path("mapVersion").asLong(),
|
||||||
|
root.path("policyVersion").asText(), root.path("inputHash").asText(), decisions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ShadowDecision> replay(DecisionEvent event) {
|
||||||
|
return event.getDecisions();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,16 @@ public final class ResourceKey implements Comparable<ResourceKey> {
|
|||||||
return new ResourceKey("CONFLICT:" + required(code));
|
return new ResourceKey("CONFLICT:" + required(code));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ResourceKey fromValue(String value) {
|
||||||
|
String normalized = required(value);
|
||||||
|
if (!normalized.startsWith("NODE:")
|
||||||
|
&& !normalized.startsWith("EDGE:")
|
||||||
|
&& !normalized.startsWith("CONFLICT:")) {
|
||||||
|
throw new IllegalArgumentException("unsupported resource key " + normalized);
|
||||||
|
}
|
||||||
|
return new ResourceKey(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
public String value() {
|
public String value() {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ class DemoScenarioTest {
|
|||||||
assertTrue(text.contains(
|
assertTrue(text.contains(
|
||||||
"SUMMARY cycles=7 activeQueues=1 history=2 protocolAttempts=3"
|
"SUMMARY cycles=7 activeQueues=1 history=2 protocolAttempts=3"
|
||||||
));
|
));
|
||||||
|
assertTrue(text.contains(
|
||||||
|
"SHADOW-SUMMARY mode=OFFLINE_SHADOW decisions=2 grants=1 holds=1 writes=0"
|
||||||
|
));
|
||||||
|
|
||||||
int assign = text.indexOf("ASSIGN task=TASK-OUT-1");
|
int assign = text.indexOf("ASSIGN task=TASK-OUT-1");
|
||||||
int avoid = text.indexOf("AVOID queue=");
|
int avoid = text.indexOf("AVOID queue=");
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.ft546.wcs.shadow;
|
||||||
|
|
||||||
|
import com.ft546.wcs.shadow.application.ShadowTrafficCoordinator;
|
||||||
|
import com.ft546.wcs.shadow.application.ShadowWriteForbiddenException;
|
||||||
|
import com.ft546.wcs.shadow.application.ShadowWritePort;
|
||||||
|
import com.ft546.wcs.shadow.decision.DecisionType;
|
||||||
|
import com.ft546.wcs.shadow.decision.RollingDispatchPolicy;
|
||||||
|
import com.ft546.wcs.shadow.decision.ShadowCandidate;
|
||||||
|
import com.ft546.wcs.shadow.map.EffectiveTrafficMap;
|
||||||
|
import com.ft546.wcs.shadow.map.TrafficMapCompiler;
|
||||||
|
import com.ft546.wcs.shadow.replay.DecisionEvent;
|
||||||
|
import com.ft546.wcs.shadow.replay.DecisionReplayService;
|
||||||
|
import com.ft546.wcs.shadow.resource.ResourceKey;
|
||||||
|
import com.ft546.wcs.shadow.resource.ResourceLedger;
|
||||||
|
import com.ft546.wcs.shadow.snapshot.TrafficSnapshot;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
class ShadowCoordinatorReplayTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shadowPortsRejectEveryAuthoritativeWrite() {
|
||||||
|
ShadowWritePort port = new ShadowWritePort();
|
||||||
|
|
||||||
|
assertThrows(ShadowWriteForbiddenException.class, () -> port.sendPlcCommand("V1"));
|
||||||
|
assertThrows(ShadowWriteForbiddenException.class, () -> port.updateBusinessState("T1"));
|
||||||
|
assertThrows(ShadowWriteForbiddenException.class, () -> port.writeReservation("SEG-1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void coordinatorProducesStableEventWithoutMutatingAuthoritativeLedger() {
|
||||||
|
EffectiveTrafficMap map = map();
|
||||||
|
ResourceLedger ledger = new ResourceLedger(map.getCapacities());
|
||||||
|
ledger.evaluate("BLOCKER", Collections.singleton(ResourceKey.node("B")));
|
||||||
|
ShadowTrafficCoordinator coordinator =
|
||||||
|
new ShadowTrafficCoordinator(map, ledger, new RollingDispatchPolicy());
|
||||||
|
TrafficSnapshot snapshot =
|
||||||
|
new TrafficSnapshot(9L, Instant.parse("2026-07-30T05:00:00Z"),
|
||||||
|
"FT546", 2L, "POLICY-1", Collections.emptyList());
|
||||||
|
ShadowCandidate candidate = ShadowCandidate.waiting(
|
||||||
|
"T1", "V1", false, 5, 12, 1,
|
||||||
|
Collections.singleton(ResourceKey.node("B")), null);
|
||||||
|
|
||||||
|
DecisionEvent first = coordinator.run(snapshot, Collections.singletonList(candidate));
|
||||||
|
DecisionEvent second = coordinator.run(snapshot, Collections.singletonList(candidate));
|
||||||
|
|
||||||
|
assertEquals(first.getInputHash(), second.getInputHash());
|
||||||
|
assertEquals(DecisionType.HOLD, first.getDecisions().get(0).getType());
|
||||||
|
assertEquals(1, ledger.protectedCount(ResourceKey.node("B")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readsAndReplaysGoldenDecisionEvent() throws Exception {
|
||||||
|
InputStream input = getClass().getResourceAsStream("/shadow/golden/basic-conflict.json");
|
||||||
|
DecisionReplayService replay = new DecisionReplayService();
|
||||||
|
|
||||||
|
DecisionEvent event = replay.read(input);
|
||||||
|
|
||||||
|
assertEquals("golden-basic-conflict", event.getDecisionId());
|
||||||
|
assertEquals(DecisionType.HOLD, replay.replay(event).get(0).getType());
|
||||||
|
assertEquals("NO_SAFE_YIELD_SITE", replay.replay(event).get(0).getReasonCode());
|
||||||
|
assertEquals(Collections.singleton(ResourceKey.node("B")),
|
||||||
|
replay.replay(event).get(0).getConflictingResources());
|
||||||
|
}
|
||||||
|
|
||||||
|
private EffectiveTrafficMap map() {
|
||||||
|
LinkedHashSet<String> nodes = new LinkedHashSet<String>(Arrays.asList("A", "B", "C"));
|
||||||
|
LinkedHashSet<TrafficMapCompiler.EdgeDefinition> edges =
|
||||||
|
new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>();
|
||||||
|
edges.add(new TrafficMapCompiler.EdgeDefinition("A", "B"));
|
||||||
|
edges.add(new TrafficMapCompiler.EdgeDefinition("B", "C"));
|
||||||
|
return new TrafficMapCompiler().compile(new TrafficMapCompiler.MapDefinition(
|
||||||
|
"FT546", 2L, "ZONE-1", true, "sha", "sha", nodes, edges,
|
||||||
|
new LinkedHashMap<ResourceKey, Integer>(), Collections.singleton("A"),
|
||||||
|
Collections.singleton("C")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"decisionId": "golden-basic-conflict",
|
||||||
|
"tickSeq": 9,
|
||||||
|
"mapCode": "FT546",
|
||||||
|
"mapVersion": 2,
|
||||||
|
"policyVersion": "POLICY-1",
|
||||||
|
"inputHash": "golden-input-sha256",
|
||||||
|
"decisions": [
|
||||||
|
{
|
||||||
|
"taskId": "T1",
|
||||||
|
"vehicleId": "V1",
|
||||||
|
"type": "HOLD",
|
||||||
|
"reasonCode": "NO_SAFE_YIELD_SITE",
|
||||||
|
"conflictingResources": ["NODE:B"],
|
||||||
|
"blockingVehicles": ["BLOCKER"],
|
||||||
|
"yieldSite": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user