feat(qa): citation→corpus resolution as non-blocking warning (GAP-20, FU-7)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -287,6 +287,50 @@ def check_sequential_numbering(blocks: list[dict]) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def check_citation_resolution(case_id: UUID, decision_id=None) -> dict:
|
||||||
|
"""GAP-20/INV-AUD3: every cited case_law_id must resolve to the corpus.
|
||||||
|
|
||||||
|
Reads case_law_ids from the decision's write_block audit provenance and
|
||||||
|
verifies each resolves. Unresolvable → NON-BLOCKING warning + audit event.
|
||||||
|
"""
|
||||||
|
from legal_mcp.services import audit
|
||||||
|
|
||||||
|
rows = await audit.get_audit_log(case_id=case_id, action="write_block", limit=200)
|
||||||
|
ids = set()
|
||||||
|
for r in rows:
|
||||||
|
details = r.get("details") or {}
|
||||||
|
if isinstance(details, str):
|
||||||
|
try:
|
||||||
|
details = json.loads(details)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
details = {}
|
||||||
|
for raw in (details.get("sources") or {}).get("case_law_ids", []):
|
||||||
|
try:
|
||||||
|
ids.add(UUID(str(raw)))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not ids:
|
||||||
|
return {"name": "citation_resolution", "passed": True, "errors": [], "severity": "warning"}
|
||||||
|
|
||||||
|
res = await db.resolve_citation_case_law_ids(list(ids))
|
||||||
|
if not res["unresolved"]:
|
||||||
|
return {"name": "citation_resolution", "passed": True, "errors": [], "severity": "warning"}
|
||||||
|
|
||||||
|
await audit.log_action_safe(
|
||||||
|
"citation_unresolved", case_id=case_id,
|
||||||
|
details={"unresolved": [str(x) for x in res["unresolved"]]},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"name": "citation_resolution",
|
||||||
|
"passed": False,
|
||||||
|
"severity": "warning",
|
||||||
|
"errors": [
|
||||||
|
f"{len(res['unresolved'])} ציטוטים אינם פתירים לקורפוס — דורש אימות יו\"ר",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── Main validation ───────────────────────────────────────────────
|
# ── Main validation ───────────────────────────────────────────────
|
||||||
|
|
||||||
async def validate_decision(case_id: UUID) -> dict:
|
async def validate_decision(case_id: UUID) -> dict:
|
||||||
@@ -334,6 +378,8 @@ async def validate_decision(case_id: UUID) -> dict:
|
|||||||
check_no_duplication(blocks),
|
check_no_duplication(blocks),
|
||||||
check_sequential_numbering(blocks),
|
check_sequential_numbering(blocks),
|
||||||
])
|
])
|
||||||
|
# Async, non-blocking warning: citation→corpus resolution (GAP-20/INV-AUD3)
|
||||||
|
results.append(await check_citation_resolution(case_id, decision["id"]))
|
||||||
|
|
||||||
critical_failures = sum(1 for r in results if not r["passed"] and r["severity"] == "critical")
|
critical_failures = sum(1 for r in results if not r["passed"] and r["severity"] == "critical")
|
||||||
all_passed = all(r["passed"] for r in results)
|
all_passed = all(r["passed"] for r in results)
|
||||||
|
|||||||
Reference in New Issue
Block a user