feat: add deployment zone overlays

This commit is contained in:
2026-05-24 15:55:05 +02:00
parent 62236f6453
commit ff1c4ce05b
28 changed files with 1282 additions and 26 deletions

View File

@@ -11,6 +11,7 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from .deployment_overlay import normalize_deployment_overlay
from .discovery import normalize_identity_part, short_fingerprint
from .loader import load_yaml, repo_root
from .schema_validation import draft202012_validator
@@ -619,7 +620,7 @@ def _identity_from_evidence(root: dict[str, Any], item: dict[str, Any]) -> dict[
or declared_slug
or Path(str(source.get("path") or "")).name
)
return {
candidate = {
"identity_type": "Repository",
"label": identity_slug,
"graph_id": identity_slug,
@@ -635,6 +636,10 @@ def _identity_from_evidence(root: dict[str, Any], item: dict[str, Any]) -> dict[
},
"confidence": 0.9 if evidence_type == "repository_checkout" else 0.85,
}
overlay = normalize_deployment_overlay(source, attributes)
if overlay:
candidate["deployment_overlay"] = overlay
return candidate
if evidence_type in {"deployment_automation", "infrastructure_manifest"}:
path = str(source.get("path") or "")
return {
@@ -665,7 +670,7 @@ def _identity_from_evidence(root: dict[str, Any], item: dict[str, Any]) -> dict[
}
if evidence_type == "endpoint_contract":
path = str(source.get("path") or "")
return {
candidate = {
"identity_type": "Endpoint",
"label": Path(path).name or "endpoint-contract",
"graph_id": path,
@@ -677,6 +682,10 @@ def _identity_from_evidence(root: dict[str, Any], item: dict[str, Any]) -> dict[
"attributes": {**attributes, "source_evidence_type": evidence_type},
"confidence": 0.75,
}
overlay = normalize_deployment_overlay(source, attributes)
if overlay:
candidate["deployment_overlay"] = overlay
return candidate
if evidence_type == "host_path_match":
path = str(source.get("path") or "")
return {
@@ -901,6 +910,7 @@ def _add_identity_candidate(
evidence_ids: list[str],
aliases: list[str],
attributes: dict[str, Any],
deployment_overlay: dict[str, Any] | None = None,
confidence: float,
) -> None:
normalized_type = normalize_identity_part(identity_type)
@@ -924,6 +934,9 @@ def _add_identity_candidate(
incoming["subfabric_id"] = subfabric_id
if owner_actor_id:
incoming["owner_actor_id"] = owner_actor_id
overlay = normalize_deployment_overlay(deployment_overlay or {}, attributes)
if overlay:
incoming["deployment_overlay"] = overlay
existing = candidates.get(stable_key)
if existing is None:
@@ -933,6 +946,11 @@ def _add_identity_candidate(
existing["aliases"] = _unique_strings([*existing.get("aliases", []), *incoming["aliases"]])
existing["evidence_ids"] = _unique_strings([*existing.get("evidence_ids", []), *incoming["evidence_ids"]])
existing["attributes"] = {**existing.get("attributes", {}), **incoming["attributes"]}
if incoming.get("deployment_overlay"):
existing["deployment_overlay"] = normalize_deployment_overlay(
existing.get("deployment_overlay") if isinstance(existing.get("deployment_overlay"), dict) else {},
incoming["deployment_overlay"],
)
if incoming.get("owner_actor_id") and existing.get("owner_actor_id") and incoming["owner_actor_id"] != existing["owner_actor_id"]:
existing["attributes"]["ambiguous_owner_actor_ids"] = _unique_strings(
[existing["owner_actor_id"], incoming["owner_actor_id"], *existing["attributes"].get("ambiguous_owner_actor_ids", [])]
@@ -973,6 +991,7 @@ def _candidate_graph(candidates: list[dict[str, Any]], manifest: dict[str, Any])
"fabric_id": candidate.get("fabric_id", ""),
"subfabric_id": candidate.get("subfabric_id", ""),
"owner_actor_id": candidate.get("owner_actor_id", ""),
"deployment_overlay": candidate.get("deployment_overlay", {}),
}
for candidate in sorted(candidates, key=lambda item: item["stable_key"])
]

View File

@@ -0,0 +1,107 @@
from __future__ import annotations
from typing import Any
OVERLAY_FIELDS = (
"deployment_environment",
"deployment_scenario",
"routing_authority",
"access_zone",
"policy_authority",
"exposure_class",
)
ROUTE_EVIDENCE_FIELDS = (
"host",
"hostname",
"port",
"protocol",
"route",
"route_url",
"path",
"scheme",
)
_ALIASES = {
"deployment_environment": ("deployment_environment", "environment"),
"deployment_scenario": ("deployment_scenario", "deployment_scenario_id", "scenario"),
"routing_authority": ("routing_authority",),
"access_zone": ("access_zone",),
"policy_authority": ("policy_authority",),
"exposure_class": ("exposure_class", "exposure"),
}
def normalize_deployment_overlay(*sources: object) -> dict[str, Any]:
"""Collect deployment-zone overlay fields without changing fabric membership."""
overlay: dict[str, Any] = {}
route_evidence: dict[str, Any] = {}
for source in sources:
if not isinstance(source, dict):
continue
nested = source.get("deployment_overlay")
if isinstance(nested, dict):
_merge_overlay_source(overlay, route_evidence, nested)
_merge_overlay_source(overlay, route_evidence, source)
if route_evidence:
overlay["route_evidence"] = route_evidence
return {key: value for key, value in overlay.items() if _has_value(value)}
def graph_explorer_overlay_fields(overlay: dict[str, Any]) -> dict[str, Any]:
route = overlay.get("route_evidence") if isinstance(overlay.get("route_evidence"), dict) else {}
return {
"deploymentOverlay": overlay,
"deploymentEnvironment": str(overlay.get("deployment_environment") or ""),
"deploymentScenario": str(overlay.get("deployment_scenario") or ""),
"routingAuthority": str(overlay.get("routing_authority") or ""),
"accessZone": str(overlay.get("access_zone") or ""),
"policyAuthority": str(overlay.get("policy_authority") or ""),
"exposureClass": str(overlay.get("exposure_class") or ""),
"routeHost": str(route.get("host") or ""),
"routeHostname": str(route.get("hostname") or ""),
"routePort": route.get("port") if route.get("port") not in (None, "") else "",
"routeProtocol": str(route.get("protocol") or ""),
"route": str(route.get("route") or route.get("route_url") or ""),
}
def _merge_overlay_source(overlay: dict[str, Any], route_evidence: dict[str, Any], source: dict[str, Any]) -> None:
for field in OVERLAY_FIELDS:
if field in overlay:
continue
value = _first_value(source, _ALIASES[field])
if _has_value(value):
overlay[field] = value
nested_route = source.get("route_evidence")
if isinstance(nested_route, dict):
for field in ROUTE_EVIDENCE_FIELDS:
value = nested_route.get(field)
if _has_value(value) and field not in route_evidence:
route_evidence[field] = value
for field in ROUTE_EVIDENCE_FIELDS:
value = source.get(field)
if _has_value(value) and field not in route_evidence:
route_evidence[field] = value
def _first_value(source: dict[str, Any], keys: tuple[str, ...]) -> Any:
for key in keys:
value = source.get(key)
if _has_value(value):
return value
return ""
def _has_value(value: Any) -> bool:
if isinstance(value, dict):
return any(_has_value(item) for item in value.values())
if isinstance(value, list):
return any(_has_value(item) for item in value)
return value not in (None, "")

View File

@@ -3,6 +3,8 @@ from __future__ import annotations
import json
from typing import Any
from .deployment_overlay import normalize_deployment_overlay
FINANCIAL_API_VERSION = "railiance.fabric/v1alpha2"
FINANCIAL_SCHEMA_VERSION = "financial-fabric-v1"
@@ -19,7 +21,7 @@ RELATIONSHIP_CATEGORIES = {
"evidence",
}
FABRIC_KINDS = {"Fabric", "Subfabric"}
ENVIRONMENT_LABELS = {"local", "dev", "staging", "prod", "lab", "all"}
ENVIRONMENT_LABELS = {"local", "dev", "test", "staging", "prod", "lab", "all"}
def is_financial_graph_export(graph: dict[str, Any]) -> bool:
@@ -125,6 +127,7 @@ def financial_graph_errors(graph: dict[str, Any]) -> list[str]:
_validate_containment(errors, f"nodes[{index}]", node, netkingdom_id, fabric_kinds, accepted=review_state == "accepted")
_validate_ownership(errors, f"nodes[{index}]", node, actor_roles, accepted=review_state == "accepted")
_validate_optional_object(errors, f"nodes[{index}].accounting", node, "accounting")
_validate_overlay(errors, f"nodes[{index}].deployment_overlay", node)
edges = _indexed_items(errors, graph, "edges")
for index, edge in edges:
@@ -141,6 +144,7 @@ def financial_graph_errors(graph: dict[str, Any]) -> list[str]:
errors.append(f"{path}.relationship_category {category!r} is not valid")
_validate_evidence(errors, path, edge)
_validate_optional_object(errors, f"{path}.accounting", edge, "accounting")
_validate_overlay(errors, f"{path}.deployment_overlay", edge)
if edge_type == "provides_utility_to" and category != "utility":
errors.append(f"{path}.relationship_category must be 'utility' for provides_utility_to edges")
if category == "utility":
@@ -211,6 +215,13 @@ def _materialize_node(node: dict[str, Any]) -> None:
for key in ("containment", "ownership", "accounting"):
if key not in node and isinstance(attrs.get(key), dict):
node[key] = attrs[key]
overlay = normalize_deployment_overlay(
node.get("deployment_overlay") if isinstance(node.get("deployment_overlay"), dict) else {},
attrs,
node.get("containment") if isinstance(node.get("containment"), dict) else {},
)
if overlay:
node["deployment_overlay"] = overlay
node.setdefault("evidence", _legacy_evidence(node, attrs))
@@ -218,6 +229,12 @@ def _materialize_edge(edge: dict[str, Any]) -> None:
attrs = edge.get("attributes") if isinstance(edge.get("attributes"), dict) else {}
if "accounting" not in edge and isinstance(attrs.get("accounting"), dict):
edge["accounting"] = attrs["accounting"]
overlay = normalize_deployment_overlay(
edge.get("deployment_overlay") if isinstance(edge.get("deployment_overlay"), dict) else {},
attrs,
)
if overlay:
edge["deployment_overlay"] = overlay
edge.setdefault("relationship_category", _relationship_category(edge))
edge.setdefault("evidence", _legacy_evidence(edge, attrs))
if edge.get("relationship_category") == "utility":
@@ -362,6 +379,18 @@ def _validate_optional_object(errors: list[str], path: str, item: dict[str, Any]
errors.append(f"{path} must be an object")
def _validate_overlay(errors: list[str], path: str, item: dict[str, Any]) -> None:
overlay = item.get("deployment_overlay")
if overlay is None:
return
if not isinstance(overlay, dict):
errors.append(f"{path} must be an object")
return
route = overlay.get("route_evidence")
if route is not None and not isinstance(route, dict):
errors.append(f"{path}.route_evidence must be an object")
def _required_object(errors: list[str], path: str, item: dict[str, Any], key: str) -> dict[str, Any]:
value = item.get(key)
if not isinstance(value, dict):

View File

@@ -5,6 +5,7 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from .deployment_overlay import normalize_deployment_overlay
from .financial import (
FINANCIAL_API_VERSION,
FINANCIAL_SCHEMA_VERSION,
@@ -97,6 +98,13 @@ def _financial_node_from_legacy(
accounting = _object(result["attributes"].get("accounting")) or accounting_default
if _has_value(accounting):
result["accounting"] = json.loads(json.dumps(accounting))
overlay = normalize_deployment_overlay(
node.get("deployment_overlay") if isinstance(node.get("deployment_overlay"), dict) else {},
result["attributes"],
result["containment"],
)
if _has_value(overlay):
result["deployment_overlay"] = overlay
for key in ("canon_category", "canon_anchor", "mapping_fit"):
if node.get(key):
result[key] = node[key]
@@ -113,6 +121,12 @@ def _financial_edge_from_legacy(edge: dict[str, Any]) -> dict[str, Any]:
for key in ("canonical_type", "canon_anchor", "mapping_fit", "display_only", "evidence_state"):
if key in edge:
result[key] = edge[key]
overlay = normalize_deployment_overlay(
edge.get("deployment_overlay") if isinstance(edge.get("deployment_overlay"), dict) else {},
result["attributes"],
)
if _has_value(overlay):
result["deployment_overlay"] = overlay
return result

View File

@@ -340,9 +340,12 @@ def _export_attributes(declaration: Declaration) -> dict[str, Any]:
def _base_export_attributes(declaration: Declaration) -> dict[str, Any]:
source_links = declaration.metadata.get("source_links", [])
return {
attributes = {
"owner": declaration.metadata.get("owner", ""),
"description": declaration.spec.get("description", ""),
"source_path": str(declaration.path),
"source_links": source_links if isinstance(source_links, list) else [],
}
if isinstance(declaration.spec.get("deployment_overlay"), dict):
attributes["deployment_overlay"] = declaration.spec["deployment_overlay"]
return attributes

View File

@@ -7,6 +7,7 @@ from typing import Any
from urllib.parse import urlparse
from .canon import edge_canon_mapping
from .deployment_overlay import graph_explorer_overlay_fields, normalize_deployment_overlay
DISPLAY_STATES = ("show", "blur", "hide", "highlight", "remove")
@@ -134,6 +135,12 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
"layer",
"kind",
"environment",
"deploymentEnvironment",
"deploymentScenario",
"routingAuthority",
"accessZone",
"policyAuthority",
"exposureClass",
"serverHost",
"lifecycle",
"unresolved",
@@ -147,7 +154,17 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
"repo",
"domain",
"environment",
"deploymentEnvironment",
"deploymentScenario",
"routingAuthority",
"accessZone",
"policyAuthority",
"exposureClass",
"serverHost",
"routeHost",
"routeHostname",
"routePort",
"routeProtocol",
"kind",
"layer",
"edgeType",
@@ -163,7 +180,15 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
{"id": "repo", "label": "Repo", "type": "string"},
{"id": "domain", "label": "Domain", "type": "string"},
{"id": "environment", "label": "Environment", "type": "string"},
{"id": "deploymentEnvironment", "label": "Deployment Environment", "type": "string"},
{"id": "deploymentScenario", "label": "Deployment Scenario", "type": "string"},
{"id": "routingAuthority", "label": "Routing Authority", "type": "string"},
{"id": "accessZone", "label": "Access Zone", "type": "string"},
{"id": "policyAuthority", "label": "Policy Authority", "type": "string"},
{"id": "exposureClass", "label": "Exposure Class", "type": "string"},
{"id": "serverHost", "label": "Server Host", "type": "string"},
{"id": "routeHost", "label": "Route Host", "type": "string"},
{"id": "routePort", "label": "Route Port", "type": "number"},
{"id": "lifecycle", "label": "Lifecycle", "type": "string"},
{"id": "reviewState", "label": "Review State", "type": "string"},
{"id": "unresolved", "label": "Unresolved", "type": "boolean"},
@@ -193,6 +218,14 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
"repo",
"domain",
"lifecycle",
"deploymentEnvironment",
"deploymentScenario",
"routingAuthority",
"accessZone",
"policyAuthority",
"exposureClass",
"routeHost",
"routePort",
"canonCategory",
"evidenceState",
"unresolved",
@@ -215,6 +248,31 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
],
},
"modes": [
{
"id": "by-fabric",
"label": "Fabric",
"description": "Group and filter by fabric/accountability fields.",
},
{
"id": "by-deployment-environment",
"label": "Environment",
"description": "Group and filter by deployment environment.",
},
{
"id": "by-deployment-scenario",
"label": "Scenario",
"description": "Group and filter by deployment scenario.",
},
{
"id": "by-routing-authority",
"label": "Routing",
"description": "Group and filter by routing authority.",
},
{
"id": "by-access-zone",
"label": "Access Zone",
"description": "Group and filter by intended access zone.",
},
{
"id": "full",
"label": "Full",
@@ -335,6 +393,10 @@ def fabric_graph_explorer_payload(
if source_kind == "Repository":
continue
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
overlay_data = _overlay_data(
node.get("deployment_overlay") if isinstance(node.get("deployment_overlay"), dict) else {},
attributes,
)
kind = _presentation_kind(source_kind, attributes)
layer = _layer_for_kind(kind)
is_unresolved = node_id in unresolved
@@ -353,6 +415,8 @@ def fabric_graph_explorer_payload(
"repo": str(node.get("repo", "")),
"domain": str(node.get("domain", "")),
"lifecycle": str(node.get("lifecycle", "")),
"environment": overlay_data["deploymentEnvironment"],
**overlay_data,
"canonCategory": str(
node.get("canon_category") or attributes.get("canon_category") or ""
),
@@ -522,12 +586,17 @@ def _presentation_edge_type(edge_type: str, source: str, target: str, node_kinds
def _edge_metadata(edge: dict[str, Any], edge_type: str) -> dict[str, Any]:
canon_mapping = edge_canon_mapping(edge_type)
attributes = edge.get("attributes") if isinstance(edge.get("attributes"), dict) else {}
return {
"canonical_type": str(edge.get("canonical_type") or canon_mapping.canonical_type),
"canon_anchor": str(edge.get("canon_anchor") or canon_mapping.canon_anchor),
"mapping_fit": str(edge.get("mapping_fit") or canon_mapping.fit),
"display_only": bool(edge.get("display_only", canon_mapping.display_only)),
"evidence_state": str(edge.get("evidence_state") or "declared"),
"deployment_overlay": normalize_deployment_overlay(
edge.get("deployment_overlay") if isinstance(edge.get("deployment_overlay"), dict) else {},
attributes,
),
}
@@ -630,6 +699,12 @@ def _append_infrastructure_elements(
for endpoint in service_endpoints
if _environment_matches(environment, endpoint["environments"])
]
deployment_overlay = normalize_deployment_overlay(
attributes,
{"deployment_environment": environment},
*[endpoint["deployment_overlay"] for endpoint in matching_endpoints],
)
overlay_data = _overlay_data(deployment_overlay)
server_hosts = sorted({endpoint["host"] for endpoint in matching_endpoints})
deployment_data = {
"id": deployment_id,
@@ -643,6 +718,7 @@ def _append_infrastructure_elements(
"domain": str(service.get("domain") or ""),
"lifecycle": str(service.get("lifecycle") or ""),
"environment": environment,
**overlay_data,
"serviceId": service_id,
"serverHosts": server_hosts,
"reviewState": "accepted",
@@ -676,6 +752,10 @@ def _append_infrastructure_elements(
host = endpoint["host"]
port = endpoint["port"]
protocol = endpoint["protocol"]
endpoint_overlay_data = _overlay_data(
endpoint["deployment_overlay"],
{"deployment_environment": environment},
)
server_id = server_ids_by_host.get(host)
endpoint_key = _endpoint_key(host, port, protocol)
port_id = port_ids_by_endpoint.get(endpoint_key)
@@ -695,6 +775,7 @@ def _append_infrastructure_elements(
"domain": str(service.get("domain") or ""),
"lifecycle": "active",
"environment": environment,
**endpoint_overlay_data,
"serverHost": host,
"reviewState": "accepted",
"freshnessState": "current",
@@ -706,6 +787,7 @@ def _append_infrastructure_elements(
"host": host,
"source_interface_id": endpoint["interface_id"],
"endpoint_url": endpoint["url"],
"deployment_overlay": endpoint_overlay_data["deploymentOverlay"],
},
"displayState": "show",
"visibilitySource": "default",
@@ -731,6 +813,7 @@ def _append_infrastructure_elements(
"domain": str(service.get("domain") or ""),
"lifecycle": "active",
"environment": environment,
**endpoint_overlay_data,
"serverHost": host,
"reviewState": "accepted",
"freshnessState": "current",
@@ -744,6 +827,7 @@ def _append_infrastructure_elements(
"protocol": protocol,
"source_interface_id": endpoint["interface_id"],
"endpoint_url": endpoint["url"],
"deployment_overlay": endpoint_overlay_data["deploymentOverlay"],
},
"displayState": "show",
"visibilitySource": "default",
@@ -783,6 +867,16 @@ def _endpoints_by_service(source_nodes: list[dict[str, Any]]) -> dict[str, list[
"url": url,
"interface_id": str(node.get("id") or ""),
"environments": _environments(attributes),
"deployment_overlay": normalize_deployment_overlay(
attributes,
endpoint,
{
"host": host,
"port": port,
"protocol": protocol,
"route": url,
},
),
}
)
return endpoints
@@ -895,6 +989,7 @@ def _edge_element(
mapping_fit: str = "",
display_only: bool = False,
evidence_state: str = "",
deployment_overlay: dict[str, Any] | None = None,
) -> dict[str, Any]:
source_layer = node_layers.get(source, "unknown")
target_layer = node_layers.get(target, "unknown")
@@ -910,6 +1005,7 @@ def _edge_element(
strength = _edge_strength(edge_type)
layout = _layout_hints(edge_type, source_layer, target_layer, same_repo)
edge_id = f"edge:{edge_index}:{source}:{edge_type}:{target}"
overlay_data = _overlay_data(deployment_overlay or {})
return {
"data": {
"id": edge_id,
@@ -930,6 +1026,7 @@ def _edge_element(
"mappingFit": mapping_fit,
"displayOnly": display_only,
"evidenceState": evidence_state,
**overlay_data,
"strength": strength,
"edgeWidth": _edge_width(strength),
"sameLayer": source_layer == target_layer,
@@ -959,6 +1056,10 @@ def _edge_element(
}
def _overlay_data(*sources: object) -> dict[str, Any]:
return graph_explorer_overlay_fields(normalize_deployment_overlay(*sources))
def _layout_hints(
edge_type: str,
source_layer: str,

View File

@@ -574,7 +574,10 @@ def graph_explorer_page() -> str:
const elementText = (data) => [
data.id, data.stableKey, data.label, data.name, data.description,
data.repo, data.domain, data.kind, data.layer, data.edgeType
data.repo, data.domain, data.kind, data.layer, data.edgeType,
data.deploymentEnvironment, data.deploymentScenario, data.routingAuthority,
data.accessZone, data.policyAuthority, data.exposureClass,
data.routeHost, data.routeHostname, data.routePort, data.routeProtocol, data.route
].join(" ").toLowerCase();
const overrideKey = (element) => element.data("stableKey") || element.id();
@@ -614,6 +617,37 @@ def graph_explorer_page() -> str:
const selectedEdgeTypes = () => checkedValues(edgeTypeFilter);
const hasValue = (value) => value !== undefined && value !== null && value !== "";
const overlayRuleAttributes = [
"deploymentEnvironment",
"deploymentScenario",
"routingAuthority",
"accessZone",
"policyAuthority",
"exposureClass",
"routeHost",
"routeHostname",
"routePort",
"routeProtocol",
];
const zoneModeFields = {
"by-fabric": ["domain", "repo", "kind"],
"by-deployment-environment": ["deploymentEnvironment"],
"by-deployment-scenario": ["deploymentScenario"],
"by-routing-authority": ["routingAuthority"],
"by-access-zone": ["accessZone"],
};
const zoneModeLabels = {
"by-fabric": "Fabric",
"by-deployment-environment": "Deployment Environment",
"by-deployment-scenario": "Deployment Scenario",
"by-routing-authority": "Routing Authority",
"by-access-zone": "Access Zone",
};
const summarizeSelection = (selected, allValues, labels, noun) => {
if (selected.size === 0) return `No ${noun}`;
if (selected.size === allValues.length) return `All ${noun}`;
@@ -657,11 +691,21 @@ def graph_explorer_page() -> str:
strength: "Strength",
sameRepo: "Same repo",
layoutAffinity: "Layout affinity",
deploymentEnvironment: "Deployment Environment",
deploymentScenario: "Deployment Scenario",
routingAuthority: "Routing Authority",
accessZone: "Access Zone",
policyAuthority: "Policy Authority",
exposureClass: "Exposure Class",
routeHost: "Route Host",
routeHostname: "Route Hostname",
routePort: "Route Port",
routeProtocol: "Route Protocol",
};
const ruleAttributeCandidates = {
node: ["any", "repo", "kind", "reviewState", "unresolved", "lifecycle"],
edge: ["any", "repo", "kind", "reviewState", "unresolved", "strength", "sameRepo", "layoutAffinity"],
node: ["any", "repo", "kind", "reviewState", "unresolved", "lifecycle", ...overlayRuleAttributes],
edge: ["any", "repo", "kind", "reviewState", "unresolved", "strength", "sameRepo", "layoutAffinity", ...overlayRuleAttributes],
};
const renderOptions = (select, options, current = "") => {
@@ -835,6 +879,79 @@ def graph_explorer_page() -> str:
(!type || edge.data("edgeType") === type)
) : [];
const routeLabel = (data) => {
if (hasValue(data.route)) return data.route;
const host = data.routeHost || data.routeHostname || "";
const protocol = data.routeProtocol || "";
const port = data.routePort || "";
if (!hasValue(host) && !hasValue(port)) return "";
const authority = hasValue(port) ? `${host}:${port}` : host;
return [protocol, authority].filter(hasValue).join(" ");
};
const hasRouteEvidence = (data) =>
hasValue(data.route) ||
hasValue(data.routeHost) ||
hasValue(data.routeHostname) ||
hasValue(data.routePort);
const zoneWarningsForData = (data) => {
const warnings = [];
if (hasRouteEvidence(data) && !hasValue(data.policyAuthority)) {
warnings.push("route without policy authority");
}
return warnings;
};
const groupCounts = (elements, field) => {
const counts = new Map();
elements.forEach((element) => {
const value = element.data(field);
if (hasValue(value)) counts.set(String(value), (counts.get(String(value)) || 0) + 1);
});
return Array.from(counts.entries())
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
};
const renderMapOverview = () => {
const visibleElements = cy
? cy.elements().filter((element) => element.style("display") !== "none").toArray()
: [];
const visibleNodes = visibleElements.filter((element) => element.isNode()).length;
const visibleEdges = visibleElements.filter((element) => element.isEdge()).length;
const fields = zoneModeFields[activeMode] || [];
const title = zoneModeLabels[activeMode] || "Full";
detailTitle.textContent = activeMode === "full" ? "Fabric Map" : `${title} View`;
detailSummary.textContent = `${visibleNodes} nodes / ${visibleEdges} edges visible`;
detailPills.innerHTML = activeMode === "full" ? "" : `<span class="pill">${escapeHtml(activeMode)}</span>`;
const rows = [{label: "visible", value: `${visibleNodes} nodes / ${visibleEdges} edges`}];
fields.forEach((field) => {
const counts = groupCounts(visibleElements, field);
rows.push({
label: ruleAttributeLabels[field] || humanize(field),
value: counts.length
? counts.slice(0, 8).map(([value, count]) => `${value} (${count})`).join(", ")
: "no annotated visible entities",
state: counts.length ? "" : "warning",
});
});
const warnings = visibleElements
.flatMap((element) => zoneWarningsForData(element.data()).map((warning) =>
`${elementLabel(element)}: ${warning}`
));
warnings.slice(0, 6).forEach((warning) => rows.push({label: "warning", value: warning, state: "warning"}));
if (warnings.length > 6) {
rows.push({label: "warning", value: `${warnings.length - 6} additional route warnings`, state: "warning"});
}
detailList.innerHTML = rows
.filter((row) => row.value)
.map((row) => `<li class="${orientationStateClass(row.state)}"><strong>${escapeHtml(row.label)}</strong> ${escapeHtml(row.value)}</li>`)
.join("");
};
const collectionArray = (collection) => Array.from(collection || []);
const orientationStateClass = (state) =>
@@ -1145,6 +1262,12 @@ def graph_explorer_page() -> str:
if (activeMode === "unresolved") {
return new Set(cy.elements().filter((element) => element.data("unresolved") === true).map((element) => element.id()));
}
const zoneFields = zoneModeFields[activeMode] || [];
if (zoneFields.length && activeMode !== "by-fabric") {
return new Set(cy.elements().filter((element) =>
zoneFields.some((field) => hasValue(element.data(field)))
).map((element) => element.id()));
}
if (!selected) return null;
if (activeMode === "selected-path") {
const collection = selected.union(selected.predecessors()).union(selected.successors());
@@ -1204,6 +1327,7 @@ def graph_explorer_page() -> str:
hiddenSummary.textContent = removed ? `Hidden ${hidden} / Removed ${removed}` : `Hidden ${hidden}`;
updateLabelVisibility();
updateSelectionAnchor();
if (!selected) renderMapOverview();
if (options.redrawOnRemove && previousRemoved !== ruleRemovalSignature()) runLayout();
};
@@ -1211,10 +1335,7 @@ def graph_explorer_page() -> str:
selected = element || null;
if (!element) {
orientationContext = null;
detailTitle.textContent = "Fabric Map";
detailSummary.textContent = "No selection";
detailPills.innerHTML = "";
detailList.innerHTML = "";
renderMapOverview();
orientationTitle.textContent = "Select a service, interface, dependency, or registered-only repo.";
orientationList.innerHTML = "";
orientationActions.innerHTML = "";
@@ -1241,6 +1362,14 @@ def graph_explorer_page() -> str:
["mapping", data.mappingFit],
["display only", data.displayOnly === true ? "yes" : ""],
["strength", data.strength],
["deployment environment", data.deploymentEnvironment],
["deployment scenario", data.deploymentScenario],
["routing authority", data.routingAuthority],
["access zone", data.accessZone],
["policy authority", data.policyAuthority],
["exposure", data.exposureClass],
["route", routeLabel(data)],
...zoneWarningsForData(data).map((warning) => ["warning", warning]),
...Object.entries(links),
...refs.map((ref) => [ref.label || ref.kind || "source", ref.path || ref.url || ref.ref || ""])
];

View File

@@ -15,6 +15,7 @@ import yaml
from .canon import edge_canon_mapping, evidence_state_for, node_canon_mapping, source_kind_from_anchor
from .connectors import ConnectorConfig, apply_connectors
from .deployment_overlay import normalize_deployment_overlay
from .discovery import (
attribute_stable_key,
discovery_stable_key,
@@ -1146,6 +1147,22 @@ def _add_runtime_endpoint(
if not host or port_number is None:
return ""
runtime_attributes = {
"host": host,
"runtime_target_type": server_type,
**(attributes or {}),
}
overlay = _runtime_deployment_overlay(
host=host,
port=port_number,
protocol=protocol_value,
domain=domain,
server_type=server_type,
attributes=runtime_attributes,
)
if overlay:
runtime_attributes["deployment_overlay"] = overlay
target_kind = _runtime_target_kind(host, server_type)
target_key = discovery_stable_key(context.repo_slug, target_kind, host)
context.accumulator.add_node(
@@ -1156,11 +1173,7 @@ def _add_runtime_endpoint(
provenance=provenance,
source_anchor=anchor,
aliases=[host],
attributes={
"host": host,
"runtime_target_type": server_type,
**(attributes or {}),
},
attributes=runtime_attributes,
confidence=confidence,
)
@@ -1175,10 +1188,9 @@ def _add_runtime_endpoint(
source_anchor=anchor,
aliases=[port_label],
attributes={
"host": host,
"port": port_number,
"protocol": protocol_value,
**(attributes or {}),
**runtime_attributes,
},
confidence=confidence,
)
@@ -1333,6 +1345,79 @@ def _add_domain_route(
)
def _runtime_deployment_overlay(
*,
host: str,
port: int,
protocol: str,
domain: str,
server_type: str,
attributes: dict[str, object],
) -> dict[str, Any]:
source = str(attributes.get("source") or server_type or "").strip()
overlay: dict[str, Any] = {
"routing_authority": _routing_authority(source, server_type),
"route_evidence": {
"host": host,
"hostname": _normalize_domain(domain) or host,
"port": port,
"protocol": protocol,
"route": str(attributes.get("endpoint_url") or domain or host),
"scheme": attributes.get("scheme", ""),
},
}
if _is_loopback_host(host):
overlay.update(
{
"deployment_environment": "dev",
"deployment_scenario": "bernd-laptop",
"access_zone": "private-dev",
"policy_authority": "local-loopback-binding",
"exposure_class": "local-only",
}
)
elif _mentions_scenario(host, domain, "coulombcore"):
overlay.update(
{
"deployment_environment": "test",
"deployment_scenario": "coulombcore",
"access_zone": "collaborator-test",
"exposure_class": "collaborator-test",
}
)
elif _mentions_scenario(host, domain, "railiance01"):
overlay.update(
{
"deployment_environment": "prod",
"deployment_scenario": "railiance01",
"access_zone": "production-public" if _looks_like_domain(domain or host) else "production-admin",
"exposure_class": "production-public" if _looks_like_domain(domain or host) else "production-admin",
}
)
return normalize_deployment_overlay(overlay)
def _routing_authority(source: str, server_type: str) -> str:
source_value = source.strip().lower()
if source_value == "docker-compose":
return "docker-compose"
if source_value.startswith("kubernetes-") or server_type == "kubernetes-service-dns":
return "kubernetes"
if server_type == "declared-endpoint":
return "declared-endpoint"
return source_value or server_type
def _is_loopback_host(host: str) -> bool:
value = _normalize_host(host)
return value in {"localhost", "127.0.0.1", "::1"}
def _mentions_scenario(host: str, domain: str, scenario: str) -> bool:
needle = scenario.strip().lower()
return needle in _normalize_host(host) or needle in _normalize_domain(domain)
def _compose_port_bindings(service: dict[str, Any]) -> list[dict[str, object]]:
ports = service.get("ports")
if not isinstance(ports, list):