feat(halachot): חיפוש/איתור בתור-ההלכות + הערת חלון-תצוגה (פסיקה מחוץ ל-500 נגישה)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

תור-ההלכות שלף רק 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>
This commit is contained in:
2026-06-17 03:58:46 +00:00
parent b4cb0a69c3
commit 896df0cb8c
4 changed files with 165 additions and 15 deletions

View File

@@ -5284,6 +5284,7 @@ async def list_halachot(
cluster: bool = False,
include_equivalents: bool = False,
include_panel_round: bool = False,
search: str | None = None,
) -> list[dict]:
"""List halachot with optional triage controls (#84).
@@ -5291,6 +5292,10 @@ async def list_halachot(
truncated_quote / quote_unverified / non_decision / thin_restatement /
nli_unsupported / near_duplicate). These belong in a 'needs extraction
fix' bucket, not the chair's approve queue (#84.1).
search — free-text locate within the queue (case_number / case_name /
rule_statement, case-insensitive). Filters server-side so a pending
halacha ranked below the display window is still reachable; without it
the queue only ever shows the top ``limit`` by priority.
order_by_priority — replace FIFO with an active-learning order (#84.3, #133/FU-3):
panel-disagreement first (the panel SPLIT, then ran INCOMPLETE — the
labels of highest learning value: the chair's call resolves a genuine
@@ -5320,6 +5325,14 @@ async def list_halachot(
if exclude_low_quality:
# a clean item has an empty/NULL quality_flags array
conditions.append("COALESCE(array_length(h.quality_flags, 1), 0) = 0")
if search and search.strip():
# locate by decision identity OR rule text — server-side so an item
# ranked below the display window is still reachable (no client-only filter).
conditions.append(
f"(cl.case_number ILIKE ${idx} OR cl.case_name ILIKE ${idx} "
f"OR h.rule_statement ILIKE ${idx})")
params.append(f"%{search.strip()}%")
idx += 1
where_sql = f"WHERE {' AND '.join(conditions)}" if conditions else ""
# #133/FU-3: rank the panel's latest verdict so splits/incompletes — the
# highest-value active-learning labels — float to the top of the queue.
@@ -5387,6 +5400,48 @@ async def list_halachot(
return out
async def count_halachot(
review_status: str | None = None,
practice_area: str | None = None,
exclude_low_quality: bool = False,
search: str | None = None,
) -> int:
"""Full count for the same filter ``list_halachot`` uses (sans limit/offset).
Powers the queue's "showing N of TOTAL" note so the chair knows how many
pending items sit beyond the display window. Mirrors the WHERE exactly —
one source of truth, no parallel counting logic.
"""
pool = await get_pool()
conditions = []
params: list = []
idx = 1
if review_status:
conditions.append(f"h.review_status = ${idx}")
params.append(review_status)
idx += 1
if practice_area:
conditions.append(f"${idx} = ANY(h.practice_areas)")
params.append(practice_area)
idx += 1
if exclude_low_quality:
conditions.append("COALESCE(array_length(h.quality_flags, 1), 0) = 0")
if search and search.strip():
conditions.append(
f"(cl.case_number ILIKE ${idx} OR cl.case_name ILIKE ${idx} "
f"OR h.rule_statement ILIKE ${idx})")
params.append(f"%{search.strip()}%")
idx += 1
where_sql = f"WHERE {' AND '.join(conditions)}" if conditions else ""
sql = f"""
SELECT count(*)
FROM halachot h
LEFT JOIN case_law cl ON cl.id = h.case_law_id
{where_sql}
"""
return await pool.fetchval(sql, *params) or 0
async def _annotate_panel_rounds(pool, out: list[dict]) -> None:
"""Attach the LATEST 3-judge panel round to each row (#133/FU-2), display-only.