fix(qa): enforce critical-QA gate on export + fix neutral_background critical-but-passed (GAP-15/16, INV-QA3/EX3)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 17:58:50 +00:00
parent 1473bdf3c2
commit 084b31cd9b
4 changed files with 203 additions and 1 deletions

View File

@@ -1509,6 +1509,37 @@ async def get_decision_by_case(case_id: UUID) -> dict | None:
return d
async def get_critical_qa_failures(case_id: UUID) -> list[dict]:
"""Return critical-severity failures from the case's latest QA run.
``qa_results`` is cleared+rewritten per ``validate_decision`` run, so the
current rows for a ``case_id`` ARE the latest run. Returns rows where
``severity='critical' AND passed=false``. Callers distinguish "no QA run
yet" (no rows at all) via ``qa_run_exists`` below.
"""
pool = await get_pool()
async with pool.acquire() as conn:
rows = await conn.fetch(
"""SELECT check_name, severity, passed, errors
FROM qa_results
WHERE case_id = $1 AND severity = 'critical' AND passed = false
ORDER BY check_name""",
case_id,
)
return [dict(r) for r in rows]
async def qa_run_exists(case_id: UUID) -> bool:
"""True if a QA run has ever been recorded for this case (any rows)."""
pool = await get_pool()
async with pool.acquire() as conn:
n = await conn.fetchval(
"SELECT count(*) FROM qa_results WHERE case_id = $1",
case_id,
)
return bool(n)
async def update_decision(decision_id: UUID, **fields) -> None:
if not fields:
return

View File

@@ -67,7 +67,7 @@ def check_neutral_background(blocks: list[dict]) -> dict:
"""בדיקת ניטרליות בלוק הרקע (ו)."""
vav = next((b for b in blocks if b["block_id"] == "block-vav"), None)
if not vav or not vav.get("content"):
return {"name": "neutral_background", "passed": True, "errors": [], "severity": "critical"}
return {"name": "neutral_background", "passed": True, "errors": [], "severity": "warning"}
errors = []
lines = vav["content"].split("\n")

View File

@@ -399,6 +399,26 @@ async def export_docx(case_number: str, output_path: str = "") -> str:
case_id = UUID(case["id"])
# INV-EX3 / INV-QA3: a decision cannot be exported while critical QA gates
# fail (or before QA has been run at all). Gate on the STORED qa_results —
# cheap SELECT, no LLM re-run.
if not await db.qa_run_exists(case_id):
return json.dumps({
"status": "error",
"message": "ייצוא נחסם: בקרת איכות (QA) טרם רצה על התיק. "
"הרץ validate_decision לפני ייצוא.",
}, ensure_ascii=False, indent=2)
critical = await db.get_critical_qa_failures(case_id)
if critical:
gate_names = ", ".join(r["check_name"] for r in critical)
return json.dumps({
"status": "error",
"message": f"ייצוא נחסם: שערי QA קריטיים נכשלו ({gate_names}). "
f"תקן את הליקויים והרץ validate_decision מחדש לפני ייצוא.",
"failed_gates": [r["check_name"] for r in critical],
}, ensure_ascii=False, indent=2)
try:
path = await docx_exporter.export_decision(case_id, output_path or None)
# Register this export as the new source of truth