extract_and_store read the WHOLE full_text, so a ruling a party cited in its
own arguments (block ז / appellant_claims / parties_claims) was recorded as the
deciding committee citing it — and surfaced as "צוטט ע״י דפנה". Example: ערר
130/10 appears only in the appellant_claims chunk of decision 1200-12-25, yet
was attributed to the chair.
Fix: build the extraction text from the discussion/ruling chunks via the
halacha extractor's _select_extractable_chunks (G2 — one definition of
"reasoning sections": legal_analysis/ruling/conclusion + discussion-anchor,
excluding facts/claims/intro). Falls back to full_text only for un-chunked rows.
A citation now reflects the deciding body's reliance, not a party's argument.
Existing data reconciled via a one-off (backup
data/audit/citation-edges-backup-*.csv): per source decision, dropped edges
whose cited number is absent from the discussion-only set — 82 of 398 removed
(incl. ערר 130/10 ↔ 1200-12-25); 316 genuine edges kept.
Invariants: G1 (correct at the source) · G2 (single reasoning-section selector,
shared with halacha extraction) · INV-LRN2 (quality-at-source).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Topic hubs in /graph are built from case_law.subject_tags. The documented
extraction contract (precedent_library tool examples: קווי_בניין,
מועד_קביעת_שומה) and ~99% of the corpus store plain multi-word Hebrew tags
with underscores between words ("היטל_השבחה"). A single case (8126-03-25,
יעקב עמיאל) was tagged with spaces ("היטל השבחה"), which the graph renders as
a SECOND, distinct topic hub — a duplicate of the underscore form. The data
was normalized separately; this enforces the convention at the source so no
write path can re-introduce the split.
_normalize_subject_tags() is applied at the three (and only) case_law write
chokepoints in db.py — create_external_case_law, create_internal_committee_decision,
update_case_law — so the rule cannot be bypassed (G1: normalize at source, not
in the read/graph path). Tags carrying punctuation/digits/dashes
(e.g. "פטור מותנה — סעיף 19(ג)") are left untouched; only plain Hebrew word
phrases ([א-ת]+ separated by spaces) are converted. Also dedups post-normalize.
digests.subject_tags (TEXT[], a different graph layer) and canonical_halachot
are intentionally out of scope.
Invariants: maintains G1 (fix at source, not in the read projection),
G2 (graph_api stays a pure read projection — no parallel normalization there).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the chair-approved redesign of /missing-precedents (Claude Design
card 09). The "צוטט ע״י" and "תיק" columns were empty for ~98% of rows because
the corpus-decision reliance lives in precedent_internal_citations (keyed to
case_law), a different system than missing_precedents (keyed to cases).
Backend (G2 — read-time bridge, no stored duplication):
- list_missing_precedents: new cited_by_chairs / cited_by_decisions columns,
resolved from the citation graph by normalized docket number (same
normalization the relinker uses). For an open gap cited by committee
decisions, surfaces the chair(s) (e.g. דפנה תמיר) + the deciding case numbers.
Frontend (matches approved mockup):
- "צוטט ע״י" shows the chair chip (bridge) with priority over the generic
discovery-source chips; "תיק" shows the deciding decision number(s) + "+N".
- Status filter reduced to פתוח / נסגר (removed הועלה, לא-רלוונטי, הכל).
- "פסיקה" column collapses to a single line when case_name is empty — fixes the
duplicated-citation two-row rendering.
- Lifecycle note simplified to open → closed.
Verified: bridge SQL returns chair+decisions on live data; tsc --noEmit clean;
py_compile clean.
Follow-up (data, separate): LLM backfill of legal_topic (נושא); create
missing_precedents rows for the 28 graph-cited rulings not yet tracked.
Invariants: G2 (single source of truth — bridge derived at read, no duplicated
column) · INV-AH context (closing source gaps).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
precedent_internal_citations resolves cited_case_law_id only at extraction
time. When a decision cites a ruling that isn't in the corpus yet, the edge is
stored with cited_case_law_id=NULL. The ruling is often uploaded days later —
but the orphan edge was never re-resolved, so the citation graph permanently
under-counted real connectivity (same stale-derived-value pattern as the
searchable flag). Worse, external rulings store case_number WITHOUT the court
prefix ("3213/97") while citations carry it ("ע\"א 3213/97"), so a large share
of "missing" citations were actually present-but-unlinked.
Fix:
- new citation_extractor.relink_orphan_citations(case_law_id=None): re-resolves
orphan edges via the canonical _resolve_case_law_id (no parallel matching, G2);
scoped to one row, or full sweep when None. Idempotent.
- ingest_document calls it on every upload (non-fatal) so the graph self-heals.
Existing backlog already re-linked via a one-off sweep against the live DB:
linked 128 edges → in-corpus distinct rulings 67→178, unlinked distinct
262→143. So real "cited but not uploaded" ≈ 143, not the 262 the stale graph
implied.
Invariants: G1 (re-resolve the derived link at the write) · G2 (single resolver).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `searchable` completeness flag is recomputed only at ingest end and after
the Gemini metadata extractor. Internal-committee decisions skip Gemini, so
when their summary/subject_tags/metadata are filled after ingest the flag goes
stale and the row stays invisible to RAG despite being complete. Found 4 such
decisions (1049-06-21, 8126-03-25 [יעקב עמיאל], 8181-21 [האוניברסיטה העברית],
85074-04-25) — all complete (chunks+embeddings, extraction+halacha completed,
metadata present) yet searchable=false.
Fix: recompute_searchable() at the end of update_case_law — the single
chokepoint for metadata patches — so the derived flag never drifts from the
content (G1). Existing stale rows already corrected via a one-off canonical
recompute_searchable(None) run (corpus: 328 -> 332 searchable).
Invariants: INV-DM1 (completeness contract) · G1 (normalize derived value at
the write, no read-time patch).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Haifa (מחוז חיפה) and the North (מחוז הצפון) are separate Israeli planning
districts, each with its own appeals committee. Two divergent bugs collapsed
Haifa into צפון:
- services/internal_decisions.py _VALID_DISTRICTS omitted "חיפה" (while the
tools-layer VALID_DISTRICTS and db.py citation-abbrev map both include it —
a G2 single-source-of-truth divergence)
- _COURT_TO_DISTRICT mapped "חיפה" -> "צפון", so Haifa courts derived צפון
Result: 2 Haifa committee decisions (1074-08-23, 8508-03-24) were mis-filed
under צפון, and "חיפה" never appeared as a district.
Changes:
- add "חיפה" to service _VALID_DISTRICTS
- _COURT_TO_DISTRICT: ("חיפה","צפון") -> ("חיפה","חיפה")
- SCHEMA_V43: reclassify legacy internal rows whose court mentions חיפה from
צפון -> חיפה (normalize at source, G1; idempotent)
Verified against live DB (rollback txn): 2 rows move צפון(6)->צפון(4)+חיפה(2).
Invariants: G1 (normalize at source) · G2 (single source of truth for the
valid-district set — follow-up: consolidate the 3 divergent lists).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ועדת-ערר decisions (source_kind=internal_committee / source_type=
appeals_committee) are persuasive authority — they do not bind another
committee, nor the committee itself. The stored is_binding column wrongly
defaulted to True across the FastAPI form + service/db layers, so 46 of 92
committee rows were marked binding and got the BINDING halacha-extraction
prompt. Authority is structural for this source (INV-DM7) — normalize at the
source (G1), not trust the input.
Changes:
- db.create_internal_committee_decision: coerce is_binding=False (structural)
- db.create_external_case_law: coerce False when source_type=appeals_committee
- db.update_case_law: coerce False when a patch relabels source_type=
appeals_committee (Gemini reclassification path)
- internal_decisions.migrate_from_external_corpus: set is_binding=FALSE on
the external→internal reclassification UPDATE
- service + FastAPI-form defaults: True → False for the internal path
- SCHEMA_V42: backfill legacy committee rows (is_binding True→False) +
CHECK constraint case_law_committee_not_binding_check so a binding
committee row can never be written again ("so it doesn't recur")
- spec: X8-field-provenance aligned to INV-DM7
Verified against live DB in a rollback transaction: 46 rows flip to 0;
court_ruling (239) + cited_only (31) unaffected; binding-committee INSERT
rejected by the constraint.
Invariants: INV-DM7 (authority ⊥ rule-type) · G1 (normalize at source) ·
G2 (single source of truth, no parallel path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chaim 2026-06-20: a unanimous 3-vote principle must outrank a 2-vote one
regardless of score (cross-model agreement is the more reliable keep signal).
apply_cap now sorts survivors by (votes, score), matching cluster_candidates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase C — scripts/cull_principles.py: re-adjudicates every existing 'original'
principle with the SAME panel regime (panel_keep_score → classify → apply_cap),
reversible (CSV backup + rejected canonical recoverable), usage-throttled.
panel_extraction.panel_keep_score + apply_cap (shared, G2). Dry-run on 3
decisions: 37→15 survive.
Phase D — services/principles.py: source-derived label הלכה (binding court) /
כלל פרשני (committee) / עיקרון (persuasive); umbrella עקרונות משפטיים. Wired into
canonical_halacha_get/list (principle_class+principle_label). UI string changes
deferred to the Claude Design gate. spec INV-LRN7; SCRIPTS.md; 7 new tests; 428 green.
Phase E needs no new code — synthesis already targets pending_synthesis, which the
cull leaves only on survivors (rejected canonicals → 'rejected').
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
extract() routes to _extract_via_panel when HALACHA_PANEL_REGIME_ENABLED: the
3-model panel proposes → votes/score → approval rule → dedup vs corpus (known
links as citation, frees a cap slot) → cap HALACHA_PANEL_MAX_NEW genuinely-new
principles/decision (by score), rest dropped. Replaces single-model auto-approve;
legacy path kept as <2-judge fallback. db.store_panel_principles persists the
pre-decided verdict + source-aware canonical create/link (G9 reviewer=panel:...).
Dry-run validated on 29468-08-23: ~18 → 4 principles. 6 new tests; full suite 422 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Grounded (INV-AH) multi-instance synthesis with drift guard + chair gate
(pending_review, G10). Single path used by backfill, MCP tool, nightly drain.
HELD from production run pending the principles-redesign (rename+cull, #152).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UI changes to halacha-review-panel.tsx:
- instance_type badge (עיקרון מקורי / ציטוט / יישום) in meta row
- "מוזכר ב-N פסיקות" pill when instance_count > 1
- CanonicalSection component: canonical_statement (view + edit), instances accordion
Backend:
- list_halachot SQL: adds canonical_id, instance_type, canonical_statement,
instance_count via LEFT JOIN canonical_halachot
- list_canonical_instances(canonical_id) → compact rows for accordion
- GET /api/canonical-halachot/{canonical_id}/instances endpoint
- PATCH /api/halachot/{id}: canonical_statement propagates to canonical_halachot
- HalachaUpdateRequest: canonical_statement field
- useCanonicalInstances hook + CanonicalInstance type in precedent-library.ts
INV-G10 (chair gate): only the chair can edit canonical_statement (same
flow as rule_statement — PATCH /api/halachot/{id} with reviewer="דפנה").
G2: canonical data flows through canonical_halachot, not a parallel store.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- halacha_batch_reconcile.py --link now exits with error (V41 freezes
equivalent_halachot; equivalences live in canonical_id instead)
- link_equivalent_halachot emits DeprecationWarning (callers ≥ Python 3.2)
- SCRIPTS.md already marks --link as deprecated; no further doc change needed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces canonical_halachot table: one row per unique legal principle,
replacing the equivalent_halachot bidirectional-link model (V28/G2 improvement).
Per-precedent halachot rows become instances that point to their canonical.
Schema (V41):
- canonical_halachot: canonical_statement, rule_type, practice_areas,
subject_tags, embedding (ivfflat), review_status (pending_synthesis→published),
first_established_in FK → case_law, instance_count.
- halachot: +canonical_id FK, +instance_type (original|citation|application),
+treatment; rule_statement + embedding become nullable for citation instances.
- halacha_citation_corroboration: +canonical_id FK so X11 aggregates at
principle level, not instance level. store_corroboration auto-populates it
via INSERT...SELECT.
New DB functions: create_canonical_halacha, nearest_canonical_halacha
(threshold search for Phase 3 lookup-before-insert), refresh_canonical_instance_count,
get_canonical_halacha (principle + instance list).
Backfill: scripts/backfill_canonical_halachot.py — dry-run by default,
--apply to execute. Uses union-find over equivalent_halachot pairs, picks
canonical representative (corroboration→confidence→earliest), creates canonicals,
sets canonical_id + instance_type on all instances.
Invariants: G2 (equivalent_halachot deprecated post-backfill), INV-G10
(canonical review_status gate), INV-DM7 (authority derived, not stored),
INV-AH (canonical_statement grounded in source statements, pending_synthesis
until chair approves).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
**סיבת-שורש:** רג'קס respondent_claims כיסה רק צורת רבים (המשיבים/המשיבין),
ולא יחיד נקבה (המשיבה) ויחיד זכר (המשיב). הכותרת "טענות המשיבה:" בתיק 8181-21
נבלעה לתוך מקטע ruling → חולצה כהלכה שגויה.
**שלוש שכבות הגנה:**
1. chunker.py — הרחבת SECTION_PATTERNS לכסות יחיד/זכר/נקבה + תגובת/תשובת
2. halacha_extractor.py — עיגון חיובי: drop ל-ruling chunks שלפני legal_analysis
3. halacha_quality.py — FLAG_PARTY_CLAIM: זיהוי שפת-טענות-צד בציטוט התומך
**היקף:** 93 תיקים עם 0 chunks של טענות (כנראה בגלל כותרות ביחיד שלא הוכרו);
628 הלכות מאושרות מתיקים אלה — חלקן עשויות להיות תקינות, יש לעשות re-chunk.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
במשיכה מ-mavat מופה display_name=E_NAME — השם התיאורי הארוך של התכנית
(מהות התכנית), שמקומו בשדה הייעוד ולא בשם שבלוק ט מצטט. תוקן:
display_name = "תכנית <NUMB>" (צורת-ציטוט נקייה); E_NAME נשמר כ-fallback
ל-purpose בלבד (כשאין GOALS) כדי לא לאבד את התוכן.
דוגמה (101-0721050): שם-תצוגה "תכנית 101-0721050", ייעוד="שינוי הבנוי
המאושר בתכנית 6394... מתחם הוג'יטק, גבעת רם". אומת חי.
py_compile ✅ unit-test ✅ e2e חי ✅. backend-בלבד (גשר) — בלי שינוי-UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
כש-extract_plans מוצא מספר-תכנית עם תוקף-חסר (תאריך-רשומות / י"פ),
upsert_candidates ממלא את החוסר מ-מנהל-התכנון לפני ה-upsert. הרשומה
עדיין נכנסת pending_review — ההעשרה משנה את המועמד, לא את שער-היו"ר.
שמרני בכוונה:
- ממלא רק שדות-חסרים — לא דורס ערכים מעוגני-תיק (display_name/purpose
מהחילוץ נשמרים).
- מגודר לפורמט-mavat מודרני (\d{2,4}-\d{6,8}); מספרים-ישנים (מי/820,
תמ"א 38) מדולגים (לא יבזבזו השקת-דפדפן).
- תקרה PLAN_ENRICH_MAX_PER_CALL=8 (מתועד אם נחצה — בלי silent-cap).
- fail-soft: גשר-למטה / לא-נמצא / חסום → המועמד נשאר כפי-שחולץ (לוג,
לא בליעה שקטה).
- דגל-כיבוי PLAN_ENRICH_FROM_MAVAT=0.
- מקור-ההעשרה מסומן ב-model_used="claude_local+mavat".
INV-AH: ערך-תוקף שנמשך נושא מקור (mavat); שדה-חסר נשאר ריק. G10: שער-
היו"ר נשמר. G2: מרחיב את plans_fetch (#292), לא מסלול מקביל.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- הסרת כרטיס "סקירת התיק" הדל (3 אלמנטים בלבד)
- כפתור "התחל תהליך" עלה לפס-הפעולות בראש הדף (ליד "העלאת מסמכים")
- כרטיס חדש AgentActivityPreview: 4 הissues האחרונים + קישור לטאב מלא
- "תוצאה צפויה" ירדה לשורת מטא-דאטה בראיל הסטטוס (מוצגת רק אם נקבעה)
- תיקון באג: expected_outcome לא נשמר — guard if expected_outcome: הוחלף ב-is not None
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
שני פערים שצפו מ-/precedents בחילוץ-ההלכות:
1. **practice_area לא סומן** — השדה הועבר ל-LLM כקונטקסט-קריאה-בלבד ולא חולץ
מעולם, כך שהעלאות שהשאירו אותו ריק נשארו ריקות והרדיו ב-/precedents הופיע
ללא בחירה. עכשיו נגזר ב-apply_to_record: עדיפות לגזירה דטרמיניסטית מקידומת
מספר-התיק (1xxx→rishuy, 8xxx→היטל, 9xxx→197 — מקור-אמת לדוקטי ועדת-ערר,
INV-AH rule-based), ובנפילה — סיווג-תוכן של ה-LLM (שדה practice_area חדש
בפרומפט, אנום-סגור) עבור פסקי-בית-משפט שהקידומת שלהם אינה מקודדת תחום.
ממלא רק כשריק (G1 — נרמול במקור, לא תיקון-בקריאה).
2. **שם-יו"ר לא חולץ** (למשל 1132-09-24) — המיזוג היה מגודר על
source_kind=='internal_committee' בלבד, ודילג בשקט על החלטות-ועדה שהועלו
במסלול הפסיקה החיצוני (external_upload + source_type=appeals_committee, כמו
החלטת ת"א מנבו) — היו"ר ישב בבלוק-החתימה אך לא חולץ. עכשיו מגודר על "האם זו
החלטת-ועדה" (source_type/level אפקטיביים), לעולם לא על פסק-בית-משפט. ה-CHECK
כופה non-empty רק ל-internal_committee, לכן כתיבה ל-external בטוחה.
חיזוק-פרומפט (לבקשת היו"ר): chair_name מציין מפורשות את בלוק-החתימה הדו-טורי
(מזכיר↔יו"ר — לקחת את צד-היו"ר) ומזהיר לא לחלץ יו"ר של פסקי-דין **מצוטטים**
בגוף ההחלטה.
UI (לוגיקה-בלבד, פטור משער-העיצוב): edit-sheet מסנכרן-מחדש מהרשומה הטרייה בכל
פתיחה (re-arm על סגירה) ו-usePrecedent עושה poll בזמן חילוץ — כך מילוי-רקע של
practice_area/chair_name מופיע בלי refresh מלא ("הכפתור לא נשאר מסומן").
בדיקות: test_metadata_extract_chair_practice_area.py (6 תרחישי-מיזוג, offline).
Invariants: G1 (נרמול-במקור), G2 (אותו extractor, לא מסלול מקביל),
INV-AH (גזירה דטרמיניסטית מועדפת, abstention כשאין ודאות).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
תור-ההלכות שלף רק 500 ממתינות בעלות-עדיפות מתוך ~1,372, בלי שום דרך
לאתר פס"ד מסוים. הלכה מדורגת מתחת לחלון (למשל 1180-11-25, מקומות 921/1305)
פשוט נעלמה — הספירה בספרייה הציגה "2 ממתינות" אך התור לא הראה אותן.
- list_halachot: פרמטר search (case_number/case_name/rule_statement, ILIKE)
— סינון בצד-השרת כך שפריט מתחת לחלון נשאר נגיש. מרחיב את מסלול-השליפה
היחיד, לא יוצר מסלול מקביל (G2).
- count_halachot: ספירה מלאה לאותו פילטר (ל-"N מתוך TOTAL").
- /api/halachot: search + with_total (ברירת-מחדל off; קוראים קיימים לא מושפעים).
- UI (תור-הלכות): שורת-חיפוש מהוקצבת (debounce 300ms), הערת "חלון התצוגה"
(500 מתוך הסך), ושורת-הקשר-תוצאה עם ניקוי. בחיפוש — כל הקבוצות התואמות
נפתחות; הניווט המקלדתי והשערים (G10) ללא שינוי.
מאומת מול ה-DB: search='1180-11-25' → 2 הממתינות (index 20 נקייה→להכרעתך,
index 23 nli_unsupported→דורש תיקון-חילוץ); count=2.
עיצוב: עבר שער Claude Design (X17, כרטיס 19-halacha-queue-unified) ואושר.
Invariants: מקיים G2 (מסלול-שליפה יחיד), INV-G10 (שער-יו"ר ללא שינוי),
INV-IA (מקור-אמת יחיד לתור). ללא בליעת-שגיאות.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
המספרים בתגי תור-ההלכות היו תקרות-שאילתה ולא ספירה אמיתית:
- "ממתינות 500" = pendingData.items.length עם limit=500
- "נדחו 1000 / אושרו 1000" = useHalachotByStatus(...,1000) — תקרה 1000
ובלי refetchInterval התגים התעדכנו רק בכניסה לדף.
המקור האמיתי כבר קיים: /api/precedent-library/stats מריץ COUNT(*) אמיתי
(pending=1373, approved=2100). מוסיף לו halachot_rejected + halachot_deferred,
מחבר את תגי-ה-HalachaReviewPanel למקור הזה, ומוסיף polling (30s) כדי שהם
יתעדכנו חי. מסיר את useHalachaCount המיותר.
תור-העבודה עצמו עדיין נטען עד 500 פריטים (cap-עבודה לגיטימי); רק תצוגת
הספירות תוקנה להציג את הסך-האמיתי.
Invariants: מקיים G1 (נרמול-במקור — ספירה אמיתית מ-COUNT(*) במקום len(rows)
מתוקרת בקריאה) ו-G2 (מאחד על מקור-הספירה הקיים, ללא endpoint-ספירה מקביל).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ה-backfill של citation_formatted חשף קריסה ב-apply_to_record: כשפסק-דין
חיצוני מכיל docket שכבר שייך לרשומה כפולה אחרת, נרמול case_number → docket-נקי
נתקל ב-uq_case_law_external_number ומפיל את כל המיזוג (כולל הציטוט).
דוגמה: 'ע"א 3213/97' → '3213/97' שכבר קיים (כפילות נקר).
- db.case_number_collides(case_number, exclude_id) — בודק אם docket כבר שייך
לרשומה לא-internal אחרת (האינדקס החלקי).
- apply_to_record — מדלג על נרמול ה-case_number כשיש התנגשות (כפילות לדדופ
בהמשך, לא ענייננו כאן) וממשיך לכתוב את הציטוט. no-silent-swallow: מתעד warning.
- scripts/backfill_precedent_citations.py — try/except per-row + מונה שגיאות,
כך ששורה אחת לא מפילה את האצווה.
אומת: ריצה-מחדש מלאה ללא קריסה (0 שגיאות); ההתנגשות תועדה ודולגה כצפוי;
פסיקת בית-משפט: 224/228 מולאו, 4 נמנעו (חסר צדדים/תאריך — abstention, INV-AH).
test_fu2b_reconcile ✓.
Invariants: INV-AH (abstention) · G1 (נרמול-בכתיבה נשמר, רק לא קורס) ·
חוקה §6 (אין בליעה שקטה — דילוג מתועד).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
תיק יכול להיות <kind>_extraction_status='pending' עם _requested_at=NULL —
מעולם-לא-נכנס-לתור (מסלולי bulk/מיגרציה, או status שנכתב לפני החותם), והדריינר
(סורק requested_at IS NOT NULL) עיוור אליו לנצח → ה-backlog מתנקז בשקט לאפס.
requeue_stale_processing_extractions מרפא רק 'processing'. נצפה 2026-06-14:
96 תיקים pending אך 0 בתור (תוקנו ידנית).
תיקון (G1 — שחזור invariant במקור, G2 — predicate יחיד):
- db.reconcile_orphaned_pending_extractions(kind=) — kind-agnostic, מחזיר את
invariant "שורה ברת-חילוץ ⇒ בתור": חותם requested_at ל-rows שהם pending +
requested_at IS NULL + EXTRACTION_ELIGIBLE_PREDICATE (אותו מסנן של #140 —
cited_only/chunkless לעולם לא נדחפים). אידמפוטנטי (rows מסומנים לא נתפסים).
- precedent_library.process_pending_extractions קורא reconcile אחרי requeue_stale
ולפני list — תיקים-משוחזרים נקלטים באותו pass. מנגנון-ריפוי יחיד (G2), לא מסלול
מקביל; requeue_stale='processing', reconcile='pending'.
- request_halacha_extraction מציב status='pending' עם החותם (סימטרי ל-metadata)
— סוגר את חלון-ה-drift שמייצר pending+NULL מלכתחילה.
מצב חי נקי (0 יתומים-כשירים אחרי התיקון-הידני); זהו תיקון מונע — הדריינר יְרַפֵּא
יתומים עתידיים אוטומטית.
בדיקות: test_extraction_orphan_reconcile (predicate משותף, pending+NULL בלבד,
מובחן מ-requeue_stale, request_halacha סימטרי), שני ה-kinds. כל 349 עוברות.
Invariants: G1, G2 (predicate משותף עם #140, ריפוי יחיד), INV-G3/INV-DUR1 (X16),
INV-G4 (אין בליעה שקטה — reconcile מתעד), G12.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
31 שורות case_law עם source_kind='cited_only' (ציטוט-בלבד, ללא full_text/chunks)
נושאות halacha_extraction_status='pending' רק כברירת-מחדל ומזהמות את מונה ה-pending
ובמתזמר/בדף-התפעול — אין להן מה לחלץ.
תיקון (G1 — תיקון-במקור, G2 — מסנן יחיד משותף):
- db.EXTRACTION_ELIGIBLE_PREDICATE — מקור-אמת יחיד ל"שורה ברת-חילוץ" (source_kind
<> 'cited_only' AND יש precedent_chunks). מוחל ב-list_pending_extraction_requests;
#139 יעשה בו שימוש-חוזר ל-reconcile (אותו כלל, לא כפול).
- מוני-snapshot מסננים cited_only: halacha_drain_supervisor.db_snapshot,
web/app.py meta+hal_ext (GROUP BY status).
- reconcile_metadata_status.py מורחב לכסות גם את תור-ההלכות: cited_only→'skipped'
(אותו terminal-state כמו צד-המטא, תור-תאום, G2). בוצע על ה-DB החי: 31 הועברו
ל-'skipped' (metadata כבר היה מיושב — אידמפוטנטי). התפלגות-אחרי: halacha
pending=9 (עבודה אמיתית), skipped=31, completed=309.
בדיקות: test_extraction_queue_eligibility (predicate + list_pending מחיל אותו,
שני ה-kinds). כל 345 בדיקות mcp עוברות. guards נקיים.
Invariants: G1 (terminal-state אמיתי במקור), G2 (predicate יחיד, ללא תור מקביל),
INV-DM1 (stub לא-searchable אינו מועמד-חילוץ), G12 (leak-guard נקי).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
צד-המטא-דאטה (precedent_metadata_extractor) קרא רק GEMINI_API_KEY בעוד בסביבה
קיים GOOGLE_GEMINI_API_KEY — תוקן ב-PR #255 (fallback). הבאג המשני שנותר: כש-
extract_and_apply החזיר 'no_metadata' (כשל-Gemini), מסלול-הדריינר
process_pending_extractions התיישב ל-metadata_status='completed' ללא-תנאי, כך
שהרשומה ננטשה בשקט עם מטא ריק והדריינר לא חזר אליה (נצפה: da2d9ccb '4491-02-21',
5fabdac5 '14306-09-23' — completed אך court/date/summary ריקים).
תיקון (G1 — אבחנת-מקור):
- extract_and_apply מבדיל תוצאה-ריקה: יש full_text → 'extraction_failed' (חולף,
בר-retry); אין full_text → 'no_metadata' (אין מה לחלץ).
- process_pending_extractions (metadata): 'extraction_failed' → חוזר ל-'pending'
(משמר את חותם-התור) במקום להתיישב 'completed'. retry-loop הקיים מנסה שוב,
ואחרי-מיצוי הרשומה נשארת בתור. מסלול reextract_metadata כבר עקבי (חוזר pending
על כל מה שאינו completed/no_changes).
תיקון-נתון (בוצע ידנית דרך כלי-MCP precedent_extract_metadata): da2d9ccb +
5fabdac5 חולצו-מחדש בהצלחה (court/date/summary/headnote/tags מלאים). 0 נותרו
external 'completed-but-empty'.
הערה: מפתח-Gemini אינו נדרש ב-Coolify — המחלץ רץ רק מקומית (precedent_library →
extract_and_apply, host ~/.env עם GOOGLE_GEMINI_API_KEY); app.py מייבא רק את
הקבוע PLACEHOLDER_PENDING_EXTRACTION, לא את פונקציית-החילוץ.
בדיקות: test_metadata_extract_failure_status (transient/permanent/missing). כל
335 בדיקות mcp עוברות. guards נקיים.
Invariants: G1 (אבחנת-מקור, לא התיישבות-בקריאה), INV-G3/X16 (עמידות — בר-retry),
G12 (leak-guard נקי).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
בהעלאה דרך "פסיקה-חסרה" (ענף ועדת-ערר), כשטופס case_number ריק המסלול נפל-לאחור
לציטוט המלא (committee_case_number = case_number.strip() or citation), כך שמחרוזת-
תצוגה עם שמות-צדדים הושתלה בשדה-המזהה — הפרת INV-ID2/INV-ID1 (X1). נצפה על
precedent 1bf0bae0 (ערר 85074-04-25 רפאל לוי/חולון): case_number=85074/0425,
case_name=ציטוט שלם.
תיקון (G1 — נרמול-במקור, G2 — שימוש-חוזר בפרסר הקנוני):
- court_citation.case_number_from_citation(citation) — מחזיר את אסימון-המספר
המנורמל בלבד (classify; '' כשאין מספר). חולץ נכון 85074-04-25 גם מתוך
"ערר (ת\"א 85074-04-25) ...". reuse של הפרסר היחיד, בלי regex מקביל.
- web/app.py (ענף ועדת-ערר): fallback דרך case_number_from_citation; אם אין
מספר — HTTPException 400 "נא להזין מספר-תיק ידנית" במקום השתלת ציטוט-מלא.
- db._canonical_case_number: מוקשח לחלץ את אסימון-המספר (זורק זנב שמות-צדדים),
כך ששדה-המזהה לעולם לא נשמר מזוהם — גם בקריאה ישירה (committee + active cases).
מספר נקי חוזר ללא שינוי; חודש לא מומצא (X1 §1).
- תיקון-נתון: scripts/fix_137_committee_case_number.py (בוצע) — 1bf0bae0:
case_number→85074-04-25, case_name→צדדים, token ב-citation_formatted.
אומת היחיד עם canon(num)≠num ב-internal_committee. אידמפוטנטי.
מחוץ-לתחום (תועד כ-follow-up): מסלול external (precedent_library) משתמש בציטוט-
מלא כמזהה-מורשת — זהו פריט-המיגרציה X1 §5 (138 רשומות external/cited_only),
לא הבאג הזה. prefill ב-UI של /missing-precedents — דורש שער Claude Design.
בדיקות: test_court_citation (case_number_from_citation: party-strip/forms/empty),
test_canonical_case_number (harden). כל 339 בדיקות mcp עוברות. guards נקיים.
Invariants: G1 (נרמול-במקור), INV-ID1/ID2 (מזהה מנורמל, אין ציטוט-מלא כמזהה),
G2 (פרסר יחיד), G12 (leak-guard נקי).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
כשה-legal-halacha-drain קרס עם "RuntimeError: Event loop is closed", ה-finally
שמריץ pg_advisory_unlock + pool.release לא רץ, וחיבור-הנעילה הייעודי נשאר חי,
idle, מחזיק את הנעילה הגלובלית — כל extract עתידי החזיר status='busy' לצמיתות
עד pg_terminate_backend ידני (~4.5 דק', CMP-174, 2026-06-14).
תיקון (G1 — נרמול-במקור, G2 — אותה נעילה, בלי מסלול מקביל):
- KEEPALIVE: משימת-רקע נוגעת בחיבור-הנעילה כל 30ש' → state_change נשאר טרי.
חילוץ חי לעולם לא נראה "תקוע"; קריסה מקפיאה את ה-keepalive ואת state_change.
- שחזור-עצמי בכניסה (_acquire_global_lock): כש-pg_try_advisory_lock נכשל, בודקים
את ה-holder; רק backend idle עם state_change ישן מ-_LOCK_STALE_AFTER (150ש',
5× keepalive) הוא orphan דלוף → pg_terminate_backend ואז acquire מחדש.
backend 'active' או idle-טרי = חילוץ חי, לעולם לא נהרג (מניעת ה-box-freeze).
- נדחתה אופציית pg_advisory_xact_lock: הייתה כופה transaction פתוח לאורך דקות
(idle-in-transaction bloat) ועדיין לא משחררת מיידית חיבור-orphan חי.
הערה: השתמשתי במונח DB-סטנדרטי "keepalive" (לא "heartbeat") כי leak_guard מסמן
את "heartbeat" כסמל ספציפי-Paperclip (G12).
בדיקות: tests/test_halacha_lock_selfheal.py (7) — free/live-holder/active-holder/
stale-orphan-reclaim/no-holder/keepalive-stop/extract-busy. כל 332 בדיקות mcp עוברות.
Invariants: G1 (תיקון-במקור), G2 (אותה נעילה), G3/X16 (עמידות-פייפליין), G12 (leak-guard נקי).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
שני באגים בקליטת-פסיקה חיצונית (התגלו בתיק 1132-09-24 שהועלה דרך "פסקה חסרה"):
1. case_number קיבל את מחרוזת-הציטוט המלאה במקום דוקט נקי. הסיבה: overwrite_case_number=True
הועבר רק לנתיב-הפנימי (internal_decisions); נתיב-הדריינר ל-external השאיר את הציטוט שב-
case_number (precedent_library: case_number=citation). היקף: 122 רשומות external_upload.
2. source_type לא נאכף מול precedent_level — רק ה-prompt ביקש מה-LLM. כשה-LLM פלט
level=ועדת_ערר_מחוזית אך source_type=court_ruling, ההחלטה סווגה בספרייה כ"פסיקת בית משפט".
תיקון (ב-apply_to_record, כך שכל הנתיבים נהנים):
• case_number מנורמל לדוקט הנקי כש-(א) caller כופה או (ב) הערך הנוכחי ציטוט-צורני (רווח/אורך>20);
guard _is_clean_docket מבטיח שלעולם לא נכתב ערך לא-דוקט לשדה-הזהות (LLM-זבל נדחה).
• _source_type_for_level גוזר source_type מ-precedent_level ודורס אי-עקביות (ועדת_ערר_*→
appeals_committee; עליון/מנהלי→court_ruling) — מקור-אמת אחד, לא הישענות על עקביות-LLM.
נבדק: 18 unit-tests (docket-validation, level→type mapping) + 3 integration-tests מול
apply_to_record עם DB מדומה (נרמול, אי-דריסת-דוקט-תקין, דחיית-זבל, אכיפת-עקביות). py_compile נקי.
תיקון-נקודתי כבר בוצע ידנית ל-1132-09-24. Backfill ל-122 בנפרד (TaskMaster #141).
Invariants: G1 (תיקון-במקור), G2 (אותו extractor — בלי מסלול מקביל), INV-AH (מקור-אמת
דטרמיניסטי לסיווג, לא ניחוש-LLM). G11 (זהות-תיק נקייה).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>