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
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

View file

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

View file

@ -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"],

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)
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"]:

View file

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