generated from coulomb/repo-seed
feat: compare accountability update deltas
This commit is contained in:
@@ -246,6 +246,105 @@ def build_ownership_review(
|
||||
return review
|
||||
|
||||
|
||||
def build_update_delta(
|
||||
current_identity_projection: dict[str, Any],
|
||||
current_ownership_review: dict[str, Any],
|
||||
*,
|
||||
previous_identity_projection: dict[str, Any] | None = None,
|
||||
previous_ownership_review: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
previous_identity_projection = previous_identity_projection or {}
|
||||
previous_ownership_review = previous_ownership_review or {}
|
||||
current_nodes = {
|
||||
item["stable_key"]: item
|
||||
for item in current_identity_projection.get("identity_candidates", [])
|
||||
if isinstance(item, dict) and item.get("stable_key")
|
||||
}
|
||||
previous_nodes = {
|
||||
item["stable_key"]: item
|
||||
for item in previous_identity_projection.get("identity_candidates", [])
|
||||
if isinstance(item, dict) and item.get("stable_key")
|
||||
}
|
||||
current_edges = {
|
||||
item["id"]: item
|
||||
for item in current_identity_projection.get("candidate_graph", {}).get("edges", [])
|
||||
if isinstance(item, dict) and item.get("id")
|
||||
}
|
||||
previous_edges = {
|
||||
item["id"]: item
|
||||
for item in previous_identity_projection.get("candidate_graph", {}).get("edges", [])
|
||||
if isinstance(item, dict) and item.get("id")
|
||||
}
|
||||
current_review = {
|
||||
item["stable_key"]: item
|
||||
for item in current_ownership_review.get("items", [])
|
||||
if isinstance(item, dict) and item.get("stable_key")
|
||||
}
|
||||
previous_review = {
|
||||
item["stable_key"]: item
|
||||
for item in previous_ownership_review.get("items", [])
|
||||
if isinstance(item, dict) and item.get("stable_key")
|
||||
}
|
||||
|
||||
node_delta = _delta_sets(previous_nodes, current_nodes)
|
||||
edge_delta = _delta_sets(previous_edges, current_edges)
|
||||
ownership_changes = _field_changes(previous_review, current_review, "ownership")
|
||||
containment_changes = _field_changes(previous_review, current_review, "containment")
|
||||
review_state_changes = [
|
||||
key
|
||||
for key in sorted(set(previous_review) & set(current_review))
|
||||
if previous_review[key].get("review_state") != current_review[key].get("review_state")
|
||||
]
|
||||
blocker_changes = _field_changes(previous_review, current_review, "blockers")
|
||||
meaningful_changes = _unique_strings(
|
||||
[
|
||||
*node_delta["added"],
|
||||
*node_delta["changed"],
|
||||
*node_delta["removed"],
|
||||
*edge_delta["added"],
|
||||
*edge_delta["changed"],
|
||||
*edge_delta["removed"],
|
||||
*ownership_changes,
|
||||
*containment_changes,
|
||||
*review_state_changes,
|
||||
*blocker_changes,
|
||||
]
|
||||
)
|
||||
delta = {
|
||||
"apiVersion": "railiance.fabric/v1alpha2",
|
||||
"kind": "AccountabilityUpdateDelta",
|
||||
"generated_at": _utc_now(),
|
||||
"current": current_identity_projection.get("evidence_run", {}),
|
||||
"previous": previous_identity_projection.get("evidence_run", {}),
|
||||
"node_delta": node_delta,
|
||||
"edge_delta": edge_delta,
|
||||
"change_sets": {
|
||||
"ownership": ownership_changes,
|
||||
"containment": containment_changes,
|
||||
"review_state": review_state_changes,
|
||||
"blockers": blocker_changes,
|
||||
},
|
||||
"summary": {
|
||||
"nodes_added": len(node_delta["added"]),
|
||||
"nodes_changed": len(node_delta["changed"]),
|
||||
"nodes_removed": len(node_delta["removed"]),
|
||||
"nodes_unchanged": len(node_delta["unchanged"]),
|
||||
"edges_added": len(edge_delta["added"]),
|
||||
"edges_changed": len(edge_delta["changed"]),
|
||||
"edges_removed": len(edge_delta["removed"]),
|
||||
"edges_unchanged": len(edge_delta["unchanged"]),
|
||||
"meaningful_change_count": len(meaningful_changes),
|
||||
"promotion_needed": bool(meaningful_changes),
|
||||
},
|
||||
}
|
||||
validator = draft202012_validator(repo_root() / "schemas" / "accountability-update-delta.schema.yaml")
|
||||
errors = sorted(validator.iter_errors(delta), 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 update delta at {location}: {errors[0].message}")
|
||||
return delta
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AccountabilityEvidenceStore:
|
||||
path: Path
|
||||
@@ -741,6 +840,44 @@ def _ownership_item(
|
||||
return item
|
||||
|
||||
|
||||
def _delta_sets(previous: dict[str, dict[str, Any]], current: dict[str, dict[str, Any]]) -> dict[str, list[str]]:
|
||||
previous_keys = set(previous)
|
||||
current_keys = set(current)
|
||||
common = previous_keys & current_keys
|
||||
changed = [
|
||||
key
|
||||
for key in sorted(common)
|
||||
if short_fingerprint(_stable_payload(previous[key]), length=16)
|
||||
!= short_fingerprint(_stable_payload(current[key]), length=16)
|
||||
]
|
||||
return {
|
||||
"added": sorted(current_keys - previous_keys),
|
||||
"changed": changed,
|
||||
"removed": sorted(previous_keys - current_keys),
|
||||
"unchanged": sorted(common - set(changed)),
|
||||
}
|
||||
|
||||
|
||||
def _field_changes(
|
||||
previous: dict[str, dict[str, Any]],
|
||||
current: dict[str, dict[str, Any]],
|
||||
field: str,
|
||||
) -> list[str]:
|
||||
return [
|
||||
key
|
||||
for key in sorted(set(previous) & set(current))
|
||||
if previous[key].get(field) != current[key].get(field)
|
||||
]
|
||||
|
||||
|
||||
def _stable_payload(value: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
key: data
|
||||
for key, data in value.items()
|
||||
if key not in {"generated_at", "decision"}
|
||||
}
|
||||
|
||||
|
||||
def _add_identity_candidate(
|
||||
candidates: dict[str, dict[str, Any]],
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user