From 113445def9cb0d6e4888fa918d85c7bd03ae6f9a Mon Sep 17 00:00:00 2001 From: codex Date: Mon, 31 Aug 2026 22:18:26 +0200 Subject: [PATCH] feat: publish authoritative fabric cutover export Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c --- docs/registry-api.md | 8 +++++++ railiance_fabric/cli.py | 21 ++++++++++++++--- railiance_fabric/registry.py | 44 +++++++++++++++++++++++++++++++++--- railiance_fabric/server.py | 22 ++++++++++++++++++ tests/test_registry.py | 23 +++++++++++++++++++ 5 files changed, 112 insertions(+), 6 deletions(-) diff --git a/docs/registry-api.md b/docs/registry-api.md index fff0f70..e7a237c 100644 --- a/docs/registry-api.md +++ b/docs/registry-api.md @@ -98,6 +98,7 @@ components and services in the submitted SBOM. ```text GET /exports/state-hub +GET /exports/financial GET /exports/backstage GET /exports/xregistry GET /exports/libraries/xregistry @@ -113,6 +114,13 @@ for existing State Hub views until `STATE-WP-0051` materializes v1alpha2 fields. Use `railiance-fabric export --format financial` and `examples/exports/financial-fabric-v1.json` as the vNext contract references. +`GET /exports/financial` always serves the same accepted snapshot set projected +through `railiance.fabric/v1alpha2` / `financial-fabric-v1`. Its `source.commit` +is a deterministic digest of the accepted repo revisions, and `source.snapshots` +retains the contributing revision set. Retirement and financial-domain +consumers should use this endpoint instead of relying on the combined graph's +legacy/vNext family. + ## Guarded Reset ```text diff --git a/railiance_fabric/cli.py b/railiance_fabric/cli.py index b964e11..908a8d4 100644 --- a/railiance_fabric/cli.py +++ b/railiance_fabric/cli.py @@ -348,14 +348,15 @@ def main(argv: list[str] | None = None) -> int: if args.command == "export": graph = _load_graph_or_exit(args.paths) + export_payload = _export_with_provenance(graph.to_export(), args.paths) 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)) + print(json.dumps(fabric_graph_explorer_payload(export_payload), indent=2, sort_keys=True)) elif args.format == "financial": - print(json.dumps(financial_export_from_legacy(graph.to_export()), indent=2, sort_keys=True)) + print(json.dumps(financial_export_from_legacy(export_payload), indent=2, sort_keys=True)) else: - print(graph.to_json()) + print(json.dumps(export_payload, indent=2, sort_keys=True)) return 0 if args.command == "scan": @@ -1648,6 +1649,20 @@ def _primary_repo_path(paths: list[Path]) -> Path: return path.parent if path.is_file() else path +def _export_with_provenance(graph: dict[str, Any], paths: list[Path]) -> dict[str, Any]: + """Attach stable producer provenance to an operator export.""" + repo_path = _primary_repo_path(paths) + payload = json.loads(json.dumps(graph)) + payload["generated_at"] = _utc_now() + payload["source"] = { + "producer": "railiance-fabric", + "repo": "railiance-fabric", + "commit": _git_value(repo_path, "rev-parse", "HEAD") or "working-tree", + "path": ",".join(path.as_posix() for path in paths) or ".", + } + return payload + + def _slugify(value: str) -> str: return re.sub(r"-+", "-", re.sub(r"[^a-z0-9]", "-", value.lower())).strip("-") or "repo" diff --git a/railiance_fabric/registry.py b/railiance_fabric/registry.py index c69d119..394d0ab 100644 --- a/railiance_fabric/registry.py +++ b/railiance_fabric/registry.py @@ -16,6 +16,7 @@ from .financial import ( materialize_financial_graph_export, merge_financial_graph_exports, ) +from .financial_baseline import financial_export_from_legacy from .loader import load_yaml, repo_root from .schema_validation import draft202012_validator @@ -470,10 +471,11 @@ class RegistryStore: def combined_graph(self) -> dict[str, Any]: snapshots = self.latest_snapshots() if snapshots and all(is_financial_graph_export(snapshot["graph"]) for snapshot in snapshots): - return merge_financial_graph_exports( + graph = merge_financial_graph_exports( [snapshot["graph"] for snapshot in snapshots], generated_at=_utc_now(), ) + return _with_registry_provenance(graph, snapshots) nodes: dict[str, dict[str, Any]] = {} edges: list[dict[str, str]] = [] for snapshot in snapshots: @@ -491,14 +493,21 @@ class RegistryStore: nodes[node_id] = node for edge in projected_edges: edges.append(_edge_with_canon_metadata(edge)) - return { + graph = { "apiVersion": "railiance.fabric/v1alpha1", "kind": "FabricGraphExport", "generated_at": _utc_now(), - "source": {"repo": "registry", "commit": "", "path": ""}, "nodes": [nodes[key] for key in sorted(nodes)], "edges": sorted(edges, key=lambda edge: (edge["from"], edge["to"], edge["type"])), } + return _with_registry_provenance(graph, snapshots) + + def financial_graph(self) -> dict[str, Any]: + """Return the accepted graph through the financial Fabric contract.""" + graph = self.combined_graph() + if is_financial_graph_export(graph): + return graph + return financial_export_from_legacy(graph) def add_artifact(self, payload: dict[str, Any]) -> dict[str, Any]: repo_slug = _required_text(payload, "repo_slug") @@ -1394,6 +1403,35 @@ def _with_source(graph: dict[str, Any], repo_slug: str, commit: str, generated_a return copy +def _with_registry_provenance( + graph: dict[str, Any], snapshots: list[dict[str, Any]] +) -> dict[str, Any]: + """Identify the exact accepted snapshot set behind a combined export.""" + copy = json.loads(json.dumps(graph)) + revisions = [ + { + "repo_slug": str(snapshot["repo_slug"]), + "commit": str(snapshot["commit"]), + "generated_at": str(snapshot["generated_at"]), + } + for snapshot in sorted( + snapshots, + key=lambda item: (str(item["repo_slug"]), str(item["commit"])), + ) + ] + raw = json.dumps(revisions, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + revision_digest = hashlib.sha256(raw.encode("utf-8")).hexdigest() + copy["source"] = { + "producer": "railiance-fabric", + "repo": "railiance-fabric", + "registry": "accepted-snapshots", + "commit": f"snapshot-set:sha256:{revision_digest}", + "path": "registry://accepted-snapshots", + "snapshots": revisions, + } + return copy + + def _snapshot_dict(row: sqlite3.Row) -> dict[str, Any]: return { "id": row["id"], diff --git a/railiance_fabric/server.py b/railiance_fabric/server.py index 78e222b..3b10bf5 100644 --- a/railiance_fabric/server.py +++ b/railiance_fabric/server.py @@ -26,6 +26,22 @@ from .registry import ( ) +def _graph_summary(graph: dict[str, Any]) -> dict[str, Any]: + source = graph.get("source") if isinstance(graph.get("source"), dict) else {} + return { + "apiVersion": graph.get("apiVersion"), + "kind": graph.get("kind"), + "schema_version": graph.get("schema_version"), + "generated_at": graph.get("generated_at"), + "source": source, + "node_count": len(graph.get("nodes", [])), + "edge_count": len(graph.get("edges", [])), + "actor_count": len(graph.get("actors", [])), + "fabric_count": len(graph.get("fabrics", [])), + "unresolved_count": len(graph.get("unresolved", [])), + } + + @dataclass(frozen=True) class HtmlResponse: body: str @@ -93,6 +109,10 @@ class RegistryHandler(BaseHTTPRequestHandler): return HTTPStatus.OK, self.store.search(_query_one(query, "q")) if parts == ["graph", "nodes"]: return HTTPStatus.OK, self.store.combined_graph()["nodes"] + if parts == ["graph", "edges"]: + return HTTPStatus.OK, self.store.combined_graph()["edges"] + if parts == ["graph", "summary"]: + return HTTPStatus.OK, _graph_summary(self.store.combined_graph()) if len(parts) == 3 and parts[0] == "graph" and parts[1] == "nodes": return HTTPStatus.OK, self.store.graph_node_detail(parts[2]) if parts == ["graph", "providers"]: @@ -107,6 +127,8 @@ class RegistryHandler(BaseHTTPRequestHandler): return HTTPStatus.OK, {"lines": dependency_path_lines(self.store.combined_graph(), _query_one(query, "service_id"))} if parts == ["exports", "state-hub"]: return HTTPStatus.OK, self.store.combined_graph() + if parts == ["exports", "financial"]: + return HTTPStatus.OK, self.store.financial_graph() if parts == ["exports", "reset-archive"]: return HTTPStatus.OK, self.store.reset_archive() if parts == ["exports", "backstage"]: diff --git a/tests/test_registry.py b/tests/test_registry.py index a6659a7..cb000e3 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -323,7 +323,13 @@ def test_registry_http_service_serves_queries(tmp_path: Path) -> None: ) == 0 assert store.latest_snapshot("railiance-fabric")["commit"] == "test-cli" second_export = build_graph([Path(".")]).to_export() + removed_node_id = second_export["nodes"][-1]["id"] second_export["nodes"] = second_export["nodes"][:-1] + second_export["edges"] = [ + edge + for edge in second_export["edges"] + if edge["from"] != removed_node_id and edge["to"] != removed_node_id + ] _post_json( f"{base_url}/repositories/railiance-fabric/snapshots", { @@ -352,6 +358,14 @@ def test_registry_http_service_serves_queries(tmp_path: Path) -> None: timeout=5, ) as response: providers_payload = json.loads(response.read()) + with urllib.request.urlopen(f"{base_url}/graph/edges", timeout=5) as response: + edges_payload = json.loads(response.read()) + with urllib.request.urlopen(f"{base_url}/graph/summary", timeout=5) as response: + graph_summary_payload = json.loads(response.read()) + with urllib.request.urlopen(f"{base_url}/exports/state-hub", timeout=5) as response: + state_hub_export = json.loads(response.read()) + with urllib.request.urlopen(f"{base_url}/exports/financial", timeout=5) as response: + financial_export = json.loads(response.read()) artifact_payload = _post_json( f"{base_url}/artifacts", { @@ -404,6 +418,15 @@ def test_registry_http_service_serves_queries(tmp_path: Path) -> None: with urllib.request.urlopen(f"{base_url}/exports/libraries/xregistry", timeout=5) as response: library_projection_payload = json.loads(response.read()) assert providers_payload[0]["provider_id"] == "railiance-platform.openbao.runtime-secrets" + assert edges_payload + assert graph_summary_payload["node_count"] == len(state_hub_export["nodes"]) + assert graph_summary_payload["edge_count"] == len(state_hub_export["edges"]) + assert state_hub_export["source"]["producer"] == "railiance-fabric" + assert state_hub_export["source"]["commit"].startswith("snapshot-set:sha256:") + assert state_hub_export["source"]["snapshots"][-1]["commit"] == "test-cli-2" + assert financial_export["apiVersion"] == "railiance.fabric/v1alpha2" + assert financial_export["schema_version"] == "financial-fabric-v1" + assert financial_export["source"]["commit"] == state_hub_export["source"]["commit"] assert snapshots_payload[0]["commit"] == "test-cli-2" assert status_payload["counts"]["repositories"] == 1 assert status_payload["counts"]["snapshots"] == 3