generated from coulomb/repo-seed
Compare commits
2 Commits
d060b1c896
...
1ad432270b
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ad432270b | |||
| f1cd74dc7b |
@@ -156,7 +156,9 @@ reach it here?" without mutating the underlying Fabric responsibility boundary.
|
||||
|
||||
Zone boundary overlays are a visual layer over the graph canvas. They should be
|
||||
computed from visible node positions after layout/filtering rather than modeled
|
||||
as graph parent nodes. The default boundary grouping is `deploymentEnvironment`:
|
||||
as graph parent nodes. The overlay should be backed by explicit zone definitions
|
||||
and a resolver so visible nodes are assigned to at most one rendered zone. The
|
||||
default boundary grouping is `deploymentEnvironment`:
|
||||
|
||||
| Overlay label | Matching nodes |
|
||||
|---------------|----------------|
|
||||
@@ -180,6 +182,12 @@ Useful warnings for the graph explorer include:
|
||||
- local-only surfaces that appear in shared or production scenarios;
|
||||
- conflicting port or host claims within the same deployment scenario.
|
||||
|
||||
Zone resolvers should also expose scoped diagnostics in zone detail panels. The
|
||||
initial diagnostic set includes empty zone seed sets, visible nodes matched by
|
||||
multiple zone definitions, and edges crossing zone boundaries. Attraction
|
||||
diagnostics such as multiple attraction candidates or depth-limit stops belong
|
||||
to the same resolver diagnostic channel when attraction rules are enabled.
|
||||
|
||||
## Repo-Scoping Compatibility
|
||||
|
||||
Repo-scoping can adapt without a rewrite because its current graph payload
|
||||
|
||||
@@ -988,10 +988,257 @@ def graph_explorer_page() -> str:
|
||||
.split("")
|
||||
.reduce((total, character) => (total * 31 + character.charCodeAt(0)) >>> 0, 0);
|
||||
|
||||
const normalizeDeploymentZoneValue = (value) => {
|
||||
const raw = String(value || "").trim();
|
||||
const normalized = raw.toLowerCase();
|
||||
if (!normalized || normalized === "all") return "";
|
||||
if (normalized === "dev") return "dev-tegwick";
|
||||
if (normalized === "test" || normalized === "staging") return "test";
|
||||
if (normalized === "prod") return "prod";
|
||||
return raw;
|
||||
};
|
||||
|
||||
const defaultZoneDefinitions = {
|
||||
deploymentEnvironment: [
|
||||
{
|
||||
id: "deploymentEnvironment:dev-tegwick",
|
||||
label: "dev-tegwick",
|
||||
field: "deploymentEnvironment",
|
||||
value: "dev-tegwick",
|
||||
rank: 0,
|
||||
membership: {
|
||||
field: "deploymentEnvironment",
|
||||
op: "equals",
|
||||
value: "dev-tegwick",
|
||||
normalize: "deploymentEnvironment",
|
||||
},
|
||||
presentation: {height: 10, color: "#0f766e", fill: "rgba(15, 118, 110, .07)"},
|
||||
},
|
||||
{
|
||||
id: "deploymentEnvironment:test",
|
||||
label: "test",
|
||||
field: "deploymentEnvironment",
|
||||
value: "test",
|
||||
rank: 1,
|
||||
membership: {
|
||||
field: "deploymentEnvironment",
|
||||
op: "equals",
|
||||
value: "test",
|
||||
normalize: "deploymentEnvironment",
|
||||
},
|
||||
presentation: {height: 20, color: "#2563eb", fill: "rgba(37, 99, 235, .07)"},
|
||||
},
|
||||
{
|
||||
id: "deploymentEnvironment:prod",
|
||||
label: "prod",
|
||||
field: "deploymentEnvironment",
|
||||
value: "prod",
|
||||
rank: 2,
|
||||
membership: {
|
||||
field: "deploymentEnvironment",
|
||||
op: "equals",
|
||||
value: "prod",
|
||||
normalize: "deploymentEnvironment",
|
||||
},
|
||||
presentation: {height: 30, color: "#be123c", fill: "rgba(190, 18, 60, .07)"},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const zoneElementData = (element) => {
|
||||
if (!element) return {};
|
||||
if (typeof element.data === "function") return element.data();
|
||||
return element.data && typeof element.data === "object" ? element.data : element;
|
||||
};
|
||||
|
||||
const zoneFieldValue = (data, field) => {
|
||||
if (!field) return "";
|
||||
let current = data || {};
|
||||
String(field).split(".").forEach((part) => {
|
||||
current = current && typeof current === "object" ? current[part] : undefined;
|
||||
});
|
||||
return current;
|
||||
};
|
||||
|
||||
const zoneRuleValue = (data, rule) => {
|
||||
const value = zoneFieldValue(data, rule.field);
|
||||
if (rule.normalize === "deploymentEnvironment") return normalizeDeploymentZoneValue(value);
|
||||
return hasValue(value) ? String(value).trim() : "";
|
||||
};
|
||||
|
||||
const zoneRuleMatches = (data, rule, emptyMatches = false) => {
|
||||
if (!rule || !Object.keys(rule).length) return emptyMatches;
|
||||
if (Array.isArray(rule.all)) {
|
||||
return rule.all.every((child) => zoneRuleMatches(data, child));
|
||||
}
|
||||
if (Array.isArray(rule.any)) {
|
||||
return rule.any.some((child) => zoneRuleMatches(data, child));
|
||||
}
|
||||
if (Array.isArray(rule.rules)) {
|
||||
return rule.rules.every((child) => zoneRuleMatches(data, child));
|
||||
}
|
||||
const actual = zoneRuleValue(data, rule);
|
||||
if (rule.op === "exists") return hasValue(actual);
|
||||
if (rule.op === "in") {
|
||||
const expected = Array.isArray(rule.value) ? rule.value : [rule.value];
|
||||
return expected.map((value) => String(value)).includes(String(actual));
|
||||
}
|
||||
return String(actual) === String(rule.value ?? "");
|
||||
};
|
||||
|
||||
const zoneDescriptorFromDefinition = (definition) => ({
|
||||
field: definition.field,
|
||||
id: definition.id,
|
||||
label: definition.label || definition.id,
|
||||
value: definition.value || definition.label || definition.id,
|
||||
rank: definition.rank,
|
||||
presentation: definition.presentation || {},
|
||||
membership: definition.membership || {},
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const accessZoneDefinition = (value) => {
|
||||
const label = String(value || "").trim();
|
||||
if (!label) return null;
|
||||
return {
|
||||
id: `accessZone:${label}`,
|
||||
label,
|
||||
field: "accessZone",
|
||||
value: label,
|
||||
rank: 10 + hashString(label) % 20,
|
||||
membership: {field: "accessZone", op: "equals", value: label},
|
||||
presentation: {},
|
||||
};
|
||||
};
|
||||
|
||||
const zoneDiagnosticCodes = {
|
||||
emptySeedSet: "ZONE_EMPTY_SEED_SET",
|
||||
nodeSeededByMultipleZones: "ZONE_NODE_SEEDED_BY_MULTIPLE_ZONES",
|
||||
nodeAttractedByMultipleZones: "ZONE_NODE_ATTRACTED_BY_MULTIPLE_ZONES",
|
||||
attractionDepthLimitReached: "ZONE_ATTRACTION_DEPTH_LIMIT_REACHED",
|
||||
edgeCrossesZoneBoundary: "ZONE_EDGE_CROSSES_ZONE_BOUNDARY",
|
||||
};
|
||||
|
||||
const zoneDefinitionsForData = (data, grouping = activeZoneGrouping) => {
|
||||
if (grouping === "deploymentEnvironment") return defaultZoneDefinitions.deploymentEnvironment;
|
||||
if (grouping === "accessZone") {
|
||||
const definition = accessZoneDefinition(zoneFieldValue(data, "accessZone"));
|
||||
return definition ? [definition] : [];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const zoneDefinitionsForElements = (elements, grouping = activeZoneGrouping) => {
|
||||
if (grouping === "deploymentEnvironment") return defaultZoneDefinitions.deploymentEnvironment;
|
||||
if (grouping === "accessZone") {
|
||||
const values = new Set();
|
||||
elements.forEach((element) => {
|
||||
const value = String(zoneFieldValue(zoneElementData(element), "accessZone") || "").trim();
|
||||
if (value) values.add(value);
|
||||
});
|
||||
return Array.from(values)
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
.map(accessZoneDefinition)
|
||||
.filter(Boolean);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const chooseZoneCandidate = (candidates) => candidates
|
||||
.slice()
|
||||
.sort((left, right) => {
|
||||
const leftHeight = Number(left.definition.presentation?.height) || 0;
|
||||
const rightHeight = Number(right.definition.presentation?.height) || 0;
|
||||
return rightHeight - leftHeight || left.index - right.index || left.definition.id.localeCompare(right.definition.id);
|
||||
})[0];
|
||||
|
||||
const resolveZoneInstances = (elements, definitions) => {
|
||||
const zones = new Map();
|
||||
definitions.forEach((definition, index) => {
|
||||
zones.set(definition.id, {
|
||||
...zoneDescriptorFromDefinition(definition),
|
||||
definition,
|
||||
definitionIndex: index,
|
||||
nodes: [],
|
||||
elements: [],
|
||||
nodeIds: new Set(),
|
||||
elementIds: new Set(),
|
||||
diagnostics: [],
|
||||
});
|
||||
});
|
||||
const assignments = new Map();
|
||||
const addElementToZone = (zoneId, element) => {
|
||||
const zone = zones.get(zoneId);
|
||||
if (!zone) return;
|
||||
const elementId = element.id ? element.id() : zoneElementData(element).id;
|
||||
if (elementId && zone.elementIds.has(elementId)) return;
|
||||
if (elementId) zone.elementIds.add(elementId);
|
||||
zone.elements.push(element);
|
||||
};
|
||||
elements.filter((element) => element.isNode && element.isNode()).forEach((node) => {
|
||||
const data = zoneElementData(node);
|
||||
const candidates = definitions
|
||||
.map((definition, index) => ({definition, index}))
|
||||
.filter((candidate) => zoneRuleMatches(data, candidate.definition.membership));
|
||||
if (!candidates.length) return;
|
||||
if (candidates.length > 1) {
|
||||
const diagnostic = {
|
||||
severity: "warning",
|
||||
code: zoneDiagnosticCodes.nodeSeededByMultipleZones,
|
||||
message: `${elementLabel(node)} matched multiple zone definitions.`,
|
||||
};
|
||||
candidates.forEach((candidate) => zones.get(candidate.definition.id)?.diagnostics.push(diagnostic));
|
||||
}
|
||||
const chosen = chooseZoneCandidate(candidates).definition;
|
||||
const zone = zones.get(chosen.id);
|
||||
if (!zone) return;
|
||||
assignments.set(node.id(), chosen.id);
|
||||
zone.nodeIds.add(node.id());
|
||||
zone.nodes.push(node);
|
||||
addElementToZone(chosen.id, node);
|
||||
});
|
||||
zones.forEach((zone) => {
|
||||
if (!zone.nodes.length) {
|
||||
zone.diagnostics.push({
|
||||
severity: "warning",
|
||||
code: zoneDiagnosticCodes.emptySeedSet,
|
||||
message: `${zone.label} has no visible seed nodes.`,
|
||||
});
|
||||
}
|
||||
});
|
||||
elements.filter((element) => element.isEdge && element.isEdge()).forEach((edge) => {
|
||||
const data = zoneElementData(edge);
|
||||
const zoneIds = new Set(
|
||||
definitions
|
||||
.filter((definition) => zoneRuleMatches(data, definition.membership))
|
||||
.map((definition) => definition.id)
|
||||
);
|
||||
const sourceZoneId = assignments.get(edge.data("source"));
|
||||
const targetZoneId = assignments.get(edge.data("target"));
|
||||
if ((sourceZoneId || targetZoneId) && sourceZoneId !== targetZoneId) {
|
||||
const diagnostic = {
|
||||
severity: "info",
|
||||
code: zoneDiagnosticCodes.edgeCrossesZoneBoundary,
|
||||
message: `${elementLabel(edge)} crosses a zone boundary.`,
|
||||
};
|
||||
[sourceZoneId, targetZoneId]
|
||||
.filter(Boolean)
|
||||
.forEach((zoneId) => zones.get(zoneId)?.diagnostics.push(diagnostic));
|
||||
}
|
||||
if (sourceZoneId) zoneIds.add(sourceZoneId);
|
||||
if (targetZoneId) zoneIds.add(targetZoneId);
|
||||
zoneIds.forEach((zoneId) => addElementToZone(zoneId, edge));
|
||||
});
|
||||
return zones;
|
||||
};
|
||||
|
||||
const zoneStyle = (zone) =>
|
||||
zonePalette[zone.id] || fallbackZonePalette[hashString(zone.id) % fallbackZonePalette.length];
|
||||
zone.presentation?.color
|
||||
? {color: zone.presentation.color, fill: zone.presentation.fill || zonePalette[zone.id]?.fill || "rgba(37, 99, 235, .07)"}
|
||||
: zonePalette[zone.id] || fallbackZonePalette[hashString(zone.id) % fallbackZonePalette.length];
|
||||
|
||||
const zoneRank = (zone) => {
|
||||
if (Number.isFinite(zone.rank)) return zone.rank;
|
||||
if (zone.id === "deploymentEnvironment:dev-tegwick") return 0;
|
||||
if (zone.id === "deploymentEnvironment:test") return 1;
|
||||
if (zone.id === "deploymentEnvironment:prod") return 2;
|
||||
@@ -1011,36 +1258,9 @@ def graph_explorer_page() -> str:
|
||||
};
|
||||
|
||||
const zoneForData = (data, grouping = activeZoneGrouping) => {
|
||||
if (grouping === "deploymentEnvironment") {
|
||||
const value = String(data.deploymentEnvironment || "").trim();
|
||||
if (!value) return null;
|
||||
const normalized = value.toLowerCase();
|
||||
if (normalized === "all") return null;
|
||||
const label = normalized === "dev"
|
||||
? "dev-tegwick"
|
||||
: normalized === "test" || normalized === "staging"
|
||||
? "test"
|
||||
: normalized === "prod"
|
||||
? normalized
|
||||
: value;
|
||||
return {
|
||||
field: "deploymentEnvironment",
|
||||
id: `deploymentEnvironment:${label}`,
|
||||
label,
|
||||
value,
|
||||
};
|
||||
}
|
||||
if (grouping === "accessZone") {
|
||||
const value = String(data.accessZone || "").trim();
|
||||
if (!value) return null;
|
||||
return {
|
||||
field: "accessZone",
|
||||
id: `accessZone:${value}`,
|
||||
label: value,
|
||||
value,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
const definition = zoneDefinitionsForData(data, grouping)
|
||||
.find((candidate) => zoneRuleMatches(data, candidate.membership));
|
||||
return definition ? zoneDescriptorFromDefinition(definition) : null;
|
||||
};
|
||||
|
||||
const renderedNodeBox = (node) => {
|
||||
@@ -1091,30 +1311,22 @@ def graph_explorer_page() -> str:
|
||||
};
|
||||
|
||||
const collectZoneSummaries = () => {
|
||||
const groups = new Map();
|
||||
if (!cy) return groups;
|
||||
cy.elements().filter((element) => element.style("display") !== "none").forEach((element) => {
|
||||
const zone = zoneForData(element.data());
|
||||
if (!zone) return;
|
||||
if (!groups.has(zone.id)) {
|
||||
groups.set(zone.id, {...zone, nodes: [], elements: []});
|
||||
}
|
||||
const group = groups.get(zone.id);
|
||||
group.elements.push(element);
|
||||
if (element.isNode()) group.nodes.push(element);
|
||||
});
|
||||
return groups;
|
||||
if (!cy) return new Map();
|
||||
const visibleElements = cy.elements()
|
||||
.filter((element) => element.style("display") !== "none")
|
||||
.toArray();
|
||||
return resolveZoneInstances(
|
||||
visibleElements,
|
||||
zoneDefinitionsForElements(visibleElements, activeZoneGrouping)
|
||||
);
|
||||
};
|
||||
|
||||
const zoneCountsForField = (elements, field) => {
|
||||
if (field !== "deploymentEnvironment" && field !== "accessZone") return groupCounts(elements, field);
|
||||
const groups = new Map();
|
||||
elements.forEach((element) => {
|
||||
const zone = zoneForData(element.data(), field);
|
||||
if (!zone) return;
|
||||
groups.set(zone.label, (groups.get(zone.label) || 0) + 1);
|
||||
});
|
||||
return Array.from(groups.entries())
|
||||
const zones = resolveZoneInstances(elements, zoneDefinitionsForElements(elements, field));
|
||||
return Array.from(zones.values())
|
||||
.filter((zone) => zone.elements.length > 0)
|
||||
.map((zone) => [zone.label, zone.elements.length])
|
||||
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
|
||||
};
|
||||
|
||||
@@ -1136,6 +1348,8 @@ def graph_explorer_page() -> str:
|
||||
.flatMap((element) => zoneWarningsForData(element.data()).map((warning) =>
|
||||
`${elementLabel(element)}: ${warning}`
|
||||
));
|
||||
const diagnostics = (zone.diagnostics || [])
|
||||
.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`);
|
||||
const rows = [
|
||||
["visible nodes", String(visibleNodes.length)],
|
||||
["deployment environments", valuesFor("deploymentEnvironment")],
|
||||
@@ -1143,12 +1357,14 @@ def graph_explorer_page() -> str:
|
||||
["access zones", valuesFor("accessZone")],
|
||||
["routing authorities", valuesFor("routingAuthority")],
|
||||
["policy authorities", valuesFor("policyAuthority")],
|
||||
...diagnostics.slice(0, 8).map((diagnostic) => ["diagnostic", diagnostic]),
|
||||
...warnings.slice(0, 8).map((warning) => ["warning", warning]),
|
||||
];
|
||||
if (diagnostics.length > 8) rows.push(["diagnostic", `${diagnostics.length - 8} additional zone diagnostics`]);
|
||||
if (warnings.length > 8) rows.push(["warning", `${warnings.length - 8} additional route warnings`]);
|
||||
detailList.innerHTML = rows
|
||||
.filter(([, value]) => value)
|
||||
.map(([key, value]) => `<li class="${key === "warning" ? "orientation-warning" : ""}"><strong>${escapeHtml(key)}</strong> ${escapeHtml(value)}</li>`)
|
||||
.map(([key, value]) => `<li class="${key === "warning" || key === "diagnostic" ? "orientation-warning" : ""}"><strong>${escapeHtml(key)}</strong> ${escapeHtml(value)}</li>`)
|
||||
.join("");
|
||||
|
||||
const contextIds = new Set(visibleNodes.map((node) => node.id()));
|
||||
@@ -1161,6 +1377,7 @@ def graph_explorer_page() -> str:
|
||||
orientationList.innerHTML = [
|
||||
{label: "grouping", value: ruleAttributeLabels[zone.field] || humanize(zone.field), state: "good"},
|
||||
{label: "zone", value: zone.label},
|
||||
{label: "diagnostics", value: diagnostics.length ? String(diagnostics.length) : "none", state: diagnostics.length ? "warning" : "good"},
|
||||
{label: "warnings", value: warnings.length ? String(warnings.length) : "none", state: warnings.length ? "warning" : "good"},
|
||||
].map((row) => `
|
||||
<li class="orientation-item ${orientationStateClass(row.state)}">
|
||||
|
||||
@@ -332,6 +332,7 @@ def resolve_zones(
|
||||
edge_records,
|
||||
enabled_definitions,
|
||||
seed_node_ids_by_zone_id,
|
||||
diagnostics,
|
||||
)
|
||||
for candidate in attraction_candidates:
|
||||
candidates_by_node_id[candidate.node_id].append(candidate)
|
||||
@@ -406,6 +407,16 @@ def resolve_zones(
|
||||
internal_edge_ids_by_zone_id[source_zone_id].append(edge.id)
|
||||
continue
|
||||
if source_zone_id or target_zone_id:
|
||||
zone_ids = tuple(sorted(str(zone_id) for zone_id in {source_zone_id, target_zone_id} - {None}))
|
||||
diagnostics.append(
|
||||
ZoneDiagnostic(
|
||||
severity="INFO",
|
||||
code="ZONE_EDGE_CROSSES_ZONE_BOUNDARY",
|
||||
message=f"Edge {edge.id} crosses a zone boundary.",
|
||||
edge_id=edge.id,
|
||||
zone_ids=zone_ids,
|
||||
)
|
||||
)
|
||||
boundary_edges.append(
|
||||
ZoneBoundaryEdge(
|
||||
edge_id=edge.id,
|
||||
@@ -468,6 +479,7 @@ def _attraction_candidates(
|
||||
edge_records: tuple[_EdgeRecord, ...],
|
||||
enabled_definitions: tuple[tuple[int, ZoneDefinition], ...],
|
||||
seed_node_ids_by_zone_id: Mapping[str, set[str]],
|
||||
diagnostics: list[ZoneDiagnostic],
|
||||
) -> list[_Candidate]:
|
||||
nodes_by_id = {node.id: node for node in node_records}
|
||||
adjacency: dict[str, list[_EdgeRecord]] = defaultdict(list)
|
||||
@@ -476,6 +488,7 @@ def _attraction_candidates(
|
||||
adjacency[edge.target].append(edge)
|
||||
|
||||
candidates: dict[tuple[str, str], _Candidate] = {}
|
||||
depth_limit_diagnostics: set[tuple[str, str]] = set()
|
||||
for definition_order, definition in enabled_definitions:
|
||||
seed_node_ids = seed_node_ids_by_zone_id.get(definition.id, set())
|
||||
if not seed_node_ids:
|
||||
@@ -486,6 +499,27 @@ def _attraction_candidates(
|
||||
while queue:
|
||||
node_id, depth = queue.popleft()
|
||||
if depth >= rule.depth:
|
||||
if _has_matching_attraction_neighbor(
|
||||
node_id,
|
||||
adjacency.get(node_id, []),
|
||||
nodes_by_id,
|
||||
rule,
|
||||
):
|
||||
key = (definition.id, node_id)
|
||||
if key not in depth_limit_diagnostics:
|
||||
depth_limit_diagnostics.add(key)
|
||||
diagnostics.append(
|
||||
ZoneDiagnostic(
|
||||
severity="INFO",
|
||||
code="ZONE_ATTRACTION_DEPTH_LIMIT_REACHED",
|
||||
message=(
|
||||
f"Zone {definition.id} reached attraction depth "
|
||||
f"{rule.depth} at node {node_id}."
|
||||
),
|
||||
node_id=node_id,
|
||||
zone_ids=(definition.id,),
|
||||
)
|
||||
)
|
||||
continue
|
||||
for edge in adjacency.get(node_id, []):
|
||||
if not _edge_matches_attraction_rule(edge, rule):
|
||||
@@ -521,6 +555,24 @@ def _attraction_candidates(
|
||||
return sorted(candidates.values(), key=lambda candidate: (candidate.node_id, candidate.zone_id))
|
||||
|
||||
|
||||
def _has_matching_attraction_neighbor(
|
||||
node_id: str,
|
||||
edges: Iterable[_EdgeRecord],
|
||||
nodes_by_id: Mapping[str, _NodeRecord],
|
||||
rule: ZoneAttractionRule,
|
||||
) -> bool:
|
||||
for edge in edges:
|
||||
if not _edge_matches_attraction_rule(edge, rule):
|
||||
continue
|
||||
neighbor_id = _neighbor_for_direction(node_id, edge, rule.direction)
|
||||
if not neighbor_id:
|
||||
continue
|
||||
neighbor = nodes_by_id.get(neighbor_id)
|
||||
if neighbor and _rule_matches(neighbor.data, rule.node_filter, empty_matches=True):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _node_records(nodes: Iterable[Mapping[str, Any]]) -> tuple[_NodeRecord, ...]:
|
||||
records: list[_NodeRecord] = []
|
||||
for order, element in enumerate(nodes):
|
||||
|
||||
@@ -415,6 +415,14 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
|
||||
assert "ruleRemovalSignature" in page
|
||||
assert "zoneModeFields" in page
|
||||
assert "zoneForData" in page
|
||||
assert "defaultZoneDefinitions" in page
|
||||
assert "resolveZoneInstances" in page
|
||||
assert "zoneDiagnosticCodes" in page
|
||||
assert "ZONE_EMPTY_SEED_SET" in page
|
||||
assert "ZONE_NODE_SEEDED_BY_MULTIPLE_ZONES" in page
|
||||
assert "ZONE_EDGE_CROSSES_ZONE_BOUNDARY" in page
|
||||
assert "zone.diagnostics" in page
|
||||
assert 'normalize: "deploymentEnvironment"' in page
|
||||
assert "zoneBoundsForNodes" in page
|
||||
assert "renderZoneOverlay" in page
|
||||
assert "renderZoneDetails" in page
|
||||
|
||||
@@ -91,6 +91,9 @@ def test_resolver_assigns_seed_nodes_and_boundary_edges() -> None:
|
||||
assert resolution.boundary_edges[0].edge_id == "edge.prod-test"
|
||||
assert resolution.boundary_edges[0].source_zone_id == "prod"
|
||||
assert resolution.boundary_edges[0].target_zone_id == "test"
|
||||
assert "ZONE_EDGE_CROSSES_ZONE_BOUNDARY" in {
|
||||
diagnostic.code for diagnostic in resolution.diagnostics
|
||||
}
|
||||
|
||||
|
||||
def test_resolver_attracts_nodes_by_edge_type_direction_and_depth() -> None:
|
||||
@@ -128,6 +131,9 @@ def test_resolver_attracts_nodes_by_edge_type_direction_and_depth() -> None:
|
||||
assert "far" not in resolution.node_assignments
|
||||
assert resolution.zone_by_id("prod").internal_edge_ids == ("edge.seed-near",)
|
||||
assert resolution.zone_by_id("prod").boundary_edge_ids == ("edge.near-far",)
|
||||
assert "ZONE_ATTRACTION_DEPTH_LIMIT_REACHED" in {
|
||||
diagnostic.code for diagnostic in resolution.diagnostics
|
||||
}
|
||||
|
||||
|
||||
def test_resolver_keeps_seed_membership_over_attraction() -> None:
|
||||
@@ -229,3 +235,19 @@ def test_resolver_serializes_resolution() -> None:
|
||||
assert serialized["zones"][0]["id"] == "prod"
|
||||
assert serialized["node_assignments"]["svc"]["zone_id"] == "prod"
|
||||
assert serialized["node_assignments"]["svc"]["reason"] == "seed"
|
||||
|
||||
|
||||
def test_resolver_reports_empty_zone_seed_set() -> None:
|
||||
resolution = resolve_zones(
|
||||
nodes=[_node("svc", deploymentEnvironment="dev")],
|
||||
edges=[],
|
||||
zone_definitions=[
|
||||
{
|
||||
"id": "prod",
|
||||
"membership": {"field": "deploymentEnvironment", "op": "equals", "value": "prod"},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert resolution.zone_by_id("prod").node_ids == ()
|
||||
assert "ZONE_EMPTY_SEED_SET" in {diagnostic.code for diagnostic in resolution.diagnostics}
|
||||
|
||||
@@ -95,7 +95,7 @@ summaries, and conflict diagnostics. Covered the core behavior in
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0022-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "c89d8d53-72a4-4590-a0e5-67b012c3550c"
|
||||
```
|
||||
@@ -114,11 +114,20 @@ The current operator-facing behavior should remain available:
|
||||
Expected result: existing graph explorer tests continue to pass, with new tests
|
||||
showing that the UI obtains zone rectangles from resolved zone instances.
|
||||
|
||||
Result: Refactored the graph explorer overlay to use explicit default zone
|
||||
definitions and a client-side `resolveZoneInstances()` path. Deployment
|
||||
environment zones now resolve through declarative membership rules with
|
||||
`deploymentEnvironment` normalization for `dev` -> `dev-tegwick`,
|
||||
`test`/`staging` -> `test`, `prod` -> `prod`, and ignored `all` values. Access
|
||||
zone overlays are generated as dynamic definitions from visible graph evidence.
|
||||
The overlay keeps the single-zone visible-node assignment behavior while
|
||||
preserving edge evidence in zone details.
|
||||
|
||||
## Task 4: Add Zone Diagnostics To The Explorer
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0022-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "d140cb5b-6a35-4cb0-ab68-e39e708c08e9"
|
||||
```
|
||||
@@ -137,6 +146,12 @@ Diagnostics should include at least:
|
||||
Expected result: zone detail panels show scoped diagnostics, and tests verify
|
||||
that diagnostics are generated by the resolver rather than ad hoc UI checks.
|
||||
|
||||
Result: Added resolver diagnostics for empty seed sets, overlapping zone
|
||||
membership, attraction depth-limit stops, and boundary-crossing edges. The graph
|
||||
explorer now surfaces scoped zone diagnostics in the selected zone detail panel
|
||||
and orientation context, with assertions proving diagnostics come from the zone
|
||||
resolver path.
|
||||
|
||||
## Task 5: Persist Zone View Settings In Profiles
|
||||
|
||||
```task
|
||||
|
||||
Reference in New Issue
Block a user