Files
railiance-fabric/railiance_fabric/financial_baseline.py

156 lines
5.8 KiB
Python

from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from .deployment_overlay import normalize_deployment_overlay
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": [],
}
graph["generated_at"] = legacy_graph.get("generated_at") or _utc_now()
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))
overlay = normalize_deployment_overlay(
node.get("deployment_overlay") if isinstance(node.get("deployment_overlay"), dict) else {},
result["attributes"],
result["containment"],
)
if _has_value(overlay):
result["deployment_overlay"] = overlay
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]
overlay = normalize_deployment_overlay(
edge.get("deployment_overlay") if isinstance(edge.get("deployment_overlay"), dict) else {},
result["attributes"],
)
if _has_value(overlay):
result["deployment_overlay"] = overlay
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, "")
def _utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")