feat(cases): תצוגת "פסיקה שצוטטה בהחלטה" בעמוד-התיק + שחזור חיווט-הרמס
UI שביקש חיים: בכניסה להחלטה רואים את הפסיקה שצוטטה בתוכה — מקושרת לספרייה
(קליק → /precedents/[id]) מול חסרה (סומנה אוטומטית להעלאה).
- web/app.py: GET /api/cases/{case}/citations — מהשורה internal_committee של
ההחלטה ב-case_law → precedent_internal_citations: linked (join case_law) +
missing (unresolved + האם flagged ב-missing_precedents).
- web-ui: lib/api/citations.ts (hook) + CitationsSection ב-drafts-panel
(מוצג כשההחלטה בספרייה). מקושרת=ירוק/קליק, חסרה=ענבר "סומנה להעלאה".
- scripts/curator_apply_pipeline_branch.py: מקור-אמת לחיווט-הכפתורים של הרמס
(ה-prompt חי רק ב-Paperclip DB). מקדים branch שמריץ את pipeline-ה-final
ל-wake reason final_learning_*/final_halacha_* (HOME/DOTENV/DATA_DIR מוחלטים
→ מפתחות DeepSeek+Gemini + DATA_DIR נפתרים נכון). idempotent, שני הסוכנים.
כבר הוחל ב-DB; הסקריפט לשחזור אחרי reset.
אומת: py_compile ✓ · tsc ✓ · החיווט אומת חי על 8126 (deepseek+gemini, dedup,
✓ pipeline הושלם). G2 (יכולת חסרה) · INV-LRN1/G10 נשמרים.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,7 @@
|
|||||||
| `style_lesson_panel.py` | python | **פאנל-סגנון דו-סוכני (למידה כפולה).** על-גבי דיסטילציית-ה-Opus (draft↔final ב-`draft_final_pairs.analysis`), שני שופטים בלתי-תלויים — DeepSeek + Gemini-2.5-flash — מצביעים לכל לקח על השאלה הגסה "האם זו הנחיית-סגנון מופשטת ובת-הכללה (INV-LRN5 — קול ולא מהות)?". הסכמה 2/2-keep → נכתב כ-`decision_lesson` (`source=panel:deepseek+gemini`); 2/2-drop → לא נכתב; פיצול/substance → מוסלם ליו"ר. `--apply` הפיך, מגבה ל-`data/audit/`. הטמעה ל-SKILL.md/lessons.md נשארת שער-יו"ר ידני (INV-G10). מפתחות כמו פאנל-ההלכות. **חובה מקומי**. `--case <num>` / `--pair-id <uuid>`. | שלב-למידה במסלול-הסופי |
|
| `style_lesson_panel.py` | python | **פאנל-סגנון דו-סוכני (למידה כפולה).** על-גבי דיסטילציית-ה-Opus (draft↔final ב-`draft_final_pairs.analysis`), שני שופטים בלתי-תלויים — DeepSeek + Gemini-2.5-flash — מצביעים לכל לקח על השאלה הגסה "האם זו הנחיית-סגנון מופשטת ובת-הכללה (INV-LRN5 — קול ולא מהות)?". הסכמה 2/2-keep → נכתב כ-`decision_lesson` (`source=panel:deepseek+gemini`); 2/2-drop → לא נכתב; פיצול/substance → מוסלם ליו"ר. `--apply` הפיך, מגבה ל-`data/audit/`. הטמעה ל-SKILL.md/lessons.md נשארת שער-יו"ר ידני (INV-G10). מפתחות כמו פאנל-ההלכות. **חובה מקומי**. `--case <num>` / `--pair-id <uuid>`. | שלב-למידה במסלול-הסופי |
|
||||||
| `final_learning_pipeline.py` | python | **תזמור שלב-הלמידה (פקודה אחת).** מופעל ע"י הרמס כשלוחצים "הרץ למידת-קול" במסלול-הסופי. דטרמיניסטי: (1) `ingest_final_version` עם נתיב-הסופי, (2) רישום לקורפוס-הסגנון (idempotent), (3) `style_lesson_panel --apply`. מקפל את הזרימה לפקודה אחת כדי שהסוכן לא ירכיב כמה קריאות (חסין). idempotent. **חובה מקומי**. `--case <num>`. | אוטו (כפתור run-learning) / ידני |
|
| `final_learning_pipeline.py` | python | **תזמור שלב-הלמידה (פקודה אחת).** מופעל ע"י הרמס כשלוחצים "הרץ למידת-קול" במסלול-הסופי. דטרמיניסטי: (1) `ingest_final_version` עם נתיב-הסופי, (2) רישום לקורפוס-הסגנון (idempotent), (3) `style_lesson_panel --apply`. מקפל את הזרימה לפקודה אחת כדי שהסוכן לא ירכיב כמה קריאות (חסין). idempotent. **חובה מקומי**. `--case <num>`. | אוטו (כפתור run-learning) / ידני |
|
||||||
| `final_halacha_pipeline.py` | python | **תזמור שלב-אימות-ההלכות (פקודה אחת).** מופעל ע"י הרמס כשלוחצים "הרץ אימות-הלכות". דטרמיניסטי: (1) `extract_internal_citations(chair)`, (2) `corroboration.build_all()`, (3) `halacha_panel_approve --apply`. **חובה מקומי**. `--case <num>` / `--limit N` (תקרת תור). | אוטו (כפתור run-halacha) / ידני |
|
| `final_halacha_pipeline.py` | python | **תזמור שלב-אימות-ההלכות (פקודה אחת).** מופעל ע"י הרמס כשלוחצים "הרץ אימות-הלכות". דטרמיניסטי: (1) `extract_internal_citations(chair)`, (2) `corroboration.build_all()`, (3) `halacha_panel_approve --apply`. **חובה מקומי**. `--case <num>` / `--limit N` (תקרת תור). | אוטו (כפתור run-halacha) / ידני |
|
||||||
|
| `curator_apply_pipeline_branch.py` | python | **מקור-אמת לחיווט-הכפתורים של הרמס.** prompt-ה-curator חי רק ב-Paperclip DB (`agents.adapter_config.promptTemplate`). הסקריפט מקדים branch כך שיקיצה עם reason `final_learning_*`/`final_halacha_*` מריצה את ה-pipeline המתאים (HOME/DOTENV/DATA_DIR מוחלטים → DeepSeek+Gemini keys + DATA_DIR נפתרים נכון) ועוצרת, אחרת §A/§B כרגיל. idempotent (מסיר branch קודם). מחיל על שני הסוכנים (CMP+CMPA). `--verify`. **להריץ אחרי reset/יצירה-מחדש של סוכן-curator.** | אחרי reset prompt של curator |
|
||||||
| `halacha_panel_audit.py` | python | **רשת-ביטחון לפאנל** (selective-prediction monitoring) — דוגם הלכות שאושרו ע"י הפאנל (`reviewer LIKE 'panel:%'`), מריץ עליהן **שוב** את הצבעת-ה-KEEP של 3 השופטים, ומציף כל מקרה שכעת נוטה DROP (false-keep פוטנציאלי). report-only כברירת-מחדל; `--flag` מחזיר את ה-flips ל-`pending_review` לסקירת-יו"ר. `--sample N`/`--seed`. בסיס 2026-06-07: 0/15. מיועד להרצה תקופתית (שבועי). מייבא שופטים מ-`halacha_panel_approve`. **חובה מקומי**. | תקופתי (שבועי) — ניטור |
|
| `halacha_panel_audit.py` | python | **רשת-ביטחון לפאנל** (selective-prediction monitoring) — דוגם הלכות שאושרו ע"י הפאנל (`reviewer LIKE 'panel:%'`), מריץ עליהן **שוב** את הצבעת-ה-KEEP של 3 השופטים, ומציף כל מקרה שכעת נוטה DROP (false-keep פוטנציאלי). report-only כברירת-מחדל; `--flag` מחזיר את ה-flips ל-`pending_review` לסקירת-יו"ר. `--sample N`/`--seed`. בסיס 2026-06-07: 0/15. מיועד להרצה תקופתית (שבועי). מייבא שופטים מ-`halacha_panel_approve`. **חובה מקומי**. | תקופתי (שבועי) — ניטור |
|
||||||
| `halacha_panel_calibrate.py` | python | **כיול מדיניות-ההצבעה של הפאנל** (Trust-or-Escalate, ICLR 2025). מריץ את שאלת-ה-KEEP של `halacha_panel_approve` על מדגם-הזהב ומודד מול `is_holding` (הציר-הגס) precision+coverage לכל מדיניות (unanimous/majority) + ספירת false-keep/false-drop. נותן את **אחוז-הטעות בפועל** לבחירת סף-סיכון α. מייבא שופטים מ-`halacha_panel_approve` (מקור-אמת יחיד). read-only, **חובה מקומי**. | ידני — לפני חיווט `--apply` |
|
| `halacha_panel_calibrate.py` | python | **כיול מדיניות-ההצבעה של הפאנל** (Trust-or-Escalate, ICLR 2025). מריץ את שאלת-ה-KEEP של `halacha_panel_approve` על מדגם-הזהב ומודד מול `is_holding` (הציר-הגס) precision+coverage לכל מדיניות (unanimous/majority) + ספירת false-keep/false-drop. נותן את **אחוז-הטעות בפועל** לבחירת סף-סיכון α. מייבא שופטים מ-`halacha_panel_approve` (מקור-אמת יחיד). read-only, **חובה מקומי**. | ידני — לפני חיווט `--apply` |
|
||||||
| `halacha_rule_role_backfill.py` | python | **INV-DM7** — backfill חד-פעמי: מסווג-מחדש את ההלכות הישנות (`rule_type IN ('binding','persuasive')` — ערכי-סמכות שנשמרו במסווה תפקיד לפני פיצול הצירים) לאחד מחמשת **תפקידי-הכלל** (holding/interpretive/procedural/application/obiter) דרך claude_session המקומי (אפס עלות). **לא נוגע בסמכות** (נגזרת מ-`precedent_level`). `--apply` (ברירת-מחדל dry-run) / `--limit N` / `--concurrency`. כותב backup CSV ל-`data/audit/` תחילה. fail-safe (פריט שנכשל → נשמר ערך ישן). **חובה מקומי** (claude_session). | ידני חד-פעמי אחרי deploy של פיצול-הסמכות |
|
| `halacha_rule_role_backfill.py` | python | **INV-DM7** — backfill חד-פעמי: מסווג-מחדש את ההלכות הישנות (`rule_type IN ('binding','persuasive')` — ערכי-סמכות שנשמרו במסווה תפקיד לפני פיצול הצירים) לאחד מחמשת **תפקידי-הכלל** (holding/interpretive/procedural/application/obiter) דרך claude_session המקומי (אפס עלות). **לא נוגע בסמכות** (נגזרת מ-`precedent_level`). `--apply` (ברירת-מחדל dry-run) / `--limit N` / `--concurrency`. כותב backup CSV ל-`data/audit/` תחילה. fail-safe (פריט שנכשל → נשמר ערך ישן). **חובה מקומי** (claude_session). | ידני חד-פעמי אחרי deploy של פיצול-הסמכות |
|
||||||
|
|||||||
107
scripts/curator_apply_pipeline_branch.py
Normal file
107
scripts/curator_apply_pipeline_branch.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Reproducible source-of-truth for the curator's PIPELINE-WAKE branch.
|
||||||
|
|
||||||
|
The Hermes curator's prompt lives ONLY in the Paperclip DB
|
||||||
|
(agents.adapter_config.promptTemplate), not in a repo file. This script (re-)prepends
|
||||||
|
a branch so that a wake whose reason starts with `final_learning_` / `final_halacha_`
|
||||||
|
runs the matching local pipeline script and stops — instead of the default §A/§B
|
||||||
|
style-curation routine. Without it, clicking the run-learning / run-halacha buttons
|
||||||
|
wakes the curator but it does style-curation, never the pipeline.
|
||||||
|
|
||||||
|
Idempotent: strips any prior branch (by the ORIG_START marker) and re-prepends the
|
||||||
|
current one. Applies to BOTH company curators (CMP + CMPA) so cross-company sync stays
|
||||||
|
consistent. Re-run this after any curator-prompt reset / agent re-creation.
|
||||||
|
|
||||||
|
python3 scripts/curator_apply_pipeline_branch.py # apply
|
||||||
|
python3 scripts/curator_apply_pipeline_branch.py --verify # show head, no write
|
||||||
|
|
||||||
|
Env knobs (defaults shown):
|
||||||
|
PAPERCLIP_DB_URL=postgresql://paperclip:paperclip@localhost:54329/paperclip
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
|
import asyncpg
|
||||||
|
|
||||||
|
PAPERCLIP_DB = os.environ.get(
|
||||||
|
"PAPERCLIP_DB_URL", "postgresql://paperclip:paperclip@localhost:54329/paperclip"
|
||||||
|
)
|
||||||
|
CURATOR_AGENTS = [
|
||||||
|
"60dce831-5c5b-4bae-bda9-5282d506f0dc", # CMP (1xxx — licensing)
|
||||||
|
"d6f7c55d-570a-46b8-8d72-1286d07da0d8", # CMPA (8xxx/9xxx — betterment)
|
||||||
|
]
|
||||||
|
MARKER = "### PIPELINE-WAKE BRANCH (auto)"
|
||||||
|
ORIG_START = "אתה מנהל ידע" # first line of the canonical curator prompt
|
||||||
|
|
||||||
|
# HOME=/home/chaim is the root fix: the Hermes agent runs with $HOME pointing at its
|
||||||
|
# profile dir, so Path.home() / expanduser('~') would resolve DATA_DIR and the
|
||||||
|
# DeepSeek/Gemini key files wrong. Pinning HOME (+ DOTENV_PATH/DATA_DIR) makes the
|
||||||
|
# pipeline resolve the case files AND both judge keys correctly.
|
||||||
|
BRANCH = MARKER + r'''
|
||||||
|
לפני כל דבר אחר — בדוק אם זו יקיצת-pipeline אוטומטית. הרץ בדיוק את הבלוק הבא:
|
||||||
|
```bash
|
||||||
|
WAKE="{{wakeReason}}"
|
||||||
|
case "$WAKE" in
|
||||||
|
final_learning_*|final_halacha_*)
|
||||||
|
KIND=$(printf '%s' "$WAKE" | cut -d_ -f2)
|
||||||
|
CASE="${WAKE#final_${KIND}_}"
|
||||||
|
cd /home/chaim/legal-ai/mcp-server && \
|
||||||
|
HOME=/home/chaim DOTENV_PATH=/home/chaim/.env DATA_DIR=/home/chaim/legal-ai/data \
|
||||||
|
nohup .venv/bin/python ../scripts/final_${KIND}_pipeline.py --case "$CASE" \
|
||||||
|
> "/tmp/final_${KIND}_${CASE}.log" 2>&1 &
|
||||||
|
sleep 2
|
||||||
|
echo "PIPELINE_STARTED final_${KIND}_pipeline case=$CASE log=/tmp/final_${KIND}_${CASE}.log"
|
||||||
|
;;
|
||||||
|
*) echo "NO_PIPELINE_WAKE" ;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
אם הפלט הוא `PIPELINE_STARTED ...` — **זו כל המשימה**: כתוב comment קצר בעברית ("הופעל צינור <KIND> לתיק <CASE>; התוצאות יופיעו ב-/training (סגנון) או /approvals + /precedents (הלכות) תוך מספר דקות."), סגור את ה-issue (status=done), ו**סיים מיד — אל תמשיך לסעיפים שלמטה**.
|
||||||
|
אם הפלט הוא `NO_PIPELINE_WAKE` — המשך כרגיל לתבנית שלמטה.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
async def main(verify: bool) -> int:
|
||||||
|
conn = await asyncpg.connect(PAPERCLIP_DB)
|
||||||
|
try:
|
||||||
|
for aid in CURATOR_AGENTS:
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"SELECT name, adapter_config->>'promptTemplate' AS t FROM agents WHERE id=$1",
|
||||||
|
aid,
|
||||||
|
)
|
||||||
|
if not row or not row["t"]:
|
||||||
|
print(f"{aid}: NO TEMPLATE — skip")
|
||||||
|
continue
|
||||||
|
t = row["t"]
|
||||||
|
if verify:
|
||||||
|
has = MARKER in t
|
||||||
|
print(f"{aid} ({row['name']}): branch={'present' if has else 'MISSING'} "
|
||||||
|
f"len={len(t)}")
|
||||||
|
continue
|
||||||
|
i = t.find(ORIG_START)
|
||||||
|
if i < 0:
|
||||||
|
print(f"{aid}: ORIG_START not found — skip (manual check)")
|
||||||
|
continue
|
||||||
|
new = BRANCH + t[i:] # strip any prior branch, re-prepend
|
||||||
|
await conn.execute(
|
||||||
|
"UPDATE agents SET adapter_config = "
|
||||||
|
"jsonb_set(adapter_config,'{promptTemplate}', to_jsonb($2::text)), "
|
||||||
|
"updated_at = now() WHERE id=$1",
|
||||||
|
aid, new,
|
||||||
|
)
|
||||||
|
print(f"{aid} ({row['name']}): branch applied; template now {len(new)} chars")
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("--verify", action="store_true", help="report only, no write")
|
||||||
|
raise SystemExit(asyncio.run(main(ap.parse_args().verify)))
|
||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
BLOCK_LABELS,
|
BLOCK_LABELS,
|
||||||
type FeedbackCategory,
|
type FeedbackCategory,
|
||||||
} from "@/lib/api/feedback";
|
} from "@/lib/api/feedback";
|
||||||
|
import { useCaseCitations } from "@/lib/api/citations";
|
||||||
import type { CaseStatus } from "@/lib/api/cases";
|
import type { CaseStatus } from "@/lib/api/cases";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
@@ -47,6 +48,8 @@ import {
|
|||||||
Brain,
|
Brain,
|
||||||
Scale,
|
Scale,
|
||||||
Stamp,
|
Stamp,
|
||||||
|
Link2,
|
||||||
|
AlertTriangle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
/* Statuses at which a draft is considered ready */
|
/* Statuses at which a draft is considered ready */
|
||||||
@@ -336,6 +339,9 @@ export function DraftsPanel({
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* ── Precedents cited inside the signed decision ── */}
|
||||||
|
<CitationsSection caseNumber={caseNumber} />
|
||||||
|
|
||||||
{/* ── Exports list ── */}
|
{/* ── Exports list ── */}
|
||||||
<section>
|
<section>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
@@ -587,6 +593,69 @@ export function DraftsPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Precedents cited inside the decision ──────────── */
|
||||||
|
|
||||||
|
function CitationsSection({ caseNumber }: { caseNumber: string }) {
|
||||||
|
const { data, isLoading } = useCaseCitations(caseNumber);
|
||||||
|
|
||||||
|
// Nothing to show until the signed decision is in the precedent library.
|
||||||
|
if (isLoading || !data?.in_library) return null;
|
||||||
|
if (!data.linked.length && !data.missing.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Scale className="w-4 h-4 text-navy" />
|
||||||
|
<h3 className="text-navy text-base">פסיקה שצוטטה בהחלטה</h3>
|
||||||
|
<Badge variant="outline" className="text-[0.65rem]">
|
||||||
|
{data.linked.length} בספרייה · {data.missing.length} חסרות
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-rule overflow-hidden divide-y divide-rule">
|
||||||
|
{data.linked.map((c) => (
|
||||||
|
<a
|
||||||
|
key={`l-${c.citation}`}
|
||||||
|
href={`/precedents/${c.cited_id}`}
|
||||||
|
className="flex items-center gap-2 px-4 py-2.5 text-sm hover:bg-rule-soft/20"
|
||||||
|
>
|
||||||
|
<Link2 className="w-3.5 h-3.5 text-success shrink-0" />
|
||||||
|
<span className="font-medium text-ink">{c.citation}</span>
|
||||||
|
{c.case_name && (
|
||||||
|
<span className="text-ink-muted truncate">— {c.case_name}</span>
|
||||||
|
)}
|
||||||
|
<Badge className="ms-auto bg-success-bg text-success border-success/40 text-[0.65rem] shrink-0">
|
||||||
|
בספרייה
|
||||||
|
</Badge>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
{data.missing.map((c) => (
|
||||||
|
<div
|
||||||
|
key={`m-${c.citation}`}
|
||||||
|
className="flex items-center gap-2 px-4 py-2.5 text-sm"
|
||||||
|
>
|
||||||
|
<AlertTriangle className="w-3.5 h-3.5 text-gold-deep shrink-0" />
|
||||||
|
<span className="font-medium text-ink">{c.citation}</span>
|
||||||
|
<Badge
|
||||||
|
className={`ms-auto text-[0.65rem] shrink-0 border ${
|
||||||
|
c.flagged
|
||||||
|
? "bg-gold-wash text-gold-deep border-gold/40"
|
||||||
|
: "bg-rule-soft text-ink-soft border-rule"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{c.flagged ? "חסרה — סומנה להעלאה" : "חסרה בספרייה"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-[0.7rem] text-ink-muted mt-2">
|
||||||
|
פסיקה מקושרת מחזקת את ההלכות שלה (corroboration); חסרות מסומנות אוטומטית
|
||||||
|
להעלאה לספרייה.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── New feedback dialog (case-scoped) ─────────────── */
|
/* ── New feedback dialog (case-scoped) ─────────────── */
|
||||||
|
|
||||||
function NewCaseFeedbackDialog({ caseNumber }: { caseNumber: string }) {
|
function NewCaseFeedbackDialog({ caseNumber }: { caseNumber: string }) {
|
||||||
|
|||||||
37
web-ui/src/lib/api/citations.ts
Normal file
37
web-ui/src/lib/api/citations.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Citations domain — precedents CITED inside a case's signed decision.
|
||||||
|
* Powers the case-page "פסיקה שצוטטה בהחלטה" panel.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { apiRequest } from "./client";
|
||||||
|
|
||||||
|
export type LinkedCitation = {
|
||||||
|
citation: string;
|
||||||
|
cited_id: string;
|
||||||
|
case_name: string;
|
||||||
|
court: string;
|
||||||
|
precedent_level: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MissingCitation = {
|
||||||
|
citation: string;
|
||||||
|
flagged: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CaseCitations = {
|
||||||
|
in_library: boolean;
|
||||||
|
case_law_id?: string;
|
||||||
|
linked: LinkedCitation[];
|
||||||
|
missing: MissingCitation[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useCaseCitations(caseNumber: string | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["case-citations", caseNumber ?? ""],
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
apiRequest<CaseCitations>(`/api/cases/${caseNumber}/citations`, { signal }),
|
||||||
|
enabled: Boolean(caseNumber),
|
||||||
|
staleTime: 10_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
59
web/app.py
59
web/app.py
@@ -3222,6 +3222,65 @@ async def api_get_active_draft(case_number: str):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/cases/{case_number}/citations")
|
||||||
|
async def api_case_citations(case_number: str):
|
||||||
|
"""Precedents CITED inside this case's signed decision — split into those linked to
|
||||||
|
the precedent library and those still missing from it (flagged for upload).
|
||||||
|
|
||||||
|
Powers the case-page "פסיקה שצוטטה בהחלטה" panel. Source: the decision's row in
|
||||||
|
case_law (source_kind='internal_committee') → precedent_internal_citations."""
|
||||||
|
case = await db.get_case_by_number(case_number)
|
||||||
|
if not case:
|
||||||
|
raise HTTPException(404, f"תיק {case_number} לא נמצא")
|
||||||
|
pool = await db.get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
law_id = await conn.fetchval(
|
||||||
|
"SELECT id FROM case_law WHERE case_number = $1 "
|
||||||
|
"AND source_kind = 'internal_committee' ORDER BY created_at DESC LIMIT 1",
|
||||||
|
case_number,
|
||||||
|
)
|
||||||
|
if not law_id:
|
||||||
|
# decision not yet in the library (e.g. final not uploaded)
|
||||||
|
return {"in_library": False, "linked": [], "missing": []}
|
||||||
|
linked = await conn.fetch(
|
||||||
|
"SELECT pic.cited_case_number, cl.id AS cited_id, cl.case_name, cl.court, "
|
||||||
|
" cl.precedent_level "
|
||||||
|
"FROM precedent_internal_citations pic "
|
||||||
|
"JOIN case_law cl ON cl.id = pic.cited_case_law_id "
|
||||||
|
"WHERE pic.source_case_law_id = $1 "
|
||||||
|
"ORDER BY pic.cited_case_number",
|
||||||
|
law_id,
|
||||||
|
)
|
||||||
|
missing = await conn.fetch(
|
||||||
|
"SELECT DISTINCT pic.cited_case_number, "
|
||||||
|
" EXISTS(SELECT 1 FROM missing_precedents mp "
|
||||||
|
" WHERE mp.citation = pic.cited_case_number AND mp.status = 'open') AS flagged "
|
||||||
|
"FROM precedent_internal_citations pic "
|
||||||
|
"WHERE pic.source_case_law_id = $1 AND pic.cited_case_law_id IS NULL "
|
||||||
|
" AND pic.cited_case_number <> '' "
|
||||||
|
"ORDER BY pic.cited_case_number",
|
||||||
|
law_id,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"in_library": True,
|
||||||
|
"case_law_id": str(law_id),
|
||||||
|
"linked": [
|
||||||
|
{
|
||||||
|
"citation": r["cited_case_number"],
|
||||||
|
"cited_id": str(r["cited_id"]),
|
||||||
|
"case_name": r["case_name"] or "",
|
||||||
|
"court": r["court"] or "",
|
||||||
|
"precedent_level": r["precedent_level"] or "",
|
||||||
|
}
|
||||||
|
for r in linked
|
||||||
|
],
|
||||||
|
"missing": [
|
||||||
|
{"citation": r["cited_case_number"], "flagged": r["flagged"]}
|
||||||
|
for r in missing
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/cases/{case_number}/exports/{filename}/mark-final")
|
@app.post("/api/cases/{case_number}/exports/{filename}/mark-final")
|
||||||
async def api_mark_final(case_number: str, filename: str):
|
async def api_mark_final(case_number: str, filename: str):
|
||||||
"""Mark an export as the final version — copies to training corpus."""
|
"""Mark an export as the final version — copies to training corpus."""
|
||||||
|
|||||||
Reference in New Issue
Block a user