feat(hearing): פאנל "מה קרה בדיון" — חשיפת ניתוח-הפרוטוקול ב-UI (#226)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 59s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

תוצר 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>
This commit is contained in:
2026-07-05 12:16:35 +00:00
parent 6414e5f94a
commit ab5d34a9d3
8 changed files with 753 additions and 5 deletions

View File

@@ -0,0 +1,134 @@
/**
* 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",
"",
];