generated from coulomb/repo-seed
Expose review decision audit metadata
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user