תוצר analyze_protocol (איך הטענות הכתובות התפתחו בדיון בעל-פה) היה שכבת ידע-תיק ללא endpoint ותצוגה — נצרך רק downstream ע"י הכותב בבלוק-י. פאנל חדש בתחתית טאב "טיעונים ועמדות", מתחת לכרטיס-הטיעונים (INV-IA1: לא טאב-מקביל). Backend: - SCHEMA_V51: cases.hearing_attendees JSONB — בית קנוני לנוכחות-בדיון (panel_members/appellants_present/respondents_present). נבדל סמנטית מ-decisions.panel_members (מותב חותם-ההחלטה) → אין מסלול-מקביל (G2). - protocol_analyzer._extract_header: מאכלס hearing_attendees; refresh על re-run (מתקן #223), לא דורס עם חילוץ ריק. אישור-משתמש: "אפשרות א" (persist+הצגה). - GET /api/cases/{n}/protocol-analysis — header (תאריך+נוכחים קנוניים) + verdicts מקובצים לפי צד. קריאה-בלבד, container-safe, ריק≠שגיאה. - POST /api/cases/{n}/analyze-protocol — מעיר את המנתח (analyze_protocol מריץ claude מקומי, נעדר בקונטיינר) כתבנית aggregate-arguments. - מגע-Paperclip רק דרך agent_platform_port (G12): wake_analyst_for_protocol_analysis. Frontend: - lib/api/protocol-analysis.ts (hooks read+trigger) + HearingChangesPanel (סרגל-כותרת, פסי-סינון התחזק/נטען-לראשונה, כרטיסים לפי צד, ציטוט-ראיה). - חיווט לטאב arguments מתחת ל-LegalArgumentsPanel. עיצוב: מוקאפ 27-case-hearing-changes-panel אושר ע"י חיים דרך שער Claude Design (X17). Invariants: מקיים G2 (בית קנוני יחיד, לא מסלול-מקביל), G1 (נרמול-במקור), G12 (שער-הפלטפורמה), INV-IA1 (לא טאב-מקביל), INV-AH (evidence_quote כבר נאכף במקור). אימות: py_compile + tsc --noEmit + eslint ירוקים; runtime של ה-endpoint = post-deploy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
/**
|
|
* Protocol comparative analysis — the "מה קרה בדיון" panel (#226).
|
|
*
|
|
* `analyze_protocol` compares the ערר hearing protocol against the written
|
|
* pleadings and records, per argument, whether it was strengthened, newly
|
|
* raised, or dropped at the hearing — plus the sharpened legal question and a
|
|
* verbatim evidence quote. This module exposes the read + trigger endpoints.
|
|
*/
|
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { apiRequest } from "./client";
|
|
import type { LegalArgumentParty } from "./legal-arguments";
|
|
|
|
export type ProtocolChangeType = "strengthened" | "newly_raised" | "dropped";
|
|
|
|
/** Empty string = an unassigned party_role on a verdict row. */
|
|
export type ProtocolPartyRole = LegalArgumentParty | "";
|
|
|
|
export type ProtocolAnalysisRow = {
|
|
id: string;
|
|
case_id: string;
|
|
document_id: string;
|
|
party_role: ProtocolPartyRole;
|
|
change_type: ProtocolChangeType;
|
|
/** The pleaded argument this verdict matched (strengthened/dropped); null for newly_raised. */
|
|
argument_id: string | null;
|
|
argument_title: string;
|
|
summary: string;
|
|
sharpened_question: string;
|
|
evidence_quote: string;
|
|
page_number: number | null;
|
|
created_at?: string;
|
|
};
|
|
|
|
export type ProtocolHeader = {
|
|
hearing_date: string | null;
|
|
protocol_title: string;
|
|
protocol_document_id: string;
|
|
panel_members: string[];
|
|
appellants_present: string[];
|
|
respondents_present: string[];
|
|
};
|
|
|
|
export type ProtocolAnalysisResponse = {
|
|
case_number: string;
|
|
total: number;
|
|
header: ProtocolHeader;
|
|
by_change: Partial<Record<ProtocolChangeType, number>>;
|
|
by_party: Partial<Record<ProtocolPartyRole, ProtocolAnalysisRow[]>>;
|
|
analysis: ProtocolAnalysisRow[];
|
|
};
|
|
|
|
export const protocolAnalysisKeys = {
|
|
all: ["protocol-analysis"] as const,
|
|
byCase: (caseNumber: string) =>
|
|
[...protocolAnalysisKeys.all, caseNumber] as const,
|
|
};
|
|
|
|
export function useProtocolAnalysis(caseNumber: string | undefined) {
|
|
return useQuery({
|
|
queryKey: protocolAnalysisKeys.byCase(caseNumber ?? ""),
|
|
queryFn: ({ signal }) =>
|
|
apiRequest<ProtocolAnalysisResponse>(
|
|
`/api/cases/${caseNumber}/protocol-analysis`,
|
|
{ signal },
|
|
),
|
|
enabled: Boolean(caseNumber),
|
|
staleTime: 10_000,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The analysis runs on the legal-analyst agent (host-side, where the `claude`
|
|
* CLI lives) — NOT inline in the FastAPI container. The endpoint delegates via
|
|
* a Paperclip wakeup (`queued`), or reports `skipped` when no analyst route is
|
|
* available (the chair can then run the MCP tool manually).
|
|
*/
|
|
export type AnalyzeProtocolResult =
|
|
| {
|
|
status: "queued";
|
|
sub_issue_id: string;
|
|
analyst_id: string;
|
|
main_issue_id: string;
|
|
}
|
|
| {
|
|
status: "skipped";
|
|
reason: "no_api_key" | "no_analyst" | "no_issue" | string;
|
|
company_id?: string;
|
|
};
|
|
|
|
export function useAnalyzeProtocol(caseNumber: string | undefined) {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (documentId?: string) =>
|
|
apiRequest<AnalyzeProtocolResult>(
|
|
`/api/cases/${caseNumber}/analyze-protocol${
|
|
documentId ? `?document_id=${encodeURIComponent(documentId)}` : ""
|
|
}`,
|
|
{ method: "POST" },
|
|
),
|
|
onSuccess: () => {
|
|
if (caseNumber) {
|
|
qc.invalidateQueries({
|
|
queryKey: protocolAnalysisKeys.byCase(caseNumber),
|
|
});
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
export const CHANGE_LABELS_HE: Record<ProtocolChangeType, string> = {
|
|
strengthened: "התחזק בדיון",
|
|
newly_raised: "נטען לראשונה",
|
|
dropped: "נזנח בדיון",
|
|
};
|
|
|
|
export const PROTOCOL_PARTY_LABELS_HE: Record<ProtocolPartyRole, string> = {
|
|
appellant: "עוררים",
|
|
respondent: "משיבים",
|
|
committee: "ועדה מקומית",
|
|
permit_applicant: "מבקשי היתר",
|
|
unknown: "צד לא מזוהה",
|
|
"": "צד לא מסווג",
|
|
};
|
|
|
|
/** Display order for the per-party sections. */
|
|
export const PROTOCOL_PARTY_ORDER: ProtocolPartyRole[] = [
|
|
"appellant",
|
|
"committee",
|
|
"respondent",
|
|
"permit_applicant",
|
|
"unknown",
|
|
"",
|
|
];
|