generated from coulomb/repo-seed
feat: seed railiance financial baseline
This commit is contained in:
65
fabric/financial/railiance-netkingdom.yaml
Normal file
65
fabric/financial/railiance-netkingdom.yaml
Normal 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.
|
||||
@@ -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
|
||||
|
||||
137
railiance_fabric/financial_baseline.py
Normal file
137
railiance_fabric/financial_baseline.py
Normal 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, "")
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user