diff --git a/railiance_fabric/graph_explorer_ui.py b/railiance_fabric/graph_explorer_ui.py
index 833d11c..74d2021 100644
--- a/railiance_fabric/graph_explorer_ui.py
+++ b/railiance_fabric/graph_explorer_ui.py
@@ -137,6 +137,10 @@ def graph_explorer_page() -> str:
@@ -181,6 +185,8 @@ def graph_explorer_page() -> str:
const detailSummary = document.getElementById("detail-summary");
const detailPills = document.getElementById("detail-pills");
const detailList = document.getElementById("detail-list");
+ const orientationTitle = document.getElementById("orientation-title");
+ const orientationList = document.getElementById("orientation-list");
const legend = document.getElementById("legend");
let cy = null;
let selected = null;
@@ -268,6 +274,8 @@ def graph_explorer_page() -> str:
detailSummary.textContent = "No selection";
detailPills.innerHTML = "";
detailList.innerHTML = "";
+ orientationTitle.textContent = "Select a service, interface, dependency, or registered-only repo.";
+ orientationList.innerHTML = "";
return;
}
const data = element.data();
@@ -291,6 +299,78 @@ def graph_explorer_page() -> str:
.filter(([, value]) => value)
.map(([key, value]) => `
${escapeHtml(key)} ${escapeHtml(value)}`)
.join("");
+ renderOrientation(element);
+ };
+
+ const renderOrientation = (element) => {
+ const data = element.data();
+ const rows = [];
+ if (data.kind === "Repository" && data.lifecycle === "registered-only") {
+ orientationTitle.textContent = "Onboarding gap";
+ rows.push(["repo", data.repo], ["next", "sync Fabric declarations or register a graph snapshot"]);
+ } else if (data.kind === "InterfaceDeclaration") {
+ orientationTitle.textContent = "Interface consumers";
+ cy.edges().filter((edge) =>
+ edge.data("target") === data.id && edge.data("edgeType") === "uses_interface"
+ ).forEach((edge) => {
+ const dependency = edge.data("source");
+ const consumerEdge = cy.edges().filter((candidate) =>
+ candidate.data("target") === dependency && candidate.data("edgeType") === "consumes"
+ )[0];
+ const consumerId = consumerEdge ? consumerEdge.data("source") : "";
+ const consumer = consumerId ? cy.getElementById(consumerId).data("name") || consumerId : "unknown";
+ rows.push(["consumer", `${consumer} -> ${dependency}`]);
+ });
+ if (rows.length === 0) rows.push(["consumer", "no accepted consumers in current graph"]);
+ } else if (data.kind === "ServiceDeclaration") {
+ orientationTitle.textContent = "Dependency path";
+ cy.edges().filter((edge) =>
+ edge.data("source") === data.id && edge.data("edgeType") === "consumes"
+ ).forEach((edge) => {
+ const dependency = edge.data("target");
+ const providerEdges = cy.edges().filter((candidate) =>
+ candidate.data("source") === dependency && String(candidate.data("edgeType")).startsWith("binds:")
+ );
+ if (providerEdges.length === 0) {
+ rows.push(["requires", `${dependency} -> unresolved`]);
+ return;
+ }
+ providerEdges.forEach((providerEdge) => {
+ const target = providerEdge.data("target");
+ const provider = cy.getElementById(target).data("name") || target;
+ rows.push(["requires", `${dependency} -> ${provider} (${providerEdge.data("edgeType")})`]);
+ });
+ });
+ if (rows.length === 0) rows.push(["requires", "no declared dependencies"]);
+ } else if (data.kind === "DependencyDeclaration") {
+ orientationTitle.textContent = "Dependency binding";
+ cy.edges().filter((edge) =>
+ edge.data("source") === data.id && (
+ String(edge.data("edgeType")).startsWith("binds:") ||
+ edge.data("edgeType") === "uses_interface"
+ )
+ ).forEach((edge) => {
+ const target = edge.data("target");
+ const targetName = cy.getElementById(target).data("name") || target;
+ rows.push([edge.data("edgeType"), targetName]);
+ });
+ if (rows.length === 0) rows.push(["binding", "no provider binding in current graph"]);
+ } else if (data.kind === "CapabilityDeclaration") {
+ orientationTitle.textContent = "Provider surface";
+ cy.edges().filter((edge) =>
+ edge.data("target") === data.id && edge.data("edgeType") === "provides"
+ ).forEach((edge) => rows.push(["service", cy.getElementById(edge.data("source")).data("name") || edge.data("source")]));
+ cy.edges().filter((edge) =>
+ edge.data("source") === data.id && edge.data("edgeType") === "available_via"
+ ).forEach((edge) => rows.push(["interface", cy.getElementById(edge.data("target")).data("name") || edge.data("target")]));
+ if (rows.length === 0) rows.push(["surface", "no provider surface in current graph"]);
+ } else {
+ orientationTitle.textContent = "Graph context";
+ rows.push(["neighbors", `${element.neighborhood().nodes().length} connected nodes`]);
+ }
+ orientationList.innerHTML = rows
+ .map(([key, value]) => `${escapeHtml(key)} ${escapeHtml(value)}`)
+ .join("");
};
const applyFocus = () => {
diff --git a/tests/test_graph_explorer.py b/tests/test_graph_explorer.py
index f7c0b82..c5ede49 100644
--- a/tests/test_graph_explorer.py
+++ b/tests/test_graph_explorer.py
@@ -190,6 +190,9 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
assert 'id="mode-select"' in page
assert 'id="layout-select"' in page
assert 'id="profile-select"' in page
+ assert 'id="orientation-list"' in page
+ assert "Interface consumers" in page
+ assert "Dependency path" in page
assert "cytoscape.min.js" in page
assert "/exports/graph-explorer/manifest" in page
assert 'data-override="hide"' in page
diff --git a/workplans/RAIL-FAB-WP-0008-interactive-fabric-map.md b/workplans/RAIL-FAB-WP-0008-interactive-fabric-map.md
index 8df996a..ec343a0 100644
--- a/workplans/RAIL-FAB-WP-0008-interactive-fabric-map.md
+++ b/workplans/RAIL-FAB-WP-0008-interactive-fabric-map.md
@@ -205,7 +205,7 @@ Acceptance notes:
```task
id: RAIL-FAB-WP-0008-T04
-status: in_progress
+status: done
priority: high
state_hub_task_id: "75c1f234-026c-44ed-9c88-db39653b81e0"
```
@@ -228,7 +228,7 @@ Acceptance notes:
```task
id: RAIL-FAB-WP-0008-T05
-status: todo
+status: in_progress
priority: medium
state_hub_task_id: "64fe53f1-fbea-4624-8f52-1b5e2a27cf67"
```