feat(citation-verify): frontend "אימות פסיקה" tab in the decision editor (#154)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s

Frontend half of the citation-verification panel (backend in #323), as the agreed
third tab in /compose — matching the approved X17 mockup 24-citation-verification:

- lib/api/citation-verification.ts: hand-written types + useCitationVerification
  query + useVerifyCitation mutation (POST verify, invalidates the view).
- components/compose/citation-verification-panel.tsx: per legal argument →
  supporting corpus precedents with the cited_by authority chips (אומץ ×N /
  אובחן ×N), the exact supporting_quote, ✓ מאמת / ✗ לא רלוונטי verify actions, a
  per-citation chair-note field, and the per-issue 📡 radar (unlinked digests,
  pointer-only). Summary strip + INV-AH/INV-DIG1 reminders.
- compose page: third tab "אימות פסיקה" alongside עורך הבלוקים / עמדות וטענות.

No api:types regen needed (endpoints return assembled dicts; types hand-written
per the lib/api convention). tsc + eslint clean.

Invariants: INV-AH (writer cites only verified), INV-DIG1 (digest never cited),
G2 (consumes the one backend view). Visual matches the gate-approved mockup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:19:03 +00:00
parent d7855f6284
commit 5108c854cf
3 changed files with 371 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
/**
* Citation-verification domain (X11 Phase 2 / #154) — the compose "אימות פסיקה" tab.
*
* Per legal argument: in-corpus supporting precedents with the cumulative authority
* signal (cited_by — followed/distinguished), their verify state + chair note, and
* per-issue radar (unlinked digests). The chair verifies before the writer cites
* (INV-AH). Backed by GET/POST /api/cases/{n}/citation-verification[/verify].
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "./client";
export type CitedBy = {
total: number;
positive: number;
negative: number;
unclassified: number;
by_treatment: Record<string, number>;
};
export type SupportingPrecedent = {
case_law_id: string;
case_number: string;
case_name: string;
quote: string;
score: number;
cited_by: CitedBy;
attached_id: string | null;
verified: boolean;
chair_note: string;
};
export type RadarLead = {
digest_id: string;
yomon_number?: string | number | null;
headline: string;
underlying_citation: string;
underlying_court: string;
score: number;
missing_precedent_id: string | null;
missing_precedent_status: string | null;
action: "new_lead" | "gap_open" | "fetched" | "available_link" | string;
matched_issues?: string[];
};
export type ArgumentBlock = {
argument_id: string;
title: string;
legal_topic: string;
priority: string;
party: string;
supporting: SupportingPrecedent[];
radar: RadarLead[];
};
export type CitationVerificationView = {
status: string;
case_number: string;
arguments: ArgumentBlock[];
summary: {
arguments_total: number;
arguments_with_support: number;
verified: number;
radar_leads: number;
};
};
export type VerifyCitationBody = {
argument_id: string;
case_law_id?: string;
quote?: string;
citation?: string;
chair_note?: string | null;
verified: boolean;
attached_id?: string;
};
export function useCitationVerification(caseNumber: string | undefined) {
return useQuery({
queryKey: ["citation-verification", caseNumber ?? ""],
queryFn: ({ signal }) =>
apiRequest<CitationVerificationView>(
`/api/cases/${caseNumber}/citation-verification`,
{ signal },
),
enabled: Boolean(caseNumber),
staleTime: 10_000,
});
}
export function useVerifyCitation(caseNumber: string | undefined) {
const qc = useQueryClient();
return useMutation({
mutationFn: (body: VerifyCitationBody) =>
apiRequest(`/api/cases/${caseNumber}/citation-verification/verify`, {
method: "POST",
body,
}),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["citation-verification", caseNumber ?? ""] });
},
});
}