הצבירה קיבצה את כל המשיבים תחת מפלגה אחת "respondent", כך שכתבי-תשובה נפרדים (משיבות 2-3 מול משיבים 4-6) עם עמדות שעלולות להיות מנוגדות נבלעו לרשימה אחת. התגלה בתיק 1043-02-26. השינוי מפצל את הצדדים הרב-משתתפים (respondent/permit_applicant) לפי source_document — כל כתב-תשובה משותף = יחידת-ליטיגציה קוהרנטית נפרדת — ומאחסן את התווית ב-legal_arguments.party_name (מיגרציה V50). עוררים/ועדה נשארים קבוצה יחידה. בונוס: פיצול ה-129 טענות ל-קריאות-Claude קטנות מתקן את הכשל-בשקט המקורי (קריאה של 100+ פרופוזיציות החזירה non-JSON והפילה את כל הצד). - db.py: SCHEMA_V50_SQL — legal_arguments.party_name (ADD COLUMN IF NOT EXISTS, אידמפוטנטי) - argument_aggregator: קיבוץ (party, party_name); _build_prompt מקבל שם-כתב; SELECT מחזיר party_name - UI: PartySection מתת-קבץ לפי כתב-תשובה; טיפוס LegalArgument.party_name טווח v1: הקיבוץ לפי source_document; אכלוס party_name ב-extractor וזיהוי עמדות-מנוגדות בבלוק ז/י — המשך ב-#224. Invariants: G1 (נרמול-במקור — party_name בסכמה, לא תיקון-בקריאה) · G2 (אין מסלול-צבירה מקביל, אותו pipeline) · §6 (אין בליעה שקטה — הכשל-בשקט תוקן). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
144 lines
3.9 KiB
TypeScript
144 lines
3.9 KiB
TypeScript
/**
|
|
* Legal Arguments domain — aggregated propositions (claim de-dup).
|
|
*
|
|
* Each raw "claim" is an extracted proposition from a litigation brief;
|
|
* the LLM-driven aggregator groups them by party into 6-12 distinct
|
|
* legal arguments. These hooks expose the read + trigger endpoints.
|
|
*/
|
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { apiRequest } from "./client";
|
|
|
|
export type LegalArgumentParty =
|
|
| "appellant"
|
|
| "respondent"
|
|
| "committee"
|
|
| "permit_applicant"
|
|
| "unknown";
|
|
|
|
export type LegalArgumentPriority =
|
|
| "threshold"
|
|
| "substantive"
|
|
| "procedural"
|
|
| "relief";
|
|
|
|
export type SupportingProposition = {
|
|
id: string;
|
|
text: string;
|
|
source_document: string | null;
|
|
};
|
|
|
|
export type LegalArgument = {
|
|
id: string;
|
|
case_id: string;
|
|
party: LegalArgumentParty;
|
|
/**
|
|
* The specific pleading this argument belongs to, for sides that may comprise
|
|
* several litigants with opposing positions (respondent / permit_applicant
|
|
* briefs). Empty for single-voice sides (appellant / committee). (#224)
|
|
*/
|
|
party_name?: string;
|
|
argument_index: number;
|
|
argument_title: string;
|
|
argument_body: string;
|
|
legal_topic: string | null;
|
|
priority: LegalArgumentPriority;
|
|
cited_precedents?: string[] | null;
|
|
created_at?: string;
|
|
updated_at?: string;
|
|
supporting_claims: string[];
|
|
/** Raw extracted propositions (id + full text) backing this argument. */
|
|
supporting_propositions?: SupportingProposition[];
|
|
};
|
|
|
|
export type LegalArgumentsResponse = {
|
|
case_number: string;
|
|
total: number;
|
|
by_party: Partial<Record<LegalArgumentParty, LegalArgument[]>>;
|
|
arguments: LegalArgument[];
|
|
};
|
|
|
|
export const legalArgumentsKeys = {
|
|
all: ["legal-arguments"] as const,
|
|
byCase: (caseNumber: string) =>
|
|
[...legalArgumentsKeys.all, caseNumber] as const,
|
|
};
|
|
|
|
export function useLegalArguments(caseNumber: string | undefined) {
|
|
return useQuery({
|
|
queryKey: legalArgumentsKeys.byCase(caseNumber ?? ""),
|
|
queryFn: ({ signal }) =>
|
|
apiRequest<LegalArgumentsResponse>(
|
|
`/api/cases/${caseNumber}/legal-arguments`,
|
|
{ signal },
|
|
),
|
|
enabled: Boolean(caseNumber),
|
|
staleTime: 10_000,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The aggregation runs on the legal-analyst agent (host-side, where the
|
|
* `claude` CLI lives) — NOT inline in the FastAPI container. The endpoint
|
|
* either delegates via a Paperclip wakeup (`queued`) or short-circuits on a
|
|
* cheap in-container DB pre-check (`no_claims` / `exists`). `skipped` means
|
|
* no analyst route was available; the chair can run the MCP tool manually.
|
|
*/
|
|
export type AggregateArgumentsResult =
|
|
| {
|
|
status: "queued";
|
|
sub_issue_id: string;
|
|
analyst_id: string;
|
|
main_issue_id: string;
|
|
}
|
|
| {
|
|
status: "no_claims" | "exists";
|
|
total: number;
|
|
message: string;
|
|
}
|
|
| {
|
|
status: "skipped";
|
|
reason: "no_api_key" | "no_analyst" | "no_issue" | string;
|
|
company_id?: string;
|
|
};
|
|
|
|
export function useAggregateArguments(caseNumber: string | undefined) {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (force: boolean = false) =>
|
|
apiRequest<AggregateArgumentsResult>(
|
|
`/api/cases/${caseNumber}/aggregate-arguments${force ? "?force=true" : ""}`,
|
|
{ method: "POST" },
|
|
),
|
|
onSuccess: () => {
|
|
if (caseNumber) {
|
|
qc.invalidateQueries({
|
|
queryKey: legalArgumentsKeys.byCase(caseNumber),
|
|
});
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
export const PARTY_LABELS_HE: Record<LegalArgumentParty, string> = {
|
|
appellant: "עוררים",
|
|
respondent: "משיבים",
|
|
committee: "ועדה מקומית",
|
|
permit_applicant: "מבקשי היתר",
|
|
unknown: "צד לא מזוהה",
|
|
};
|
|
|
|
export const PRIORITY_LABELS_HE: Record<LegalArgumentPriority, string> = {
|
|
threshold: "סף",
|
|
substantive: "מהותי",
|
|
procedural: "פגם הליך",
|
|
relief: "סעד",
|
|
};
|
|
|
|
export const PRIORITY_ORDER: LegalArgumentPriority[] = [
|
|
"threshold",
|
|
"substantive",
|
|
"procedural",
|
|
"relief",
|
|
];
|