generated from coulomb/repo-seed
Complete graph explorer projection
This commit is contained in:
@@ -35,6 +35,7 @@ railiance-fabric unresolved
|
||||
railiance-fabric blast-radius openbao-kv-v2-mount
|
||||
railiance-fabric export --format json
|
||||
railiance-fabric export --format mermaid
|
||||
railiance-fabric export --format graph-explorer
|
||||
```
|
||||
|
||||
See `docs/discovery-queries.md` for command details.
|
||||
|
||||
@@ -78,8 +78,19 @@ Export the graph as Mermaid:
|
||||
railiance-fabric export --format mermaid
|
||||
```
|
||||
|
||||
Export the graph as the manifest-compatible graph explorer payload:
|
||||
|
||||
```bash
|
||||
railiance-fabric export --format graph-explorer
|
||||
```
|
||||
|
||||
The JSON export has two top-level arrays:
|
||||
|
||||
- `nodes`: service, capability, interface, dependency, and binding nodes
|
||||
- `edges`: graph relationships such as `provides`, `exposes`,
|
||||
`available_via`, `consumes`, `binds:<status>`, and `uses_interface`
|
||||
|
||||
The graph explorer payload wraps those nodes and edges as Cytoscape-compatible
|
||||
elements with stable keys, layers, display state, visual facets, source
|
||||
references, and deep links. The registry service exposes the same projection at
|
||||
`GET /exports/graph-explorer`.
|
||||
|
||||
@@ -23,6 +23,12 @@ GET /exports/graph-explorer/manifest
|
||||
GET /exports/graph-explorer
|
||||
```
|
||||
|
||||
The local CLI can emit the same payload for repo-local inspection:
|
||||
|
||||
```bash
|
||||
railiance-fabric export --format graph-explorer
|
||||
```
|
||||
|
||||
The manifest tells a graph shell where to load data, which fields are stable,
|
||||
which layers exist, which filter fields are available, and which modes the host
|
||||
supports.
|
||||
|
||||
@@ -12,6 +12,7 @@ from pathlib import Path
|
||||
|
||||
from .loader import declaration_files, load_yaml
|
||||
from .graph import FabricGraph, build_graph
|
||||
from .graph_explorer import fabric_graph_explorer_payload
|
||||
from .validation import validate_roots
|
||||
|
||||
|
||||
@@ -57,9 +58,9 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
blast.add_argument("interface", help="Interface type or interface declaration id.")
|
||||
blast.add_argument("paths", nargs="*", type=Path, default=[Path(".")])
|
||||
|
||||
export = sub.add_parser("export", help="Export graph as JSON or Mermaid.")
|
||||
export = sub.add_parser("export", help="Export graph as JSON, Mermaid, or graph-explorer payload.")
|
||||
export.add_argument("paths", nargs="*", type=Path, default=[Path(".")])
|
||||
export.add_argument("--format", choices=["json", "mermaid"], default="json")
|
||||
export.add_argument("--format", choices=["json", "mermaid", "graph-explorer"], default="json")
|
||||
|
||||
registry = sub.add_parser("registry", help="Feed a running Railiance Fabric registry service.")
|
||||
registry_sub = registry.add_subparsers(dest="registry_command", required=True)
|
||||
@@ -131,7 +132,12 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
if args.command == "export":
|
||||
graph = _load_graph_or_exit(args.paths)
|
||||
print(graph.to_mermaid() if args.format == "mermaid" else graph.to_json())
|
||||
if args.format == "mermaid":
|
||||
print(graph.to_mermaid())
|
||||
elif args.format == "graph-explorer":
|
||||
print(json.dumps(fabric_graph_explorer_payload(graph.to_export()), indent=2, sort_keys=True))
|
||||
else:
|
||||
print(graph.to_json())
|
||||
return 0
|
||||
|
||||
if args.command == "registry":
|
||||
|
||||
@@ -267,13 +267,16 @@ def _escape_mermaid(value: str) -> str:
|
||||
|
||||
def _export_attributes(declaration: Declaration) -> dict[str, Any]:
|
||||
spec = declaration.spec
|
||||
base = _base_export_attributes(declaration)
|
||||
if declaration.kind == "ServiceDeclaration":
|
||||
return {
|
||||
**base,
|
||||
"provides_capabilities": list(spec.get("provides_capabilities", [])),
|
||||
"exposes_interfaces": list(spec.get("exposes_interfaces", [])),
|
||||
}
|
||||
if declaration.kind == "CapabilityDeclaration":
|
||||
return {
|
||||
**base,
|
||||
"capability_type": spec.get("capability_type", ""),
|
||||
"service_id": spec.get("service_id", ""),
|
||||
"interface_ids": list(spec.get("interface_ids", [])),
|
||||
@@ -281,6 +284,7 @@ def _export_attributes(declaration: Declaration) -> dict[str, Any]:
|
||||
}
|
||||
if declaration.kind == "InterfaceDeclaration":
|
||||
return {
|
||||
**base,
|
||||
"interface_type": spec.get("interface_type", ""),
|
||||
"service_id": spec.get("service_id", ""),
|
||||
"capability_ids": list(spec.get("capability_ids", [])),
|
||||
@@ -291,6 +295,7 @@ def _export_attributes(declaration: Declaration) -> dict[str, Any]:
|
||||
requires = spec.get("requires", {})
|
||||
interface = spec.get("interface", {})
|
||||
return {
|
||||
**base,
|
||||
"consumer_service_id": spec.get("consumer_service_id", ""),
|
||||
"requires_capability_id": requires.get("capability_id", ""),
|
||||
"requires_capability_type": requires.get("capability_type", ""),
|
||||
@@ -300,9 +305,20 @@ def _export_attributes(declaration: Declaration) -> dict[str, Any]:
|
||||
}
|
||||
if declaration.kind == "BindingAssertion":
|
||||
return {
|
||||
**base,
|
||||
"dependency_id": spec.get("dependency_id", ""),
|
||||
"provider_capability_id": spec.get("provider_capability_id", ""),
|
||||
"provider_interface_id": spec.get("provider_interface_id", ""),
|
||||
"status": spec.get("status", ""),
|
||||
}
|
||||
return {}
|
||||
return base
|
||||
|
||||
|
||||
def _base_export_attributes(declaration: Declaration) -> dict[str, Any]:
|
||||
source_links = declaration.metadata.get("source_links", [])
|
||||
return {
|
||||
"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 [],
|
||||
}
|
||||
|
||||
@@ -282,13 +282,13 @@ def fabric_graph_explorer_payload(
|
||||
"unresolved": is_unresolved,
|
||||
"confidence": 0.45 if is_unresolved else 1.0,
|
||||
"visualSize": 34 if layer == "binding" else 46 if is_unresolved else 50,
|
||||
"ownership": "repo",
|
||||
"ownership": str(attributes.get("owner") or "repo"),
|
||||
"attributes": attributes,
|
||||
"displayState": "show",
|
||||
"visibilitySource": "default",
|
||||
"visibilityReason": "default",
|
||||
"sourceReferences": _source_references(node),
|
||||
"deepLinks": _node_links(node_id),
|
||||
"deepLinks": _node_links(node_id, attributes),
|
||||
},
|
||||
"classes": " ".join(
|
||||
part
|
||||
@@ -433,7 +433,7 @@ def _edge_strength(edge_type: str) -> str:
|
||||
status = edge_type.split(":", 1)[1]
|
||||
if status in {"missing", "disputed"}:
|
||||
return "weak"
|
||||
if status == "accepted":
|
||||
if status in {"accepted", "exact"}:
|
||||
return "strong"
|
||||
return "medium"
|
||||
return _EDGE_STRENGTH.get(edge_type, "medium")
|
||||
@@ -460,7 +460,7 @@ def _unresolved_dependency_ids(
|
||||
if source not in dependency_ids or not edge_type.startswith("binds:"):
|
||||
continue
|
||||
status = edge_type.split(":", 1)[1]
|
||||
if status in {"accepted", "candidate"}:
|
||||
if status in {"accepted", "candidate", "exact", "compatible"}:
|
||||
resolved.add(source)
|
||||
if status in {"missing", "disputed"}:
|
||||
unresolved.add(source)
|
||||
@@ -470,6 +470,9 @@ def _unresolved_dependency_ids(
|
||||
def _node_description(kind: str, attributes: object) -> str:
|
||||
if not isinstance(attributes, dict):
|
||||
return ""
|
||||
description = str(attributes.get("description", ""))
|
||||
if description:
|
||||
return description
|
||||
if kind == "CapabilityDeclaration":
|
||||
return str(attributes.get("capability_type", ""))
|
||||
if kind == "InterfaceDeclaration":
|
||||
@@ -492,14 +495,21 @@ def _source_references(node: dict[str, Any]) -> list[dict[str, str]]:
|
||||
attributes = node.get("attributes")
|
||||
references: list[dict[str, str]] = []
|
||||
if isinstance(attributes, dict):
|
||||
source_path = str(attributes.get("source_path") or "")
|
||||
if source_path:
|
||||
references.append({"label": "Declaration", "path": source_path})
|
||||
for source in attributes.get("source_links", []):
|
||||
if isinstance(source, dict):
|
||||
references.append({key: str(value) for key, value in source.items()})
|
||||
return references
|
||||
|
||||
|
||||
def _node_links(node_id: str) -> dict[str, str]:
|
||||
return {"registry": f"/graph/nodes/{node_id}"}
|
||||
def _node_links(node_id: str, attributes: dict[str, Any] | None = None) -> dict[str, str]:
|
||||
links = {"registry": f"/graph/nodes/{node_id}"}
|
||||
source_path = str((attributes or {}).get("source_path") or "")
|
||||
if source_path:
|
||||
links["sourceFile"] = source_path
|
||||
return links
|
||||
|
||||
|
||||
def _repository_links(repository: dict[str, Any]) -> dict[str, str]:
|
||||
|
||||
@@ -6,6 +6,7 @@ import urllib.request
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
from railiance_fabric.cli import main as cli_main
|
||||
from railiance_fabric.graph import build_graph
|
||||
from railiance_fabric.graph_explorer import (
|
||||
fabric_graph_explorer_manifest,
|
||||
@@ -47,7 +48,22 @@ def test_graph_explorer_manifest_and_payload_validate() -> None:
|
||||
assert registered_only["data"]["reviewState"] == "candidate"
|
||||
assert registered_only["data"]["unresolved"] is True
|
||||
assert any(edge["data"]["edgeType"] == "declares" for edge in edges)
|
||||
assert any(node["data"]["sourceReferences"] for node in nodes if node["data"]["kind"] != "Repository")
|
||||
assert payload["metrics"]["registered_repo_count"] == 2
|
||||
assert payload["metrics"]["unresolved_count"] == 0
|
||||
|
||||
|
||||
def test_cli_exports_graph_explorer_payload(capsys) -> None:
|
||||
assert cli_main(["export", "--format", "graph-explorer"]) == 0
|
||||
payload = json.loads(capsys.readouterr().out)
|
||||
|
||||
_validate_schema("graph-explorer-payload.schema.yaml", payload)
|
||||
assert payload["kind"] == "GraphExplorerPayload"
|
||||
assert any(
|
||||
element["data"].get("sourceReferences")
|
||||
for element in payload["elements"]
|
||||
if "source" not in element["data"]
|
||||
)
|
||||
|
||||
|
||||
def test_graph_explorer_payload_accepts_repo_scoping_shape() -> None:
|
||||
|
||||
@@ -184,7 +184,7 @@ Acceptance notes:
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0008-T03
|
||||
status: in_progress
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ecd967fc-05ed-4cda-bca2-cf74e26e60b3"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user