generated from coulomb/repo-seed
feat: resolve accountability ownership reviews
This commit is contained in:
@@ -194,6 +194,58 @@ def build_identity_projection(
|
||||
return projection
|
||||
|
||||
|
||||
def build_ownership_review(
|
||||
identity_projection: dict[str, Any],
|
||||
manifest: dict[str, Any],
|
||||
*,
|
||||
review_decisions: dict[str, dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
review_decisions = review_decisions or {}
|
||||
actor_roles = {
|
||||
actor.get("id"): actor.get("role", "")
|
||||
for actor in manifest.get("actors", [])
|
||||
if isinstance(actor, dict)
|
||||
}
|
||||
fabric_owners: dict[str, str] = {}
|
||||
fabric_kinds: dict[str, str] = {}
|
||||
for fabric in manifest.get("fabrics", []):
|
||||
if not isinstance(fabric, dict):
|
||||
continue
|
||||
fabric_id = str(fabric.get("id") or "")
|
||||
fabric_kinds[fabric_id] = str(fabric.get("kind") or "")
|
||||
fabric_owners[fabric_id] = str(fabric.get("tenant_actor_id") or fabric.get("lord_actor_id") or "")
|
||||
|
||||
items = [
|
||||
_ownership_item(candidate, actor_roles, fabric_owners, fabric_kinds, review_decisions.get(candidate["stable_key"]))
|
||||
for candidate in identity_projection.get("identity_candidates", [])
|
||||
if isinstance(candidate, dict)
|
||||
]
|
||||
review = {
|
||||
"apiVersion": "railiance.fabric/v1alpha2",
|
||||
"kind": "AccountabilityOwnershipReview",
|
||||
"generated_at": _utc_now(),
|
||||
"evidence_run": identity_projection.get("evidence_run", {}),
|
||||
"items": sorted(items, key=lambda item: item["stable_key"]),
|
||||
"summary": {
|
||||
"total": len(items),
|
||||
"accepted": sum(1 for item in items if item["review_state"] == "accepted"),
|
||||
"needs_review": sum(1 for item in items if item["review_state"] == "needs_review"),
|
||||
"unresolved_ownership": sum(
|
||||
1 for item in items if item["ownership"]["resolution"] == "unresolved"
|
||||
),
|
||||
"ambiguous_containment": sum(
|
||||
1 for item in items if item["containment"]["status"] == "ambiguous"
|
||||
),
|
||||
},
|
||||
}
|
||||
validator = draft202012_validator(repo_root() / "schemas" / "accountability-ownership-review.schema.yaml")
|
||||
errors = sorted(validator.iter_errors(review), key=lambda error: list(error.path))
|
||||
if errors:
|
||||
location = ".".join(str(part) for part in errors[0].path) or "<root>"
|
||||
raise ValueError(f"invalid accountability ownership review at {location}: {errors[0].message}")
|
||||
return review
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AccountabilityEvidenceStore:
|
||||
path: Path
|
||||
@@ -252,6 +304,21 @@ class AccountabilityEvidenceStore:
|
||||
|
||||
create index if not exists idx_accountability_identity_candidates_run
|
||||
on accountability_identity_candidates(run_id);
|
||||
|
||||
create table if not exists accountability_review_decisions (
|
||||
id integer primary key autoincrement,
|
||||
stable_key text not null,
|
||||
decision text not null,
|
||||
reviewer text not null,
|
||||
owner_actor_id text,
|
||||
fabric_id text,
|
||||
subfabric_id text,
|
||||
note text,
|
||||
created_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_accountability_review_decisions_stable_key
|
||||
on accountability_review_decisions(stable_key, id desc);
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -350,6 +417,59 @@ class AccountabilityEvidenceStore:
|
||||
).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def add_review_decision(
|
||||
self,
|
||||
*,
|
||||
stable_key: str,
|
||||
decision: str,
|
||||
reviewer: str,
|
||||
owner_actor_id: str = "",
|
||||
fabric_id: str = "",
|
||||
subfabric_id: str = "",
|
||||
note: str = "",
|
||||
) -> dict[str, Any]:
|
||||
self.init_schema()
|
||||
created_at = _utc_now()
|
||||
with self._connect() as db:
|
||||
cursor = db.execute(
|
||||
"""
|
||||
insert into accountability_review_decisions (
|
||||
stable_key, decision, reviewer, owner_actor_id, fabric_id,
|
||||
subfabric_id, note, created_at
|
||||
) values (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(stable_key, decision, reviewer, owner_actor_id, fabric_id, subfabric_id, note, created_at),
|
||||
)
|
||||
decision_id = int(cursor.lastrowid)
|
||||
return {
|
||||
"id": decision_id,
|
||||
"stable_key": stable_key,
|
||||
"decision": decision,
|
||||
"reviewer": reviewer,
|
||||
"owner_actor_id": owner_actor_id,
|
||||
"fabric_id": fabric_id,
|
||||
"subfabric_id": subfabric_id,
|
||||
"note": note,
|
||||
"created_at": created_at,
|
||||
}
|
||||
|
||||
def latest_review_decisions(self) -> dict[str, dict[str, Any]]:
|
||||
self.init_schema()
|
||||
with self._connect() as db:
|
||||
rows = db.execute(
|
||||
"""
|
||||
select *
|
||||
from accountability_review_decisions
|
||||
where id in (
|
||||
select max(id)
|
||||
from accountability_review_decisions
|
||||
group by stable_key
|
||||
)
|
||||
order by stable_key
|
||||
"""
|
||||
).fetchall()
|
||||
return {row["stable_key"]: dict(row) for row in rows}
|
||||
|
||||
def list_evidence(self, run_id: int) -> list[dict[str, Any]]:
|
||||
with self._connect() as db:
|
||||
rows = db.execute(
|
||||
@@ -491,6 +611,136 @@ def _identity_from_evidence(root: dict[str, Any], item: dict[str, Any]) -> dict[
|
||||
return None
|
||||
|
||||
|
||||
def _ownership_item(
|
||||
candidate: dict[str, Any],
|
||||
actor_roles: dict[str, str],
|
||||
fabric_owners: dict[str, str],
|
||||
fabric_kinds: dict[str, str],
|
||||
decision: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
attributes = candidate.get("attributes") if isinstance(candidate.get("attributes"), dict) else {}
|
||||
blockers: list[str] = []
|
||||
fabric_id = str(candidate.get("fabric_id") or "")
|
||||
subfabric_id = str(candidate.get("subfabric_id") or "")
|
||||
owner_actor_id = str(candidate.get("owner_actor_id") or "")
|
||||
resolution = "explicit" if owner_actor_id else "unresolved"
|
||||
inherited_from = ""
|
||||
|
||||
if attributes.get("ambiguous_aliases"):
|
||||
blockers.append("ambiguous_identity")
|
||||
if attributes.get("ambiguous_owner_actor_ids"):
|
||||
blockers.append("ambiguous_ownership")
|
||||
|
||||
if candidate.get("identity_type") not in {"Actor", "Netkingdom"}:
|
||||
if not fabric_id:
|
||||
blockers.append("containment_unresolved")
|
||||
elif fabric_id not in fabric_kinds:
|
||||
blockers.append("unknown_fabric")
|
||||
if subfabric_id and subfabric_id not in fabric_kinds:
|
||||
blockers.append("unknown_subfabric")
|
||||
|
||||
if not owner_actor_id and subfabric_id and fabric_owners.get(subfabric_id):
|
||||
owner_actor_id = fabric_owners[subfabric_id]
|
||||
resolution = "inherited"
|
||||
inherited_from = subfabric_id
|
||||
if not owner_actor_id and fabric_id and fabric_owners.get(fabric_id):
|
||||
owner_actor_id = fabric_owners[fabric_id]
|
||||
resolution = "inherited"
|
||||
inherited_from = fabric_id
|
||||
if candidate.get("identity_type") == "Actor" and not owner_actor_id:
|
||||
owner_actor_id = str(candidate.get("graph_id") or "")
|
||||
resolution = "explicit" if owner_actor_id else "unresolved"
|
||||
|
||||
decision_payload: dict[str, Any] | None = None
|
||||
if decision:
|
||||
decision_payload = {
|
||||
"decision": decision.get("decision", ""),
|
||||
"reviewer": decision.get("reviewer", ""),
|
||||
"note": decision.get("note", ""),
|
||||
"created_at": decision.get("created_at", ""),
|
||||
}
|
||||
if decision.get("fabric_id"):
|
||||
fabric_id = str(decision["fabric_id"])
|
||||
if decision.get("subfabric_id"):
|
||||
subfabric_id = str(decision["subfabric_id"])
|
||||
if decision.get("owner_actor_id"):
|
||||
owner_actor_id = str(decision["owner_actor_id"])
|
||||
resolution = "review_decision"
|
||||
inherited_from = ""
|
||||
if decision.get("decision") == "accept":
|
||||
blockers = [
|
||||
blocker
|
||||
for blocker in blockers
|
||||
if blocker
|
||||
not in {
|
||||
"ambiguous_identity",
|
||||
"ambiguous_ownership",
|
||||
"containment_unresolved",
|
||||
"unknown_fabric",
|
||||
"unknown_subfabric",
|
||||
"ownership_unresolved",
|
||||
"unknown_owner_actor",
|
||||
}
|
||||
]
|
||||
if candidate.get("identity_type") not in {"Actor", "Netkingdom"}:
|
||||
if not fabric_id:
|
||||
blockers.append("containment_unresolved")
|
||||
elif fabric_id not in fabric_kinds:
|
||||
blockers.append("unknown_fabric")
|
||||
if subfabric_id and subfabric_id not in fabric_kinds:
|
||||
blockers.append("unknown_subfabric")
|
||||
|
||||
if not owner_actor_id:
|
||||
blockers.append("ownership_unresolved")
|
||||
resolution = "unresolved"
|
||||
if owner_actor_id and owner_actor_id not in actor_roles:
|
||||
blockers.append("unknown_owner_actor")
|
||||
|
||||
review_state = "candidate"
|
||||
if blockers:
|
||||
review_state = "needs_review"
|
||||
if decision:
|
||||
if decision.get("decision") == "reject":
|
||||
review_state = "rejected"
|
||||
elif decision.get("decision") == "needs_review":
|
||||
review_state = "needs_review"
|
||||
elif decision.get("decision") == "accept" and owner_actor_id and not blockers:
|
||||
review_state = "accepted"
|
||||
elif decision.get("decision") == "accept":
|
||||
review_state = "needs_review"
|
||||
blockers.append("accepted_without_resolved_owner_or_containment")
|
||||
|
||||
containment_status = "resolved"
|
||||
if "containment_unresolved" in blockers:
|
||||
containment_status = "unresolved"
|
||||
elif "unknown_fabric" in blockers or "unknown_subfabric" in blockers:
|
||||
containment_status = "ambiguous"
|
||||
|
||||
item = {
|
||||
"stable_key": candidate["stable_key"],
|
||||
"identity_type": candidate["identity_type"],
|
||||
"label": candidate["label"],
|
||||
"review_state": review_state,
|
||||
"ownership": {
|
||||
"owner_actor_id": owner_actor_id,
|
||||
"owner_role": actor_roles.get(owner_actor_id, ""),
|
||||
"resolution": resolution,
|
||||
},
|
||||
"containment": {
|
||||
"fabric_id": fabric_id,
|
||||
"subfabric_id": subfabric_id,
|
||||
"status": containment_status,
|
||||
},
|
||||
"blockers": _unique_strings(blockers),
|
||||
"evidence_ids": candidate.get("evidence_ids", []),
|
||||
}
|
||||
if inherited_from:
|
||||
item["ownership"]["inherited_from"] = inherited_from
|
||||
if decision_payload:
|
||||
item["decision"] = decision_payload
|
||||
return item
|
||||
|
||||
|
||||
def _add_identity_candidate(
|
||||
candidates: dict[str, dict[str, Any]],
|
||||
*,
|
||||
@@ -535,6 +785,11 @@ def _add_identity_candidate(
|
||||
existing["aliases"] = _unique_strings([*existing.get("aliases", []), *incoming["aliases"]])
|
||||
existing["evidence_ids"] = _unique_strings([*existing.get("evidence_ids", []), *incoming["evidence_ids"]])
|
||||
existing["attributes"] = {**existing.get("attributes", {}), **incoming["attributes"]}
|
||||
if incoming.get("owner_actor_id") and existing.get("owner_actor_id") and incoming["owner_actor_id"] != existing["owner_actor_id"]:
|
||||
existing["attributes"]["ambiguous_owner_actor_ids"] = _unique_strings(
|
||||
[existing["owner_actor_id"], incoming["owner_actor_id"], *existing["attributes"].get("ambiguous_owner_actor_ids", [])]
|
||||
)
|
||||
existing["review_state"] = "needs_review"
|
||||
for key in ("fabric_id", "subfabric_id", "owner_actor_id", "graph_id"):
|
||||
if incoming.get(key) and not existing.get(key):
|
||||
existing[key] = incoming[key]
|
||||
|
||||
@@ -18,6 +18,7 @@ from .accountability_roots import (
|
||||
DEFAULT_ROOT_MANIFEST_PATH,
|
||||
AccountabilityEvidenceStore,
|
||||
build_identity_projection,
|
||||
build_ownership_review,
|
||||
collect_accountability_root_evidence,
|
||||
load_accountability_root_manifest,
|
||||
)
|
||||
@@ -123,8 +124,22 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
discover_roots.add_argument("--include-remote", action="store_true", help="Allow HTTP reads from configured remote roots.")
|
||||
discover_roots.add_argument("--max-items-per-root", type=int, default=200)
|
||||
discover_roots.add_argument("--identity-projection", action="store_true", help="Print normalized identity candidates instead of raw evidence.")
|
||||
discover_roots.add_argument("--ownership-review", action="store_true", help="Print ownership resolution and review blockers.")
|
||||
discover_roots.add_argument("--store-db", type=Path, default=None, help="Persist evidence and identity candidates in a SQLite store.")
|
||||
|
||||
review_identity = sub.add_parser(
|
||||
"review-identity",
|
||||
help="Persist a review decision for a stable accountability identity candidate.",
|
||||
)
|
||||
review_identity.add_argument("stable_key")
|
||||
review_identity.add_argument("--store-db", type=Path, required=True)
|
||||
review_identity.add_argument("--decision", choices=["accept", "needs_review", "reject"], required=True)
|
||||
review_identity.add_argument("--owner-actor-id", default="")
|
||||
review_identity.add_argument("--fabric-id", default="")
|
||||
review_identity.add_argument("--subfabric-id", default="")
|
||||
review_identity.add_argument("--reviewer", default="operator")
|
||||
review_identity.add_argument("--note", default="")
|
||||
|
||||
registry = sub.add_parser("registry", help="Feed a running Railiance Fabric registry service.")
|
||||
registry_sub = registry.add_subparsers(dest="registry_command", required=True)
|
||||
|
||||
@@ -345,10 +360,26 @@ def main(argv: list[str] | None = None) -> int:
|
||||
max_items_per_root=args.max_items_per_root,
|
||||
)
|
||||
projection = build_identity_projection(payload, manifest)
|
||||
store = AccountabilityEvidenceStore(args.store_db) if args.store_db else None
|
||||
decisions = store.latest_review_decisions() if store else {}
|
||||
ownership_review = build_ownership_review(projection, manifest, review_decisions=decisions)
|
||||
if args.store_db:
|
||||
store = AccountabilityEvidenceStore(args.store_db)
|
||||
store.add_evidence_run(payload, projection)
|
||||
print(json.dumps(projection if args.identity_projection else payload, indent=2, sort_keys=True))
|
||||
output = ownership_review if args.ownership_review else projection if args.identity_projection else payload
|
||||
print(json.dumps(output, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
if args.command == "review-identity":
|
||||
decision = AccountabilityEvidenceStore(args.store_db).add_review_decision(
|
||||
stable_key=args.stable_key,
|
||||
decision=args.decision,
|
||||
reviewer=args.reviewer,
|
||||
owner_actor_id=args.owner_actor_id,
|
||||
fabric_id=args.fabric_id,
|
||||
subfabric_id=args.subfabric_id,
|
||||
note=args.note,
|
||||
)
|
||||
print(json.dumps(decision, indent=2, sort_keys=True))
|
||||
return 0
|
||||
|
||||
if args.command == "registry":
|
||||
|
||||
Reference in New Issue
Block a user