feat(plans): מרשם-תכניות קנוני (V38) + נוסח-ציטוט אחיד דטרמיניסטי לבלוק ט
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

מוסיף ישות קנונית לתכניות בניין-עיר (תב"ע) שחוזרות בין תיקים — SSOT לזהות+תוקף
(פרסום למתן תוקף ברשומות + מס' ילקוט-הפרסומים) + משפט-ייעוד — במקום גזירה-מחדש
מהשומות בכל תיק. בלוק ט מצטט את התוקף בנוסח אחיד דטרמיניסטי (format_plan_citation),
כך שתאריך-פרסום/מס'-ילקוט לעולם לא מהוזים ע"י ה-LLM.

- DB: טבלת plans (V38) + CRUD + _normalize_plan_number (G1) + format_plan_citation;
  upsert idempotent (G3) עם כלל-מיזוג: תוקף מאושר לא נדרס — סתירה נרשמת ב-discrepancies
  (G10 / אין בליעה שקטה).
- services/plans_extractor.py: חילוץ עובדתי (claude CLI מקומי) → pending_review.
- block_writer.py: _build_plans_registry_context מזריק משפטי-ציטוט מאושרים בלבד לבלוק ט;
  תכניות חסרות/לא-מאושרות מסומנות במפורש (לא נבלעות).
- tools/plans.py + server.py: extract_plans / plan_get / plan_search / plan_list /
  plan_upsert / plan_review (שער-יו"ר G10), עם extract/get-symmetry (X9).
- scripts/backfill_plans_registry.py: ייבוא מקורפוס-ההחלטות (טיוטות + סופיי-דפנה).
- docs: block-schema (בלוק ט), SKILL, spec 02-data-model + 04.

Invariants: G1/INV-DM2/X1 (מזהה מנורמל בכתיבה) · G2/INV-DM6 (מקור-אמת יחיד, appraiser_facts
ללא שינוי) · G3 (upsert) · INV-DM4/G9 (provenance) · INV-DM5/G10 (review_status) ·
INV-AH (ציטוט דטרמיניסטי) · G5 (lookup לא קורפוס) · G11/block-schema (נוסח-הציטוט) · X9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 13:46:26 +00:00
parent 83293ca619
commit 4be9cf8543
11 changed files with 929 additions and 2 deletions

View File

@@ -196,6 +196,11 @@ BLOCK_PROMPTS = {
1. **תכניות חלות** — מבנה הירכי: תכניות ארציות → מחוזיות → מקומיות. ציטוט ישיר מהוראות תכנית עם **הדגשה** של מילים מכריעות.
2. **תת-פרק היתרים** — כותרת משנה "היתרים" (או "היתרי בנייה שניתנו במקרקעין"). פירוט ההיתרים הרלוונטיים על פי השומות שהוגשו לתיק.
## ציטוט תכנית ותוקפה (קריטי — מרשם-התכניות):
- לכל תכנית, לחלק **הזהות והתוקף** (מספר-התכנית + מתי פורסמה למתן תוקף ברשומות + מס' ילקוט-הפרסומים) — השתמש **ככתבו** במשפט-הציטוט הקנוני המופיע תחת "מרשם-התכניות" למטה. **אל תמציא** תאריך-פרסום או מספר-ילקוט.
- את הייעוד והניתוח התכנוני אתה רשאי לנסח בסגנון דפנה; את תאריך-התוקף ומספר-הילקוט — לעולם לא.
- אם תכנית זוהתה בתיק אך **חסרה במרשם או טרם אושרה** — הזכר את התכנית בלי לקבוע תאריך-תוקף, ואל תנחש תאריך.
## כללי ציון סתירות בין שמאים (קריטי):
- אם שני שמאים או יותר מסרו מידע שונה על אותה תכנית או היתר — חובה לסמן זאת במפורש בנוסח ניטרלי, למשל:
> "יצוין כי שמאי הוועדה ציין כי תכנית פלונית חלה על המקרקעין במלואה, בעוד שמאי העורר סבר כי חלקה של התכנית בלבד חל"
@@ -215,6 +220,9 @@ BLOCK_PROMPTS = {
## תכניות שזוהו (ממטא-דאטה של מסמכים):
{plans_context}
## מרשם-התכניות — משפטי-ציטוט קנוניים (מקור-אמת לתוקף; השתמש ככתבם):
{plans_registry_context}
## עובדות שמאיות שחולצו (תכניות + היתרים, פרק לכל שמאי):
{appraiser_facts_context}
@@ -333,6 +341,7 @@ async def write_block(
claims_context = await _build_claims_context(case_id)
direction_context = _build_direction_context(decision)
plans_context = await _build_plans_context(case_id)
plans_registry_context = await _build_plans_registry_context(case_id)
daphna_style_exemplars, case_law_citations, _precedent_case_law_ids = (
await _build_precedents_context(case_id, block_id)
)
@@ -371,6 +380,7 @@ async def write_block(
claims_context=claims_context,
direction_context=direction_context,
plans_context=plans_context,
plans_registry_context=plans_registry_context,
daphna_style_exemplars=daphna_style_exemplars,
case_law_citations=case_law_citations,
style_context=style_context,
@@ -580,6 +590,60 @@ async def _build_plans_context(case_id: UUID) -> str:
return "(לא זוהו תכניות)"
async def _build_plans_registry_context(case_id: UUID) -> str:
"""Render chair-APPROVED canonical plan citation sentences from the registry (V38).
The identity+validity clause is deterministic (db.format_plan_citation), so the
writer never invents publication dates or ילקוט numbers (INV-AH). Plans seen in
the case but absent from the registry — or present yet not approved — are listed
explicitly as gaps, never silently dropped (חוקה §6, אין בליעה שקטה).
"""
idents: set[str] = set()
for f in await db.list_appraiser_facts(case_id, fact_type="plan"):
if f.get("identifier"):
idents.add(f["identifier"])
for doc in await db.list_documents(case_id):
metadata = doc.get("metadata") or {}
if isinstance(metadata, str):
metadata = json.loads(metadata)
for p in (metadata.get("references", {}) or {}).get("plans", []):
name = (p.get("plan_name") or "").strip()
if name:
idents.add(name)
if not idents:
return "(לא זוהו תכניות בתיק. הרץ extract_plans ואשר אותן במרשם-התכניות.)"
approved: list[dict] = []
unapproved: list[dict] = []
missing: list[str] = []
seen_numbers: set[str] = set()
for ident in sorted(idents):
plan = await db.get_plan_by_number(ident)
if plan is None:
missing.append(ident)
continue
if plan["plan_number"] in seen_numbers:
continue
seen_numbers.add(plan["plan_number"])
(approved if plan["review_status"] == "approved" else unapproved).append(plan)
lines: list[str] = []
if approved:
lines.append("### משפטי-ציטוט קנוניים (מאושרים — השתמש בהם ככתבם לזהות+תוקף):")
for p in approved:
lines.append(f"- {p.get('citation_formatted') or db.format_plan_citation(p)}")
if unapproved:
lines.append('\n### תכניות במרשם שטרם אושרו (אל תצטט מהן תוקף — ממתינות לאישור-יו"ר):')
for p in unapproved:
lines.append(f"- {p['display_name'] or p['plan_number']} (status={p['review_status']})")
if missing:
lines.append("\n### תכניות שזוהו בתיק אך חסרות במרשם (הרץ extract_plans ואשר):")
for ident in missing:
lines.append(f"- {ident}")
return "\n".join(lines)
APPRAISER_SIDE_LABEL_HE = {
"committee": "שמאי הוועדה המקומית",
"appellant": "שמאי העורר",
@@ -1003,6 +1067,7 @@ async def get_block_context(case_id: UUID, block_id: str, instructions: str = ""
claims_context = await _build_claims_context(case_id)
direction_context = _build_direction_context(decision)
plans_context = await _build_plans_context(case_id)
plans_registry_context = await _build_plans_registry_context(case_id)
daphna_style_exemplars, case_law_citations, _ = (
await _build_precedents_context(case_id, block_id)
)
@@ -1037,6 +1102,7 @@ async def get_block_context(case_id: UUID, block_id: str, instructions: str = ""
claims_context=claims_context,
direction_context=direction_context,
plans_context=plans_context,
plans_registry_context=plans_registry_context,
daphna_style_exemplars=daphna_style_exemplars,
case_law_citations=case_law_citations,
style_context=style_context,