feat: seed railiance financial baseline

This commit is contained in:
2026-05-24 01:20:07 +02:00
parent 7d9b49764b
commit 13188a6ae1
5 changed files with 247 additions and 3 deletions

View File

@@ -0,0 +1,65 @@
apiVersion: railiance.fabric/v1alpha2
kind: FinancialFabricBaseline
metadata:
id: railiance.netkingdom-baseline
name: Railiance Netkingdom Baseline
spec:
netkingdom:
id: railiance.netkingdom
name: Railiance Netkingdom
king_actor_id: actor.railiance.king
actors:
- id: actor.railiance.king
kind: FabricActor
role: king
name: Railiance King
description: Responsible for the Railiance netkingdom and recovery authority.
authority:
recovery_authority: true
secrets_authority: true
backup_authority: true
termination_authority: true
- id: actor.railiance.primary-lord
kind: FabricActor
role: lord
name: Railiance Primary Lord
description: Pays for the current Railiance infrastructure boundary.
fabrics:
- id: fabric.railiance.primary
kind: Fabric
name: Railiance Primary Fabric
netkingdom_id: railiance.netkingdom
lord_actor_id: actor.railiance.primary-lord
parent_fabric_id: null
status: active
boundary:
boundary_type: fabric
criterion: financial_and_operational_accountability
payment_responsibility: actor.railiance.primary-lord
operational_responsibility: actor.railiance.king
recovery_responsibility: actor.railiance.king
evidence_refs: []
defaults:
containment:
netkingdom_id: railiance.netkingdom
fabric_id: fabric.railiance.primary
subfabric_id: null
environment: local
deployment_scenario_id: null
ownership:
owner_actor_id: actor.railiance.primary-lord
owner_role: lord
resolution: inherited
inherited_from: fabric.railiance.primary
supporting_actor_ids:
- actor.railiance.king
accounting:
cost_center_id: null
profit_center_id: null
allocation_model: null
future_subfabric_template:
kind: Subfabric
parent_fabric_id: fabric.railiance.primary
netkingdom_id: railiance.netkingdom
tenant_actor_role: tenant
note: Add a tenant actor and subfabric without changing the root fabric criterion.

View File

@@ -15,6 +15,7 @@ from typing import Any
from urllib.parse import quote
from .connectors import ConnectorConfig
from .financial_baseline import financial_export_from_legacy
from .loader import declaration_files, load_yaml
from .graph import FabricGraph, build_graph
from .graph_explorer import fabric_graph_explorer_payload
@@ -72,9 +73,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, Mermaid, or graph-explorer payload.")
export = sub.add_parser("export", help="Export graph as JSON, Mermaid, graph-explorer, or financial payload.")
export.add_argument("paths", nargs="*", type=Path, default=[Path(".")])
export.add_argument("--format", choices=["json", "mermaid", "graph-explorer"], default="json")
export.add_argument("--format", choices=["json", "mermaid", "graph-explorer", "financial"], default="json")
scan = sub.add_parser("scan", help="Scan a repo for deterministic discovery candidates.")
scan.add_argument("path", nargs="?", type=Path, default=Path("."))
@@ -310,6 +311,8 @@ def main(argv: list[str] | None = None) -> int:
print(graph.to_mermaid())
elif args.format == "graph-explorer":
print(json.dumps(fabric_graph_explorer_payload(graph.to_export()), indent=2, sort_keys=True))
elif args.format == "financial":
print(json.dumps(financial_export_from_legacy(graph.to_export()), indent=2, sort_keys=True))
else:
print(graph.to_json())
return 0

View File

