Chair-directed navigation/layout fixes (existing components, no new design):
1. "פתח עורך החלטה" moved into the case-page band actions (right after "העלאת
מסמכים") so it's reachable from EVERY tab, not only סקירה. Removed the now-
redundant full-width CTA from the overview tab.
2. Prominent "→ חזרה לדף התיק" back-link added to the /compose header (the
breadcrumb link was too subtle).
3. Home cases table: rail trimmed 360→280px and the title cell made min-w-0 so the
table gets the width it needs and no longer shows a horizontal scrollbar.
No backend / API change. The larger NEW citation-verification panel stays gated
behind Claude Design (preview 24-citation-verification already pushed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The radar's query was built from dozens of raw claims (procedural-heavy noise), so
matches were thematic but imprecise and gave no reason WHY a lead is relevant. Now:
- Prefer the analyst's distilled legal_arguments (argument_title + legal_topic — one
crisp CREAC issue per row) over raw claims.
- Search EACH issue separately and MERGE, so every lead is attributed to the case
issue(s) it answers (`matched_issues`) — the chair sees "this ruling is for your
'זכות עמידה' issue", not just a blended score.
- Fall back to the raw-claims blended query pre-aggregation; `source` reports the path.
- Shared `_radar_enrich` helper (gap status + action + matched_issues), bounded to 25
issues to cap the per-issue fan-out.
Validated: 8124-09-24 (32 args → per-issue) surfaces betterment rulings each tagged to
its issue (היעדר השבחה / זהות הנישום / סעיף 7(ב)); 1044-03-26 (0 args) falls back to
claims unchanged. No tool/endpoint signature change (new fields pass through the dict).
Invariants: G2 (reuses the one digest search + arg accessor), INV-DIG1 (radar only).
No schema change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A digest pointing at a ruling we don't hold yet ("unlinked") was captured globally
(missing_precedents inbox) but never surfaced IN THE CONTEXT of the case being
decided — so a relevant ruling known only via a digest could fall through the cracks
at the moment it matters. Adds the case-contextual radar:
- db.search_digests_semantic: new `linked_only` filter (False = unlinked-only target set).
- digest_library.case_digest_radar(case_number): builds the case topic from title +
appeal_subtype + the analyst's claims, embeds once, matches against UNLINKED digests,
and returns leads enriched with the underlying ruling's gap status + suggested action
(new_lead / gap_open / fetched / available_link).
- MCP tool `digest_radar` + endpoint GET /api/cases/{n}/digest-radar (for the agent and
the future case-page lead).
INV-DIG1 preserved: radar only — every lead points at the underlying RULING (fetch /
upload / link), never cites the digest. Read-only.
Validated on 8124-09-24 (היטל השבחה): 5 on-topic unlinked digests, scores 0.64–0.72.
The visible case-page panel is a separate UI change → goes through the design gate.
Invariants: G2 (reuses the one digest semantic-search + gap/citation resolvers),
INV-DIG1 (no digest citation), INV-DIG3 (gap surfacing). No schema change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The internal citation graph fed only RANKING (raw in-degree), and the per-citation
TREATMENT was never classified — so a precedent distinguished N times got the same
authority boost as one followed N times (INV-COR2 violation), and the signal never
reached the agents' reasoning. Wires the full path:
Phase 1 — scripts/classify_citation_treatments.py: classify each linked edge's
treatment (followed/distinguished/…) from its match_context via
corroboration.classify_treatment (Opus 4.8 @ xhigh, local), filling
precedent_internal_citations.treatment. Idempotent.
Phase 2 — db.refresh_verified_layer: count only NON-negative treatments toward
verified/cite_count (INV-COR2/COR4). Unclassified counts as neutral-positive so
the signal degrades gracefully before classification runs.
Phase 3 — db.citation_authority(ids): per-precedent {total, positive, negative,
unclassified, by_treatment}. Surfaced as `cited_by` in search_precedent_library
hits and precedent_library_get, and `treatment` per incoming citation.
Phase 4 — legal-researcher/analyst/writer prompts: weigh & ARGUE authority
("הלכה שאומצה ב-N החלטות ועדת-ערר"), flag distinguished/overruled, never invent
the count (INV-AH; writer is read-only of the analyst).
Auto-approval stays kill-switched off (chair gate preserved, INV-G10). No schema
change (treatment column already existed). Operational: run the classifier +
refresh_verified_layer over the 379 edges, then sync agents across companies.
Invariants: G2 (one classifier + one authority query, reused), INV-COR2/COR3/COR4
(negative never corroborates; point-specific; ≥N), INV-G10 (no auto-approval),
INV-AH (no invented numbers).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause: the upload form "מראה המקום" field is stored only as case_number;
citation_formatted was left empty and filled ONLY by the async metadata
extraction, which runs in the local drainer (not the container) because it was
lumped with halacha extraction. Result: the chair saw an empty מראה-מקום during
the window and re-typed it by hand.
Fix (hybrid — Option C):
- Seed: _create_external_record / create_external_case_law store the chair's typed
citation as citation_formatted at INSERT, so the field is never blank. A
cited_only→external promotion preserves an existing non-empty value (prior edit).
- Enrich: ingest_precedent runs metadata extraction INLINE in-container
(gemini_session is direct REST, no local CLI) with force_citation=True, upgrading
the seed to the canonical derived citation (parties + reporter + date) before the
chair opens the page. Best-effort: on no key / API failure the seed remains and
the queued metadata drain stays as the fallback. Halacha extraction is untouched
(stays local — claude_session).
- apply_to_record / extract_and_apply gain force_citation: re-assemble even when
citation_formatted is non-empty, writing only when assembly SUCCEEDS (seed
preserved on abstention). The drainer keeps the default (False) so a chair's
manual edit in /precedents/[id] is never clobbered.
Ops: GOOGLE_GEMINI_API_KEY added to the legal-ai Coolify app (runtime) — the SoT
value from Infisical nautilus:/external-apis/gemini, validated against the API.
Invariants: G2 (one citation-resolution + one metadata-extraction path, reused —
no parallel logic), INV-ID2/X1§3 (citation_formatted stays a derived field; the
seed is provisional and upgraded), INV-G10 (chair edits preserved). No schema change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The precedent-detail "ציטוטים מקושרים" panel rendered only MANUAL appeal-chain
links (case_law_relations), so it stayed empty even when decisions cite the
ruling — the automatic citation graph (precedent_internal_citations) was never
surfaced. e.g. עע"מ 317/10 (שפר) has 12 incoming citations in the DB, none shown.
Fills the existing panel from the citation graph (data/logic only, no new
visual design — gate-exempt per web-ui/AGENTS.md):
- citation_extractor.list_citations_to_case_law: return source precedent_level/
court/date too (additive; reuses the one canonical incoming query — G2).
- precedent_library.get_precedent: add incoming_citations[], shaped like the
RelatedCase row. No parallel resolution path.
- web-ui: render incoming citations in the same row style, read-only (no unlink),
de-duped against manual links; manual "קשר" linking preserved alongside.
Invariants: G2 (single citation-resolution path, reused), G1 (graph self-heals
via existing relink_orphan_citations on upload). No schema change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The /graph "משנה / עד שנה" dropdown was hardcoded to 1994–2026, so dated
precedents below 1994 were unreachable by the year filter. After backfilling
decision dates, the corpus now has precedents from 1982 (ע"א 725/81), 1988
(910/86) and 1990 (בג"ץ 1578/90).
Floor → 1980 (covers the oldest); ceiling → current year via getFullYear()
so the range never ages out. Pure UI-logic change — same year_from/year_to
params, no new data path (G2), no backend touch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The table fetched limit:200 while the header shows the true open count (~497,
via a separate COUNT). With accordion grouping this became visible: sections
summed to exactly 200 (e.g. 16 chair / 173 digest / 11 other) — and 90 of the
106 committee rows (the high-value "cited by Dafna" ones) were hidden past row
200. True buckets: chair 106 / digest 328 / other 63.
- web-ui: list limit 200 → 1000 so all open rows load; accordion section counts
(computed from the loaded set) now equal the header total.
- backend: list cap 500 → 2000 to allow it (response stays a few hundred KB).
Logic/data only — no visual change (bug-fix exception to the design gate).
tsc --noEmit clean; py_compile clean.
Follow-up: when the backlog (digests grow daily) approaches the cap, move to
server-side per-bucket counts + pagination/virtualization (design gate).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chaim directive 2026-06-20: no halacha queue ever; trusted = only what a chair
actually cited, in context; corpus grows by citations; few-verified-now is fine.
nli-audit: nli_unsupported flag is 97% false-positive (29/30) — the 'noise' was
imaginary. Two layers: raw background (auto, no gate) vs verified (auto-canon from
citations). +06 indexed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Chair-approved (Claude Design card 09): split the flat table into collapsible
sections by where the gap surfaced, so the high-value "cited by Dafna" rulings
aren't buried among the 330 digest rows.
- Three <details> accordion sections within the active status tab:
· "צוטט ע״י דפנה" — rows with a resolved chair from the citation-graph bridge
(cited_by_chairs non-empty); open by default.
· "יומון" — discovery_source='digest'; collapsed.
· "אחר" — party-cited / manual remainder; collapsed.
- Row JSX extracted to renderRow() and reused across sections; shared
TableHeaderRow(); grouping via useMemo over the current result set.
- Empty sections are hidden; section header shows source chip + count.
No backend change — groups on existing cited_by_chairs / discovery_source.
tsc --noEmit clean.
Invariants: INV-IA* (task-oriented surface, single gate) — UI only.
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>
25 primary sources, 21/25 claims verified (3-vote adversarial). Verdict: keep
all principles, rank by precedent-level citation centrality at retrieval time
(not a destructive holding-level cull — that's where automation provably errs),
gate human review near-zero via conformal/selective-prediction.
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>