feat(goldset): soft consistency warning between is_holding and type

"לא הלכה" + "מחייבת" (or any holding-type) is a logical contradiction — binding
means it IS the holding. Likewise "הלכה" + application/obiter. The three controls
are independent, so the combo was clickable with no signal.

Adds a non-blocking amber warning under the type buttons when is_holding and
correct_type contradict (holding ↔ binding/interpretive/procedural/persuasive;
not-holding ↔ application/obiter). Soft by design — flags the inconsistency for
the tagger to fix without forcing, leaving room for genuine edge cases.

Verified: tsc --noEmit exits 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 13:40:05 +00:00
parent b107654ee4
commit 5aa3d4ed99

View File

@@ -1,7 +1,7 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { Check, X, ChevronDown, ChevronLeft, Info } from "lucide-react";
import { Check, X, ChevronDown, ChevronLeft, Info, AlertTriangle } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -21,6 +21,23 @@ const TYPES: { value: string; label: string }[] = [
{ 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 —
// application (fact-bound) or obiter (not decided). Other pairings contradict.
const HOLDING_TYPES = new Set(["binding", "interpretive", "procedural", "persuasive"]);
const NON_HOLDING_TYPES = new Set(["application", "obiter"]);
function inconsistentTag(it: GoldsetItem): string | null {
if (it.is_holding === null || !it.correct_type) return null;
if (it.is_holding === true && NON_HOLDING_TYPES.has(it.correct_type)) {
return "סימנת \"הלכה\" אך הסוג הוא יישום/אמרת-אגב — אלה דווקא הסיבות שמשהו אינו הלכה.";
}
if (it.is_holding === false && HOLDING_TYPES.has(it.correct_type)) {
return "סימנת \"לא הלכה\" אך הסוג מציין הלכה (מחייבת/פרשני/…); ל\"לא\" מתאים יישום או אמרת-אגב.";
}
return null;
}
const FLAG_LABELS: Record<string, string> = {
non_decision: "אי-הכרעה", truncated_quote: "ציטוט קטוע", thin_restatement: "ניסוח דק",
quote_unverified: "ציטוט לא מאומת", nli_unsupported: "כלל לא נגזר", application: "יישום",
@@ -232,6 +249,12 @@ function TagCard({
onClick={() => onTag({ correct_type: t.value })}>{t.label}</Button>
))}
</div>
{inconsistentTag(it) && (
<p className="mt-1 text-[0.68rem] text-amber-700 flex items-start gap-1">
<AlertTriangle className="w-3 h-3 mt-0.5 shrink-0" />
{inconsistentTag(it)}
</p>
)}
</div>
{/* quote_complete */}
<div>