fix(halacha): split authority (derived) from rule_role — stop source-conflation (INV-DM7)
The extractor classified rule_type by SOURCE bindingness (higher-court→binding, committee→persuasive) instead of by rule KIND. The gold-set proved it: 'binding' appeared on 19/19 external rulings & 0 committees; 'persuasive' on 13/13 committees & 0 external — only 58% agreement with the human role tags. The two axes (authority vs rule role) were crammed into one enum. This splits them per INV-DM7: - authority (binding/persuasive) — DERIVED from case_law.precedent_level (עליון/מנהלי→binding, ועדת_ערר_מחוזית→persuasive), never stored, never LLM-guessed. New helper halacha_quality.derive_authority; surfaced read-only in list_halachot / goldset_list / search results. - rule_type — now the rule ROLE only: holding/interpretive/procedural/ application/obiter. Both extractor prompts unified to this vocabulary; _coerce_halacha no longer defaults rule_type from the source; legacy binding→holding / persuasive→interpretive fold for safety. UI: authority shown as a separate read-only badge (gold=מחייב / muted=משכנע) across the review queue, precedent detail, and gold-set; the gold-set role selector drops binding/persuasive and adds מהותי (holding). Migration: scripts/halacha_rule_role_backfill.py re-classifies the 276 pre-split binding/persuasive rows into a genuine role via local claude_session (run after deploy). Gold-set correct_type/ai_correct_type 'binding'→'holding' via SQL. Sources (≥3, per research-decision policy): OASIS LegalRuleML v1.0 (appliesAuthority/Strength as metadata orthogonal to rule logic) · SemEval-2023 Task 6 LegalEval (rhetorical roles by function, authority kept separate) · Bluebook signals (weight-of-authority is a separate dimension). Invariants: ESTABLISHES INV-DM7. Upholds G1 (normalize at source — extractor classifies role, system derives authority) and G2 (single source of truth — authority derived, not a parallel stored field). Tests: 211 pass + new derive_authority/coerce coverage. web-ui build + tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -155,6 +155,14 @@ RAG freshness (Lewis et al., 2020, NeurIPS) | סטטוס: verified
|
|||||||
**אכיפה:** CHECK על enums; FK על `cited_precedents`/`decision_paragraphs.citations`; איחוד `case_precedents`↔`case_law`.
|
**אכיפה:** CHECK על enums; FK על `cited_precedents`/`decision_paragraphs.citations`; איחוד `case_precedents`↔`case_law`.
|
||||||
**הפרה ידועה:** 20+ enums כ-TEXT חופשי; `legal_arguments.cited_precedents TEXT[]` ללא-FK (הזיות-LLM נבלעות); `case_precedents` מול `case_law` מקבילות ([gap-audit GAP-40/42/43](gap-audit.md)).
|
**הפרה ידועה:** 20+ enums כ-TEXT חופשי; `legal_arguments.cited_precedents TEXT[]` ללא-FK (הזיות-LLM נבלעות); `case_precedents` מול `case_law` מקבילות ([gap-audit GAP-40/42/43](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-DM7: סיווג-הלכה — סמכות (נגזרת) ⊥ תפקיד-כלל (מסווג). שני צירים, לא enum אחד
|
||||||
|
**כלל:** ל-`halachot` שני צירי-סיווג **אורתוגונליים** שאסור לערבב בשדה אחד:
|
||||||
|
- **סמכות (`authority`) — נגזרת בלבד, לא מאוחסנת, לא מנוחשת ע"י LLM.** `binding` (מקור מחייב את הוועדה: עליון/מנהלי) מול `persuasive` (מקור משכנע: ועדת-ערר אחרת). נגזרת דטרמיניסטית מ-`case_law.precedent_level` (`עליון`/`מנהלי`→binding; `ועדת_ערר_מחוזית`→persuasive). מקור-אמת יחיד — מחושבת בקריאה, אין עמודה כפולה ([G1](00-constitution.md#inv-g1-נרמול-במקור-לא-תיקון-בקריאה)/[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)).
|
||||||
|
- **תפקיד-כלל (`rule_type`/rule_role) — מסווג ע"י ה-LLM.** `holding` (עיקרון מהותי הכרחי להכרעה — ratio/Wambaugh) · `interpretive` (פרשנות חוק/מונח/תכנית) · `procedural` (סדר-דין: סמכות/מועדים/נטל) · `application` (החלה תלוית-עובדות — לרוב לא-הלכה) · `obiter` (אמרת-אגב). **`binding`/`persuasive` אינם ערכי תפקיד** — הם סמכות-מקור.
|
||||||
|
**הנדסי.** מופע של [G1](00-constitution.md#inv-g1-נרמול-במקור-לא-תיקון-בקריאה) (נרמול במקור: המחלץ מסווג תפקיד, לא ממציא סמכות נגזירה) ו-[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים).
|
||||||
|
**מקורות:** OASIS LegalRuleML v1.0 (`appliesAuthority`/`Strength` כ-metadata אורתוגונלי, נפרד מלוגיקת-הכלל) · SemEval-2023 Task 6 LegalEval (rhetorical-roles לפי תפקיד, סמכות נשמרת בנפרד) · Bluebook signals (משקל-סמכות = ציר נפרד מהפרופוזיציה) | סטטוס: verified (≥3 מקורות).
|
||||||
|
**ההפרה שתוקנה:** `halacha_extractor` סיווג `rule_type` לפי bindingness-של-המקור (`_coerce_halacha(is_binding)`, ברירת-מחדל `binding`/`persuasive`, guard binding→persuasive) — כלומר חישב **סמכות** במסווה של **תפקיד**. אומת אמפירית על מדגם-הזהב: `binding` שימש 19/19 פסקים חיצוניים ו-0 ועדות; `persuasive` 13/13 ועדות ו-0 חיצוניים → סיווג-לפי-מקור, התאמה לתיוג-אנושי 58% בלבד. התיקון מעביר סמכות לציר-נגזר ומשחרר את ה-LLM לסווג תפקיד נטו.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. מצב קיים מול יעד — audit-findings
|
## 4. מצב קיים מול יעד — audit-findings
|
||||||
|
|||||||
@@ -664,8 +664,10 @@ CREATE TABLE IF NOT EXISTS halachot (
|
|||||||
case_law_id UUID REFERENCES case_law(id) ON DELETE CASCADE,
|
case_law_id UUID REFERENCES case_law(id) ON DELETE CASCADE,
|
||||||
halacha_index INTEGER NOT NULL,
|
halacha_index INTEGER NOT NULL,
|
||||||
rule_statement TEXT NOT NULL,
|
rule_statement TEXT NOT NULL,
|
||||||
rule_type TEXT DEFAULT 'binding',
|
rule_type TEXT DEFAULT 'interpretive',
|
||||||
-- binding | interpretive | procedural | obiter
|
-- rule ROLE only (INV-DM7): holding | interpretive | procedural |
|
||||||
|
-- application | obiter. authority (binding/persuasive) is DERIVED
|
||||||
|
-- from case_law.precedent_level, never stored here.
|
||||||
reasoning_summary TEXT DEFAULT '',
|
reasoning_summary TEXT DEFAULT '',
|
||||||
supporting_quote TEXT NOT NULL,
|
supporting_quote TEXT NOT NULL,
|
||||||
page_reference TEXT DEFAULT '',
|
page_reference TEXT DEFAULT '',
|
||||||
@@ -4052,7 +4054,7 @@ async def store_halachot(case_law_id: UUID, halachot: list[dict]) -> int:
|
|||||||
case_law_id,
|
case_law_id,
|
||||||
i,
|
i,
|
||||||
h["rule_statement"],
|
h["rule_statement"],
|
||||||
h.get("rule_type", "binding"),
|
h.get("rule_type", "interpretive"),
|
||||||
h.get("reasoning_summary", ""),
|
h.get("reasoning_summary", ""),
|
||||||
h["supporting_quote"],
|
h["supporting_quote"],
|
||||||
h.get("page_reference", ""),
|
h.get("page_reference", ""),
|
||||||
@@ -4193,7 +4195,7 @@ async def store_halachot_for_chunk(
|
|||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,
|
||||||
$12, $13, $14, $15, $16, {reviewed_at_clause})""",
|
$12, $13, $14, $15, $16, {reviewed_at_clause})""",
|
||||||
case_law_id, base + inserted, h["rule_statement"],
|
case_law_id, base + inserted, h["rule_statement"],
|
||||||
h.get("rule_type", "binding"), h.get("reasoning_summary", ""),
|
h.get("rule_type", "interpretive"), h.get("reasoning_summary", ""),
|
||||||
h["supporting_quote"], h.get("page_reference", ""),
|
h["supporting_quote"], h.get("page_reference", ""),
|
||||||
h.get("practice_areas", []), h.get("subject_tags", []),
|
h.get("practice_areas", []), h.get("subject_tags", []),
|
||||||
h.get("cites", []), confidence, h.get("quote_verified", False),
|
h.get("cites", []), confidence, h.get("quote_verified", False),
|
||||||
@@ -4299,6 +4301,8 @@ async def list_halachot(
|
|||||||
d = dict(r)
|
d = dict(r)
|
||||||
if d.get("decision_date") is not None:
|
if d.get("decision_date") is not None:
|
||||||
d["decision_date"] = d["decision_date"].isoformat()
|
d["decision_date"] = d["decision_date"].isoformat()
|
||||||
|
# authority is DERIVED from the source, never stored (INV-DM7)
|
||||||
|
d["authority"] = halacha_quality.derive_authority(d.get("precedent_level"))
|
||||||
out.append(d)
|
out.append(d)
|
||||||
if cluster and out:
|
if cluster and out:
|
||||||
await _annotate_clusters(pool, out)
|
await _annotate_clusters(pool, out)
|
||||||
@@ -4721,7 +4725,7 @@ async def goldset_list(batch: str = "default") -> list[dict]:
|
|||||||
" g.ai_is_holding, g.ai_correct_type, g.ai_rationale, g.ai_generated_at, "
|
" g.ai_is_holding, g.ai_correct_type, g.ai_rationale, g.ai_generated_at, "
|
||||||
" h.rule_statement, h.supporting_quote, h.reasoning_summary, "
|
" h.rule_statement, h.supporting_quote, h.reasoning_summary, "
|
||||||
" h.rule_type, h.confidence, h.quality_flags, h.review_status, "
|
" h.rule_type, h.confidence, h.quality_flags, h.review_status, "
|
||||||
" cl.case_number, cl.case_name, cl.source_type "
|
" cl.case_number, cl.case_name, cl.source_type, cl.precedent_level "
|
||||||
"FROM halacha_goldset g JOIN halachot h ON h.id = g.halacha_id "
|
"FROM halacha_goldset g JOIN halachot h ON h.id = g.halacha_id "
|
||||||
"LEFT JOIN case_law cl ON cl.id = h.case_law_id "
|
"LEFT JOIN case_law cl ON cl.id = h.case_law_id "
|
||||||
"WHERE g.batch = $1 ORDER BY g.created_at, g.id", batch,
|
"WHERE g.batch = $1 ORDER BY g.created_at, g.id", batch,
|
||||||
@@ -4735,6 +4739,8 @@ async def goldset_list(batch: str = "default") -> list[dict]:
|
|||||||
d["ai_generated_at"] = d["ai_generated_at"].isoformat()
|
d["ai_generated_at"] = d["ai_generated_at"].isoformat()
|
||||||
if d.get("confidence") is not None:
|
if d.get("confidence") is not None:
|
||||||
d["confidence"] = float(d["confidence"])
|
d["confidence"] = float(d["confidence"])
|
||||||
|
# authority is DERIVED from the source, never stored (INV-DM7)
|
||||||
|
d["authority"] = halacha_quality.derive_authority(d.get("precedent_level"))
|
||||||
out.append(d)
|
out.append(d)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -4792,7 +4798,7 @@ async def goldset_score(batch: str = "default") -> dict:
|
|||||||
for r in labeled:
|
for r in labeled:
|
||||||
rule = r.get("rule_statement") or ""
|
rule = r.get("rule_statement") or ""
|
||||||
quote = r.get("supporting_quote") or ""
|
quote = r.get("supporting_quote") or ""
|
||||||
rtype = r.get("rule_type") or "binding"
|
rtype = r.get("rule_type") or "interpretive"
|
||||||
qc = r["quote_complete"] if r["quote_complete"] is not None else True
|
qc = r["quote_complete"] if r["quote_complete"] is not None else True
|
||||||
truly_bad = r["is_holding"] is False
|
truly_bad = r["is_holding"] is False
|
||||||
flags = halacha_quality.compute_quality_flags(rule, quote, "", qc, rtype)
|
flags = halacha_quality.compute_quality_flags(rule, quote, "", qc, rtype)
|
||||||
@@ -4990,6 +4996,8 @@ async def search_precedent_library_semantic(
|
|||||||
_conf = float(d.get("confidence") or 0.0)
|
_conf = float(d.get("confidence") or 0.0)
|
||||||
d["score"] = float(d["score"]) + max(_conf * 0.06, 0.0)
|
d["score"] = float(d["score"]) + max(_conf * 0.06, 0.0)
|
||||||
d["type"] = "halacha"
|
d["type"] = "halacha"
|
||||||
|
# authority is DERIVED from the source, never stored (INV-DM7)
|
||||||
|
d["authority"] = halacha_quality.derive_authority(d.get("precedent_level"))
|
||||||
results.append(d)
|
results.append(d)
|
||||||
|
|
||||||
rows = await pool.fetch(chunk_sql, *c_params)
|
rows = await pool.fetch(chunk_sql, *c_params)
|
||||||
|
|||||||
@@ -76,8 +76,12 @@ EXTRACTABLE_SECTIONS = ("legal_analysis", "ruling", "conclusion")
|
|||||||
# wants to be able to cite "another committee reached the same conclusion"
|
# wants to be able to cite "another committee reached the same conclusion"
|
||||||
# even though it is not binding.
|
# even though it is not binding.
|
||||||
#
|
#
|
||||||
# The schema's rule_type field accepts six values:
|
# The prompt branches on is_binding only to choose the EXTRACTION STRATEGY
|
||||||
# binding | interpretive | procedural | obiter | application | persuasive
|
# (what to pull, how to phrase) — NOT the rule_type. rule_type is the rule
|
||||||
|
# ROLE and uses the SAME five values for both sources (INV-DM7):
|
||||||
|
# holding | interpretive | procedural | application | obiter
|
||||||
|
# The authority axis (binding/persuasive) is derived from the source, never
|
||||||
|
# a rule_type value — so the model never classifies it.
|
||||||
|
|
||||||
HALACHA_EXTRACTION_PROMPT_BINDING = """אתה משפטן בכיר המתמחה בדיני תכנון ובניה (ועדות ערר, היטל השבחה, פיצויים לפי סעיף 197 לחוק התכנון והבניה). תפקידך: לחלץ הלכות מחייבות מתוך פסק דין/החלטה משפטית של ערכאה עליונה (עליון / מנהלי).
|
HALACHA_EXTRACTION_PROMPT_BINDING = """אתה משפטן בכיר המתמחה בדיני תכנון ובניה (ועדות ערר, היטל השבחה, פיצויים לפי סעיף 197 לחוק התכנון והבניה). תפקידך: לחלץ הלכות מחייבות מתוך פסק דין/החלטה משפטית של ערכאה עליונה (עליון / מנהלי).
|
||||||
|
|
||||||
@@ -101,10 +105,12 @@ HALACHA_EXTRACTION_PROMPT_BINDING = """אתה משפטן בכיר המתמחה
|
|||||||
|
|
||||||
הלכה אחת יכולה לחול על כמה תחומים — practice_areas הוא array ולא string יחיד.
|
הלכה אחת יכולה לחול על כמה תחומים — practice_areas הוא array ולא string יחיד.
|
||||||
|
|
||||||
## סוגי הלכה (rule_type)
|
## סוג הכלל (rule_type) — מהות הכלל בלבד, לא סמכות-המקור
|
||||||
- binding — הלכה מחייבת שהוחלה על התיק.
|
**אל תסווג "מחייב/משכנע"** — דרגת-המחייבות נגזרת אוטומטית מזהות הערכאה. כאן בחר רק את **סוג הכלל**:
|
||||||
- interpretive — פרשנות סעיף חוק/תכנית שאומצה.
|
- holding — עיקרון מהותי שהיה הכרחי להכרעה (ה-ratio; מבחן Wambaugh: שלילתו הייתה משנה את התוצאה).
|
||||||
- procedural — כלל פרוצדורלי (סמכות, מועדים, הליכי שמיעה).
|
- interpretive — פרשנות הוראת-חוק/מונח/תכנית שאומצה.
|
||||||
|
- procedural — כלל סדר-דין (סמכות, מועדים, זכות-עמידה, מיצוי הליכים, נטל).
|
||||||
|
- application — החלת כלל על עובדות התיק (תלוי-עובדות; לרוב לא-הלכה בת-הכללה).
|
||||||
- obiter — אמרת אגב חשובה (חלץ רק אם משמעותית; סמן confidence נמוך).
|
- obiter — אמרת אגב חשובה (חלץ רק אם משמעותית; סמן confidence נמוך).
|
||||||
|
|
||||||
## פלט נדרש
|
## פלט נדרש
|
||||||
@@ -112,7 +118,7 @@ HALACHA_EXTRACTION_PROMPT_BINDING = """אתה משפטן בכיר המתמחה
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"rule_statement": "ניסוח הכלל בלשון משפטית מדויקת בגוף שלישי, 1-3 משפטים.",
|
"rule_statement": "ניסוח הכלל בלשון משפטית מדויקת בגוף שלישי, 1-3 משפטים.",
|
||||||
"rule_type": "binding",
|
"rule_type": "holding",
|
||||||
"reasoning_summary": "תמצית ההיגיון: למה בית המשפט הגיע לכלל הזה (1-2 משפטים).",
|
"reasoning_summary": "תמצית ההיגיון: למה בית המשפט הגיע לכלל הזה (1-2 משפטים).",
|
||||||
"supporting_quote": "ציטוט מילולי מדויק מהפסק התומך בכלל. חייב להופיע מילה במילה בטקסט הקלט.",
|
"supporting_quote": "ציטוט מילולי מדויק מהפסק התומך בכלל. חייב להופיע מילה במילה בטקסט הקלט.",
|
||||||
"page_reference": "פס' 12 / עמ' 8 — ככל שניתן לזהות מהקלט.",
|
"page_reference": "פס' 12 / עמ' 8 — ככל שניתן לזהות מהקלט.",
|
||||||
@@ -139,11 +145,11 @@ HALACHA_EXTRACTION_PROMPT_PERSUASIVE = """אתה משפטן בכיר המתמח
|
|||||||
|
|
||||||
המקור הזה **אינו** מקור להלכות מחייבות חדשות (binding rules). הלכות מחייבות מגיעות מהעליון/מנהלי. עם זאת, יש כאן ערך משמעותי שצריך לחלץ — איך הפנל הזה ניתח ויישם את הדין הקיים. כשנכתוב החלטה עתידית, נצטט מהמקור הזה כ"גם ועדת הערר ב-X הגיעה למסקנה דומה" — לא כסמכות מחייבת, אלא כתמיכה משכנעת.
|
המקור הזה **אינו** מקור להלכות מחייבות חדשות (binding rules). הלכות מחייבות מגיעות מהעליון/מנהלי. עם זאת, יש כאן ערך משמעותי שצריך לחלץ — איך הפנל הזה ניתח ויישם את הדין הקיים. כשנכתוב החלטה עתידית, נצטט מהמקור הזה כ"גם ועדת הערר ב-X הגיעה למסקנה דומה" — לא כסמכות מחייבת, אלא כתמיכה משכנעת.
|
||||||
|
|
||||||
**יש לחלץ:**
|
**יש לחלץ** (סווג לפי **סוג הכלל** בלבד — אל תסווג "מחייב/משכנע", דרגת-המחייבות נגזרת אוטומטית):
|
||||||
- **יישום של הלכה ידועה** (rule_type=`application`) — הפנל החיל הלכה ידועה (של עליון/מנהלי) על עובדות הנידונות. תצטט את ניסוח הכלל **כפי שהוצג כאן** (לא בהכרח כפי שנקבע במקור) ואת התוצאה.
|
- **יישום של הלכה ידועה** (rule_type=`application`) — הפנל החיל הלכה ידועה (של עליון/מנהלי) על עובדות הנידונות. תצטט את ניסוח הכלל **כפי שהוצג כאן** (לא בהכרח כפי שנקבע במקור) ואת התוצאה.
|
||||||
- **עקרון פרשני שאומץ** (rule_type=`interpretive`) — איך הפנל פירש סעיף חוק / תכנית, באופן שניתן לאמץ.
|
- **עקרון פרשני שאומץ** (rule_type=`interpretive`) — איך הפנל פירש סעיף חוק / תכנית, באופן שניתן לאמץ.
|
||||||
- **כלל פרוצדורלי** (rule_type=`procedural`) — קביעות בנושאי סמכות, מועדים, הליך.
|
- **כלל פרוצדורלי** (rule_type=`procedural`) — קביעות בנושאי סמכות, מועדים, הליך.
|
||||||
- **מסקנה מנומקת ומשכנעת** (rule_type=`persuasive`) — מסקנה שלמה של הפנל בסוגיה, עם ההיגיון התומך, ניתנת לציטוט כאסמכתא משכנעת.
|
- **מסקנה מהותית מנומקת** (rule_type=`holding`) — מסקנה עקרונית שלמה של הפנל בסוגיה, עם ההיגיון התומך, בת-הכללה ובת-הסתמכות.
|
||||||
|
|
||||||
**אין לחלץ:**
|
**אין לחלץ:**
|
||||||
- ממצאים עובדתיים ספציפיים לתיק או יישום על נסיבות התיק ("העורר לא הוכיח X", "במקרה דנן", שמות צדדים, סכומים קונקרטיים) — חלץ את העיקרון/היישום בניסוח בר-הכללה בלבד.
|
- ממצאים עובדתיים ספציפיים לתיק או יישום על נסיבות התיק ("העורר לא הוכיח X", "במקרה דנן", שמות צדדים, סכומים קונקרטיים) — חלץ את העיקרון/היישום בניסוח בר-הכללה בלבד.
|
||||||
@@ -175,7 +181,7 @@ HALACHA_EXTRACTION_PROMPT_PERSUASIVE = """אתה משפטן בכיר המתמח
|
|||||||
## כללי איכות
|
## כללי איכות
|
||||||
1. **נאמנות מוחלטת לציטוט** — supporting_quote חייב להיות הדבקה מדויקת מהקלט. אם אין ציטוט מתאים — אל תוסיף את ההלכה.
|
1. **נאמנות מוחלטת לציטוט** — supporting_quote חייב להיות הדבקה מדויקת מהקלט. אם אין ציטוט מתאים — אל תוסיף את ההלכה.
|
||||||
2. **מספר הלכות** — החלטה ארוכה של ועדת ערר יכולה להניב 2-8 פריטים (יישומים + מסקנות). אל תמתח את הרשימה. אם אין מה לחלץ — החזר [].
|
2. **מספר הלכות** — החלטה ארוכה של ועדת ערר יכולה להניב 2-8 פריטים (יישומים + מסקנות). אל תמתח את הרשימה. אם אין מה לחלץ — החזר [].
|
||||||
3. **rule_type מדויק** — application = יישום הלכה ידועה. interpretive = פרשנות. procedural = פרוצדורה. persuasive = מסקנה כללית בעלת ערך כאסמכתא.
|
3. **rule_type מדויק (סוג הכלל בלבד)** — application = יישום הלכה ידועה. interpretive = פרשנות. procedural = פרוצדורה. holding = מסקנה מהותית עקרונית. **לא** binding/persuasive (סמכות נגזרת אוטומטית).
|
||||||
4. **לא לפצל יתר על המידה — קריטי** — כל פריט = שאלה משפטית מובחנת אחת. פנים שונים של אותה שאלה = פריט אחד (בחר את הניסוח הכללי ביותר). אל תחזיר את אותו עיקרון בכמה ניסוחים.
|
4. **לא לפצל יתר על המידה — קריטי** — כל פריט = שאלה משפטית מובחנת אחת. פנים שונים של אותה שאלה = פריט אחד (בחר את הניסוח הכללי ביותר). אל תחזיר את אותו עיקרון בכמה ניסוחים.
|
||||||
5. **שפה** — עברית משפטית מקצועית, גוף שלישי.
|
5. **שפה** — עברית משפטית מקצועית, גוף שלישי.
|
||||||
6. **subject_tags** — 2-5 תגיות בעברית, snake_case.
|
6. **subject_tags** — 2-5 תגיות בעברית, snake_case.
|
||||||
@@ -184,10 +190,15 @@ HALACHA_EXTRACTION_PROMPT_PERSUASIVE = """אתה משפטן בכיר המתמח
|
|||||||
|
|
||||||
|
|
||||||
_VALID_PRACTICE_AREAS = {"rishuy_uvniya", "betterment_levy", "compensation_197"}
|
_VALID_PRACTICE_AREAS = {"rishuy_uvniya", "betterment_levy", "compensation_197"}
|
||||||
|
# rule_type holds the rule ROLE only — what KIND of statement it is (INV-DM7).
|
||||||
|
# The authority axis (binding/persuasive) is DERIVED from the source, never a
|
||||||
|
# rule_type value: see halacha_quality.derive_authority.
|
||||||
_VALID_RULE_TYPES = {
|
_VALID_RULE_TYPES = {
|
||||||
"binding", "interpretive", "procedural", "obiter",
|
"holding", "interpretive", "procedural", "application", "obiter",
|
||||||
"application", "persuasive",
|
|
||||||
}
|
}
|
||||||
|
# Legacy authority-as-role values → fold to the nearest genuine role. Kept so
|
||||||
|
# old LLM outputs (and pre-split rows re-fed) coerce safely.
|
||||||
|
_LEGACY_RULE_TYPE_FOLD = {"binding": "holding", "persuasive": "interpretive"}
|
||||||
|
|
||||||
|
|
||||||
def _normalize_for_comparison(text: str) -> str:
|
def _normalize_for_comparison(text: str) -> str:
|
||||||
@@ -227,13 +238,14 @@ def _verify_quote(supporting_quote: str, full_text: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _coerce_halacha(raw: dict, is_binding: bool = True) -> dict | None:
|
def _coerce_halacha(raw: dict) -> dict | None:
|
||||||
"""Validate and normalize one LLM-returned halacha dict.
|
"""Validate and normalize one LLM-returned halacha dict.
|
||||||
|
|
||||||
Returns ``None`` if the entry is missing required fields. ``is_binding``
|
Returns ``None`` if the entry is missing required fields. ``rule_type`` is
|
||||||
only affects the default rule_type when the LLM returned an unknown
|
the rule ROLE only (INV-DM7) — it is NEVER defaulted from the source's
|
||||||
value — for binding sources we default to ``binding``, otherwise to
|
bindingness (that was the source-conflation this split removed). Legacy
|
||||||
``persuasive`` (never pretend an appeals committee created halacha).
|
authority values fold to the nearest role; unknown defaults to
|
||||||
|
``interpretive`` (the most common role).
|
||||||
"""
|
"""
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
return None
|
return None
|
||||||
@@ -242,13 +254,10 @@ def _coerce_halacha(raw: dict, is_binding: bool = True) -> dict | None:
|
|||||||
if not rule_statement or not supporting_quote:
|
if not rule_statement or not supporting_quote:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
default_rule_type = "binding" if is_binding else "persuasive"
|
rule_type = (raw.get("rule_type") or "").strip().lower()
|
||||||
rule_type = (raw.get("rule_type") or default_rule_type).strip().lower()
|
rule_type = _LEGACY_RULE_TYPE_FOLD.get(rule_type, rule_type)
|
||||||
if rule_type not in _VALID_RULE_TYPES:
|
if rule_type not in _VALID_RULE_TYPES:
|
||||||
rule_type = default_rule_type
|
rule_type = "interpretive"
|
||||||
# Guard: don't let a non-binding source produce 'binding' rule_type
|
|
||||||
if not is_binding and rule_type == "binding":
|
|
||||||
rule_type = "persuasive"
|
|
||||||
|
|
||||||
practice_areas_raw = raw.get("practice_areas") or []
|
practice_areas_raw = raw.get("practice_areas") or []
|
||||||
if isinstance(practice_areas_raw, str):
|
if isinstance(practice_areas_raw, str):
|
||||||
@@ -580,7 +589,7 @@ async def _extract_impl(case_law_id: UUID, force: bool = False,
|
|||||||
return
|
return
|
||||||
cleaned: list[dict] = []
|
cleaned: list[dict] = []
|
||||||
for raw in items:
|
for raw in items:
|
||||||
coerced = _coerce_halacha(raw, is_binding=is_binding)
|
coerced = _coerce_halacha(raw)
|
||||||
if coerced is None:
|
if coerced is None:
|
||||||
continue
|
continue
|
||||||
coerced["quote_verified"] = _verify_quote(
|
coerced["quote_verified"] = _verify_quote(
|
||||||
@@ -597,10 +606,10 @@ async def _extract_impl(case_law_id: UUID, force: bool = False,
|
|||||||
coerced["quality_flags"] = flags
|
coerced["quality_flags"] = flags
|
||||||
if halacha_quality.FLAG_NON_DECISION in flags and coerced["rule_type"] != "obiter":
|
if halacha_quality.FLAG_NON_DECISION in flags and coerced["rule_type"] != "obiter":
|
||||||
coerced["rule_type"] = "obiter"
|
coerced["rule_type"] = "obiter"
|
||||||
# #81.4 — a binding-labeled rule that reads as a case-application is
|
# #81.4 — a holding-labeled rule that reads as a case-application is
|
||||||
# re-typed application (it carries FLAG_APPLICATION either way).
|
# re-typed application (it carries FLAG_APPLICATION either way).
|
||||||
elif (halacha_quality.FLAG_APPLICATION in flags
|
elif (halacha_quality.FLAG_APPLICATION in flags
|
||||||
and coerced["rule_type"] == "binding"):
|
and coerced["rule_type"] == "holding"):
|
||||||
coerced["rule_type"] = "application"
|
coerced["rule_type"] = "application"
|
||||||
cleaned.append(coerced)
|
cleaned.append(coerced)
|
||||||
# #81.3 NLI entailment — one batched judge call per chunk (fail-open).
|
# #81.3 NLI entailment — one batched judge call per chunk (fail-open).
|
||||||
|
|||||||
@@ -18,6 +18,37 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
# ── Authority axis — DERIVED from the source, never LLM-classified (INV-DM7) ──
|
||||||
|
#
|
||||||
|
# A halacha's *authority* (binding vs persuasive) is a property of WHERE it came
|
||||||
|
# from, not of the rule's content. It is therefore derived deterministically
|
||||||
|
# from ``case_law.precedent_level`` and never stored on ``halachot`` or guessed
|
||||||
|
# by the extractor — keeping it orthogonal to ``rule_type`` (the rule ROLE).
|
||||||
|
# Higher courts (עליון/מנהלי) bind the appeals committee; another committee is
|
||||||
|
# only persuasive. See docs/spec/02-data-model.md INV-DM7.
|
||||||
|
|
||||||
|
AUTHORITY_BINDING = "binding"
|
||||||
|
AUTHORITY_PERSUASIVE = "persuasive"
|
||||||
|
|
||||||
|
_BINDING_LEVELS = {"עליון", "מנהלי"}
|
||||||
|
_PERSUASIVE_LEVELS = {"ועדת_ערר_מחוזית"}
|
||||||
|
|
||||||
|
|
||||||
|
def derive_authority(precedent_level: str | None) -> str | None:
|
||||||
|
"""Map a source's precedent_level to its authority over the committee.
|
||||||
|
|
||||||
|
Returns ``"binding"`` for higher courts (עליון/מנהלי), ``"persuasive"`` for
|
||||||
|
another appeals committee (ועדת_ערר_מחוזית), or ``None`` when the level is
|
||||||
|
unknown/empty (never guesses). Pure — the single source of truth for the
|
||||||
|
authority axis (INV-DM7).
|
||||||
|
"""
|
||||||
|
level = (precedent_level or "").strip()
|
||||||
|
if level in _BINDING_LEVELS:
|
||||||
|
return AUTHORITY_BINDING
|
||||||
|
if level in _PERSUASIVE_LEVELS:
|
||||||
|
return AUTHORITY_PERSUASIVE
|
||||||
|
return None
|
||||||
|
|
||||||
# ── Hebrew text normalization (shared with the extractor's quote check) ──
|
# ── Hebrew text normalization (shared with the extractor's quote check) ──
|
||||||
|
|
||||||
_HEB_QUOTE_VARIANTS = "\"'׳״‘’“”«»„′″"
|
_HEB_QUOTE_VARIANTS = "\"'׳״‘’“”«»„′″"
|
||||||
@@ -337,7 +368,7 @@ def compute_quality_flags(
|
|||||||
supporting_quote: str,
|
supporting_quote: str,
|
||||||
reasoning_summary: str = "",
|
reasoning_summary: str = "",
|
||||||
quote_verified: bool = True,
|
quote_verified: bool = True,
|
||||||
rule_type: str = "binding",
|
rule_type: str = "interpretive",
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""Return the list of quality flags for one halacha (empty == clean).
|
"""Return the list of quality flags for one halacha (empty == clean).
|
||||||
|
|
||||||
|
|||||||
46
mcp-server/tests/test_halacha_coerce.py
Normal file
46
mcp-server/tests/test_halacha_coerce.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""rule_type coercion after the authority/role split (INV-DM7).
|
||||||
|
|
||||||
|
The extractor's rule_type holds the rule ROLE only — it is never defaulted from
|
||||||
|
the source's bindingness. Legacy authority values fold to the nearest role.
|
||||||
|
"""
|
||||||
|
from legal_mcp.services.halacha_extractor import (
|
||||||
|
_LEGACY_RULE_TYPE_FOLD,
|
||||||
|
_VALID_RULE_TYPES,
|
||||||
|
_coerce_halacha,
|
||||||
|
)
|
||||||
|
|
||||||
|
_BASE = {"rule_statement": "כלל כלשהו", "supporting_quote": "ציטוט תומך כלשהו"}
|
||||||
|
|
||||||
|
|
||||||
|
def _rt(rule_type):
|
||||||
|
return _coerce_halacha({**_BASE, "rule_type": rule_type})["rule_type"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_roles_are_the_five_roles_only():
|
||||||
|
assert _VALID_RULE_TYPES == {
|
||||||
|
"holding", "interpretive", "procedural", "application", "obiter",
|
||||||
|
}
|
||||||
|
assert "binding" not in _VALID_RULE_TYPES
|
||||||
|
assert "persuasive" not in _VALID_RULE_TYPES
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_authority_values_fold_to_a_role():
|
||||||
|
assert _rt("binding") == "holding"
|
||||||
|
assert _rt("persuasive") == "interpretive"
|
||||||
|
assert _LEGACY_RULE_TYPE_FOLD == {"binding": "holding", "persuasive": "interpretive"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_genuine_roles_pass_through():
|
||||||
|
for role in ("holding", "interpretive", "procedural", "application", "obiter"):
|
||||||
|
assert _rt(role) == role
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_or_missing_defaults_to_interpretive():
|
||||||
|
assert _rt("nonsense") == "interpretive"
|
||||||
|
assert _coerce_halacha(_BASE)["rule_type"] == "interpretive"
|
||||||
|
|
||||||
|
|
||||||
|
def test_coerce_rejects_rows_missing_required_fields():
|
||||||
|
assert _coerce_halacha({"rule_statement": "x"}) is None
|
||||||
|
assert _coerce_halacha({"supporting_quote": "y"}) is None
|
||||||
|
assert _coerce_halacha("not a dict") is None
|
||||||
@@ -211,23 +211,40 @@ def test_application_flag_from_rule_type():
|
|||||||
assert hq.FLAG_APPLICATION in flags
|
assert hq.FLAG_APPLICATION in flags
|
||||||
|
|
||||||
|
|
||||||
def test_application_flag_from_deixis_even_if_binding():
|
def test_application_flag_from_deixis_even_if_holding():
|
||||||
flags = hq.compute_quality_flags(
|
flags = hq.compute_quality_flags(
|
||||||
"במקרה דנן נדחה הערר", "כפי שקבענו במקרה דנן נדחה הערר",
|
"במקרה דנן נדחה הערר", "כפי שקבענו במקרה דנן נדחה הערר",
|
||||||
rule_type="binding",
|
rule_type="holding",
|
||||||
)
|
)
|
||||||
assert hq.FLAG_APPLICATION in flags
|
assert hq.FLAG_APPLICATION in flags
|
||||||
|
|
||||||
|
|
||||||
def test_clean_binding_rule_has_no_flags():
|
def test_clean_holding_rule_has_no_flags():
|
||||||
flags = hq.compute_quality_flags(
|
flags = hq.compute_quality_flags(
|
||||||
"ועדת הערר מוסמכת לדון בטענות חוקתיות הנוגעות לתכנית",
|
"ועדת הערר מוסמכת לדון בטענות חוקתיות הנוגעות לתכנית",
|
||||||
"הוועדה מוסמכת לדון אף בטענות מסוג זה, ככל שהן נוגעות לתכנית שבנדון.",
|
"הוועדה מוסמכת לדון אף בטענות מסוג זה, ככל שהן נוגעות לתכנית שבנדון.",
|
||||||
rule_type="binding",
|
rule_type="holding",
|
||||||
)
|
)
|
||||||
assert flags == []
|
assert flags == []
|
||||||
|
|
||||||
|
|
||||||
|
# ── INV-DM7: authority is DERIVED from the source, never a rule_type value ──
|
||||||
|
|
||||||
|
def test_derive_authority_binding_for_higher_courts():
|
||||||
|
assert hq.derive_authority("עליון") == "binding"
|
||||||
|
assert hq.derive_authority("מנהלי") == "binding"
|
||||||
|
|
||||||
|
|
||||||
|
def test_derive_authority_persuasive_for_committee():
|
||||||
|
assert hq.derive_authority("ועדת_ערר_מחוזית") == "persuasive"
|
||||||
|
|
||||||
|
|
||||||
|
def test_derive_authority_none_for_unknown_or_empty():
|
||||||
|
assert hq.derive_authority("") is None
|
||||||
|
assert hq.derive_authority(None) is None
|
||||||
|
assert hq.derive_authority("משהו אחר") is None
|
||||||
|
|
||||||
|
|
||||||
# ── #82.3 lexical near-duplicate signal ──
|
# ── #82.3 lexical near-duplicate signal ──
|
||||||
|
|
||||||
def test_jaccard_high_for_reworded_same_rule():
|
def test_jaccard_high_for_reworded_same_rule():
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
| `nevo_ratio_benchmark.py` | python | **#86.3** — מדידת איכות חילוץ-הלכות מול ה-מיני-רציו של נבו (gold-set מקצועי חינמי). לכל פסק עם `nevo_ratio` (או נגזר מ-`full_text` אם טרם בוצע backfill): LLM-judge מקומי (`claude_session`, אפס עלות) ממפה סמנטית את הלכות-המערכת מול הלכות-נבו ומפיק **recall** (כיסוי הלכות-נבו), **precision** (אחוז הלכותינו הממופות), **granularity** (יחס פירוק — איתות over-extraction ל-#81.5). `--case <num>` / `--all [--limit N]` / `--model` / `--out`. כותב CSV ל-`data/audit/`. רץ עם venv של mcp-server (דורש Claude CLI מקומי). אומת על בג"ץ 1764/05: recall 0.875, precision 1.0, granularity 1.75x | ידני — מדידת-איכות (CI/ad-hoc) |
|
| `nevo_ratio_benchmark.py` | python | **#86.3** — מדידת איכות חילוץ-הלכות מול ה-מיני-רציו של נבו (gold-set מקצועי חינמי). לכל פסק עם `nevo_ratio` (או נגזר מ-`full_text` אם טרם בוצע backfill): LLM-judge מקומי (`claude_session`, אפס עלות) ממפה סמנטית את הלכות-המערכת מול הלכות-נבו ומפיק **recall** (כיסוי הלכות-נבו), **precision** (אחוז הלכותינו הממופות), **granularity** (יחס פירוק — איתות over-extraction ל-#81.5). `--case <num>` / `--all [--limit N]` / `--model` / `--out`. כותב CSV ל-`data/audit/`. רץ עם venv של mcp-server (דורש Claude CLI מקומי). אומת על בג"ץ 1764/05: recall 0.875, precision 1.0, granularity 1.75x | ידני — מדידת-איכות (CI/ad-hoc) |
|
||||||
| `halacha_goldset.py` | python | **#81.7** — הארנס gold-set לאיכות חילוץ-הלכות. `export --n N` מייצא מדגם מרובד (לפי precedent×rule_type) ל-CSV עם עמודות-תיוג ריקות (`is_holding`/`correct_type`/`quote_complete`) לתיוג ידני (חיים/דפנה). `score --in <csv>` קורא את ה-CSV המתויג ומודד כל ולידטור (`compute_quality_flags`/`is_fact_dependent`/`is_quote_truncated`/`is_thin_restatement`) מול אמת-המידה האנושית: P/R/F1 + confusion. בסיס ל-#81.8 (כיול סף האישור). מייבא את אותם ולידטורים שה-extractor מריץ. רץ עם venv של mcp-server. **הערה:** קיים גם דף-תיוג אינטראקטיבי DB-backed (`/goldset`) — זה ה-CSV-fallback | ידני — export→תיוג→score |
|
| `halacha_goldset.py` | python | **#81.7** — הארנס gold-set לאיכות חילוץ-הלכות. `export --n N` מייצא מדגם מרובד (לפי precedent×rule_type) ל-CSV עם עמודות-תיוג ריקות (`is_holding`/`correct_type`/`quote_complete`) לתיוג ידני (חיים/דפנה). `score --in <csv>` קורא את ה-CSV המתויג ומודד כל ולידטור (`compute_quality_flags`/`is_fact_dependent`/`is_quote_truncated`/`is_thin_restatement`) מול אמת-המידה האנושית: P/R/F1 + confusion. בסיס ל-#81.8 (כיול סף האישור). מייבא את אותם ולידטורים שה-extractor מריץ. רץ עם venv של mcp-server. **הערה:** קיים גם דף-תיוג אינטראקטיבי DB-backed (`/goldset`) — זה ה-CSV-fallback | ידני — export→תיוג→score |
|
||||||
| `goldset_ai_recommend.py` | python | **#81.7 QA** — מייצר **חוות-דעת-AI שנייה** (claude מקומי, אפס עלות) לכל פריט ב-`halacha_goldset`: `is_holding`+`type`+נימוק, נשמר ב-`ai_*` ומוצג בדף לצד התיוג האנושי לזיהוי אי-הסכמות. **עצמאי** מהוולידטורים שנמדדים (אין מעגליות) ו**לא** מוחל אוטומטית. `--force` (חידוש)/`--limit N`. **חובה מקומי** (claude_session). | ידני — לאחר יצירת/הרחבת batch |
|
| `goldset_ai_recommend.py` | python | **#81.7 QA** — מייצר **חוות-דעת-AI שנייה** (claude מקומי, אפס עלות) לכל פריט ב-`halacha_goldset`: `is_holding`+`type`+נימוק, נשמר ב-`ai_*` ומוצג בדף לצד התיוג האנושי לזיהוי אי-הסכמות. **עצמאי** מהוולידטורים שנמדדים (אין מעגליות) ו**לא** מוחל אוטומטית. `--force` (חידוש)/`--limit N`. **חובה מקומי** (claude_session). | ידני — לאחר יצירת/הרחבת batch |
|
||||||
|
| `halacha_rule_role_backfill.py` | python | **INV-DM7** — backfill חד-פעמי: מסווג-מחדש את ההלכות הישנות (`rule_type IN ('binding','persuasive')` — ערכי-סמכות שנשמרו במסווה תפקיד לפני פיצול הצירים) לאחד מחמשת **תפקידי-הכלל** (holding/interpretive/procedural/application/obiter) דרך claude_session המקומי (אפס עלות). **לא נוגע בסמכות** (נגזרת מ-`precedent_level`). `--apply` (ברירת-מחדל dry-run) / `--limit N` / `--concurrency`. כותב backup CSV ל-`data/audit/` תחילה. fail-safe (פריט שנכשל → נשמר ערך ישן). **חובה מקומי** (claude_session). | ידני חד-פעמי אחרי deploy של פיצול-הסמכות |
|
||||||
| `halacha_batch_reconcile.py` | python | **#82.7** — dedup חוצה-פסקים offline (שמרני, **dry-run בלבד**). dedup-on-insert משווה רק תוך-פסק; כאן סף מחמיר (cosine ≥0.95, `--cosine`) ולא-הרסני: מאתר זוגות הלכות near-duplicate בין פסקים שונים (pgvector `<=>` exact) עם איתות לקסיקלי (Jaccard/Levenshtein) ומדווח ל-CSV ב-`data/audit/` לסקירת היו"ר. לא מדלג/ממזג/מוחק. `--include-pending`. **`--link`** רושם את הזוגות שנמצאו כ-`equivalent_halachot` (parallel authority, #84.2 — קישור-מקביל ברמת-הלכה, **לא** ציטוט; idempotent, לא-הרסני). רץ עם venv של mcp-server. אומת: 800 הלכות → 5 זוגות (קושרו). | ידני — דוח-סקירה / `--link` לקישור |
|
| `halacha_batch_reconcile.py` | python | **#82.7** — dedup חוצה-פסקים offline (שמרני, **dry-run בלבד**). dedup-on-insert משווה רק תוך-פסק; כאן סף מחמיר (cosine ≥0.95, `--cosine`) ולא-הרסני: מאתר זוגות הלכות near-duplicate בין פסקים שונים (pgvector `<=>` exact) עם איתות לקסיקלי (Jaccard/Levenshtein) ומדווח ל-CSV ב-`data/audit/` לסקירת היו"ר. לא מדלג/ממזג/מוחק. `--include-pending`. **`--link`** רושם את הזוגות שנמצאו כ-`equivalent_halachot` (parallel authority, #84.2 — קישור-מקביל ברמת-הלכה, **לא** ציטוט; idempotent, לא-הרסני). רץ עם venv של mcp-server. אומת: 800 הלכות → 5 זוגות (קושרו). | ידני — דוח-סקירה / `--link` לקישור |
|
||||||
| `calibrate_halacha_dedup.py` | python | **#82.1** — כיול ספי ה-dedup הלקסיקלי (#82.3) מול gold-set הניקוי. קורא `halacha-cleanup-manifest-*.csv` (זוגות duplicate↔survivor מתויגי-אדם), טוען טקסט-survivor מה-DB, ו-sweep של (jaccard_min × levenshtein_min) עם P/R/F1, מסמן את נקודת-העבודה המוגדרת. אימת ש-(0.55, 0.70) → **precision 1.0** (אפס false-merge), recall 0.30 — מתאים לאיתות-משני שחוסם auto-approve. `--manifest <path>`. רץ עם venv של mcp-server | חד-פעמי — כיול (בוצע 2026-06-06) |
|
| `calibrate_halacha_dedup.py` | python | **#82.1** — כיול ספי ה-dedup הלקסיקלי (#82.3) מול gold-set הניקוי. קורא `halacha-cleanup-manifest-*.csv` (זוגות duplicate↔survivor מתויגי-אדם), טוען טקסט-survivor מה-DB, ו-sweep של (jaccard_min × levenshtein_min) עם P/R/F1, מסמן את נקודת-העבודה המוגדרת. אימת ש-(0.55, 0.70) → **precision 1.0** (אפס false-merge), recall 0.30 — מתאים לאיתות-משני שחוסם auto-approve. `--manifest <path>`. רץ עם venv של mcp-server | חד-פעמי — כיול (בוצע 2026-06-06) |
|
||||||
| `audit_corpus_integrity.py` | python | בדיקה תקופתית של עקביות הקורפוס — 3 בדיקות SQL read-only על `case_law` ו-`cases`: (A) `external_upload` עם prefix פנימי `ערר`/`בל"מ`; (B) `internal_committee` חסר `chair_name`/`district`; (C) `cases.practice_area` מחוץ ל-{`rishuy_uvniya`, `betterment_levy`, `compensation_197`, `''`}. כותב log מצטבר ל-`data/logs/corpus_integrity_audit.log` ובמצב הפרות שולח wakeup ל-CEO ב-Paperclip (best-effort, רק אם `PAPERCLIP_API_URL`+`PAPERCLIP_API_KEY` מוגדרים). דגל: `--no-notify`. Idempotent, יוצא 0. **Cron יומי 07:00**: `0 7 * * * /home/chaim/legal-ai/mcp-server/.venv/bin/python /home/chaim/legal-ai/scripts/audit_corpus_integrity.py` | `0 7 * * *` (cron) |
|
| `audit_corpus_integrity.py` | python | בדיקה תקופתית של עקביות הקורפוס — 3 בדיקות SQL read-only על `case_law` ו-`cases`: (A) `external_upload` עם prefix פנימי `ערר`/`בל"מ`; (B) `internal_committee` חסר `chair_name`/`district`; (C) `cases.practice_area` מחוץ ל-{`rishuy_uvniya`, `betterment_levy`, `compensation_197`, `''`}. כותב log מצטבר ל-`data/logs/corpus_integrity_audit.log` ובמצב הפרות שולח wakeup ל-CEO ב-Paperclip (best-effort, רק אם `PAPERCLIP_API_URL`+`PAPERCLIP_API_KEY` מוגדרים). דגל: `--no-notify`. Idempotent, יוצא 0. **Cron יומי 07:00**: `0 7 * * * /home/chaim/legal-ai/mcp-server/.venv/bin/python /home/chaim/legal-ai/scripts/audit_corpus_integrity.py` | `0 7 * * *` (cron) |
|
||||||
|
|||||||
@@ -25,20 +25,21 @@ from uuid import UUID
|
|||||||
|
|
||||||
from legal_mcp.services import claude_session, db
|
from legal_mcp.services import claude_session, db
|
||||||
|
|
||||||
VALID_TYPES = {"binding", "interpretive", "obiter", "application", "procedural", "persuasive"}
|
VALID_TYPES = {"holding", "interpretive", "procedural", "application", "obiter"}
|
||||||
|
|
||||||
SYSTEM = (
|
SYSTEM = (
|
||||||
"אתה בוחן-איכות משפטי המסווג 'הלכות' שחולצו מהחלטות ועדת-ערר ומפסקי-דין. "
|
"אתה בוחן-איכות משפטי המסווג 'הלכות' שחולצו מהחלטות ועדת-ערר ומפסקי-דין. "
|
||||||
"לכל פריט הכרע שתי שאלות, באופן עצמאי ולפי המהות:\n"
|
"לכל פריט הכרע שתי שאלות, באופן עצמאי ולפי המהות:\n"
|
||||||
"1) is_holding — האם זו הלכה אמיתית בת-הכללה ובת-הסתמכות (true), או שזו יישום "
|
"1) is_holding — האם זו הלכה אמיתית בת-הכללה ובת-הסתמכות (true), או שזו יישום "
|
||||||
"תלוי-עובדות / אמרת-אגב / ציטוט-עובדה ולא כלל בר-הכללה (false).\n"
|
"תלוי-עובדות / אמרת-אגב / ציטוט-עובדה ולא כלל בר-הכללה (false).\n"
|
||||||
"2) type — הסוג הנכון: 'binding' (עיקרון הכרחי להכרעה), 'interpretive' (פרשנות "
|
"2) type — **סוג הכלל בלבד** (אל תסווג מחייב/משכנע — דרגת-המחייבות נגזרת אוטומטית "
|
||||||
"חוק/מונח/תכנית), 'procedural' (סדר-דין: מועדים/סמכות/מיצוי/נטל), 'persuasive' "
|
"מזהות הערכאה): 'holding' (עיקרון מהותי שהיה הכרחי להכרעה — ratio), 'interpretive' "
|
||||||
"(אסמכתה לא-מחייבת), 'application' (החלה על עובדות התיק — לרוב לא-הלכה), "
|
"(פרשנות חוק/מונח/תכנית), 'procedural' (סדר-דין: מועדים/סמכות/מיצוי/נטל), "
|
||||||
"'obiter' (אמרת-אגב שלא הוכרעה — לא-הלכה).\n"
|
"'application' (החלה על עובדות התיק — לרוב לא-הלכה), 'obiter' (אמרת-אגב שלא "
|
||||||
"עקביות: is_holding=true → binding/interpretive/procedural/persuasive; "
|
"הוכרעה — לא-הלכה).\n"
|
||||||
|
"עקביות: is_holding=true → holding/interpretive/procedural; "
|
||||||
"is_holding=false → application/obiter.\n"
|
"is_holding=false → application/obiter.\n"
|
||||||
'החזר JSON בלבד: {"is_holding": true/false, "type": "<אחד מהשישה>", '
|
'החזר JSON בלבד: {"is_holding": true/false, "type": "<אחד מהחמישה>", '
|
||||||
'"rationale": "<משפט אחד קצר בעברית>"}. ללא markdown.'
|
'"rationale": "<משפט אחד קצר בעברית>"}. ללא markdown.'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
151
scripts/halacha_rule_role_backfill.py
Normal file
151
scripts/halacha_rule_role_backfill.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""One-time backfill: recover the rule ROLE for pre-split halachot (INV-DM7).
|
||||||
|
|
||||||
|
Before the authority/role split, the extractor stored ``rule_type='binding'``
|
||||||
|
for higher-court sources and ``'persuasive'`` for committee sources — i.e. it
|
||||||
|
recorded the source's AUTHORITY in the role field. Those 276 rows therefore have
|
||||||
|
NO genuine role. This script re-classifies each into one of the five real roles
|
||||||
|
(holding/interpretive/procedural/application/obiter) using the same local
|
||||||
|
claude_session judge the gold-set trusts (zero API cost), and writes it back to
|
||||||
|
``halachot.rule_type``.
|
||||||
|
|
||||||
|
authority is NOT touched — it is derived from ``case_law.precedent_level`` at
|
||||||
|
read time and was never stored.
|
||||||
|
|
||||||
|
cd ~/legal-ai/mcp-server
|
||||||
|
.venv/bin/python ../scripts/halacha_rule_role_backfill.py --limit 5 # smoke (dry)
|
||||||
|
.venv/bin/python ../scripts/halacha_rule_role_backfill.py --apply # full backfill
|
||||||
|
|
||||||
|
Local-only (claude_session needs the local CLI, not the container).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import csv
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from legal_mcp.services import claude_session, db
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
AUDIT_DIR = REPO_ROOT / "data" / "audit"
|
||||||
|
|
||||||
|
VALID_ROLES = {"holding", "interpretive", "procedural", "application", "obiter"}
|
||||||
|
|
||||||
|
SYSTEM = (
|
||||||
|
"אתה משפטן בכיר המסווג 'הלכות' שחולצו מפסיקה לפי **סוג הכלל** בלבד "
|
||||||
|
"(אל תסווג מחייב/משכנע — דרגת-המחייבות נגזרת אוטומטית מזהות הערכאה). "
|
||||||
|
"בחר ערך אחד מתוך:\n"
|
||||||
|
"- holding — עיקרון מהותי שהיה הכרחי להכרעה (ratio; מבחן Wambaugh).\n"
|
||||||
|
"- interpretive — פרשנות הוראת-חוק/מונח/תכנית.\n"
|
||||||
|
"- procedural — סדר-דין: סמכות/מועדים/זכות-עמידה/מיצוי/נטל.\n"
|
||||||
|
"- application — החלה תלוית-עובדות על נסיבות התיק (לרוב לא-הלכה בת-הכללה).\n"
|
||||||
|
"- obiter — אמרת-אגב שלא הוכרעה.\n"
|
||||||
|
'החזר JSON בלבד: {"role": "<אחד מהחמישה>"}. ללא markdown, ללא הסבר.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _prompt(row: dict) -> str:
|
||||||
|
return (
|
||||||
|
f"מקור: {row.get('case_number') or ''} "
|
||||||
|
f"(precedent_level={row.get('precedent_level') or ''}).\n"
|
||||||
|
f"סיווג ישן (סמכות, להתעלם): {row.get('rule_type')}.\n\n"
|
||||||
|
f"ניסוח הכלל:\n{row.get('rule_statement') or ''}\n\n"
|
||||||
|
f"היגיון:\n{row.get('reasoning_summary') or ''}\n\n"
|
||||||
|
f"ציטוט תומך:\n{row.get('supporting_quote') or ''}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _classify(row: dict) -> str | None:
|
||||||
|
"""Return the role for one row, or None on failure (caller keeps old value)."""
|
||||||
|
try:
|
||||||
|
raw = await claude_session.query_json(_prompt(row), system=SYSTEM)
|
||||||
|
except Exception as e: # noqa: BLE001 — log and skip, never crash the batch
|
||||||
|
print(f" ! {row['id']}: judge error ({e}) — skipped", flush=True)
|
||||||
|
return None
|
||||||
|
role = ""
|
||||||
|
if isinstance(raw, dict):
|
||||||
|
role = str(raw.get("role") or "").strip().lower()
|
||||||
|
if role not in VALID_ROLES:
|
||||||
|
print(f" ? {row['id']}: invalid role {role!r} — skipped", flush=True)
|
||||||
|
return None
|
||||||
|
return role
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_legacy_rows() -> list[dict]:
|
||||||
|
pool = await db.get_pool()
|
||||||
|
rows = await pool.fetch(
|
||||||
|
"SELECT h.id, h.rule_type, h.rule_statement, h.reasoning_summary, "
|
||||||
|
" h.supporting_quote, cl.case_number, cl.precedent_level "
|
||||||
|
"FROM halachot h LEFT JOIN case_law cl ON cl.id = h.case_law_id "
|
||||||
|
"WHERE h.rule_type IN ('binding','persuasive') "
|
||||||
|
"ORDER BY h.case_law_id, h.halacha_index"
|
||||||
|
)
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def _backup(rows: list[dict]) -> Path:
|
||||||
|
ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||||
|
AUDIT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
out = AUDIT_DIR / f"halacha-rule-role-backfill-backup-{ts}.csv"
|
||||||
|
with out.open("w", encoding="utf-8", newline="") as f:
|
||||||
|
w = csv.writer(f)
|
||||||
|
w.writerow(["id", "old_rule_type", "case_number", "precedent_level"])
|
||||||
|
for r in rows:
|
||||||
|
w.writerow([r["id"], r["rule_type"], r.get("case_number") or "",
|
||||||
|
r.get("precedent_level") or ""])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
async def main(args: argparse.Namespace) -> int:
|
||||||
|
rows = await _fetch_legacy_rows()
|
||||||
|
if args.limit:
|
||||||
|
rows = rows[: args.limit]
|
||||||
|
print(f"legacy binding/persuasive rows to reclassify: {len(rows)}", flush=True)
|
||||||
|
if not rows:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
backup = _backup(rows)
|
||||||
|
print(f"backup written → {backup}", flush=True)
|
||||||
|
|
||||||
|
pool = await db.get_pool()
|
||||||
|
changed = skipped = 0
|
||||||
|
sem = asyncio.Semaphore(args.concurrency)
|
||||||
|
|
||||||
|
async def _one(row: dict):
|
||||||
|
nonlocal changed, skipped
|
||||||
|
async with sem:
|
||||||
|
role = await _classify(row)
|
||||||
|
if role is None:
|
||||||
|
skipped += 1
|
||||||
|
return
|
||||||
|
old = row["rule_type"]
|
||||||
|
print(f" {row.get('case_number') or '':<14} {old:>10} → {role}", flush=True)
|
||||||
|
if args.apply and role != old:
|
||||||
|
await pool.execute(
|
||||||
|
"UPDATE halachot SET rule_type = $2, updated_at = now() WHERE id = $1",
|
||||||
|
row["id"], role,
|
||||||
|
)
|
||||||
|
changed += 1
|
||||||
|
|
||||||
|
# process in chunks to bound concurrent CLI subprocesses
|
||||||
|
for i in range(0, len(rows), args.concurrency):
|
||||||
|
await asyncio.gather(*(_one(r) for r in rows[i : i + args.concurrency]))
|
||||||
|
|
||||||
|
mode = "APPLIED" if args.apply else "DRY-RUN (no writes)"
|
||||||
|
print(f"\n{mode}: {changed} reclassified, {skipped} skipped (kept old).", flush=True)
|
||||||
|
if not args.apply:
|
||||||
|
print("re-run with --apply to write changes.", flush=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("--apply", action="store_true", help="write changes (default: dry-run)")
|
||||||
|
ap.add_argument("--limit", type=int, default=0, help="only first N rows (smoke test)")
|
||||||
|
ap.add_argument("--concurrency", type=int, default=4, help="parallel judge calls")
|
||||||
|
sys.exit(asyncio.run(main(ap.parse_args())))
|
||||||
@@ -11,20 +11,22 @@ import {
|
|||||||
useGoldset, useGoldsetScore, useTagGoldset, useCreateGoldsetSample,
|
useGoldset, useGoldsetScore, useTagGoldset, useCreateGoldsetSample,
|
||||||
type GoldsetItem,
|
type GoldsetItem,
|
||||||
} from "@/lib/api/goldset";
|
} from "@/lib/api/goldset";
|
||||||
|
import { AuthorityBadge } from "@/components/precedents/halacha-meta";
|
||||||
|
|
||||||
|
// rule ROLE only (INV-DM7) — authority (binding/persuasive) is a SEPARATE,
|
||||||
|
// derived axis, shown read-only and never tagged here.
|
||||||
const TYPES: { value: string; label: string }[] = [
|
const TYPES: { value: string; label: string }[] = [
|
||||||
{ value: "binding", label: "מחייבת" },
|
{ value: "holding", label: "מהותי" },
|
||||||
{ value: "interpretive", label: "פרשני" },
|
{ value: "interpretive", label: "פרשני" },
|
||||||
|
{ value: "procedural", label: "פרוצדורלי" },
|
||||||
{ value: "application", label: "יישום" },
|
{ value: "application", label: "יישום" },
|
||||||
{ value: "obiter", label: "אמרת-אגב" },
|
{ value: "obiter", label: "אמרת-אגב" },
|
||||||
{ value: "procedural", label: "פרוצדורלי" },
|
|
||||||
{ value: "persuasive", label: "משכנע" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Consistency between is_holding and the type (#81.7): a real holding is
|
// Consistency between is_holding and the role (#81.7): a real holding is
|
||||||
// binding/interpretive/procedural/persuasive; a NON-holding is its reason —
|
// holding/interpretive/procedural; a NON-holding is its reason —
|
||||||
// application (fact-bound) or obiter (not decided). Other pairings contradict.
|
// application (fact-bound) or obiter (not decided). Other pairings contradict.
|
||||||
const HOLDING_TYPES = new Set(["binding", "interpretive", "procedural", "persuasive"]);
|
const HOLDING_TYPES = new Set(["holding", "interpretive", "procedural"]);
|
||||||
const NON_HOLDING_TYPES = new Set(["application", "obiter"]);
|
const NON_HOLDING_TYPES = new Set(["application", "obiter"]);
|
||||||
|
|
||||||
function inconsistentTag(it: GoldsetItem): string | null {
|
function inconsistentTag(it: GoldsetItem): string | null {
|
||||||
@@ -33,7 +35,7 @@ function inconsistentTag(it: GoldsetItem): string | null {
|
|||||||
return "סימנת \"הלכה\" אך הסוג הוא יישום/אמרת-אגב — אלה דווקא הסיבות שמשהו אינו הלכה.";
|
return "סימנת \"הלכה\" אך הסוג הוא יישום/אמרת-אגב — אלה דווקא הסיבות שמשהו אינו הלכה.";
|
||||||
}
|
}
|
||||||
if (it.is_holding === false && HOLDING_TYPES.has(it.correct_type)) {
|
if (it.is_holding === false && HOLDING_TYPES.has(it.correct_type)) {
|
||||||
return "סימנת \"לא הלכה\" אך הסוג מציין הלכה (מחייבת/פרשני/…); ל\"לא\" מתאים יישום או אמרת-אגב.";
|
return "סימנת \"לא הלכה\" אך הסוג מציין הלכה (מהותי/פרשני/…); ל\"לא\" מתאים יישום או אמרת-אגב.";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -139,11 +141,13 @@ function ScorePanel({ batch }: { batch: string }) {
|
|||||||
|
|
||||||
// ─── Rule-type help (info popover) ────────────────────────────────────────────
|
// ─── Rule-type help (info popover) ────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Role only — "כמה מחייב" (מחייב/משכנע) is the SEPARATE authority axis, derived
|
||||||
|
// automatically from the court's identity and shown as a read-only badge.
|
||||||
const TYPE_HELP: { label: string; def: string; test: string; example: string }[] = [
|
const TYPE_HELP: { label: string; def: string; test: string; example: string }[] = [
|
||||||
{
|
{
|
||||||
label: "מחייבת",
|
label: "מהותי",
|
||||||
def: "העיקרון שהיה הכרחי להכרעה — ה-holding האמיתי. בר-הסתמכות מלא.",
|
def: "העיקרון המהותי שהיה הכרחי להכרעה — ה-ratio האמיתי. בר-הסתמכות מלא.",
|
||||||
test: "מבחן וומבו: הפוך את הכלל — אם התוצאה הייתה משתנה → מחייבת.",
|
test: "מבחן וומבו: הפוך את הכלל — אם התוצאה הייתה משתנה → מהותי.",
|
||||||
example: "נטל ההוכחה בהיטל השבחה מוטל על הוועדה המקומית.",
|
example: "נטל ההוכחה בהיטל השבחה מוטל על הוועדה המקומית.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -152,6 +156,12 @@ const TYPE_HELP: { label: string; def: string; test: string; example: string }[]
|
|||||||
test: "עונה ל'מה פירוש הנורמה?' ולא ל'מה הדין?'.",
|
test: "עונה ל'מה פירוש הנורמה?' ולא ל'מה הדין?'.",
|
||||||
example: "תכלית הפטור לפי ס' 19(ב)(4) היא לעודד פעילות ציבורית.",
|
example: "תכלית הפטור לפי ס' 19(ב)(4) היא לעודד פעילות ציבורית.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "פרוצדורלי",
|
||||||
|
def: "כלל סדר-דין: מועדים, סמכות, זכות-עמידה, מיצוי הליכים, נטל.",
|
||||||
|
test: "עוסק ב'איך' מתנהל ההליך, לא במהות התכנונית.",
|
||||||
|
example: "המועד להגשת ערר הוא 30 יום.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "יישום",
|
label: "יישום",
|
||||||
def: "החלת כלל על עובדות התיק הספציפי — תלוי-עובדות, לא בר-הכללה (לרוב 'לא הלכה').",
|
def: "החלת כלל על עובדות התיק הספציפי — תלוי-עובדות, לא בר-הכללה (לרוב 'לא הלכה').",
|
||||||
@@ -160,22 +170,10 @@ const TYPE_HELP: { label: string; def: string; test: string; example: string }[]
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "אמרת-אגב",
|
label: "אמרת-אגב",
|
||||||
def: "נאמר אגב אורחא, לא הכרחי להכרעה; הערכאה לא הכריעה בו. לא מחייב.",
|
def: "נאמר אגב אורחא, לא הכרחי להכרעה; הערכאה לא הכריעה בו.",
|
||||||
test: "מבחן וומבו הפוך: היפוך הכלל לא משנה את התוצאה. דגלים: 'למעלה מן הצורך', 'מבלי לקבוע מסמרות'.",
|
test: "מבחן וומבו הפוך: היפוך הכלל לא משנה את התוצאה. דגלים: 'למעלה מן הצורך', 'מבלי לקבוע מסמרות'.",
|
||||||
example: "אף שאיננו נדרשים להכריע, נעיר כי ייתכן ש...",
|
example: "אף שאיננו נדרשים להכריע, נעיר כי ייתכן ש...",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "פרוצדורלי",
|
|
||||||
def: "כלל סדר-דין: מועדים, סמכות, זכות-עמידה, מיצוי הליכים, נטל.",
|
|
||||||
test: "עוסק ב'איך' מתנהל ההליך, לא במהות התכנונית.",
|
|
||||||
example: "המועד להגשת ערר הוא 30 יום.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "משכנע",
|
|
||||||
def: "אסמכתה לא-מחייבת את הערכאה — שכנוע בלבד.",
|
|
||||||
test: "מקור שאינו כובל: ועדת-ערר אחרת, דעת-מיעוט, ספרות.",
|
|
||||||
example: "ועדת ערר ירושלים מסתמכת על החלטת ועדת ערר ממחוז אחר.",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function RuleTypeHelp() {
|
function RuleTypeHelp() {
|
||||||
@@ -195,7 +193,7 @@ function RuleTypeHelp() {
|
|||||||
<div className="p-3 border-b border-rule">
|
<div className="p-3 border-b border-rule">
|
||||||
<p className="font-semibold text-navy text-sm">סוגי ההלכה — במה הם נבדלים</p>
|
<p className="font-semibold text-navy text-sm">סוגי ההלכה — במה הם נבדלים</p>
|
||||||
<p className="text-[0.72rem] text-ink-muted mt-0.5">
|
<p className="text-[0.72rem] text-ink-muted mt-0.5">
|
||||||
כלל-אצבע: סימנת "הלכה" → לרוב מחייבת / פרשני / פרוצדורלי / משכנע. סימנת "לא" → לרוב יישום / אמרת-אגב.
|
כלל-אצבע: סימנת “הלכה” → לרוב מהותי / פרשני / פרוצדורלי. סימנת “לא” → לרוב יישום / אמרת-אגב.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ul className="divide-y divide-rule-soft">
|
<ul className="divide-y divide-rule-soft">
|
||||||
@@ -238,6 +236,7 @@ function TagCard({
|
|||||||
{sourceLabel(it.source_type)}
|
{sourceLabel(it.source_type)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="text-[0.65rem]">מכונה: {it.rule_type}</Badge>
|
<Badge variant="outline" className="text-[0.65rem]">מכונה: {it.rule_type}</Badge>
|
||||||
|
<AuthorityBadge authority={it.authority} />
|
||||||
{it.confidence != null && (
|
{it.confidence != null && (
|
||||||
<Badge variant="outline" className="text-[0.65rem] tabular-nums">ביטחון {it.confidence.toFixed(2)}</Badge>
|
<Badge variant="outline" className="text-[0.65rem] tabular-nums">ביטחון {it.confidence.toFixed(2)}</Badge>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,15 +7,7 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CorroborationBadge } from "@/components/precedents/corroboration-badge";
|
import { CorroborationBadge } from "@/components/precedents/corroboration-badge";
|
||||||
import { useUpdateHalacha, type Halacha } from "@/lib/api/precedent-library";
|
import { useUpdateHalacha, type Halacha } from "@/lib/api/precedent-library";
|
||||||
|
import { AuthorityBadge, ruleTypeLabel } from "./halacha-meta";
|
||||||
const RULE_TYPE_LABELS: Record<string, string> = {
|
|
||||||
binding: "הלכה מחייבת",
|
|
||||||
interpretive: "פרשני",
|
|
||||||
procedural: "פרוצדורלי",
|
|
||||||
obiter: "אמרת אגב",
|
|
||||||
application: "יישום הלכה",
|
|
||||||
persuasive: "משכנע",
|
|
||||||
};
|
|
||||||
|
|
||||||
type StatusFilter = "all" | "approved" | "pending" | "rejected";
|
type StatusFilter = "all" | "approved" | "pending" | "rejected";
|
||||||
|
|
||||||
@@ -172,8 +164,9 @@ export function ExtractedHalachotSection({ halachot }: { halachot: Halacha[] })
|
|||||||
</span>
|
</span>
|
||||||
<ReviewStatusPill status={h.review_status} />
|
<ReviewStatusPill status={h.review_status} />
|
||||||
<Badge variant="outline" className="text-[0.65rem]">
|
<Badge variant="outline" className="text-[0.65rem]">
|
||||||
{RULE_TYPE_LABELS[h.rule_type] ?? h.rule_type}
|
{ruleTypeLabel(h.rule_type)}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<AuthorityBadge authority={h.authority} />
|
||||||
<Badge variant="outline" className="text-[0.65rem] tabular-nums">
|
<Badge variant="outline" className="text-[0.65rem] tabular-nums">
|
||||||
ביטחון {h.confidence.toFixed(2)}
|
ביטחון {h.confidence.toFixed(2)}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
54
web-ui/src/components/precedents/halacha-meta.tsx
Normal file
54
web-ui/src/components/precedents/halacha-meta.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"use client";
|
||||||
|
/**
|
||||||
|
* Shared halacha-classification labels + badge (INV-DM7 — two orthogonal axes).
|
||||||
|
*
|
||||||
|
* rule_type holds the rule ROLE (what KIND of statement). authority (binding vs
|
||||||
|
* persuasive) is a SEPARATE, DERIVED axis (where it came from) — rendered as a
|
||||||
|
* distinct read-only badge, never mixed into the role label.
|
||||||
|
*/
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
/** rule ROLE labels. Legacy authority values (binding/persuasive) are kept as a
|
||||||
|
* fallback so pre-backfill rows still render a Hebrew word during rollout. */
|
||||||
|
export const RULE_TYPE_LABELS: Record<string, string> = {
|
||||||
|
holding: "עיקרון מהותי",
|
||||||
|
interpretive: "פרשני",
|
||||||
|
procedural: "פרוצדורלי",
|
||||||
|
application: "יישום",
|
||||||
|
obiter: "אמרת אגב",
|
||||||
|
// legacy (pre-split) — fold to role wording until backfill completes
|
||||||
|
binding: "עיקרון מהותי",
|
||||||
|
persuasive: "פרשני",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ruleTypeLabel(t: string | null | undefined): string {
|
||||||
|
return (t && RULE_TYPE_LABELS[t]) || t || "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AUTHORITY_LABELS: Record<string, string> = {
|
||||||
|
binding: "מחייב",
|
||||||
|
persuasive: "משכנע",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Read-only authority badge — derived from the source, the chair never sets it. */
|
||||||
|
export function AuthorityBadge({
|
||||||
|
authority,
|
||||||
|
}: {
|
||||||
|
authority?: string | null;
|
||||||
|
}) {
|
||||||
|
if (!authority || !AUTHORITY_LABELS[authority]) return null;
|
||||||
|
const isBinding = authority === "binding";
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
title="דרגת-המחייבות נגזרת אוטומטית מזהות הערכאה"
|
||||||
|
className={
|
||||||
|
isBinding
|
||||||
|
? "text-[0.65rem] bg-gold/15 text-navy border-gold/50"
|
||||||
|
: "text-[0.65rem] bg-muted text-ink-muted border-border"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{AUTHORITY_LABELS[authority]}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import { practiceAreaLabel } from "./practice-area";
|
|||||||
import {
|
import {
|
||||||
useHalachotPending, useHalachotByStatus, useUpdateHalacha, useBatchReviewHalachot, type Halacha,
|
useHalachotPending, useHalachotByStatus, useUpdateHalacha, useBatchReviewHalachot, type Halacha,
|
||||||
} from "@/lib/api/precedent-library";
|
} from "@/lib/api/precedent-library";
|
||||||
|
import { AuthorityBadge, ruleTypeLabel } from "./halacha-meta";
|
||||||
|
|
||||||
/** #81 strict-rubric flags — why an item was held back from auto-approval. */
|
/** #81 strict-rubric flags — why an item was held back from auto-approval. */
|
||||||
const QUALITY_FLAG_LABELS: Record<string, string> = {
|
const QUALITY_FLAG_LABELS: Record<string, string> = {
|
||||||
@@ -40,19 +41,6 @@ function cleanCitation(s: string | null | undefined): string {
|
|||||||
return s.replace(/[--]/g, "").trim();
|
return s.replace(/[--]/g, "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const RULE_TYPE_LABELS: Record<string, string> = {
|
|
||||||
binding: "הלכה מחייבת",
|
|
||||||
interpretive: "פרשני",
|
|
||||||
procedural: "פרוצדורלי",
|
|
||||||
obiter: "אמרת אגב",
|
|
||||||
application: "יישום הלכה",
|
|
||||||
persuasive: "משכנע",
|
|
||||||
};
|
|
||||||
|
|
||||||
function ruleTypeLabel(t: string): string {
|
|
||||||
return RULE_TYPE_LABELS[t] ?? t;
|
|
||||||
}
|
|
||||||
|
|
||||||
type EditState = { rule_statement: string; reasoning_summary: string };
|
type EditState = { rule_statement: string; reasoning_summary: string };
|
||||||
|
|
||||||
// ─── Pending-queue card (full interactions) ───────────────────────────────────
|
// ─── Pending-queue card (full interactions) ───────────────────────────────────
|
||||||
@@ -118,6 +106,7 @@ function HalachaCard({
|
|||||||
<Badge variant="outline" className="text-[0.65rem]">
|
<Badge variant="outline" className="text-[0.65rem]">
|
||||||
{ruleTypeLabel(h.rule_type)}
|
{ruleTypeLabel(h.rule_type)}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<AuthorityBadge authority={h.authority} />
|
||||||
{variants.length > 0 && (
|
{variants.length > 0 && (
|
||||||
<Badge variant="outline"
|
<Badge variant="outline"
|
||||||
className="text-[0.65rem] bg-navy-soft/30 text-navy border-navy/30">
|
className="text-[0.65rem] bg-navy-soft/30 text-navy border-navy/30">
|
||||||
@@ -326,6 +315,7 @@ function HalachaRestoreCard({
|
|||||||
<Badge variant="outline" className="text-[0.65rem]">
|
<Badge variant="outline" className="text-[0.65rem]">
|
||||||
{ruleTypeLabel(h.rule_type)}
|
{ruleTypeLabel(h.rule_type)}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<AuthorityBadge authority={h.authority} />
|
||||||
<CorroborationBadge halacha={h} />
|
<CorroborationBadge halacha={h} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export type GoldsetItem = {
|
|||||||
supporting_quote: string;
|
supporting_quote: string;
|
||||||
reasoning_summary: string;
|
reasoning_summary: string;
|
||||||
rule_type: string;
|
rule_type: string;
|
||||||
|
// authority over the committee — DERIVED from the source (INV-DM7), read-only.
|
||||||
|
authority?: "binding" | "persuasive" | null;
|
||||||
confidence: number | null;
|
confidence: number | null;
|
||||||
quality_flags?: string[];
|
quality_flags?: string[];
|
||||||
review_status: string;
|
review_status: string;
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export type Halacha = {
|
|||||||
halacha_index: number;
|
halacha_index: number;
|
||||||
rule_statement: string;
|
rule_statement: string;
|
||||||
rule_type: string;
|
rule_type: string;
|
||||||
|
// authority over the committee — DERIVED from the source (INV-DM7), read-only.
|
||||||
|
authority?: "binding" | "persuasive" | null;
|
||||||
reasoning_summary: string;
|
reasoning_summary: string;
|
||||||
supporting_quote: string;
|
supporting_quote: string;
|
||||||
page_reference: string;
|
page_reference: string;
|
||||||
@@ -138,6 +140,7 @@ export type SearchHit =
|
|||||||
subject_tags: string[];
|
subject_tags: string[];
|
||||||
confidence: number;
|
confidence: number;
|
||||||
rule_type: string;
|
rule_type: string;
|
||||||
|
authority?: "binding" | "persuasive" | null;
|
||||||
case_number: string;
|
case_number: string;
|
||||||
case_name: string;
|
case_name: string;
|
||||||
court: string;
|
court: string;
|
||||||
|
|||||||
@@ -6139,8 +6139,10 @@ async def goldset_tag_ep(goldset_id: str, req: GoldsetTagRequest):
|
|||||||
gid = UUID(goldset_id)
|
gid = UUID(goldset_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(400, "מזהה לא תקין")
|
raise HTTPException(400, "מזהה לא תקין")
|
||||||
|
# correct_type is the rule ROLE only (INV-DM7) — authority (binding/
|
||||||
|
# persuasive) is derived, not a human-tagged value.
|
||||||
if req.correct_type and req.correct_type not in (
|
if req.correct_type and req.correct_type not in (
|
||||||
"binding", "interpretive", "obiter", "application", "procedural", "persuasive",
|
"holding", "interpretive", "procedural", "application", "obiter",
|
||||||
):
|
):
|
||||||
raise HTTPException(400, "correct_type לא תקין")
|
raise HTTPException(400, "correct_type לא תקין")
|
||||||
row = await db.goldset_tag(
|
row = await db.goldset_tag(
|
||||||
|
|||||||
Reference in New Issue
Block a user