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):
|
||||
|
||||
Reference in New Issue
Block a user