Merge pull request 'fix(halacha): split authority (derived) from rule_role — stop source-conflation (INV-DM7)' (#112) from worktree-halacha-authority-split into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m32s

This commit was merged in pull request #112.
This commit is contained in:
2026-06-07 18:19:43 +00:00
16 changed files with 407 additions and 92 deletions

View File

@@ -11,20 +11,22 @@ import {
useGoldset, useGoldsetScore, useTagGoldset, useCreateGoldsetSample,
type GoldsetItem,
} 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 }[] = [
{ value: "binding", label: חייבת" },
{ value: "holding", label: הותי" },
{ value: "interpretive", label: "פרשני" },
{ value: "procedural", label: "פרוצדורלי" },
{ value: "application", label: "יישום" },
{ value: "obiter", label: "אמרת-אגב" },
{ value: "procedural", label: "פרוצדורלי" },
{ value: "persuasive", label: "משכנע" },
];
// Consistency between is_holding and the type (#81.7): a real holding is
// binding/interpretive/procedural/persuasive; a NON-holding is its reason —
// Consistency between is_holding and the role (#81.7): a real holding is
// holding/interpretive/procedural; a NON-holding is its reason —
// 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"]);
function inconsistentTag(it: GoldsetItem): string | null {
@@ -33,7 +35,7 @@ function inconsistentTag(it: GoldsetItem): string | null {
return "סימנת \"הלכה\" אך הסוג הוא יישום/אמרת-אגב — אלה דווקא הסיבות שמשהו אינו הלכה.";
}
if (it.is_holding === false && HOLDING_TYPES.has(it.correct_type)) {
return "סימנת \"לא הלכה\" אך הסוג מציין הלכה (מחייבת/פרשני/…); ל\"לא\" מתאים יישום או אמרת-אגב.";
return "סימנת \"לא הלכה\" אך הסוג מציין הלכה (מהותי/פרשני/…); ל\"לא\" מתאים יישום או אמרת-אגב.";
}
return null;
}
@@ -139,11 +141,13 @@ function ScorePanel({ batch }: { batch: string }) {
// ─── 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 }[] = [
{
label: חייבת",
def: "העיקרון שהיה הכרחי להכרעה — ה-holding האמיתי. בר-הסתמכות מלא.",
test: "מבחן וומבו: הפוך את הכלל — אם התוצאה הייתה משתנה → מחייבת.",
label: הותי",
def: "העיקרון המהותי שהיה הכרחי להכרעה — ה-ratio האמיתי. בר-הסתמכות מלא.",
test: "מבחן וומבו: הפוך את הכלל — אם התוצאה הייתה משתנה → מהותי.",
example: "נטל ההוכחה בהיטל השבחה מוטל על הוועדה המקומית.",
},
{
@@ -152,6 +156,12 @@ const TYPE_HELP: { label: string; def: string; test: string; example: string }[]
test: "עונה ל'מה פירוש הנורמה?' ולא ל'מה הדין?'.",
example: "תכלית הפטור לפי ס' 19(ב)(4) היא לעודד פעילות ציבורית.",
},
{
label: "פרוצדורלי",
def: "כלל סדר-דין: מועדים, סמכות, זכות-עמידה, מיצוי הליכים, נטל.",
test: "עוסק ב'איך' מתנהל ההליך, לא במהות התכנונית.",
example: "המועד להגשת ערר הוא 30 יום.",
},
{
label: "יישום",
def: "החלת כלל על עובדות התיק הספציפי — תלוי-עובדות, לא בר-הכללה (לרוב 'לא הלכה').",
@@ -160,22 +170,10 @@ const TYPE_HELP: { label: string; def: string; test: string; example: string }[]
},
{
label: "אמרת-אגב",
def: "נאמר אגב אורחא, לא הכרחי להכרעה; הערכאה לא הכריעה בו. לא מחייב.",
def: "נאמר אגב אורחא, לא הכרחי להכרעה; הערכאה לא הכריעה בו.",
test: "מבחן וומבו הפוך: היפוך הכלל לא משנה את התוצאה. דגלים: 'למעלה מן הצורך', 'מבלי לקבוע מסמרות'.",
example: "אף שאיננו נדרשים להכריע, נעיר כי ייתכן ש...",
},
{
label: "פרוצדורלי",
def: "כלל סדר-דין: מועדים, סמכות, זכות-עמידה, מיצוי הליכים, נטל.",
test: "עוסק ב'איך' מתנהל ההליך, לא במהות התכנונית.",
example: "המועד להגשת ערר הוא 30 יום.",
},
{
label: "משכנע",
def: "אסמכתה לא-מחייבת את הערכאה — שכנוע בלבד.",
test: "מקור שאינו כובל: ועדת-ערר אחרת, דעת-מיעוט, ספרות.",
example: "ועדת ערר ירושלים מסתמכת על החלטת ועדת ערר ממחוז אחר.",
},
];
function RuleTypeHelp() {
@@ -195,7 +193,7 @@ function RuleTypeHelp() {
<div className="p-3 border-b border-rule">
<p className="font-semibold text-navy text-sm">סוגי ההלכה במה הם נבדלים</p>
<p className="text-[0.72rem] text-ink-muted mt-0.5">
כלל-אצבע: סימנת "הלכה" לרוב מחייבת / פרשני / פרוצדורלי / משכנע. סימנת "לא" לרוב יישום / אמרת-אגב.
כלל-אצבע: סימנת &ldquo;הלכה&rdquo; לרוב מהותי / פרשני / פרוצדורלי. סימנת &ldquo;לא&rdquo; לרוב יישום / אמרת-אגב.
</p>
</div>
<ul className="divide-y divide-rule-soft">
@@ -238,6 +236,7 @@ function TagCard({
{sourceLabel(it.source_type)}
</Badge>
<Badge variant="outline" className="text-[0.65rem]">מכונה: {it.rule_type}</Badge>
<AuthorityBadge authority={it.authority} />
{it.confidence != null && (
<Badge variant="outline" className="text-[0.65rem] tabular-nums">ביטחון {it.confidence.toFixed(2)}</Badge>
)}

View File

@@ -7,15 +7,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { CorroborationBadge } from "@/components/precedents/corroboration-badge";
import { useUpdateHalacha, type Halacha } from "@/lib/api/precedent-library";
const RULE_TYPE_LABELS: Record<string, string> = {
binding: "הלכה מחייבת",
interpretive: "פרשני",
procedural: "פרוצדורלי",
obiter: "אמרת אגב",
application: "יישום הלכה",
persuasive: "משכנע",
};
import { AuthorityBadge, ruleTypeLabel } from "./halacha-meta";
type StatusFilter = "all" | "approved" | "pending" | "rejected";
@@ -172,8 +164,9 @@ export function ExtractedHalachotSection({ halachot }: { halachot: Halacha[] })
</span>
<ReviewStatusPill status={h.review_status} />
<Badge variant="outline" className="text-[0.65rem]">
{RULE_TYPE_LABELS[h.rule_type] ?? h.rule_type}
{ruleTypeLabel(h.rule_type)}
</Badge>
<AuthorityBadge authority={h.authority} />
<Badge variant="outline" className="text-[0.65rem] tabular-nums">
ביטחון {h.confidence.toFixed(2)}
</Badge>

View 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>
);
}

View File

@@ -12,6 +12,7 @@ import { practiceAreaLabel } from "./practice-area";
import {
useHalachotPending, useHalachotByStatus, useUpdateHalacha, useBatchReviewHalachot, type Halacha,
} from "@/lib/api/precedent-library";
import { AuthorityBadge, ruleTypeLabel } from "./halacha-meta";
/** #81 strict-rubric flags — why an item was held back from auto-approval. */
const QUALITY_FLAG_LABELS: Record<string, string> = {
@@ -40,19 +41,6 @@ function cleanCitation(s: string | null | undefined): string {
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 };
// ─── Pending-queue card (full interactions) ───────────────────────────────────
@@ -118,6 +106,7 @@ function HalachaCard({
<Badge variant="outline" className="text-[0.65rem]">
{ruleTypeLabel(h.rule_type)}
</Badge>
<AuthorityBadge authority={h.authority} />
{variants.length > 0 && (
<Badge variant="outline"
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]">
{ruleTypeLabel(h.rule_type)}
</Badge>
<AuthorityBadge authority={h.authority} />
<CorroborationBadge halacha={h} />
</span>
</div>

View File

@@ -23,6 +23,8 @@ export type GoldsetItem = {
supporting_quote: string;
reasoning_summary: string;
rule_type: string;
// authority over the committee — DERIVED from the source (INV-DM7), read-only.
authority?: "binding" | "persuasive" | null;
confidence: number | null;
quality_flags?: string[];
review_status: string;

View File

@@ -65,6 +65,8 @@ export type Halacha = {
halacha_index: number;
rule_statement: string;
rule_type: string;
// authority over the committee — DERIVED from the source (INV-DM7), read-only.
authority?: "binding" | "persuasive" | null;
reasoning_summary: string;
supporting_quote: string;
page_reference: string;
@@ -138,6 +140,7 @@ export type SearchHit =
subject_tags: string[];
confidence: number;
rule_type: string;
authority?: "binding" | "persuasive" | null;
case_number: string;
case_name: string;
court: string;