feat: publish authoritative fabric cutover export
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
This commit is contained in:
parent
7745a965f6
commit
113445def9
5 changed files with 112 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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"]:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue