Complete graph explorer projection

This commit is contained in:
2026-05-18 17:04:17 +02:00
parent 91f329f878
commit d2056c9046
8 changed files with 77 additions and 11 deletions

View File

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

View File

@@ -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 [],
}

View File

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