From 5c2262bcf2fe686b4976f8679003bcf9508e68b3 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 15 May 2026 16:16:05 +0200 Subject: [PATCH] Expose review decision audit metadata --- src/repo_registry/core/models.py | 105 ++++++++++++++++++ src/repo_registry/core/service.py | 9 +- src/repo_registry/web_api/schemas.py | 9 ++ tests/test_agentic_review.py | 7 ++ tests/test_self_scoping_assessment_export.py | 2 + tests/test_web_api.py | 2 + ...-0014-agentic-characteristic-acceptance.md | 8 +- 7 files changed, 140 insertions(+), 2 deletions(-) diff --git a/src/repo_registry/core/models.py b/src/repo_registry/core/models.py index b0cdf73..37603e9 100644 --- a/src/repo_registry/core/models.py +++ b/src/repo_registry/core/models.py @@ -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) diff --git a/src/repo_registry/core/service.py b/src/repo_registry/core/service.py index 59253fd..4de935d 100644 --- a/src/repo_registry/core/service.py +++ b/src/repo_registry/core/service.py @@ -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, diff --git a/src/repo_registry/web_api/schemas.py b/src/repo_registry/web_api/schemas.py index d3c6941..a88dc95 100644 --- a/src/repo_registry/web_api/schemas.py +++ b/src/repo_registry/web_api/schemas.py @@ -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): diff --git a/tests/test_agentic_review.py b/tests/test_agentic_review.py index ce87f15..d6c9224 100644 --- a/tests/test_agentic_review.py +++ b/tests/test_agentic_review.py @@ -125,6 +125,13 @@ def test_agentic_reviewer_can_approve_candidate_graph_with_rationale(tmp_path): assert ability_map.abilities assert graph.abilities[0].status == "approved" assert decisions[1].action == "agentic_approve_candidate_graph" + assert decisions[1].reviewer_type == "agent" + assert decisions[1].reviewer_id == "approving-agent" + assert decisions[1].policy_version == "agentic-review-policy/test" + assert decisions[1].criteria_version == "repo-scoping-quality-criteria/v1" + assert decisions[1].criterion_ids == ["RREG-QC-004"] + assert decisions[1].evidence_refs == ["README.md", "app.py"] + assert decisions[1].decision_kind == "accepted_as_is" assert "rationale=API source and README support" in decisions[1].notes assert "criteria=RREG-QC-004" in decisions[1].notes assert "evidence=README.md, app.py" in decisions[1].notes diff --git a/tests/test_self_scoping_assessment_export.py b/tests/test_self_scoping_assessment_export.py index 4068092..93ea7de 100644 --- a/tests/test_self_scoping_assessment_export.py +++ b/tests/test_self_scoping_assessment_export.py @@ -129,3 +129,5 @@ def test_export_assessment_review_decisions_include_quality_criteria_version(tmp assert artifact["review_decisions"][0]["quality_criteria_version"] == ( "repo-scoping-quality-criteria/v1" ) + assert artifact["review_decisions"][0]["reviewer_type"] == "human" + assert artifact["review_decisions"][0]["decision_kind"] == "accepted_as_is" diff --git a/tests/test_web_api.py b/tests/test_web_api.py index 616aa14..79a60fb 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -1038,6 +1038,8 @@ def test_api_analysis_run_loop(tmp_path): decisions_response = client.get(f"/repos/{repository_id}/review-decisions") assert decisions_response.status_code == 200 assert decisions_response.json()[0]["action"] == "reject_candidate_ability" + assert decisions_response.json()[0]["reviewer_type"] == "human" + assert decisions_response.json()[0]["decision_kind"] == "rejected" run_decisions_response = client.get( f"/repos/{repository_id}/analysis-runs/" f"{run['analysis_run']['id']}/review-decisions" diff --git a/workplans/RREG-WP-0014-agentic-characteristic-acceptance.md b/workplans/RREG-WP-0014-agentic-characteristic-acceptance.md index 1b6e8b2..4b9c10d 100644 --- a/workplans/RREG-WP-0014-agentic-characteristic-acceptance.md +++ b/workplans/RREG-WP-0014-agentic-characteristic-acceptance.md @@ -175,7 +175,7 @@ validated for rationale, criteria IDs, and evidence refs before being applied. ```task id: RREG-WP-0014-T05 -status: todo +status: in_progress priority: high state_hub_task_id: "0d12559a-831e-40ff-bf82-85f45b763f07" ``` @@ -193,6 +193,12 @@ Acceptance criteria: edits/relinks". - Existing decisions remain readable through a migration or compatibility view. +Implementation note 2026-05-15: started the audit-trail compatibility view by +enriching listed review decisions with derived reviewer type, reviewer identity, +policy version, criteria version, rationale, criterion IDs, evidence refs, +accepted-after-edits marker, and decision kind. Existing stored decisions still +use the same table and remain readable. + ## T06: Add Human Override And Criteria Refinement Flow ```task