@@ -0,0 +1,137 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from .financial import (
FINANCIAL_API_VERSION,
FINANCIAL_SCHEMA_VERSION,
financial_graph_errors,
materialize_financial_graph_export,
)
from .loader import load_yaml, repo_root
DEFAULT_BASELINE_PATH = repo_root() / "fabric" / "financial" / "railiance-netkingdom.yaml"
def load_financial_baseline(path: Path | None = None) -> dict[str, Any]:
baseline_path = path or DEFAULT_BASELINE_PATH
data = load_yaml(baseline_path)
if not isinstance(data, dict):
raise ValueError(f"financial baseline must be a mapping: {baseline_path}")
if data.get("kind") != "FinancialFabricBaseline":
raise ValueError(f"financial baseline kind must be FinancialFabricBaseline: {baseline_path}")
spec = data.get("spec")
if not isinstance(spec, dict):
raise ValueError(f"financial baseline spec must be a mapping: {baseline_path}")
return spec
def financial_export_from_legacy(
legacy_graph: dict[str, Any],
baseline: dict[str, Any] | None = None,
) -> dict[str, Any]:
baseline = json.loads(json.dumps(baseline or load_financial_baseline()))
defaults = baseline.get("defaults") if isinstance(baseline.get("defaults"), dict) else {}
containment_default = defaults.get("containment") if isinstance(defaults.get("containment"), dict) else {}
ownership_default = defaults.get("ownership") if isinstance(defaults.get("ownership"), dict) else {}
accounting_default = defaults.get("accounting") if isinstance(defaults.get("accounting"), dict) else {}
graph = {
"apiVersion": FINANCIAL_API_VERSION,
"kind": "FabricGraphExport",
"schema_version": FINANCIAL_SCHEMA_VERSION,
"source": {
**_object(legacy_graph.get("source")),
"producer": "railiance-fabric",
"generation_reason": "baseline_projection",
},
"compatibility": {
"legacy_v1alpha1_supported": True,
"breaking_reset": False,
"projected_from_apiVersion": legacy_graph.get("apiVersion"),
},
"netkingdom": baseline.get("netkingdom", {}),
"actors": baseline.get("actors", []),
"fabrics": baseline.get("fabrics", []),
"nodes": [
_financial_node_from_legacy(node, containment_default, ownership_default, accounting_default)
for node in legacy_graph.get("nodes", [])
if isinstance(node, dict)
],
"edges": [
_financial_edge_from_legacy(edge)
for edge in legacy_graph.get("edges", [])
if isinstance(edge, dict)
],
"unresolved": [],
}
if legacy_graph.get("generated_at"):
graph["generated_at"] = legacy_graph["generated_at"]
materialized = materialize_financial_graph_export(graph)
errors = financial_graph_errors(materialized)
if errors:
raise ValueError(f"invalid financial baseline projection: {errors[0]}")
return materialized
def _financial_node_from_legacy(
node: dict[str, Any],
containment_default: dict[str, Any],
ownership_default: dict[str, Any],
accounting_default: dict[str, Any],
) -> dict[str, Any]:
result: dict[str, Any] = {
"id": node.get("id", ""),
"kind": _node_kind(node),
"name": node.get("name") or node.get("id", ""),
"repo": node.get("repo", ""),
"domain": node.get("domain", ""),
"lifecycle": node.get("lifecycle", ""),
"containment": json.loads(json.dumps(containment_default)),
"ownership": json.loads(json.dumps(ownership_default)),
"evidence_state": node.get("evidence_state", "declared"),
"attributes": _object(node.get("attributes")),
}
accounting = _object(result["attributes"].get("accounting")) or accounting_default
if _has_value(accounting):
result["accounting"] = json.loads(json.dumps(accounting))
for key in ("canon_category", "canon_anchor", "mapping_fit"):
if node.get(key):
result[key] = node[key]
return result
def _financial_edge_from_legacy(edge: dict[str, Any]) -> dict[str, Any]:
result = {
"from": edge.get("from", ""),
"to": edge.get("to", ""),
"type": edge.get("type", ""),
"attributes": _object(edge.get("attributes")),
}
for key in ("canonical_type", "canon_anchor", "mapping_fit", "display_only", "evidence_state"):
if key in edge:
result[key] = edge[key]
return result
def _node_kind(node: dict[str, Any]) -> str:
kind = str(node.get("kind") or "")
if kind == "ServiceDeclaration":
return "Service"
if kind == "InterfaceDeclaration":
return "UtilityInterface"
return kind or "DiscoveredEntity"
def _object(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _has_value(value: Any) -> bool:
if isinstance(value, dict):
return any(_has_value(item) for item in value.values())
if isinstance(value, list):
return any(_has_value(item) for item in value)
return value not in (None, "")

View File

@@ -9,6 +9,7 @@ from pathlib import Path
from railiance_fabric.cli import main as cli_main
from railiance_fabric.graph import build_graph
from railiance_fabric.financial import materialize_financial_graph_export
from railiance_fabric.financial_baseline import financial_export_from_legacy
from railiance_fabric.registry import (
RESET_CONFIRMATION_TOKEN,
RegistryError,
@@ -319,6 +320,29 @@ def test_registry_accepts_financial_graph_and_materializes_vnext_fields(tmp_path
assert combined["edges"][0]["relationship_category"] == "utility"
def test_current_graph_projects_to_financial_baseline() -> None:
legacy_graph = build_graph([Path(".")]).to_export()
financial_graph = financial_export_from_legacy(legacy_graph)
validate_graph_export(financial_graph)
validator = draft202012_validator(Path("schemas/state-hub-export.schema.yaml"))
assert list(validator.iter_errors(financial_graph)) == []
assert financial_graph["netkingdom"]["id"] == "railiance.netkingdom"
assert financial_graph["fabrics"][0]["id"] == "fabric.railiance.primary"
assert financial_graph["unresolved"] == []
assert all(node["containment"]["fabric_id"] == "fabric.railiance.primary" for node in financial_graph["nodes"])
assert all(node["ownership"]["owner_actor_id"] == "actor.railiance.primary-lord" for node in financial_graph["nodes"])
def test_export_cli_can_print_financial_baseline(capsys) -> None:
assert cli_main(["export", "--format", "financial", "."]) == 0
payload = json.loads(capsys.readouterr().out)
validate_graph_export(payload)
assert payload["apiVersion"] == "railiance.fabric/v1alpha2"
assert payload["compatibility"]["projected_from_apiVersion"] == "railiance.fabric/v1alpha1"
def test_registry_reset_archive_and_guarded_reset(tmp_path: Path) -> None:
store = RegistryStore(tmp_path / "registry.sqlite3")
store.init_schema()

View File

@@ -237,7 +237,7 @@ Result:
```task
id: RAIL-FAB-WP-0017-T05
status: todo
status: done
priority: medium
state_hub_task_id: "b8430050-94a8-43f6-9b3c-7928c5c6bb69"
```
@@ -262,6 +262,21 @@ Done when:
- future tenant subfabrics can be added without changing the root fabric
definition.
Result:
- Added `fabric/financial/railiance-netkingdom.yaml` as the current Railiance
netkingdom baseline with king, lord, one active fabric, inherited default
containment/ownership/accounting, and a future subfabric template.
- Added `railiance_fabric/financial_baseline.py` to load the baseline and
project legacy `v1alpha1` exports into `v1alpha2` financial Fabric exports.
- Added `railiance-fabric export --format financial` so the current graph can
emit a financial baseline projection.
- Added tests proving the current graph projects to the Railiance baseline,
validates as a financial graph, has no unresolved gaps, and gives every node
inherited ownership in `fabric.railiance.primary`.
- Verified with `python3 -m pytest tests/test_registry.py -q` and full
`python3 -m pytest`.
## T06 - Update Documentation, Fixtures, And Operator Guidance
```task