Add graph orientation details

This commit is contained in:
2026-05-18 18:36:15 +02:00
parent 8a31258785
commit 01840d848f
3 changed files with 85 additions and 2 deletions

View File

@@ -137,6 +137,10 @@ def graph_explorer_page() -> str:
<div id="detail-pills"></div>
<ul id="detail-list" class="detail-list"></ul>
</section>
<section class="section">
<p id="orientation-title" class="meta">Select a service, interface, dependency, or registered-only repo.</p>
<ul id="orientation-list" class="detail-list"></ul>
</section>
<section class="section">
<div class="button-row">
<button type="button" data-override="show">Show</button>
@@ -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]) => `<li><strong>${escapeHtml(key)}</strong> ${escapeHtml(value)}</li>`)
.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]) => `<li><strong>${escapeHtml(key)}</strong> ${escapeHtml(value)}</li>`)
.join("");
};
const applyFocus = () => {

View File

@@ -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

View File

@@ -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"
```