generated from coulomb/repo-seed
Add quality gate override flow
This commit is contained in:
@@ -126,6 +126,8 @@ def _split_audit_list(value: str) -> list[str]:
|
||||
|
||||
|
||||
def _reviewer_type(action: str) -> str:
|
||||
if action == "quality_gate_override":
|
||||
return "human"
|
||||
if action.startswith("agentic_"):
|
||||
return "agent"
|
||||
if action == "trusted_auto_approve_candidate_graph":
|
||||
@@ -152,6 +154,8 @@ def _decision_kind(action: str) -> str:
|
||||
return "downgraded"
|
||||
if "request_human_review" in action:
|
||||
return "needs_human"
|
||||
if "override" in action:
|
||||
return "override"
|
||||
if "propose_edit" in action:
|
||||
return "proposed_edit"
|
||||
if "relink" in action:
|
||||
|
||||
@@ -313,6 +313,37 @@ class RegistryService:
|
||||
"review without approving registry truth."
|
||||
)
|
||||
|
||||
def record_quality_gate_override(
|
||||
self,
|
||||
repository_id: int,
|
||||
analysis_run_id: int,
|
||||
*,
|
||||
criterion_id: str,
|
||||
element_type: str,
|
||||
element_id: int,
|
||||
reason: str,
|
||||
notes: str = "",
|
||||
) -> ReviewDecision:
|
||||
if not reason.strip():
|
||||
raise ValueError("quality gate override requires a reason")
|
||||
decision_id = self.store.create_review_decision(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
action="quality_gate_override",
|
||||
notes=(
|
||||
f"criteria_version={active_quality_criteria_version()}; "
|
||||
f"criteria={criterion_id}; "
|
||||
f"target={element_type}:{element_id}; "
|
||||
f"rationale={reason.strip()}; "
|
||||
f"notes={notes.strip()}"
|
||||
).strip(),
|
||||
)
|
||||
return next(
|
||||
decision
|
||||
for decision in self.list_review_decisions(repository_id, analysis_run_id)
|
||||
if decision.id == decision_id
|
||||
)
|
||||
|
||||
def _generate_candidates(
|
||||
self,
|
||||
repository: Repository,
|
||||
|
||||
@@ -65,6 +65,7 @@ from repo_registry.web_api.schemas import (
|
||||
IdResponse,
|
||||
ObservedFactResponse,
|
||||
QualityCriteriaRegistryResponse,
|
||||
QualityGateOverrideCreate,
|
||||
RepositoryAbilityMapResponse,
|
||||
RepositoryComparisonResponse,
|
||||
RepositoryCreate,
|
||||
@@ -471,6 +472,29 @@ def list_analysis_run_review_decisions(
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@app.post(
|
||||
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/quality-gate-overrides",
|
||||
tags=["review"],
|
||||
response_model=ReviewDecisionResponse,
|
||||
)
|
||||
def create_quality_gate_override(
|
||||
repository_id: int,
|
||||
analysis_run_id: int,
|
||||
payload: QualityGateOverrideCreate,
|
||||
service: RegistryService = Depends(get_service),
|
||||
) -> dict[str, object]:
|
||||
try:
|
||||
return asdict(
|
||||
service.record_quality_gate_override(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
**payload.model_dump(),
|
||||
)
|
||||
)
|
||||
except (NotFoundError, ValueError) as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@app.get(
|
||||
"/repos/{repository_id}/observed-facts",
|
||||
tags=["analysis"],
|
||||
|
||||
@@ -566,6 +566,14 @@ class QualityCriteriaRegistryResponse(BaseModel):
|
||||
criteria: list[QualityCriterionResponse]
|
||||
|
||||
|
||||
class QualityGateOverrideCreate(BaseModel):
|
||||
criterion_id: str
|
||||
element_type: str
|
||||
element_id: int
|
||||
reason: str
|
||||
notes: str = ""
|
||||
|
||||
|
||||
class ObservedFactResponse(BaseModel):
|
||||
id: int
|
||||
repository_id: int
|
||||
|
||||
@@ -9,6 +9,10 @@ from urllib.parse import quote_plus, urlparse
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Query
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse
|
||||
|
||||
from repo_registry.acceptance import (
|
||||
evaluate_candidate_graph_quality,
|
||||
quality_gate_outcome_dicts,
|
||||
)
|
||||
from repo_registry.core.service import RegistryService
|
||||
from repo_registry.self_scoping.comparison import compare_assessment_to_golden
|
||||
from repo_registry.self_scoping.review_store import (
|
||||
@@ -2018,6 +2022,10 @@ def analysis_run_detail(
|
||||
display_name = repository_display_name(repository)
|
||||
candidate_graph = service.candidate_graph(repository_id, analysis_run_id)
|
||||
candidate_graph_data = asdict(candidate_graph)
|
||||
quality_gate_outcomes = quality_gate_outcome_dicts(
|
||||
evaluate_candidate_graph_quality(candidate_graph)
|
||||
)
|
||||
candidate_graph_data["quality_gate_outcomes"] = quality_gate_outcomes
|
||||
facts = service.list_observed_facts(repository_id, analysis_run_id)
|
||||
chunks = service.list_content_chunks(repository_id, analysis_run_id)
|
||||
decisions = service.list_review_decisions(repository_id, analysis_run_id)
|
||||
@@ -2070,6 +2078,12 @@ def analysis_run_detail(
|
||||
</form>
|
||||
</div>
|
||||
{render_candidate_graph(candidate_graph_data, repository_id, analysis_run_id)}
|
||||
<h2>Quality Gate Outcomes</h2>
|
||||
{render_quality_gate_outcomes(
|
||||
quality_gate_outcomes,
|
||||
repository_id=repository_id,
|
||||
analysis_run_id=analysis_run_id,
|
||||
)}
|
||||
</section>
|
||||
<section class="panel">
|
||||
<div class="actions">
|
||||
@@ -2139,6 +2153,32 @@ def create_expectation_gap_from_form(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}/quality-gate-overrides")
|
||||
def create_quality_gate_override_from_form(
|
||||
repository_id: int,
|
||||
analysis_run_id: int,
|
||||
criterion_id: str = Form(...),
|
||||
element_type: str = Form(...),
|
||||
element_id: int = Form(...),
|
||||
reason: str = Form(...),
|
||||
notes: str = Form(""),
|
||||
service: RegistryService = Depends(get_service),
|
||||
) -> RedirectResponse:
|
||||
service.record_quality_gate_override(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
criterion_id=criterion_id,
|
||||
element_type=element_type,
|
||||
element_id=element_id,
|
||||
reason=reason,
|
||||
notes=notes,
|
||||
)
|
||||
return RedirectResponse(
|
||||
f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}",
|
||||
status_code=303,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/ui/repos/{repository_id}/expectation-gaps")
|
||||
def create_repository_expectation_gap_from_form(
|
||||
repository_id: int,
|
||||
@@ -4731,6 +4771,45 @@ def render_review_decisions(decisions: list) -> str:
|
||||
"""
|
||||
|
||||
|
||||
def render_quality_gate_outcomes(
|
||||
outcomes: list[dict],
|
||||
*,
|
||||
repository_id: int,
|
||||
analysis_run_id: int,
|
||||
) -> str:
|
||||
if not outcomes:
|
||||
return '<p class="muted">No quality gates fired for this run.</p>'
|
||||
rows = "\n".join(
|
||||
f"""
|
||||
<tr>
|
||||
<td><span class="pill">{escape(outcome["criterion_id"])}</span></td>
|
||||
<td>{escape(outcome["outcome"])}</td>
|
||||
<td>{escape(outcome["element_type"])} #{outcome["element_id"]}</td>
|
||||
<td>{escape(outcome["reason"])}</td>
|
||||
<td>
|
||||
<form class="stack" method="post" action="/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}/quality-gate-overrides">
|
||||
<input type="hidden" name="criterion_id" value="{escape(outcome["criterion_id"])}">
|
||||
<input type="hidden" name="element_type" value="{escape(outcome["element_type"])}">
|
||||
<input type="hidden" name="element_id" value="{outcome["element_id"]}">
|
||||
<input name="reason" required placeholder="Override reason">
|
||||
<input name="notes" placeholder="Optional notes">
|
||||
<button class="secondary" type="submit">Override</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
for outcome in outcomes
|
||||
)
|
||||
return f"""
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Criterion</th><th>Outcome</th><th>Element</th><th>Reason</th><th>Override</th></tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</table>
|
||||
"""
|
||||
|
||||
|
||||
def render_expectation_gap_form(
|
||||
*,
|
||||
action: str,
|
||||
|
||||
Reference in New Issue
Block a user