fix(#88+#87): סנכרון DB↔file אוטומטי + claims_coverage מבחין כתב-ערר מתכתובת
#88 (DB↔file, lessons #35): drafts/decision.md דרסה את עצמה רק ב-save_block_content; renumber_all_blocks + נתיבי store_block אחרים השאירו את הקובץ stale → QA נכשל פעמיים על אותה בעיה (CMPA-62). תיקון: _update_draft_file הפך ל-hook אוטומטי (מקבל decision_id, מאתר case פנימית) שנקרא מ-store_block (כל persist) ומ- renumber_all_blocks. legal-qa ממילא קורא מ-DB → שני הצדדים זהים תמיד. #87 (claims_coverage, 1033-25): טענות מתכתובת (claim_type='reply' — תגובה/ השלמת-טיעון) סומנו "לא נענו" כ-false-positive. תיקון: check_claims_coverage דורש מענה רק לטענות כתב-הערר (claim_type='claim', appellant); reply/תכתובת מוחרגות. בקבלה מלאה הסף מוקל (0.2→0.4) כי העורר זכה במלואו. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,7 +104,7 @@ CLAIMS_CHECK_PROMPT = """אתה בודק איכות החלטות משפטיות.
|
||||
"""
|
||||
|
||||
|
||||
async def check_claims_coverage(blocks: list[dict], claims: list[dict]) -> dict:
|
||||
async def check_claims_coverage(blocks: list[dict], claims: list[dict], outcome: str = "") -> dict:
|
||||
"""בדיקה סמנטית (Claude) שכל טענה נענתה בדיון."""
|
||||
yod = next((b for b in blocks if b["block_id"] == "block-yod"), None)
|
||||
if not yod or not yod.get("content"):
|
||||
@@ -114,16 +114,26 @@ async def check_claims_coverage(blocks: list[dict], claims: list[dict]) -> dict:
|
||||
if not claims:
|
||||
return {"name": "claims_coverage", "passed": True, "errors": [], "severity": "critical"}
|
||||
|
||||
# Filter: only APPELLANT claims from original pleadings.
|
||||
# Committee/permit_applicant claims are defensive positions, not claims
|
||||
# that need to be "addressed" in the discussion.
|
||||
# #87/GAP-87 — only the appellant's claims from the APPEAL PLEADING itself
|
||||
# must be addressed. claim_type: 'claim'=כתב ערר (mandatory), 'response'=כתב
|
||||
# תשובה, 'reply'=תגובה/השלמת-טיעון/תכתובת (supplementary correspondence — NOT
|
||||
# a standalone duty to answer, especially on full acceptance). Counting reply/
|
||||
# correspondence claims as "unanswered" produced false QA fails (1033-25).
|
||||
source_claims = [
|
||||
c for c in claims
|
||||
if c.get("source_document", "") != "block-zayin"
|
||||
and c.get("party_role") in ("appellant", "respondent")
|
||||
and c.get("claim_type") == "claim"
|
||||
and c.get("party_role") == "appellant"
|
||||
]
|
||||
if not source_claims:
|
||||
# Fallback: all non-block-zayin claims
|
||||
# Fallback: appellant/respondent pleadings, excluding supplementary replies.
|
||||
source_claims = [
|
||||
c for c in claims
|
||||
if c.get("source_document", "") != "block-zayin"
|
||||
and c.get("claim_type") != "reply"
|
||||
and c.get("party_role") in ("appellant", "respondent")
|
||||
]
|
||||
if not source_claims:
|
||||
source_claims = [c for c in claims if c.get("source_document", "") != "block-zayin"]
|
||||
if not source_claims:
|
||||
source_claims = claims
|
||||
@@ -165,9 +175,14 @@ async def check_claims_coverage(blocks: list[dict], claims: list[dict]) -> dict:
|
||||
total = len(source_claims)
|
||||
covered = len(addressed) + len(partial)
|
||||
|
||||
# On full acceptance the appellant prevailed in full — not every sub-claim
|
||||
# needs individual treatment (the chair noted this for correspondence claims,
|
||||
# 1033-25). Relax the missing-tolerance accordingly.
|
||||
allowed_missing_ratio = 0.4 if outcome == "full_acceptance" else 0.2
|
||||
|
||||
return {
|
||||
"name": "claims_coverage",
|
||||
"passed": len(missing) <= total * 0.2, # Allow up to 20% missing
|
||||
"passed": len(missing) <= total * allowed_missing_ratio,
|
||||
"errors": errors,
|
||||
"severity": "critical",
|
||||
"details": f"{covered}/{total} טענות נענו ({covered/total*100:.0f}%), {len(partial)} חלקית, {len(missing)} חסרות",
|
||||
@@ -361,8 +376,10 @@ async def validate_decision(case_id: UUID) -> dict:
|
||||
# Get claims
|
||||
claims = await db.get_claims(case_id)
|
||||
|
||||
# Determine appeal type
|
||||
# Determine appeal type + outcome (outcome relaxes claims coverage on full acceptance — #87)
|
||||
appeal_type = case.get("appeal_type", "licensing")
|
||||
from legal_mcp.services.lessons import canonical_outcome
|
||||
outcome = canonical_outcome(decision.get("outcome", "") or "")
|
||||
|
||||
# Run all checks
|
||||
# Run sync checks
|
||||
@@ -370,7 +387,7 @@ async def validate_decision(case_id: UUID) -> dict:
|
||||
check_neutral_background(blocks),
|
||||
]
|
||||
# Async check: claims coverage with Claude
|
||||
results.append(await check_claims_coverage(blocks, claims))
|
||||
results.append(await check_claims_coverage(blocks, claims, outcome))
|
||||
# More sync checks
|
||||
results.extend([
|
||||
check_weight_compliance(blocks, appeal_type),
|
||||
|
||||
Reference in New Issue
Block a user