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:
2026-06-06 20:54:31 +00:00
parent afc1548bca
commit a571ad535b
3 changed files with 54 additions and 26 deletions

View File

@@ -1088,37 +1088,39 @@ async def save_block_content(case_id: UUID, block_id: str, content: str) -> dict
result["generation_type"] = "claude-code"
result["model_used"] = "claude-code"
await store_block(UUID(decision["id"]), result)
await store_block(UUID(decision["id"]), result) # store_block syncs the file (#35)
await db.mark_blocks_stale(case_id, False)
# Also write/update the draft file on disk
await _update_draft_file(case_id, UUID(decision["id"]))
return result
async def _update_draft_file(case_id: UUID, decision_id: UUID) -> None:
"""Rebuild drafts/decision.md from all blocks in DB."""
from pathlib import Path
case = await db.get_case(case_id)
if not case:
return
case_dir = config.find_case_dir(case["case_number"])
draft_dir = case_dir / "drafts"
draft_dir.mkdir(parents=True, exist_ok=True)
async def _update_draft_file(decision_id: UUID) -> None:
"""Rebuild drafts/decision.md from all blocks in DB — the single
regenerate-draft hook (lessons #35 / GAP-88). Called after EVERY
decision_blocks mutation (store_block, renumber) so the on-disk file never
drifts from the DB. legal-qa validates against the DB; export and the chair
read the file — keeping them identical kills the "QA fails twice on the same
already-fixed issue" loop (CMPA-62). Resolves case from decision_id so no
caller has to thread case_id through."""
pool = await db.get_pool()
async with pool.acquire() as conn:
case_row = await conn.fetchrow(
"SELECT c.case_number FROM decisions d JOIN cases c ON c.id = d.case_id "
"WHERE d.id = $1",
decision_id,
)
if not case_row:
return
rows = await conn.fetch(
"SELECT content FROM decision_blocks WHERE decision_id = $1 AND content != '' ORDER BY block_index",
decision_id,
)
draft_dir = config.find_case_dir(case_row["case_number"]) / "drafts"
draft_dir.mkdir(parents=True, exist_ok=True)
draft_path = draft_dir / "decision.md"
draft_path.write_text("\n\n".join(row["content"] for row in rows if row["content"]), encoding="utf-8")
logger.info("Draft file updated: %s (%d blocks)", draft_path, len(rows))
logger.info("Draft file synced: %s (%d blocks)", draft_path, len(rows))
# ── Renumbering ───────────────────────────────────────────────────
@@ -1172,6 +1174,11 @@ async def renumber_all_blocks(decision_id: UUID) -> dict:
)
updated += 1
# #35 — renumber mutates content via raw UPDATE (bypasses store_block), so
# sync the draft file here too, otherwise the file keeps stale numbering.
if updated:
await _update_draft_file(decision_id)
return {"total_paragraphs": current_num - 1, "blocks_updated": updated}
@@ -1204,6 +1211,9 @@ async def store_block(decision_id: UUID, block_result: dict) -> None:
block_result["model_used"],
block_result["temperature"],
)
# #35 — regenerate the on-disk draft on every persist so DB and file stay
# identical (legal-qa reads DB; export/chair read the file).
await _update_draft_file(decision_id)
async def write_and_store_block(