feat: publish authoritative fabric cutover export
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
This commit is contained in:
codex 2026-08-31 22:18:26 +02:00
parent 7745a965f6
commit 113445def9
5 changed files with 112 additions and 6 deletions

View file

@ -98,6 +98,7 @@ components and services in the submitted SBOM.
```text ```text
GET /exports/state-hub GET /exports/state-hub
GET /exports/financial
GET /exports/backstage GET /exports/backstage
GET /exports/xregistry GET /exports/xregistry
GET /exports/libraries/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 fields. Use `railiance-fabric export --format financial` and
`examples/exports/financial-fabric-v1.json` as the vNext contract references. `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 ## Guarded Reset
```text ```text

View file

@ -348,14 +348,15 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "export": if args.command == "export":
graph = _load_graph_or_exit(args.paths) graph = _load_graph_or_exit(args.paths)
export_payload = _export_with_provenance(graph.to_export(), args.paths)
if args.format == "mermaid": if args.format == "mermaid":
print(graph.to_mermaid()) print(graph.to_mermaid())
elif args.format == "graph-explorer": 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": 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: else:
print(graph.to_json()) print(json.dumps(export_payload, indent=2, sort_keys=True))
return 0 return 0
if args.command == "scan": 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 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: def _slugify(value: str) -> str:
return re.sub(r"-+", "-", re.sub(r"[^a-z0-9]", "-", value.lower())).strip("-") or "repo" return re.sub(r"-+", "-", re.sub(r"[^a-z0-9]", "-", value.lower())).strip("-") or "repo"

View file

@ -16,6 +16,7 @@ from .financial import (
materialize_financial_graph_export, materialize_financial_graph_export,
merge_financial_graph_exports, merge_financial_graph_exports,
) )
from .financial_baseline import financial_export_from_legacy
from .loader import load_yaml, repo_root from .loader import load_yaml, repo_root
from .schema_validation import draft202012_validator from .schema_validation import draft202012_validator
@ -470,10 +471,11 @@ class RegistryStore:
def combined_graph(self) -> dict[str, Any]: def combined_graph(self) -> dict[str, Any]:
snapshots = self.latest_snapshots() snapshots = self.latest_snapshots()
if snapshots and all(is_financial_graph_export(snapshot["graph"]) for snapshot in 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], [snapshot["graph"] for snapshot in snapshots],
generated_at=_utc_now(), generated_at=_utc_now(),
) )
return _with_registry_provenance(graph, snapshots)
nodes: dict[str, dict[str, Any]] = {} nodes: dict[str, dict[str, Any]] = {}
edges: list[dict[str, str]] = [] edges: list[dict[str, str]] = []
for snapshot in snapshots: for snapshot in snapshots:
@ -491,14 +493,21 @@ class RegistryStore:
nodes[node_id] = node nodes[node_id] = node
for edge in projected_edges: for edge in projected_edges:
edges.append(_edge_with_canon_metadata(edge)) edges.append(_edge_with_canon_metadata(edge))
return { graph = {
"apiVersion": "railiance.fabric/v1alpha1", "apiVersion": "railiance.fabric/v1alpha1",
"kind": "FabricGraphExport", "kind": "FabricGraphExport",
"generated_at": _utc_now(), "generated_at": _utc_now(),
"source": {"repo": "registry", "commit": "", "path": ""},
"nodes": [nodes[key] for key in sorted(nodes)], "nodes": [nodes[key] for key in sorted(nodes)],
"edges": sorted(edges, key=lambda edge: (edge["from"], edge["to"], edge["type"])), "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]: def add_artifact(self, payload: dict[str, Any]) -> dict[str, Any]:
repo_slug = _required_text(payload, "repo_slug") 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 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]: def _snapshot_dict(row: sqlite3.Row) -> dict[str, Any]:
return { return {
"id": row["id"], "id": row["id"],

View file

@ -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) @dataclass(frozen=True)
class HtmlResponse: class HtmlResponse:
body: str body: str
@ -93,6 +109,10 @@ class RegistryHandler(BaseHTTPRequestHandler):
return HTTPStatus.OK, self.store.search(_query_one(query, "q")) return HTTPStatus.OK, self.store.search(_query_one(query, "q"))
if parts == ["graph", "nodes"]: if parts == ["graph", "nodes"]:
return HTTPStatus.OK, self.store.combined_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": if len(parts) == 3 and parts[0] == "graph" and parts[1] == "nodes":
return HTTPStatus.OK, self.store.graph_node_detail(parts[2]) return HTTPStatus.OK, self.store.graph_node_detail(parts[2])
if parts == ["graph", "providers"]: 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"))} return HTTPStatus.OK, {"lines": dependency_path_lines(self.store.combined_graph(), _query_one(query, "service_id"))}
if parts == ["exports", "state-hub"]: if parts == ["exports", "state-hub"]:
return HTTPStatus.OK, self.store.combined_graph() return HTTPStatus.OK, self.store.combined_graph()
if parts == ["exports", "financial"]:
return HTTPStatus.OK, self.store.financial_graph()
if parts == ["exports", "reset-archive"]: if parts == ["exports", "reset-archive"]:
return HTTPStatus.OK, self.store.reset_archive() return HTTPStatus.OK, self.store.reset_archive()
if parts == ["exports", "backstage"]: if parts == ["exports", "backstage"]:

View file

@ -323,7 +323,13 @@ def test_registry_http_service_serves_queries(tmp_path: Path) -> None:
) == 0 ) == 0
assert store.latest_snapshot("railiance-fabric")["commit"] == "test-cli" assert store.latest_snapshot("railiance-fabric")["commit"] == "test-cli"
second_export = build_graph([Path(".")]).to_export() second_export = build_graph([Path(".")]).to_export()
removed_node_id = second_export["nodes"][-1]["id"]
second_export["nodes"] = second_export["nodes"][:-1] 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( _post_json(
f"{base_url}/repositories/railiance-fabric/snapshots", f"{base_url}/repositories/railiance-fabric/snapshots",
{ {
@ -352,6 +358,14 @@ def test_registry_http_service_serves_queries(tmp_path: Path) -> None:
timeout=5, timeout=5,
) as response: ) as response:
providers_payload = json.loads(response.read()) 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( artifact_payload = _post_json(
f"{base_url}/artifacts", 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: with urllib.request.urlopen(f"{base_url}/exports/libraries/xregistry", timeout=5) as response:
library_projection_payload = json.loads(response.read()) library_projection_payload = json.loads(response.read())
assert providers_payload[0]["provider_id"] == "railiance-platform.openbao.runtime-secrets" 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 snapshots_payload[0]["commit"] == "test-cli-2"
assert status_payload["counts"]["repositories"] == 1 assert status_payload["counts"]["repositories"] == 1
assert status_payload["counts"]["snapshots"] == 3 assert status_payload["counts"]["snapshots"] == 3