generated from coulomb/repo-seed
Implement financial Fabric vNext read model
This commit is contained in:
@@ -97,8 +97,15 @@ async def ingest_fabric_graph_export(
|
||||
source_commit=source.commit if source else None,
|
||||
source_path=source.path if source else None,
|
||||
api_version=export.api_version,
|
||||
schema_version=export.schema_version,
|
||||
export_kind=export.kind,
|
||||
exported_at=export.generated_at,
|
||||
netkingdom_id=export.netkingdom.id if export.netkingdom else None,
|
||||
king_actor_id=export.netkingdom.king_actor_id if export.netkingdom else None,
|
||||
actor_count=len(export.actors),
|
||||
fabric_count=len(export.fabrics),
|
||||
unresolved_count=len(export.unresolved),
|
||||
compatibility=export.compatibility,
|
||||
content_hash=content_hash,
|
||||
node_count=len(export.nodes),
|
||||
edge_count=len(export.edges),
|
||||
@@ -114,6 +121,10 @@ async def ingest_fabric_graph_export(
|
||||
|
||||
for node in export.nodes:
|
||||
raw = node.model_dump(mode="json")
|
||||
containment = raw.get("containment") if isinstance(raw.get("containment"), dict) else {}
|
||||
ownership = raw.get("ownership") if isinstance(raw.get("ownership"), dict) else {}
|
||||
accounting = raw.get("accounting") if isinstance(raw.get("accounting"), dict) else {}
|
||||
evidence = raw.get("evidence") if isinstance(raw.get("evidence"), dict) else {}
|
||||
session.add(
|
||||
FabricGraphNode(
|
||||
import_id=import_run.id,
|
||||
@@ -121,14 +132,26 @@ async def ingest_fabric_graph_export(
|
||||
graph_id=node.id,
|
||||
kind=node.kind,
|
||||
name=node.name,
|
||||
repo_slug=node.repo,
|
||||
domain_slug=node.domain,
|
||||
lifecycle=node.lifecycle,
|
||||
repo_slug=node.repo or "",
|
||||
domain_slug=node.domain or "",
|
||||
lifecycle=node.lifecycle or "",
|
||||
canonical_type=raw.get("canonical_type"),
|
||||
canon_category=node.canon_category,
|
||||
canon_anchor=node.canon_anchor,
|
||||
mapping_fit=node.mapping_fit,
|
||||
evidence_state=node.evidence_state,
|
||||
evidence_state=node.evidence_state or evidence.get("state"),
|
||||
evidence_review_state=evidence.get("review_state"),
|
||||
evidence_confidence=_float_or_none(evidence.get("confidence")),
|
||||
netkingdom_id=containment.get("netkingdom_id"),
|
||||
fabric_id=containment.get("fabric_id"),
|
||||
subfabric_id=containment.get("subfabric_id"),
|
||||
environment=containment.get("environment"),
|
||||
deployment_scenario_id=containment.get("deployment_scenario_id"),
|
||||
owner_actor_id=ownership.get("owner_actor_id"),
|
||||
owner_role=ownership.get("owner_role"),
|
||||
ownership_resolution=ownership.get("resolution"),
|
||||
cost_center_id=accounting.get("cost_center_id"),
|
||||
profit_center_id=accounting.get("profit_center_id"),
|
||||
display_only=bool(raw.get("display_only", False)),
|
||||
attributes=node.attributes,
|
||||
raw_json=raw,
|
||||
@@ -137,6 +160,12 @@ async def ingest_fabric_graph_export(
|
||||
|
||||
for edge in export.edges:
|
||||
raw = edge.model_dump(mode="json", by_alias=True)
|
||||
provider = raw.get("provider") if isinstance(raw.get("provider"), dict) else {}
|
||||
consumer = raw.get("consumer") if isinstance(raw.get("consumer"), dict) else {}
|
||||
boundary = raw.get("boundary") if isinstance(raw.get("boundary"), dict) else {}
|
||||
utility = raw.get("utility") if isinstance(raw.get("utility"), dict) else {}
|
||||
accounting = raw.get("accounting") if isinstance(raw.get("accounting"), dict) else {}
|
||||
evidence = raw.get("evidence") if isinstance(raw.get("evidence"), dict) else {}
|
||||
session.add(
|
||||
FabricGraphEdge(
|
||||
import_id=import_run.id,
|
||||
@@ -148,7 +177,27 @@ async def ingest_fabric_graph_export(
|
||||
canonical_type=edge.canonical_type,
|
||||
canon_anchor=edge.canon_anchor,
|
||||
mapping_fit=edge.mapping_fit,
|
||||
evidence_state=edge.evidence_state,
|
||||
evidence_state=edge.evidence_state or evidence.get("state"),
|
||||
evidence_review_state=evidence.get("review_state"),
|
||||
evidence_confidence=_float_or_none(evidence.get("confidence")),
|
||||
relationship_category=edge.relationship_category,
|
||||
provider_owner_actor_id=provider.get("owner_actor_id"),
|
||||
provider_fabric_id=provider.get("fabric_id"),
|
||||
provider_subfabric_id=provider.get("subfabric_id"),
|
||||
consumer_owner_actor_id=consumer.get("owner_actor_id"),
|
||||
consumer_fabric_id=consumer.get("fabric_id"),
|
||||
consumer_subfabric_id=consumer.get("subfabric_id"),
|
||||
crosses_fabric_boundary=boundary.get("crosses_fabric_boundary"),
|
||||
crosses_subfabric_boundary=boundary.get("crosses_subfabric_boundary"),
|
||||
utility_type=utility.get("utility_type"),
|
||||
utility_contract_id=utility.get("contract_id"),
|
||||
utility_payment_schema_id=utility.get("payment_schema_id"),
|
||||
utility_metering_basis=utility.get("metering_basis"),
|
||||
utility_business_model=utility.get("business_model"),
|
||||
cost_center_id=accounting.get("cost_center_id"),
|
||||
profit_center_id=accounting.get("profit_center_id"),
|
||||
provider_profit_center_id=accounting.get("provider_profit_center_id"),
|
||||
consumer_cost_center_id=accounting.get("consumer_cost_center_id"),
|
||||
display_only=bool(edge.display_only),
|
||||
attributes=edge.attributes,
|
||||
raw_json=raw,
|
||||
@@ -181,6 +230,9 @@ def validate_fabric_graph_export(payload: dict[str, Any]) -> FabricGraphExportPa
|
||||
message = first.get("msg", "invalid payload")
|
||||
raise ValueError(f"invalid FabricGraphExport at {location}: {message}") from exc
|
||||
|
||||
contract_errors = _contract_errors(export, payload)
|
||||
if contract_errors:
|
||||
raise ValueError(f"invalid FabricGraphExport contract: {contract_errors[0]}")
|
||||
canon_errors = _canon_metadata_errors(export)
|
||||
if canon_errors:
|
||||
raise ValueError(f"invalid FabricGraphExport canon metadata: {canon_errors[0]}")
|
||||
@@ -198,6 +250,10 @@ def edge_key(edge: dict[str, Any]) -> str:
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _float_or_none(value: Any) -> float | None:
|
||||
return float(value) if isinstance(value, (int, float)) else None
|
||||
|
||||
|
||||
async def record_fabric_graph_error(
|
||||
session: AsyncSession,
|
||||
summary: str,
|
||||
@@ -318,6 +374,180 @@ def _canonical_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
return canonical
|
||||
|
||||
|
||||
def _contract_errors(export: FabricGraphExportPayload, payload: dict[str, Any]) -> list[str]:
|
||||
if export.api_version == "railiance.fabric/v1alpha2":
|
||||
return _financial_contract_errors(export, payload)
|
||||
return _legacy_contract_errors(export)
|
||||
|
||||
|
||||
def _legacy_contract_errors(export: FabricGraphExportPayload) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if export.schema_version:
|
||||
errors.append("v1alpha1 exports must not set schema_version")
|
||||
for index, node in enumerate(export.nodes):
|
||||
_require_fields(
|
||||
errors,
|
||||
f"nodes[{index}]",
|
||||
{
|
||||
"repo": node.repo,
|
||||
"domain": node.domain,
|
||||
"lifecycle": node.lifecycle,
|
||||
},
|
||||
("repo", "domain", "lifecycle"),
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def _financial_contract_errors(
|
||||
export: FabricGraphExportPayload, payload: dict[str, Any]
|
||||
) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for field in ("schema_version", "netkingdom", "actors", "fabrics"):
|
||||
if field not in payload:
|
||||
errors.append(f"missing required financial export field {field!r}")
|
||||
if export.schema_version != "financial-fabric-v1":
|
||||
errors.append("schema_version must be 'financial-fabric-v1' for v1alpha2 exports")
|
||||
if export.netkingdom is None:
|
||||
errors.append("netkingdom must be an object for v1alpha2 exports")
|
||||
netkingdom_id = ""
|
||||
king_actor_id = ""
|
||||
else:
|
||||
netkingdom_id = export.netkingdom.id
|
||||
king_actor_id = export.netkingdom.king_actor_id
|
||||
|
||||
actor_roles: dict[str, str] = {}
|
||||
for index, actor in enumerate(export.actors):
|
||||
if actor.id in actor_roles:
|
||||
errors.append(f"actors[{index}].id {actor.id!r} is duplicated")
|
||||
actor_roles[actor.id] = actor.role
|
||||
if king_actor_id and actor_roles.get(king_actor_id) != "king":
|
||||
errors.append("netkingdom.king_actor_id must reference an actor with role 'king'")
|
||||
|
||||
fabric_kinds: dict[str, str] = {}
|
||||
for index, fabric in enumerate(export.fabrics):
|
||||
if fabric.id in fabric_kinds:
|
||||
errors.append(f"fabrics[{index}].id {fabric.id!r} is duplicated")
|
||||
fabric_kinds[fabric.id] = fabric.kind
|
||||
if fabric.netkingdom_id != netkingdom_id:
|
||||
errors.append(f"fabrics[{index}].netkingdom_id must match netkingdom.id")
|
||||
if fabric.kind == "Fabric":
|
||||
if not fabric.lord_actor_id:
|
||||
errors.append(f"fabrics[{index}].lord_actor_id is required for Fabric")
|
||||
elif actor_roles.get(fabric.lord_actor_id) not in {"lord", "king"}:
|
||||
errors.append(f"fabrics[{index}].lord_actor_id must reference a lord or king actor")
|
||||
if fabric.kind == "Subfabric":
|
||||
if not fabric.parent_fabric_id:
|
||||
errors.append(f"fabrics[{index}].parent_fabric_id is required for Subfabric")
|
||||
elif fabric.parent_fabric_id not in fabric_kinds:
|
||||
errors.append(
|
||||
f"fabrics[{index}].parent_fabric_id references unknown fabric {fabric.parent_fabric_id!r}"
|
||||
)
|
||||
if not fabric.tenant_actor_id:
|
||||
errors.append(f"fabrics[{index}].tenant_actor_id is required for Subfabric")
|
||||
elif actor_roles.get(fabric.tenant_actor_id) != "tenant":
|
||||
errors.append(f"fabrics[{index}].tenant_actor_id must reference a tenant actor")
|
||||
|
||||
node_ids: set[str] = set()
|
||||
for index, node in enumerate(export.nodes):
|
||||
path = f"nodes[{index}]"
|
||||
if node.id in node_ids:
|
||||
errors.append(f"{path}.id {node.id!r} is duplicated")
|
||||
node_ids.add(node.id)
|
||||
if node.containment is None:
|
||||
errors.append(f"{path}.containment must be an object for v1alpha2 exports")
|
||||
else:
|
||||
if node.containment.netkingdom_id != netkingdom_id:
|
||||
errors.append(f"{path}.containment.netkingdom_id must match netkingdom.id")
|
||||
_validate_fabric_ref(errors, f"{path}.containment.fabric_id", node.containment.fabric_id, fabric_kinds, "Fabric")
|
||||
if node.containment.subfabric_id:
|
||||
_validate_fabric_ref(
|
||||
errors,
|
||||
f"{path}.containment.subfabric_id",
|
||||
node.containment.subfabric_id,
|
||||
fabric_kinds,
|
||||
"Subfabric",
|
||||
)
|
||||
if node.ownership is None:
|
||||
errors.append(f"{path}.ownership must be an object for v1alpha2 exports")
|
||||
else:
|
||||
_validate_actor_ref(errors, f"{path}.ownership.owner_actor_id", node.ownership.owner_actor_id, actor_roles)
|
||||
if actor_roles.get(node.ownership.owner_actor_id) not in {node.ownership.owner_role, ""}:
|
||||
errors.append(f"{path}.ownership.owner_role does not match referenced actor role")
|
||||
if node.evidence is None:
|
||||
errors.append(f"{path}.evidence must be an object for v1alpha2 exports")
|
||||
elif (
|
||||
node.evidence.review_state == "accepted"
|
||||
and node.ownership
|
||||
and node.ownership.resolution not in {"explicit", "inherited"}
|
||||
):
|
||||
errors.append(f"{path}.ownership.resolution must be explicit or inherited for accepted nodes")
|
||||
|
||||
for index, edge in enumerate(export.edges):
|
||||
path = f"edges[{index}]"
|
||||
if edge.from_graph_id not in node_ids:
|
||||
errors.append(f"{path}.from references unknown node {edge.from_graph_id!r}")
|
||||
if edge.to_graph_id not in node_ids:
|
||||
errors.append(f"{path}.to references unknown node {edge.to_graph_id!r}")
|
||||
if edge.relationship_category is None:
|
||||
errors.append(f"{path}.relationship_category is required for v1alpha2 exports")
|
||||
if edge.evidence is None:
|
||||
errors.append(f"{path}.evidence must be an object for v1alpha2 exports")
|
||||
if edge.edge_type == "provides_utility_to" and edge.relationship_category != "utility":
|
||||
errors.append(f"{path}.relationship_category must be 'utility' for provides_utility_to edges")
|
||||
if edge.relationship_category == "utility":
|
||||
if edge.provider is None:
|
||||
errors.append(f"{path}.provider is required for utility edges")
|
||||
else:
|
||||
_validate_utility_side(errors, f"{path}.provider", edge.provider, actor_roles, fabric_kinds)
|
||||
if edge.consumer is None:
|
||||
errors.append(f"{path}.consumer is required for utility edges")
|
||||
else:
|
||||
_validate_utility_side(errors, f"{path}.consumer", edge.consumer, actor_roles, fabric_kinds)
|
||||
if edge.boundary is None:
|
||||
errors.append(f"{path}.boundary is required for utility edges")
|
||||
if edge.utility is None:
|
||||
errors.append(f"{path}.utility is required for utility edges")
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_actor_ref(
|
||||
errors: list[str],
|
||||
path: str,
|
||||
actor_id: str,
|
||||
actor_roles: dict[str, str],
|
||||
) -> None:
|
||||
if actor_id not in actor_roles:
|
||||
errors.append(f"{path} references unknown actor {actor_id!r}")
|
||||
|
||||
|
||||
def _validate_fabric_ref(
|
||||
errors: list[str],
|
||||
path: str,
|
||||
fabric_id: str,
|
||||
fabric_kinds: dict[str, str],
|
||||
expected_kind: str,
|
||||
) -> None:
|
||||
actual_kind = fabric_kinds.get(fabric_id)
|
||||
if actual_kind is None:
|
||||
errors.append(f"{path} references unknown fabric {fabric_id!r}")
|
||||
elif actual_kind != expected_kind:
|
||||
errors.append(f"{path} must reference a {expected_kind}")
|
||||
|
||||
|
||||
def _validate_utility_side(
|
||||
errors: list[str],
|
||||
path: str,
|
||||
side: Any,
|
||||
actor_roles: dict[str, str],
|
||||
fabric_kinds: dict[str, str],
|
||||
) -> None:
|
||||
_validate_actor_ref(errors, f"{path}.owner_actor_id", side.owner_actor_id, actor_roles)
|
||||
if side.fabric_id not in fabric_kinds:
|
||||
errors.append(f"{path}.fabric_id references unknown fabric {side.fabric_id!r}")
|
||||
if side.subfabric_id:
|
||||
_validate_fabric_ref(errors, f"{path}.subfabric_id", side.subfabric_id, fabric_kinds, "Subfabric")
|
||||
|
||||
|
||||
def _canon_metadata_errors(export: FabricGraphExportPayload) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for index, node in enumerate(export.nodes):
|
||||
|
||||
Reference in New Issue
Block a user