feat: Stage A finalizers + #35/#36/#37 — critical-gap closure
Some checks failed
Build & Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build & Deploy / build-and-deploy (push) Has been cancelled
Four parallel sub-agents closed the remaining critical gaps from the 26/05 Stage A/B sprint. Each block independently tested; aggregated here. ## #30/#31 finalizers (sub-agent A) * Auto-derive practice_area in case_create from case_number prefix (1xxx→rishuy_uvniya, 8xxx→betterment_levy, 9xxx→compensation_197); default for CaseCreateRequest is now "" (the DB constraint catches any stray "appeals_committee"). * practice_area.py: derive_subtype now handles axis-B domain values (rishuy_uvniya/betterment_levy/compensation_197) without parsing the case number; new helper derive_domain_practice_area(). * Halacha re-extraction verified unnecessary — all 6 reclassified records already had is_binding=false and approved halachot. * Regression tests: 6 cases in tests/test_corpus_constraints.py covering practice_area enum, internal-committee chair/district, external-upload arar prefix, MCP guard. * UI: district input → Select dropdown (7 districts) in precedent-edit-sheet.tsx, preserving legacy free-text values. ## #37 בל"מ subtypes (sub-agent B) * 3 new appeal_subtypes: extension_request_{building_permit, betterment_levy,compensation}. APPEALS_COMMITTEE_SUBTYPES extended, SUBTYPES_BY_AREA mappings added. * New helpers: is_blam_subject(), is_blam_subtype(), derive_subtype_with_blam(case_number, subject, practice_area). case_create now uses it to auto-detect "בקשה להארכת מועד" subjects. * 3 methodology templates under docs/methodology/extension-request-*.md. * paperclip_client.py mapping updated for the 3 new subtypes (extension_request_building_permit→CMP, the other two→CMPA). * Frontend: bilingual "בל"מ" badge + filter dropdown on cases list + detail header; appeal-type-bars collapseBlam() merges בל"מ into its parent domain for aggregate bars. * Wizard auto-detects בל"מ from subject during case creation. * 3 Berlinger cases (1017/1018/1019-03-26) migrated to appeal_subtype=extension_request_building_permit via psql. ## #35 missing_precedents feature (sub-agent C) * Schema V13: missing_precedents table (citation, case_id, party, legal_topic, status, linked_case_law_id, claim_quote, ...) + FK constraints + 3 indexes. Applied via psql + idempotent migration. * 6 db.py service functions, 3 MCP tools, 6 FastAPI endpoints (POST/GET/PATCH/DELETE/upload — upload routes by citation prefix to ingest_internal_decision or ingest_precedent). * Next.js page /missing-precedents with 5 status tabs + filters + sidebar badge counter + detail drawer with metadata edit + smart upload form that switches fields per committee/court. * Bootstrap: 7 rows imported from the JSON file (3 citations × cases, all status=closed with linked_case_law_id). * legal-researcher.md: new §2ב.5 with missing_precedent_create usage + dedup semantics + tool grant. ## #36 legal_arguments aggregation (sub-agent D) * Schema V14: legal_arguments + legal_argument_propositions M:M. Applied via psql. * New service argument_aggregator.py with two functions — aggregate_claims_to_arguments() (Claude CLI / claude_session) and get_legal_arguments(). Graceful llm_unavailable handling when CLI is missing (containers). * 2 MCP tools + 2 API endpoints (POST .../aggregate-arguments as BackgroundTask, GET .../legal-arguments). * Frontend: shadcn Accordion + new legal-arguments-panel.tsx with hierarchical (party → priority badge → arguments) display, "טיעונים" tab on the case page, "חשב/חשב מחדש" buttons. * scripts/backfill_legal_arguments.py + SCRIPTS.md entry — dry-run found 8 candidate cases including 1017/1018/1019. ## Open follow-ups (intentionally deferred) * npm run api:types in web-ui (CLAUDE.md flow) — recommended before the next UI commit; not required for backend deployment. * Run backfill_legal_arguments.py --apply once the container picks up the new aggregator service. * webhook on missing-precedents upload-close to Paperclip (optional). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
222
web-ui/src/components/cases/legal-arguments-panel.tsx
Normal file
222
web-ui/src/components/cases/legal-arguments-panel.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
PARTY_LABELS_HE,
|
||||
PRIORITY_LABELS_HE,
|
||||
PRIORITY_ORDER,
|
||||
useAggregateArguments,
|
||||
useLegalArguments,
|
||||
type LegalArgument,
|
||||
type LegalArgumentParty,
|
||||
type LegalArgumentPriority,
|
||||
} from "@/lib/api/legal-arguments";
|
||||
import { toast } from "sonner";
|
||||
import { Loader2, RefreshCw, Sparkles } from "lucide-react";
|
||||
|
||||
const PRIORITY_BADGE_TONE: Record<LegalArgumentPriority, string> = {
|
||||
threshold: "bg-danger-bg/60 text-danger-strong border-danger/40",
|
||||
substantive: "bg-gold-soft/50 text-navy border-gold/40",
|
||||
procedural: "bg-rule-soft text-ink border-rule",
|
||||
relief: "bg-emerald-50 text-emerald-900 border-emerald-200",
|
||||
};
|
||||
|
||||
function groupByPriority(
|
||||
args: LegalArgument[],
|
||||
): Record<LegalArgumentPriority, LegalArgument[]> {
|
||||
const out: Record<LegalArgumentPriority, LegalArgument[]> = {
|
||||
threshold: [],
|
||||
substantive: [],
|
||||
procedural: [],
|
||||
relief: [],
|
||||
};
|
||||
for (const a of args) {
|
||||
(out[a.priority] ?? out.substantive).push(a);
|
||||
}
|
||||
for (const key of PRIORITY_ORDER) {
|
||||
out[key].sort((x, y) => x.argument_index - y.argument_index);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
type PartySectionProps = {
|
||||
party: LegalArgumentParty;
|
||||
args: LegalArgument[];
|
||||
};
|
||||
|
||||
function PartySection({ party, args }: PartySectionProps) {
|
||||
const grouped = useMemo(() => groupByPriority(args), [args]);
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-baseline justify-between border-b border-rule pb-2">
|
||||
<h3 className="text-navy text-base font-semibold">
|
||||
{PARTY_LABELS_HE[party] ?? party}
|
||||
</h3>
|
||||
<span className="text-ink-muted text-xs">
|
||||
{args.length} טיעונים
|
||||
</span>
|
||||
</div>
|
||||
{PRIORITY_ORDER.map((priority) => {
|
||||
const list = grouped[priority];
|
||||
if (!list?.length) return null;
|
||||
return (
|
||||
<div key={priority} className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${PRIORITY_BADGE_TONE[priority]} text-xs`}
|
||||
>
|
||||
{PRIORITY_LABELS_HE[priority]}
|
||||
</Badge>
|
||||
<span className="text-ink-muted text-xs">
|
||||
{list.length} טיעונים
|
||||
</span>
|
||||
</div>
|
||||
<Accordion type="multiple" className="rounded-md border border-rule bg-surface">
|
||||
{list.map((arg) => (
|
||||
<AccordionItem key={arg.id} value={arg.id} className="px-3">
|
||||
<AccordionTrigger className="text-start">
|
||||
<div className="flex flex-1 flex-col items-start gap-1">
|
||||
<span className="text-navy text-sm font-medium leading-tight">
|
||||
{arg.argument_index}. {arg.argument_title}
|
||||
</span>
|
||||
{arg.legal_topic && (
|
||||
<span className="text-ink-muted text-xs">
|
||||
{arg.legal_topic}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-2 px-1">
|
||||
<p className="text-ink leading-relaxed whitespace-pre-line">
|
||||
{arg.argument_body}
|
||||
</p>
|
||||
{arg.supporting_claims.length > 0 && (
|
||||
<p className="text-ink-muted text-xs">
|
||||
מסתמך על {arg.supporting_claims.length} פרופוזיציות
|
||||
גולמיות.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type LegalArgumentsPanelProps = {
|
||||
caseNumber: string;
|
||||
};
|
||||
|
||||
export function LegalArgumentsPanel({ caseNumber }: LegalArgumentsPanelProps) {
|
||||
const { data, isPending, isError, error } = useLegalArguments(caseNumber);
|
||||
const aggregate = useAggregateArguments(caseNumber);
|
||||
|
||||
const parties = useMemo<LegalArgumentParty[]>(() => {
|
||||
if (!data?.by_party) return [];
|
||||
const order: LegalArgumentParty[] = [
|
||||
"appellant",
|
||||
"respondent",
|
||||
"committee",
|
||||
"permit_applicant",
|
||||
"unknown",
|
||||
];
|
||||
return order.filter((p) => (data.by_party[p]?.length ?? 0) > 0);
|
||||
}, [data]);
|
||||
|
||||
const handleAggregate = (force: boolean) => {
|
||||
aggregate.mutate(force, {
|
||||
onSuccess: () => {
|
||||
toast.success(
|
||||
force
|
||||
? "הופעלה חזרה חישוב טיעונים (force). יסתיים תוך דקה."
|
||||
: "הופעל חישוב טיעונים. רענן בעוד דקה.",
|
||||
);
|
||||
},
|
||||
onError: (e) => toast.error(`שגיאה: ${(e as Error).message}`),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5 space-y-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h2 className="text-navy text-base font-semibold">
|
||||
טיעונים משפטיים
|
||||
</h2>
|
||||
<p className="text-ink-muted text-xs mt-0.5">
|
||||
טיעונים מאוגדים מתוך הפרופוזיציות הגולמיות, מקובצים לפי צד וקדימות.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={aggregate.isPending}
|
||||
onClick={() => handleAggregate(false)}
|
||||
>
|
||||
{aggregate.isPending ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin me-1.5" />
|
||||
) : (
|
||||
<Sparkles className="w-3.5 h-3.5 me-1.5" />
|
||||
)}
|
||||
חשב טיעונים
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={aggregate.isPending || !data?.total}
|
||||
onClick={() => handleAggregate(true)}
|
||||
title="חישוב מחדש (מוחק טיעונים קיימים)"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isPending ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-6 w-48" />
|
||||
<Skeleton className="h-20 w-full" />
|
||||
<Skeleton className="h-20 w-full" />
|
||||
</div>
|
||||
) : isError ? (
|
||||
<p className="text-danger text-sm">
|
||||
שגיאה בטעינת טיעונים: {(error as Error).message}
|
||||
</p>
|
||||
) : !data?.total ? (
|
||||
<p className="text-ink-muted text-sm">
|
||||
אין טיעונים מאוגדים עדיין. לחץ "חשב טיעונים" כדי להריץ את ה-aggregator.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{parties.map((party) => (
|
||||
<PartySection
|
||||
key={party}
|
||||
party={party}
|
||||
args={data.by_party[party] ?? []}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user