feat(proceeding-type): explicit ערר/בל"מ field for cases + corpus
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m40s

Same case_number can exist as both a regular appeal (ערר) and an
extension-of-time request (בל"מ), and we were inferring the difference
from appeal_subtype prefixes — fragile, and case-number lookups
weren't disambiguated. Now stored as a first-class field on both
case_law (corpus) and cases (live cases), with partial unique indexes
on (case_number, proceeding_type).

- SCHEMA_V15: column + CHECK constraints + backfill from
  appeal_subtype LIKE 'extension_request_%' + partial unique indexes
  replace the old global UNIQUE(case_number).
- derive_proceeding_type() centralizes the inference rule
  (extension_request_* → בל"מ; subject regex fallback; default ערר).
- Metadata extractor prompt asks Claude to populate the new field
  explicitly; apply_to_record writes it for internal_committee rows.
- internal_decision_upload, case_create, case_update accept an
  optional proceeding_type; FastAPI request models expose it.
- Wizard + edit dialog get a sided Select; case header renders the
  resolved label (ערר / בל"מ).
- Uploaded the 2 staged בל"מ decisions on betterment levy:
  8126/24 (סופר נוח, 13 chunks), 8047/23 (הרנון, 48 chunks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 09:17:33 +00:00
parent 1645653ba9
commit d359ab9884
15 changed files with 308 additions and 14 deletions

View File

@@ -17,7 +17,10 @@ import {
} from "@/components/ui/select";
import { PartiesField } from "@/components/wizard/parties-field";
import { useUpdateCase } from "@/lib/api/cases";
import { caseUpdateSchema, expectedOutcomes, type CaseUpdateInput } from "@/lib/schemas/case";
import {
caseUpdateSchema, expectedOutcomes, proceedingTypes,
type CaseUpdateInput,
} from "@/lib/schemas/case";
import type { CaseDetail } from "@/lib/api/cases";
/*
@@ -47,6 +50,7 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) {
respondents: data.respondents ?? [],
property_address: data.property_address ?? "",
permit_number: data.permit_number ?? "",
proceeding_type: data.proceeding_type ?? "ערר",
},
});
@@ -63,6 +67,7 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) {
respondents: data.respondents ?? [],
property_address: data.property_address ?? "",
permit_number: data.permit_number ?? "",
proceeding_type: data.proceeding_type ?? "ערר",
});
}, [open, data, form]);
@@ -104,6 +109,37 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) {
<FieldError message={form.formState.errors.subject?.message} />
</div>
<div>
<Label className="text-navy">סוג תיק</Label>
<Controller
control={form.control}
name="proceeding_type"
render={({ field }) => (
<Select
value={field.value ?? "ערר"}
onValueChange={(v) =>
field.onChange(v as CaseUpdateInput["proceeding_type"])
}
dir="rtl"
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{proceedingTypes.map((p) => (
<SelectItem key={p.value} value={p.value}>
{p.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
<p className="text-[0.7rem] text-ink-muted mt-1">
ערר = הליך עיקרי; בל&quot;מ = בקשה להארכת מועד להגשת ערר
</p>
</div>
<div className="h-px bg-rule" />
<Controller

View File

@@ -41,7 +41,7 @@ export function CaseHeader({ data }: { data?: CaseDetail }) {
<div className="space-y-2">
<div className="flex items-center gap-3 flex-wrap">
<span className="font-display text-[2rem] font-black text-navy leading-none tabular-nums">
ערר {data?.case_number ?? "—"}
{data?.proceeding_type ?? "ערר"} {data?.case_number ?? "—"}
</span>
{data?.status && <StatusBadge status={data.status} />}
{data?.archived_at && (

View File

@@ -16,7 +16,7 @@ import {
import { PartiesField } from "@/components/wizard/parties-field";
import { useCreateCase } from "@/lib/api/cases";
import {
caseCreateSchema, expectedOutcomes,
caseCreateSchema, expectedOutcomes, proceedingTypes,
type CaseCreateInput,
} from "@/lib/schemas/case";
import {
@@ -35,7 +35,7 @@ type StepKey = (typeof STEPS)[number]["key"];
/* Fields validated at each step — lets the user fix just what's on screen
* before moving forward, instead of surfacing all errors from page 1. */
const STEP_FIELDS: Record<StepKey, (keyof CaseCreateInput)[]> = {
basics: ["case_number", "title", "practice_area", "appeal_subtype"],
basics: ["case_number", "title", "proceeding_type", "practice_area", "appeal_subtype"],
parties: ["appellants", "respondents"],
details: ["subject", "hearing_date", "expected_outcome", "notes", "property_address", "permit_number"],
};
@@ -66,6 +66,7 @@ export function CaseWizard() {
expected_outcome: "",
practice_area: "appeals_committee",
appeal_subtype: "unknown",
proceeding_type: "ערר",
},
});
@@ -74,11 +75,17 @@ export function CaseWizard() {
* stop the moment they manually pick a value from the dropdown. Mirrors
* the wireSubtypeAutofill() behaviour of the vanilla UI
* (legal-ai/web/static/index.html around line 2770).
*
* proceeding_type follows the same pattern: if the user hasn't picked
* a value yet, we derive 'בל"מ' whenever the subtype lands on an
* extension_request_* value.
*/
const userTouchedSubtype = useRef(false);
const userTouchedProceeding = useRef(false);
const caseNumber = form.watch("case_number");
const practiceArea = form.watch("practice_area");
const subject = form.watch("subject");
const appealSubtype = form.watch("appeal_subtype");
useEffect(() => {
if (userTouchedSubtype.current) return;
/* derive_subtype_with_blam picks extension_request_* when subject
@@ -89,6 +96,16 @@ export function CaseWizard() {
}
}, [caseNumber, practiceArea, subject, form]);
/* proceeding_type follows appeal_subtype when the user hasn't picked
* one explicitly — extension_request_* always implies 'בל"מ'. */
useEffect(() => {
if (userTouchedProceeding.current) return;
const proc = appealSubtype.startsWith("extension_request_") ? 'בל"מ' : "ערר";
if (proc !== form.getValues("proceeding_type")) {
form.setValue("proceeding_type", proc, { shouldValidate: false });
}
}, [appealSubtype, form]);
const stepIndex = STEPS.findIndex((s) => s.key === step);
const isLast = stepIndex === STEPS.length - 1;
@@ -162,6 +179,39 @@ export function CaseWizard() {
<Input id="title" {...form.register("title")} className="mt-1" />
<FieldError message={form.formState.errors.title?.message} />
</div>
<div>
<Label className="text-navy">
סוג תיק <span className="text-danger">*</span>
</Label>
<Controller
control={form.control}
name="proceeding_type"
render={({ field }) => (
<Select
value={field.value}
onValueChange={(v) => {
userTouchedProceeding.current = true;
field.onChange(v as CaseCreateInput["proceeding_type"]);
}}
dir="rtl"
>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{proceedingTypes.map((p) => (
<SelectItem key={p.value} value={p.value}>
{p.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
<p className="text-[0.7rem] text-ink-muted mt-1">
ערר = הליך עיקרי; בל&quot;מ = בקשה להארכת מועד להגשת ערר
</p>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-navy">תחום משפטי</Label>