"""Fail-closed field ownership checks for persistence writers.
The YAML file is a baseline contract. It records domain owners and the concrete
legacy writer paths found during the AA-053 inventory. No writer is authorized
while a field remains in FIELD_AUDIT_REQUIRED.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import yaml
_ALLOWED_STATUSES = frozenset({"ASSIGNED", "FIELD_AUDIT_REQUIRED", "OPEN", "UNASSIGNED"})
class WriteAuthorityError(PermissionError):
"""Raised when a writer is not the owner of every requested field."""
@dataclass(frozen=True, slots=True)
class FieldOwner:
table: str
field: str
owner: str
status: str
known_writers: tuple[str, ...] = ()
@dataclass(frozen=True, slots=True)
class WriterPolicy:
fields: dict[tuple[str, str], FieldOwner]
writer_paths: frozenset[str] = frozenset()
def owner_for(self, table: str, field: str) -> str:
"""Return the declared owner, including fields still locked for audit."""
assignment = self.fields.get((table, field))
if assignment is None:
raise WriteAuthorityError(f"no owner declared for {table}.{field}")
return assignment.owner
def require(self, owner: str, table: str, fields: list[str] | tuple[str, ...]) -> None:
"""Require *owner* to own every field, failing closed for unknown fields."""
denied: list[str] = []
for field in fields:
assignment = self.fields.get((table, field))
if assignment is None or assignment.owner != owner or assignment.status != "ASSIGNED":
denied.append(field)
if denied:
raise WriteAuthorityError(
f"writer {owner!r} is not authorized for {table}: {', '.join(denied)}"
)
def require_writer(self, writer_path: str, table: str, fields: list[str] | tuple[str, ...]) -> None:
"""Require a concrete inventoried writer to be assigned to every field.
Inventory membership is not authorization: FIELD_AUDIT_REQUIRED and all
other non-ASSIGNED states remain denied, including for known legacy paths.
"""
denied: list[str] = []
for field in fields:
assignment = self.fields.get((table, field))
if (
assignment is None
or writer_path not in assignment.known_writers
or assignment.status != "ASSIGNED"
):
denied.append(field)
if denied:
raise WriteAuthorityError(
f"writer path {writer_path!r} is not authorized for {table}: {', '.join(denied)}"
)
def load_writer_policy(path: str | Path) -> WriterPolicy:
"""Load and validate the ownership baseline with concrete writer paths."""
document: dict[str, Any] = yaml.safe_load(Path(path).read_text(encoding="utf-8")) or {}
if not isinstance(document, dict):
raise ValueError("writer policy must be a mapping")
fields: dict[tuple[str, str], FieldOwner] = {}
writer_paths: set[str] = set()
groups = document.get("field_groups") or {}
if not isinstance(groups, dict):
raise ValueError("field_groups must be a mapping")
for group_name, group in groups.items():
if not isinstance(group, dict):
raise ValueError(f"field group {group_name!r} must be a mapping")
# The baseline's mixed group is explicitly a signals field group too.
table = "signals" if group_name.startswith("signals_") or group_name == "mixed" else group.get("table")
if not table:
continue
owner = str(group.get("owner", "UNASSIGNED"))
status = str(group.get("status", "OPEN"))
if status not in _ALLOWED_STATUSES:
raise ValueError(f"invalid writer policy status {status!r}")
raw_writers = group.get("known_writers") or []
if not isinstance(raw_writers, list) or not all(isinstance(item, str) and item.strip() for item in raw_writers):
raise ValueError(f"known_writers for {group_name!r} must be a list of non-empty strings")
known_writers = tuple(dict.fromkeys(raw_writers))
writer_paths.update(known_writers)
raw_fields = group.get("examples") or []
if not isinstance(raw_fields, list):
raise ValueError(f"examples for {group_name!r} must be a list")
for field in raw_fields:
key = (str(table), str(field))
if key in fields:
raise ValueError(f"duplicate ownership assignment for {key[0]}.{key[1]}")
fields[key] = FieldOwner(str(table), str(field), owner, status, known_writers)
return WriterPolicy(fields, frozenset(writer_paths))