diff --git a/railiance_fabric/graph_explorer.py b/railiance_fabric/graph_explorer.py index f18c985..c5e6e31 100644 --- a/railiance_fabric/graph_explorer.py +++ b/railiance_fabric/graph_explorer.py @@ -238,6 +238,13 @@ def fabric_graph_explorer_payload( elements: list[dict[str, Any]] = [] repository_index = {str(repo.get("slug", "")): repo for repo in repositories} + source_repository_node_ids = { + str(node.get("id", "")): f"repo:{str(node.get('repo', '')).strip()}" + for node in source_nodes + if str(node.get("kind", "")) == "Repository" + and str(node.get("id", "")) + and str(node.get("repo", "")).strip() + } for slug in sorted(repo_slugs): repo = repository_index.get(slug, {}) has_snapshot = slug in snapshot_repo_slugs or slug in source_repo_slugs @@ -281,6 +288,8 @@ def fabric_graph_explorer_payload( if not node_id: continue kind = str(node.get("kind", "")) + if kind == "Repository": + continue layer = _layer_for_kind(kind) is_unresolved = node_id in unresolved attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {} @@ -342,8 +351,8 @@ def fabric_graph_explorer_payload( ) for edge in source_edges: - source = str(edge.get("from", "")) - target = str(edge.get("to", "")) + source = source_repository_node_ids.get(str(edge.get("from", "")), str(edge.get("from", ""))) + target = source_repository_node_ids.get(str(edge.get("to", "")), str(edge.get("to", ""))) edge_type = str(edge.get("type", "")) if not source or not target: continue @@ -354,6 +363,8 @@ def fabric_graph_explorer_payload( repo_id = f"repo:{slug}" for node in source_nodes: node_id = str(node.get("id", "")) + if str(node.get("kind", "")) == "Repository": + continue if str(node.get("repo", "")) != slug or not node_id: continue elements.append(_edge_element(edge_index, repo_id, node_id, "declares", node_layers, node_repos)) diff --git a/tests/test_graph_explorer.py b/tests/test_graph_explorer.py index 11b08c2..f1ad411 100644 --- a/tests/test_graph_explorer.py +++ b/tests/test_graph_explorer.py @@ -72,6 +72,57 @@ def test_graph_explorer_manifest_and_payload_validate() -> None: assert "removed nodes are removed" in payload["filter"]["connected_edge_behavior"] +def test_graph_explorer_collapses_discovered_repository_nodes() -> None: + graph = { + "apiVersion": "railiance.fabric/v1alpha1", + "kind": "FabricGraphExport", + "generated_at": "2026-05-21T00:00:00Z", + "source": {"repo": "fixture-repo", "commit": "abc123"}, + "nodes": [ + { + "id": "discovery:fixture-repo:repository:fixture-repo", + "kind": "Repository", + "name": "Fixture Repo", + "repo": "fixture-repo", + "domain": "testing", + "lifecycle": "active", + "attributes": {"discovery_origin": "deterministic"}, + }, + { + "id": "discovery:fixture-repo:library:fixture-service", + "kind": "Library", + "name": "fixture-service", + "repo": "fixture-repo", + "domain": "testing", + "lifecycle": "active", + "attributes": {"language": "python"}, + }, + ], + "edges": [ + { + "from": "discovery:fixture-repo:repository:fixture-repo", + "to": "discovery:fixture-repo:library:fixture-service", + "type": "declares_package", + } + ], + } + + payload = fabric_graph_explorer_payload( + graph, + [{"slug": "fixture-repo", "name": "Fixture Repo"}], + {"fixture-repo"}, + ) + + nodes = [element for element in payload["elements"] if "source" not in element["data"]] + edges = [element for element in payload["elements"] if "source" in element["data"]] + repository_nodes = [node for node in nodes if node["data"]["kind"] == "Repository"] + declares_package = next(edge for edge in edges if edge["data"]["edgeType"] == "declares_package") + + assert [node["data"]["id"] for node in repository_nodes] == ["repo:fixture-repo"] + assert declares_package["data"]["source"] == "repo:fixture-repo" + assert declares_package["data"]["target"] == "discovery:fixture-repo:library:fixture-service" + + def test_cli_exports_graph_explorer_payload(capsys) -> None: assert cli_main(["export", "--format", "graph-explorer"]) == 0 payload = json.loads(capsys.readouterr().out) diff --git a/workplans/ADHOC-2026-05-21.md b/workplans/ADHOC-2026-05-21.md new file mode 100644 index 0000000..e2d5caa --- /dev/null +++ b/workplans/ADHOC-2026-05-21.md @@ -0,0 +1,39 @@ +--- +id: ADHOC-2026-05-21 +type: workplan +title: "Ad Hoc Fixes 2026-05-21" +domain: railiance +repo: railiance-fabric +status: finished +owner: codex +topic_slug: railiance +created: "2026-05-21" +updated: "2026-05-21" +state_hub_workstream_id: "c5126722-3b6c-4a6a-b687-bc2ebfce5d58" +--- + +# ADHOC-2026-05-21 - Ad Hoc Fixes + +## Collapse Duplicate Repository Icons + +```task +id: ADHOC-2026-05-21-T01 +status: done +priority: medium +state_hub_task_id: "8af02b06-b9a4-457a-bbdc-fa97543944a4" +``` + +After projecting all discovery candidates into the local registry graph, the +graph explorer rendered each repository twice: once as the canonical registry +repository node and once as the discovered `Repository` candidate node. + +The graph explorer now collapses discovered `Repository` nodes into the +canonical `repo:` node and rewires source graph edges to that canonical +node, preserving relationships without duplicate repo icons. + +Verification: + +- `python3 -m pytest tests/test_graph_explorer.py -q` passed. +- `python3 -m pytest` passed with 34 tests. +- Restarted the local registry and confirmed the graph explorer export now has + 35 repository nodes instead of 69.