Expose review decision audit metadata

This commit is contained in:
2026-05-15 16:16:05 +02:00
parent 43e7f7138f
commit 5c2262bcf2
7 changed files with 140 additions and 2 deletions

View File

@@ -52,6 +52,111 @@ class ReviewDecision:
action: str
notes: str
created_at: str
reviewer_type: str = "unknown"
reviewer_id: str = ""
policy_version: str = ""
criteria_version: str = ""
criterion_ids: list[str] = field(default_factory=list)
evidence_refs: list[str] = field(default_factory=list)
rationale: str = ""
accepted_after_edits: bool = False
decision_kind: str = "other"
def enrich_review_decision(decision: ReviewDecision) -> ReviewDecision:
fields = review_decision_audit_fields(decision.action, decision.notes)
return replace_review_decision(decision, **fields)
def replace_review_decision(
decision: ReviewDecision,
**fields: object,
) -> ReviewDecision:
data = {
"id": decision.id,
"repository_id": decision.repository_id,
"analysis_run_id": decision.analysis_run_id,
"action": decision.action,
"notes": decision.notes,
"created_at": decision.created_at,
"reviewer_type": decision.reviewer_type,
"reviewer_id": decision.reviewer_id,
"policy_version": decision.policy_version,
"criteria_version": decision.criteria_version,
"criterion_ids": decision.criterion_ids,
"evidence_refs": decision.evidence_refs,
"rationale": decision.rationale,
"accepted_after_edits": decision.accepted_after_edits,
"decision_kind": decision.decision_kind,
}
data.update(fields)
return ReviewDecision(**data)
def review_decision_audit_fields(action: str, notes: str) -> dict[str, object]:
parsed = _parse_review_decision_notes(notes)
return {
"reviewer_type": _reviewer_type(action),
"reviewer_id": parsed.get("reviewer", ""),
"policy_version": parsed.get("policy_version", ""),
"criteria_version": parsed.get("criteria_version", ""),
"criterion_ids": _split_audit_list(parsed.get("criteria", "")),
"evidence_refs": _split_audit_list(parsed.get("evidence", "")),
"rationale": parsed.get("rationale", ""),
"accepted_after_edits": action.endswith("_with_edits")
or action == "agentic_approve_with_edits"
or bool(parsed.get("proposed_changes")),
"decision_kind": _decision_kind(action),
}
def _parse_review_decision_notes(notes: str) -> dict[str, str]:
parsed: dict[str, str] = {}
for part in notes.split(";"):
key, separator, value = part.strip().partition("=")
if separator and key:
parsed[key] = value.strip()
return parsed
def _split_audit_list(value: str) -> list[str]:
if not value or value == "none":
return []
return [item.strip() for item in value.split(",") if item.strip()]
def _reviewer_type(action: str) -> str:
if action.startswith("agentic_"):
return "agent"
if action == "trusted_auto_approve_candidate_graph":
return "migration"
if action.startswith("quality_gate_"):
return "deterministic-gate"
if action.startswith("approve") or action.startswith("accept"):
return "human"
if action.startswith("reject") or action.startswith("edit") or action.startswith("merge"):
return "human"
if action.startswith("relink"):
return "human"
return "migration" if action.startswith("llm_extraction") else "unknown"
def _decision_kind(action: str) -> str:
if "approve_with_edits" in action:
return "accepted_after_edits"
if "approve" in action or action.startswith("accept"):
return "accepted_as_is"
if "reject" in action:
return "rejected"
if "downgrade" in action:
return "downgraded"
if "request_human_review" in action:
return "needs_human"
if "propose_edit" in action:
return "proposed_edit"
if "relink" in action:
return "relinked"
return "other"
@dataclass(frozen=True)

View File

@@ -40,6 +40,7 @@ from repo_registry.core.models import (
ReviewDecision,
ScanSummary,
SearchResult,
enrich_review_decision,
)
from repo_registry.candidate_graph.generator import CandidateGraphGenerator
from repo_registry.candidate_graph.normalization import normalize_candidate_drafts
@@ -355,7 +356,13 @@ class RegistryService:
repository_id: int,
analysis_run_id: int | None = None,
) -> list[ReviewDecision]:
return self.store.list_review_decisions(repository_id, analysis_run_id)
return [
enrich_review_decision(decision)
for decision in self.store.list_review_decisions(
repository_id,
analysis_run_id,
)
]
def record_expectation_gap(
self,

View File

@@ -513,6 +513,15 @@ class ReviewDecisionResponse(BaseModel):
action: str
notes: str
created_at: str
reviewer_type: str = "unknown"
reviewer_id: str = ""
policy_version: str = ""
criteria_version: str = ""
criterion_ids: list[str] = Field(default_factory=list)
evidence_refs: list[str] = Field(default_factory=list)
rationale: str = ""
accepted_after_edits: bool = False
decision_kind: str = "other"
class QualityCriterionResponse(BaseModel):