7 Commits

Author SHA1 Message Date
2e2d2d42b6 Prevent status regression in case_update
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m32s
CEO agent was reverting case status from "processing" to "new" when
updating metadata fields. Added ordered status list — case_update now
silently ignores status changes that would move backwards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:05:40 +00:00
c71d7b3b9c Schedule daily DB backup (cron 2am) and gitignore backup files
- backup-db.sh tested successfully (19MB, pg_dump 17)
- Scheduled: 0 2 * * * with log to data/backups/backup.log
- Added data/backups/ and data/.auto-sync.log to .gitignore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:03:11 +00:00
33e265e19c Document Garner/FJC methodology files as source material in CLAUDE.md
These are source extractions that fed into decision-methodology.md.
Not read by agents — kept as audit trail for methodology origins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:46:40 +00:00
3b260a094d Remove legacy vanilla frontend, clarify web/ vs web-ui/ in CLAUDE.md
- Delete web/static/index.html and design-system.css (replaced by Next.js)
- Remove GET / HTML route and StaticFiles import from app.py
- CLAUDE.md: document that web/ = FastAPI API, web-ui/ = Next.js frontend

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:41:02 +00:00
5c9a5d702a Clean up scripts/: archive 17, delete 5, add SCRIPTS.md registry
Active scripts (5): auto-sync-cases.sh, backup-db.sh, restore-db.sh,
notify.py, bidi_table.py

Archived (17): one-time migration/seeding scripts whose functionality
is now in MCP server or web API. Moved to scripts/.archive/

Deleted (5): zero-value scripts (duplicates, hardcoded single-case,
debug scripts)

Added scripts/SCRIPTS.md — registry of all scripts with purpose,
status, and what superseded them. CLAUDE.md updated with rule:
any script change requires SCRIPTS.md update.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:30:19 +00:00
38e79bbf92 Replace duplicate block-schema.md with symlink to docs/
skills/decision/references/block-schema.md was a stale copy that
diverged from docs/block-schema.md. Now a symlink — single source of truth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:18:15 +00:00
891f20dbb9 Clean up legacy references: update CLAUDE.md, remove dead import script
- CLAUDE.md: clarify vault was deleted, knowledge is in docs/+training/
- Remove import-final-decisions.py (migration completed, all decisions in DB)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:16:35 +00:00
31 changed files with 89 additions and 6927 deletions

2
.gitignore vendored
View File

@@ -2,6 +2,8 @@ data/uploads/
data/cases/ data/cases/
data/training/ data/training/
data/exports/ data/exports/
data/backups/
data/.auto-sync.log
mcp-server/.venv/ mcp-server/.venv/
__pycache__/ __pycache__/
*.pyc *.pyc

View File

@@ -30,7 +30,7 @@
- לקחים מהשוואת טיוטות לגרסאות סופיות - לקחים מהשוואת טיוטות לגרסאות סופיות
- סקריפט ייצוא DOCX - סקריפט ייצוא DOCX
כל החומר הועבר לתיקיית `legacy/` כקריאה בלבד. **הפרויקט הנוכחי** מעביר את הידע הזה למערכת מובנית עם PostgreSQL + pgvector + n8n. הידע שהופק מה-vault הוטמע במערכת הנוכחית — מסמכי ייחוס (`docs/`), קורפוס אימון (`data/training/`), ומבנה 12 בלוקים. ה-vault המקורי נמחק; הפרויקט הנוכחי עובד עם PostgreSQL + pgvector.
--- ---
@@ -43,6 +43,8 @@
| [`docs/migration-plan.md`](docs/migration-plan.md) | תוכנית מעבר vault → DB — טבלאות, עדיפויות, כמויות | לפני ייבוא נתונים | | [`docs/migration-plan.md`](docs/migration-plan.md) | תוכנית מעבר vault → DB — טבלאות, עדיפויות, כמויות | לפני ייבוא נתונים |
| [`docs/legal-decision-lessons.md`](docs/legal-decision-lessons.md) | לקחים מ-3 החלטות — מה עבד, מה השתנה, ביטויי מעבר חדשים | **לפני כל כתיבת החלטה** | | [`docs/legal-decision-lessons.md`](docs/legal-decision-lessons.md) | לקחים מ-3 החלטות — מה עבד, מה השתנה, ביטויי מעבר חדשים | **לפני כל כתיבת החלטה** |
| [`docs/decision-methodology.md`](docs/decision-methodology.md) | **מתודולוגיה אנליטית — איך לחשוב על החלטה מעין-שיפוטית** | **לפני כל כתיבת החלטה** | | [`docs/decision-methodology.md`](docs/decision-methodology.md) | **מתודולוגיה אנליטית — איך לחשוב על החלטה מעין-שיפוטית** | **לפני כל כתיבת החלטה** |
| `docs/garner-methodology-extraction.md` | חומר מקור: מיצוי מספרי Garner על כתיבה משפטית | רק לבדיקת מקור |
| `docs/fjc-principles-extraction.md` | חומר מקור: מיצוי מ-Judicial Writing Manual (FJC) | רק לבדיקת מקור |
| [`docs/corpus-analysis.md`](docs/corpus-analysis.md) | ניתוח שיטתי של 24 החלטות — מפת תוכן, דפוסי דיון תכנוני, פערים | **לפני כל כתיבת החלטה** | | [`docs/corpus-analysis.md`](docs/corpus-analysis.md) | ניתוח שיטתי של 24 החלטות — מפת תוכן, דפוסי דיון תכנוני, פערים | **לפני כל כתיבת החלטה** |
| [`docs/memory.md`](docs/memory.md) | הקשר כללי — skills, פרויקטים שהושלמו, מבנה vault | להתמצאות כללית | | [`docs/memory.md`](docs/memory.md) | הקשר כללי — skills, פרויקטים שהושלמו, מבנה vault | להתמצאות כללית |
| [`skills/decision/SKILL.md`](skills/decision/SKILL.md) | מדריך סגנון מלא של דפנה — טון, מבנה, ביטויים, מתודולוגיה | **לפני כל כתיבת החלטה** | | [`skills/decision/SKILL.md`](skills/decision/SKILL.md) | מדריך סגנון מלא של דפנה — טון, מבנה, ביטויים, מתודולוגיה | **לפני כל כתיבת החלטה** |
@@ -82,15 +84,28 @@
│ └── docx/ עיצוב DOCX │ └── docx/ עיצוב DOCX
├── data/ ├── data/
│ ├── training/ ← 4 החלטות לאימון (DOCX) │ ├── training/ ← 4 החלטות לאימון (DOCX)
│ ├── exports/ ← ייצוא legacy (תיקים ישנים) │ ├── exports/ ← טיוטות DOCX מיוצאות
│ └── cases/{case-number}/ ← תיקי עררים (מבנה שטוח, סטטוס ב-DB) │ └── cases/{case-number}/ ← תיקי עררים (מבנה שטוח, סטטוס ב-DB)
├── web/ ← UI + API + integration clients ├── web/ ← FastAPI backend (Python): 75 API endpoints
│ ├── app.py ← API ראשי
│ ├── paperclip_client.py ← אינטגרציית Paperclip
│ └── gitea_client.py ← אינטגרציית Gitea
├── web-ui/ ← Next.js frontend (TypeScript/React): ממשק המשתמש
│ └── next.config.ts ← proxy: /api/* → FastAPI :8000
├── mcp-server/ ← MCP server + services + tools ├── mcp-server/ ← MCP server + services + tools
└── scripts/ ← סקריפטים וכלי עזר └── scripts/ ← סקריפטים וכלי עזר (ראה scripts/SCRIPTS.md)
└── .archive/ ← סקריפטים שהושלמו (לא להריץ)
``` ```
--- ---
## כלל: עדכון `scripts/SCRIPTS.md`
בכל פעם שנוצר, נמחק, או משתנה סקריפט בתיקיית `scripts/`**חובה לעדכן את `scripts/SCRIPTS.md`** בהתאם.
הקובץ מתעד את התפקיד, הסטטוס, וההחלפה (אם יש) של כל סקריפט.
---
## ניהול משימות — TaskMaster AI ## ניהול משימות — TaskMaster AI
הפרויקט משתמש ב-**TaskMaster AI** (MCP server) לניהול משימות מובנה: הפרויקט משתמש ב-**TaskMaster AI** (MCP server) לניהול משימות מובנה:

View File

@@ -277,13 +277,27 @@ async def case_update(
""" """
from datetime import date as date_type from datetime import date as date_type
# Ordered workflow statuses — regression protection
STATUS_ORDER = [
"new", "uploading", "processing", "documents_ready",
"analyst_verified", "research_complete", "outcome_set",
"brainstorming", "direction_approved", "analysis_enriched", "ready_for_writing",
"drafting", "qa_review", "drafted",
"exported", "reviewed", "final",
]
case = await db.get_case_by_number(case_number) case = await db.get_case_by_number(case_number)
if not case: if not case:
return f"תיק {case_number} לא נמצא." return f"תיק {case_number} לא נמצא."
fields = {} fields = {}
if status: if status:
fields["status"] = status current = case.get("status", "")
cur_idx = STATUS_ORDER.index(current) if current in STATUS_ORDER else -1
new_idx = STATUS_ORDER.index(status) if status in STATUS_ORDER else -1
# Only update if advancing or status is unknown to the order
if new_idx >= cur_idx or new_idx == -1:
fields["status"] = status
if title: if title:
fields["title"] = title fields["title"] = title
if subject: if subject:

51
scripts/SCRIPTS.md Normal file
View File

@@ -0,0 +1,51 @@
# scripts/ — מדריך סקריפטים
> **כלל:** כל עדכון, יצירה, או מחיקה של סקריפט בתיקייה זו מחייב עדכון של קובץ זה.
---
## סקריפטים פעילים
| Script | Type | Purpose | Scheduled |
|--------|------|---------|-----------|
| `auto-sync-cases.sh` | bash | סנכרון תיקי ערר ל-Gitea — רץ כל דקה | `* * * * *` (cron) |
| `backup-db.sh` | bash | גיבוי PostgreSQL יומי ל-`data/backups/` (gzip) | לתזמן: `0 2 * * *` |
| `restore-db.sh` | bash | שחזור DB מגיבוי (companion ל-backup-db.sh) | ידני |
| `notify.py` | python | שליחת מייל התראה מסוכנים via SMTP (Gmail) | נקרא ע"י סוכנים |
| `bidi_table.py` | python | יצירת טבלאות box-drawing עם תמיכה ב-BiDi (עברית+אנגלית) | ספריית עזר |
## תיקיית `.archive/` — סקריפטים שהושלמו
סקריפטים חד-פעמיים שהפונקציונליות שלהם הוטמעה ב-MCP server או ב-API.
נשמרים ב-git לצורך היסטוריה — **אין להריץ אותם**.
| Script | Original Purpose | Superseded By |
|--------|-----------------|---------------|
| `backfill_pattern_frequency.py` | עדכון תדירות דפוסי סגנון ב-DB | `web/app.py::_extract_pattern_variants()` |
| `batch_upload_training.py` | העלאת קורפוס אימון (16 קבצים) | Web UI: `/api/training/upload` |
| `benchmark_embeddings.py` | השוואת מודלי embeddings (voyage-3 vs voyage-4) | הושלם — voyage-3-large נבחר |
| `benchmark_new_vs_old.py` | השוואת Google Vision vs markdown קיים | הושלם — בדיקה חד-פעמית לתיק 1130-25 |
| `decompose-decisions.py` | פירוק החלטות סופיות ל-12 בלוקים | MCP: `write_block()`, `write_all_blocks()` |
| `export-decision-docx.py` | ייצוא החלטה ל-DOCX | MCP: `export_docx()` |
| `extract-citations.py` | חילוץ ציטוטי פסיקה מבלוק י | MCP service: `references_extractor.py` |
| `extract-claims.py` | חילוץ טענות מבלוק ז | MCP: `extract_claims()` + `claims_extractor.py` |
| `extract_all_google_vision.py` | OCR בכמות עם Google Vision | MCP: `document_upload()` pipeline |
| `extract_originals.py` | חילוץ טקסט מ-PDF עם Claude Opus | MCP service: `extractor.py` |
| `extract_originals_ocr.py` | חילוץ OCR מלא מ-PDF | MCP service: `extractor.py` |
| `generate-embeddings.py` | יצירת embeddings לבלוקים ופסיקה | אוטומטי — נוצרים עם יצירת בלוקים |
| `link-claims-to-discussion.py` | קישור טענות לפסקאות דיון | MCP service: `qa_validator.py` |
| `proofread_training_corpus.py` | ניקוי Nevo מ-DOCX/PDF ל-Markdown | MCP service: `proofreader.py` + Web UI |
| `seed-appeals.py` | seeding תיקי ערר ראשוניים ל-DB | MCP: `case_create()` |
| `seed-knowledge.py` | seeding לקחים, ביטויי מעבר, פסיקה | MCP: `record_chair_feedback()`, `precedent_attach()` |
| `validate-decision.py` | ולידציה מול block-schema | MCP: `validate_decision()` + `qa_validator.py` |
## סקריפטים שנמחקו (git history בלבד)
| Script | Reason |
|--------|--------|
| `import-final-decisions.py` | מיגרציה הושלמה — כל ההחלטות ב-`data/training/` |
| `compare_extractions.py` | בדיקה חד-פעמית לתיק 1130-25 |
| `decompose-decisions-v2.py` | כפילות של v1 |
| `extract_google_vision.py` | hardcoded לתיק בודד |
| `extract_google_vision_single.py` | wrapper חד-פעמי |
| `test-search.py` | סקריפט דיבאג |

View File

@@ -1,126 +0,0 @@
"""Compare existing MD files with freshly extracted text from PDFs."""
import difflib
from pathlib import Path
DOCS_DIR = Path("/home/chaim/legal-ai/data/cases/1130-25/documents")
EXTRACTED_DIR = DOCS_DIR / "extracted"
# Map: existing MD -> extracted MD
PAIRS = [
("2025-08-14-כתב-ערר-קובר.md", "מרק קובר-כתב ערר.md", "Appeal - Kuber"),
("2025-09-01-כתב-תשובה-ליבמן-לערר.md", "תשובה לערר מטעם המשיבים.md", "Response - Livman"),
("2025-09-02-כתב-תשובה-ועדת-הראל-לערר.md", "תשובת הועדה המרחבית לערר.md", "Response - Committee"),
("2025-10-22-כתב-ערר-מטמון.md", "תשובת המשיב-יצחק מטמון.md", "Response - Matmon"),
]
def normalize(text: str) -> str:
"""Normalize text for comparison."""
# Remove markdown formatting, extra whitespace
lines = text.strip().split("\n")
lines = [l.strip() for l in lines if l.strip()]
return "\n".join(lines)
def word_overlap(a: str, b: str) -> float:
"""Calculate word-level overlap ratio."""
words_a = set(a.split())
words_b = set(b.split())
if not words_a or not words_b:
return 0.0
intersection = words_a & words_b
return len(intersection) / max(len(words_a), len(words_b))
def main():
print(f"{'=' * 70}")
print("COMPARISON: Existing MD vs Fresh PDF Extraction")
print(f"{'=' * 70}\n")
summary = []
for existing_name, extracted_name, label in PAIRS:
existing_path = DOCS_DIR / existing_name
extracted_path = EXTRACTED_DIR / extracted_name
if not existing_path.exists():
print(f"SKIP: {existing_name} not found")
continue
if not extracted_path.exists():
print(f"SKIP: {extracted_name} not found")
continue
existing_text = existing_path.read_text(encoding="utf-8")
extracted_text = extracted_path.read_text(encoding="utf-8")
existing_norm = normalize(existing_text)
extracted_norm = normalize(extracted_text)
# Stats
existing_chars = len(existing_text)
extracted_chars = len(extracted_text)
existing_words = len(existing_text.split())
extracted_words = len(extracted_text.split())
# Similarity
overlap = word_overlap(existing_norm, extracted_norm)
# Sequence matcher ratio (slower but more accurate)
# Use first 5000 chars for speed
sm = difflib.SequenceMatcher(None, existing_norm[:5000], extracted_norm[:5000])
seq_ratio = sm.ratio()
# Find lines in extracted but not in existing (new content)
existing_lines = set(existing_norm.split("\n"))
extracted_lines = set(extracted_norm.split("\n"))
new_lines = extracted_lines - existing_lines
missing_lines = existing_lines - extracted_lines
print(f"{'=' * 70}")
print(f" {label}")
print(f" Existing: {existing_name}")
print(f" Extracted: {extracted_name}")
print(f"{'=' * 70}")
print(f" {'Metric':<30} {'Existing MD':>15} {'Fresh PDF':>15} {'Diff':>10}")
print(f" {'-' * 70}")
print(f" {'Characters':<30} {existing_chars:>15,} {extracted_chars:>15,} {extracted_chars - existing_chars:>+10,}")
print(f" {'Words':<30} {existing_words:>15,} {extracted_words:>15,} {extracted_words - existing_words:>+10,}")
print(f" {'Lines':<30} {len(existing_lines):>15,} {len(extracted_lines):>15,} {len(extracted_lines) - len(existing_lines):>+10,}")
print(f" {'Word overlap':<30} {overlap:>15.1%}")
print(f" {'Sequence similarity':<30} {seq_ratio:>15.1%}")
print(f" {'Lines only in fresh PDF':<30} {len(new_lines):>15}")
print(f" {'Lines only in existing MD':<30} {len(missing_lines):>15}")
# Show sample differences
if new_lines:
print(f"\n Sample lines ONLY in fresh extraction (first 3):")
for line in sorted(new_lines)[:3]:
print(f" + {line[:100]}")
if missing_lines:
print(f"\n Sample lines ONLY in existing MD (first 3):")
for line in sorted(missing_lines)[:3]:
print(f" - {line[:100]}")
print()
summary.append({
"label": label,
"existing_words": existing_words,
"extracted_words": extracted_words,
"word_overlap": overlap,
"seq_similarity": seq_ratio,
})
# Summary table
print(f"\n{'=' * 70}")
print("SUMMARY")
print(f"{'=' * 70}")
print(f" {'Document':<25} {'Existing':>10} {'Fresh':>10} {'Overlap':>10} {'Similarity':>12}")
print(f" {'-' * 67}")
for s in summary:
print(f" {s['label']:<25} {s['existing_words']:>10,} {s['extracted_words']:>10,} {s['word_overlap']:>10.1%} {s['seq_similarity']:>12.1%}")
if __name__ == "__main__":
main()

View File

@@ -1,289 +0,0 @@
#!/usr/bin/env python3
"""Decompose final decisions into 12-block structure — V2 calibrated on הכט.
Key insight: DOCX extraction strips header blocks (א-ד). The real content
starts at block ה (opening "לפנינו"). We identify blocks by known section
headers and line-by-line analysis.
"""
import asyncio
import json
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
from legal_mcp.services.db import get_pool, init_schema, close_pool
BLOCK_DEFS = [
("block-alef", 1, "כותרת מוסדית", "template-fill"),
("block-bet", 2, "הרכב הוועדה", "template-fill"),
("block-gimel", 3, "צדדים", "template-fill"),
("block-dalet", 4, "כותרת החלטה", "template-fill"),
("block-he", 5, "פתיחה", "paraphrase"),
("block-vav", 6, "רקע עובדתי", "reproduction"),
("block-zayin", 7, "טענות הצדדים", "paraphrase"),
("block-chet", 8, "הליכים בפני ועדת הערר", "reproduction"),
("block-tet", 9, "תכניות חלות", "guided-synthesis"),
("block-yod", 10, "דיון והכרעה", "rhetorical-construction"),
("block-yod-alef", 11, "סיכום", "paraphrase"),
("block-yod-bet", 12, "חתימות", "template-fill"),
]
def find_line(lines: list[str], pattern: str, start: int = 0) -> int:
"""Find first line matching pattern (substring or regex). Returns -1 if not found."""
pat = re.compile(pattern)
for i in range(start, len(lines)):
if pat.search(lines[i]):
return i
return -1
def slice_text(lines: list[str], start: int, end: int) -> str:
"""Join lines[start:end] into text."""
if start < 0 or end <= start:
return ""
return "\n".join(lines[start:end]).strip()
def count_words(text: str) -> int:
return len(text.split()) if text else 0
def decompose(text: str) -> dict[str, str]:
"""Parse decision into blocks. Returns {block_id: content}."""
lines = text.split("\n")
n = len(lines)
blocks = {}
# Find key section headers
# Style 1: רישוי — descriptive headers ("תמצית טענות הצדדים", "דיון והכרעה")
# Style 2: היטל השבחה — numbered headers ("א. רקע עובדתי", "ו. דיון והכרעה")
opening = find_line(lines, r"^לפנינו\s|^בפנינו\s|^בפני\s*ועדת|^בפני\s*בקשה")
claims = find_line(lines, r"תמצית\s*טענות|טענות\s*הצדדים|טענות\s*העוררי")
if claims == -1:
claims = find_line(lines, r"^טענות\s*העוררי")
if claims == -1:
# היטל השבחה style: "ב. טענות העורר"
claims = find_line(lines, r"^[א-ת][\.\)]\s*טענות")
background = find_line(lines, r"^[א-ת][\.\)]\s*רקע\s*עובדתי")
proceedings = find_line(lines, r"ההליכים\s*בפני|הליכים\s*בפני|הדיון\s*בפני\s*ועדת\s*הערר")
if proceedings == -1:
# היטל השבחה: "ד. הבהרות השמאית" or similar procedural sections
proceedings = find_line(lines, r"^[א-ת][\.\)]\s*הבהרות|^[א-ת][\.\)]\s*ההליך")
plans = find_line(lines, r"תכניות\s*חלות|המסגרת\s*הנורמטיבית|הוראות\s*התכנית")
if plans == -1:
plans = find_line(lines, r"^[א-ת][\.\)]\s*המסגרת\s*הנורמטיבית")
discussion = find_line(lines, r"^דיון\s*והכרעה|^דיון$|^הכרעה$")
if discussion == -1:
discussion = find_line(lines, r"^[א-ת][\.\)]\s*דיון\s*והכרעה")
summary = find_line(lines, r"^סיכום\s*$|^סוף\s*דבר\s*$")
if summary == -1:
summary = find_line(lines, r"^[א-ת][\.\)]\s*סיכום")
signature = find_line(lines, r"^ניתנה?\s*(היום|פה\s*אחד|ביום)")
# If no explicit discussion header, look for the opening formula
if discussion == -1:
discussion = find_line(lines, r"לאחר\s*שבחנו\s*את\s*טענות")
# ── Header blocks (א-ד): everything before opening ──
if opening >= 0:
header_text = slice_text(lines, 0, opening)
if header_text:
# Try to split header, but usually DOCX extraction loses these
blocks["block-alef"] = header_text
else:
blocks["block-alef"] = ""
else:
blocks["block-alef"] = ""
blocks["block-bet"] = "" # Usually lost in extraction
blocks["block-gimel"] = ""
blocks["block-dalet"] = "החלטה"
# ── Block ה: Opening — first 1-3 paragraphs from "לפנינו" ──
if opening >= 0:
next_section = claims if claims > opening else discussion if discussion > opening else n
opening_end = opening + 1
for i in range(opening + 1, min(opening + 5, next_section)):
line = lines[i].strip()
if not line:
break
opening_end = i + 1
blocks["block-he"] = slice_text(lines, opening, opening_end)
else:
blocks["block-he"] = ""
# ── Block ו: Background ──
# Style 1 (רישוי): after opening, before claims
# Style 2 (היטל השבחה): explicit "א. רקע עובדתי" section
if background >= 0:
# Explicit background header (היטל השבחה style)
bg_end = claims if claims > background else (proceedings if proceedings > background else (discussion if discussion > background else n))
blocks["block-vav"] = slice_text(lines, background, bg_end)
# In this case, opening (ה) might not exist — "לפנינו" may be absent
elif opening >= 0 and claims > opening:
bg_start = opening + 1
he_lines = count_words(blocks.get("block-he", ""))
if he_lines > 0:
he_end = opening
for i in range(opening, min(opening + 5, claims)):
if lines[i].strip():
he_end = i + 1
else:
break
bg_start = he_end
blocks["block-vav"] = slice_text(lines, bg_start, claims)
elif opening >= 0 and discussion > opening:
blocks["block-vav"] = slice_text(lines, opening + 1, discussion)
else:
blocks["block-vav"] = ""
# ── Block ז: Claims — from claims header to next section ──
if claims >= 0:
claims_end = min(
x for x in [proceedings, plans, discussion, summary, n]
if x > claims
)
blocks["block-zayin"] = slice_text(lines, claims, claims_end)
else:
blocks["block-zayin"] = ""
# ── Block ח: Proceedings (optional) ──
if proceedings >= 0:
proc_end = min(
x for x in [plans, discussion, summary, n]
if x > proceedings
)
blocks["block-chet"] = slice_text(lines, proceedings, proc_end)
else:
blocks["block-chet"] = ""
# ── Block ט: Plans (optional) ──
if plans >= 0 and (discussion == -1 or plans < discussion):
plans_end = min(
x for x in [discussion, summary, n]
if x > plans
)
blocks["block-tet"] = slice_text(lines, plans, plans_end)
else:
blocks["block-tet"] = ""
# ── Block י: Discussion ──
if discussion >= 0:
disc_end = summary if summary > discussion else (signature if signature > discussion else n)
blocks["block-yod"] = slice_text(lines, discussion, disc_end)
else:
blocks["block-yod"] = ""
# ── Block יא: Summary ──
if summary >= 0:
summ_end = signature if signature > summary else n
blocks["block-yod-alef"] = slice_text(lines, summary, summ_end)
else:
blocks["block-yod-alef"] = ""
# ── Block יב: Signatures ──
if signature >= 0:
blocks["block-yod-bet"] = slice_text(lines, signature, n)
else:
blocks["block-yod-bet"] = ""
return blocks
async def main():
await init_schema()
pool = await get_pool()
async with pool.acquire() as conn:
decisions = await conn.fetch(
"""SELECT d.id as decision_id, c.case_number, c.title,
doc.extracted_text
FROM decisions d
JOIN cases c ON c.id = d.case_id
JOIN documents doc ON doc.case_id = d.case_id AND doc.doc_type = 'decision'
WHERE d.status = 'final'
ORDER BY c.case_number"""
)
for dec in decisions:
decision_id = dec["decision_id"]
case_number = dec["case_number"]
text = dec["extracted_text"]
total_words = count_words(text)
print(f"\n{'='*60}")
print(f"מפרק: {case_number}{dec['title']}")
print(f"סה\"כ מילים: {total_words}")
print(f"{'='*60}")
parsed = decompose(text)
async with pool.acquire() as conn:
# Delete existing blocks
await conn.execute(
"DELETE FROM decision_blocks WHERE decision_id = $1", decision_id
)
total_parsed_words = 0
for block_id, block_index, title, gen_type in BLOCK_DEFS:
content = parsed.get(block_id, "")
wc = count_words(content)
weight = round(wc / total_words * 100, 1) if total_words > 0 and wc > 0 else 0
status = "final" if wc > 0 else "empty"
total_parsed_words += wc
await conn.execute(
"""INSERT INTO decision_blocks
(decision_id, block_id, block_index, title, content,
word_count, weight_percent, generation_type, status)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)""",
decision_id, block_id, block_index, title,
content, wc, weight, gen_type, status,
)
marker = "" if wc > 0 else ""
print(f" {marker} {block_id:18s} | {title:25s} | {wc:5d} מילים | {weight:5.1f}%")
# Update decision totals
disc_words = count_words(parsed.get("block-yod", ""))
disc_paras = len([p for p in parsed.get("block-yod", "").split("\n") if p.strip() and len(p.strip()) > 20])
await conn.execute(
"UPDATE decisions SET total_words = $1, total_paragraphs = $2, updated_at = now() WHERE id = $3",
total_words, disc_paras, decision_id,
)
coverage = round(total_parsed_words / total_words * 100, 1) if total_words > 0 else 0
print(f" --- כיסוי: {total_parsed_words}/{total_words} מילים ({coverage}%)")
# Summary
async with pool.acquire() as conn:
stats = await conn.fetch(
"""SELECT block_id, count(*) as decisions,
avg(word_count) as avg_words,
avg(weight_percent) as avg_weight
FROM decision_blocks
WHERE word_count > 0
GROUP BY block_id ORDER BY block_id"""
)
print(f"\n{'='*60}")
print("סטטיסטיקה לפי בלוק (רק בלוקים עם תוכן):")
for s in stats:
print(f" {s['block_id']:18s} | {s['decisions']} החלטות | ממוצע {s['avg_words']:.0f} מילים | {s['avg_weight']:.1f}%")
await close_pool()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,66 +0,0 @@
"""Extract text from PDF using Google Cloud Vision API."""
import io
import time
from pathlib import Path
import fitz # PyMuPDF for rendering pages to images
from google.cloud import vision
API_KEY = "AIzaSyDZgUsxsy_FHkkREU7R_oQLJALU3_V26j8"
PDF_PATH = Path("/home/chaim/legal-ai/data/cases/1130-25/documents/originals/מרק קובר-כתב ערר.pdf")
OUTPUT_DIR = Path("/home/chaim/legal-ai/data/cases/1130-25/documents/extracted")
def main():
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
client = vision.ImageAnnotatorClient(
client_options={"api_key": API_KEY}
)
doc = fitz.open(str(PDF_PATH))
page_count = len(doc)
print(f"Processing: {PDF_PATH.name} ({page_count} pages)\n")
pages_text = []
total_time = 0.0
for i in range(page_count):
page = doc[i]
pix = page.get_pixmap(dpi=300)
img_bytes = pix.tobytes("png")
image = vision.Image(content=img_bytes)
print(f" Page {i+1}/{page_count}...", end=" ", flush=True)
t0 = time.time()
response = client.document_text_detection(
image=image,
image_context={"language_hints": ["he"]}
)
elapsed = time.time() - t0
total_time += elapsed
if response.error.message:
print(f"ERROR: {response.error.message}")
pages_text.append("")
continue
text = response.full_text_annotation.text if response.full_text_annotation else ""
pages_text.append(text)
print(f"{len(text):,} chars, {elapsed:.1f}s")
doc.close()
full_text = "\n\n".join(pages_text)
out_file = OUTPUT_DIR / f"{PDF_PATH.stem}.md"
out_file.write_text(full_text, encoding="utf-8")
print(f"\nTotal: {len(full_text):,} chars, {len(full_text.split()):,} words, {total_time:.1f}s")
print(f"Saved: {out_file}")
if __name__ == "__main__":
main()

View File

@@ -1,54 +0,0 @@
"""Extract text from a single PDF using Google Cloud Vision API."""
import sys
import time
from pathlib import Path
import fitz
from google.cloud import vision
API_KEY = "AIzaSyDZgUsxsy_FHkkREU7R_oQLJALU3_V26j8"
OUTPUT_DIR = Path("/home/chaim/legal-ai/data/cases/1130-25/documents/extracted")
def main():
pdf_path = Path(sys.argv[1])
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
client = vision.ImageAnnotatorClient(client_options={"api_key": API_KEY})
doc = fitz.open(str(pdf_path))
page_count = len(doc)
print(f"Processing: {pdf_path.name} ({page_count} pages)\n")
pages_text = []
total_time = 0.0
for i in range(page_count):
page = doc[i]
pix = page.get_pixmap(dpi=300)
img_bytes = pix.tobytes("png")
image = vision.Image(content=img_bytes)
print(f" Page {i+1}/{page_count}...", end=" ", flush=True)
t0 = time.time()
response = client.document_text_detection(image=image, image_context={"language_hints": ["he"]})
elapsed = time.time() - t0
total_time += elapsed
if response.error.message:
print(f"ERROR: {response.error.message}")
pages_text.append("")
continue
text = response.full_text_annotation.text if response.full_text_annotation else ""
pages_text.append(text)
print(f"{len(text):,} chars, {elapsed:.1f}s")
doc.close()
full_text = "\n\n".join(pages_text)
out_file = OUTPUT_DIR / f"{pdf_path.stem}.md"
out_file.write_text(full_text, encoding="utf-8")
print(f"\nTotal: {len(full_text):,} chars, {len(full_text.split()):,} words, {total_time:.1f}s")
print(f"Saved: {out_file}")
if __name__ == "__main__":
main()

View File

@@ -1,202 +0,0 @@
#!/usr/bin/env python3
"""Import 6 final signed decisions: extract text, store in DB."""
import asyncio
import json
import sys
from datetime import date
from pathlib import Path
from uuid import UUID
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
import fitz # PyMuPDF
from docx import Document as DocxDocument
from legal_mcp.services.db import get_pool, init_schema, close_pool
# ═══════════════════════════════════════════════════════════════════
# 6 Final Decisions
# ═══════════════════════════════════════════════════════════════════
FINAL_DECISIONS = [
{
"case_number": "1180-1181",
"file_path": "legacy/dafna-tamir/04_Archive/ערר 1180-1181 הכט/החלטה/הכט 1180-1181.pdf",
"title": "החלטה סופית — הכט 1180-1181",
"outcome": "rejected",
"decision_date": date(2026, 2, 5),
},
{
"case_number": "8255-25",
"file_path": "legacy/dafna-tamir/04_Archive/בל\"מ 8255-25 אפרים אבי נ' הוועדה המקומית לתכנון ובניה/החלטה/אליהו הרנון - להפצה.docx",
"title": "החלטה סופית — אפרים אבי 8255-25",
"outcome": "rejected",
"decision_date": None,
},
{
"case_number": "8007-24",
"file_path": "legacy/dafna-tamir/04_Archive/ערר 8007-24-עומר דרוויש-ערר על שומה מכרעת/החלטה/החלטה-סופית.docx",
"title": "החלטה סופית — עומר דרוויש 8007-24",
"outcome": "",
"decision_date": None,
},
{
"case_number": "1113/25",
"file_path": "legacy/dafna-tamir/04_Archive/ערר-1113-25-אייל-מבורך/החלטה/החלטה-1113-25-טיוטה-סופית.docx",
"title": "החלטה סופית — מבורך 1113-25",
"outcome": "",
"decision_date": None,
},
{
"case_number": "1126/25+1141/25",
"file_path": "legacy/dafna-tamir/04_Archive/ערר-1126-25-תמא-38-בית-הכרם/החלטה/בית הכרם-טיוטת החלטה-9.pdf",
"title": "החלטה סופית — בית הכרם 1126/25",
"outcome": "partial",
"decision_date": date(2026, 3, 1),
},
{
"case_number": "1128/25",
"file_path": "legacy/dafna-tamir/04_Archive/ערר-1128-25-שטרית/החלטה/1128-25 החלטה להפצה.pdf",
"title": "החלטה סופית — שטרית 1128-25",
"outcome": "",
"decision_date": None,
},
]
PROJECT_ROOT = Path(__file__).parent.parent
def extract_pdf_text(file_path: Path) -> str:
"""Extract text from PDF using PyMuPDF."""
doc = fitz.open(str(file_path))
text_parts = []
for page in doc:
text_parts.append(page.get_text())
doc.close()
return "\n".join(text_parts)
def extract_docx_text(file_path: Path) -> str:
"""Extract text from DOCX."""
doc = DocxDocument(str(file_path))
return "\n".join(p.text for p in doc.paragraphs if p.text.strip())
def extract_text(file_path: Path) -> str:
"""Extract text based on file extension."""
suffix = file_path.suffix.lower()
if suffix == ".pdf":
return extract_pdf_text(file_path)
elif suffix == ".docx":
return extract_docx_text(file_path)
else:
raise ValueError(f"Unsupported format: {suffix}")
def count_words(text: str) -> int:
return len(text.split())
async def main():
await init_schema()
pool = await get_pool()
for d in FINAL_DECISIONS:
file_path = PROJECT_ROOT / d["file_path"]
if not file_path.exists():
print(f"❌ קובץ לא נמצא: {file_path}")
continue
# Extract text
print(f"\nמחלץ טקסט: {d['title']}...")
text = extract_text(file_path)
word_count = count_words(text)
print(f" {word_count} מילים, {len(text)} תווים")
async with pool.acquire() as conn:
# Get case_id
case_id = await conn.fetchval(
"SELECT id FROM cases WHERE case_number = $1", d["case_number"]
)
if not case_id:
print(f" ⚠ תיק {d['case_number']} לא נמצא ב-DB — מדלג")
continue
# Register document
existing_doc = await conn.fetchval(
"SELECT id FROM documents WHERE file_path = $1",
str(file_path),
)
if existing_doc:
doc_id = existing_doc
print(f" מסמך כבר קיים ב-DB: {doc_id}")
# Update text
await conn.execute(
"""UPDATE documents SET extracted_text = $1, extraction_status = 'completed'
WHERE id = $2""",
text, doc_id,
)
else:
doc_id = await conn.fetchval(
"""INSERT INTO documents (case_id, doc_type, title, file_path, extracted_text, extraction_status, page_count)
VALUES ($1, 'decision', $2, $3, $4, 'completed', $5)
RETURNING id""",
case_id, d["title"], str(file_path), text,
len(fitz.open(str(file_path))) if file_path.suffix == ".pdf" else None,
)
print(f" מסמך נרשם: {doc_id}")
# Create/update decision record
existing_decision = await conn.fetchval(
"SELECT id FROM decisions WHERE case_id = $1", case_id
)
if existing_decision:
await conn.execute(
"""UPDATE decisions SET status = 'final', outcome = $1, total_words = $2,
decision_date = $3, updated_at = now() WHERE id = $4""",
d["outcome"], word_count, d["decision_date"], existing_decision,
)
decision_id = existing_decision
print(f" החלטה עודכנה: {decision_id}")
else:
decision_id = await conn.fetchval(
"""INSERT INTO decisions (case_id, version, status, outcome, outcome_summary,
total_words, decision_date, author)
VALUES ($1, 1, 'final', $2, $3, $4, $5, 'דפנה תמיר')
RETURNING id""",
case_id, d["outcome"], d["title"], word_count, d["decision_date"],
)
print(f" החלטה נוצרה: {decision_id}")
# Update case status
await conn.execute(
"UPDATE cases SET status = 'final', expected_outcome = $1, updated_at = now() WHERE id = $2",
d["outcome"], case_id,
)
print(f" ✅ הושלם: {d['case_number']}")
# Summary
async with pool.acquire() as conn:
doc_count = await conn.fetchval(
"SELECT count(*) FROM documents WHERE doc_type = 'decision' AND extraction_status = 'completed'"
)
dec_count = await conn.fetchval(
"SELECT count(*) FROM decisions WHERE status = 'final'"
)
total_words = await conn.fetchval(
"SELECT sum(total_words) FROM decisions WHERE status = 'final'"
)
await close_pool()
print(f"\n{'='*50}")
print(f"✅ סה\"כ מסמכי החלטה: {doc_count}")
print(f"✅ סה\"כ החלטות סופיות: {dec_count}")
print(f"✅ סה\"כ מילים: {total_words:,}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env python3
"""Test semantic search functions."""
import asyncio
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "mcp-server" / "src"))
from legal_mcp.services.db import search_similar_paragraphs, search_similar_case_law, search_precedents, init_schema
from legal_mcp.services.embeddings import embed_query
async def main():
await init_schema()
queries = [
"טענות קנייניות רוב דרוש בעלי דירות רכוש משותף",
"חניה תנועה חניות מצוקת חניה",
"היטל השבחה שמאי מכריע התערבות",
]
for query in queries:
print(f'=== שאילתה: "{query}" ===')
emb = await embed_query(query)
results = await search_precedents(emb, limit=3)
if not results:
print(" אין תוצאות")
else:
for i, r in enumerate(results):
score = r["score"]
cn = r["case_number"]
rtype = r["type"]
content = r["content"][:120].replace("\n", " ")
print(f" {i+1}. [{rtype}] {score:.3f} | {cn} | {content}")
print()
asyncio.run(main())

View File

@@ -1,574 +0,0 @@
# Block Schema — ארכיטקטורת מסמך החלטת ועדת ערר
מסמך זה מגדיר את המבנה הפורמלי של החלטת ועדת ערר לתכנון ובניה. הוא משמש כמקור סמכותי להגדרת בלוקים, משקלות, פרמטרי עיבוד, וכללי ולידציה.
**הפניה:** SKILL.md סעיפים 11-12 מכילים סיכום מהיר והנחיות תהליך. מסמך זה מכיל את ההגדרות המלאות.
---
## 1. יסודות תיאורטיים
ארכיטקטורת המסמך מבוססת על שילוב של ארבעה frameworks מוכרים:
### CREAC — מתודולוגיית כתיבה משפטית
Conclusion → Rule → Explanation → Application → Conclusion.
מקור: Columbia Law School, Legal Writing methodology.
**מיפוי:** חל על בלוק י (דיון) ובלוק יא (סיכום). בלוק י פותח במסקנה (C), מציג כלל משפטי (R), מסביר באמצעות פסיקה (E), מיישם על העובדות (A), וחוזר למסקנה (C). בלוק יא = C אחרון בלבד.
### Federal Judicial Center — Judicial Writing Manual
מגדיר תפקוד פונקציונלי לכל חלק בהחלטה שיפוטית:
- **Orientation** (אוריינטציה) — מי, מה, איפה → בלוקים א-ה
- **Framing** (מסגור) — הקשר עובדתי ותכנוני → בלוק ו
- **Argumentation** (טיעון) — עמדות הצדדים → בלוק ז
- **Procedural record** (תיעוד הליכי) — מה עשינו → בלוק ח
- **Deliberation** (דיון) — ניתוח משפטי → בלוקים ט-י
- **Disposition** (החלטה) — תוצאה אופרטיבית → בלוק יא
### DITA — Darwin Information Typing Architecture
סטנדרט OASIS להגדרת סוגי תוכן מובנים. מספק:
- **Content model** — אילו אלמנטים מותרים בכל בלוק
- **Constraints** — מה אסור (חשוב יותר ממה שמותר)
- **Specialization** — ירושה מסוג בסיסי עם התאמות
- **Relationships** — תלויות בין בלוקים
### Akoma Ntoso / LegalDocumentML
סטנדרט OASIS בינלאומי למסמכים משפטיים מובנים (UN/DESA). מספק:
- **Semantic mapping** — כל בלוק ממופה לרכיב מוכר בסטנדרט
- **Document class** — "judgment" (פסק דין / החלטה)
---
## 2. הגדרות בלוקים
### Block א: כותרת מוסדית / Institutional Header
**ID:** `block-alef`
**Akoma Ntoso:** `meta > identification`
**CREAC role:** none
**Functional purpose (JWM):** Orientation — מזהה את המוסד, התיק והגורם המחליט.
**Content model:**
- Types: template-field
- Elements: טבלה 2 טורים (מוסד | מספרי תיק)
- Sources: מערכת ניהול תיקים
**Constraints:**
- MUST: שם מוסד, מספר תיק, מספר תכנית/בקשה
- MUST NOT: תוכן מהותי כלשהו
- Dependencies: none
**Weight:** 1% (קבוע, לא משתנה בין סוגי עררים)
**Processing:**
- Generation type: template-fill
- Temperature: 0 | Thinking: off | Effort: min | Model: script
### Block ב: הרכב הוועדה / Panel Composition
**ID:** `block-bet`
**Akoma Ntoso:** `meta > references > TLCPerson`
**CREAC role:** none
**Functional purpose (JWM):** Orientation — מזהה את ההרכב המחליט. חשוב לביקורת שיפוטית (הרכב כשיר).
**Content model:**
- Types: template-field
- Elements: "בפני:" + יו"ר + חברים
- Sources: מערכת ניהול
**Constraints:**
- MUST: יו"ר + לפחות חבר אחד
- MUST NOT: תוכן מהותי
- Dependencies: none
**Weight:** 1% (קבוע)
**Processing:**
- Generation type: template-fill
- Temperature: 0 | Thinking: off | Effort: min | Model: script
### Block ג: צדדים / Parties
**ID:** `block-gimel`
**Akoma Ntoso:** `meta > references > TLCPerson` (appellants, respondents)
**CREAC role:** none
**Functional purpose (JWM):** Orientation — מזהה את הצדדים וב"כ. מגדיר את מסגרת הדיון.
**Content model:**
- Types: template-field
- Elements: עוררים + "נגד" + משיבים + ב"כ
- Sources: כתב ערר, כתב תשובה
**Constraints:**
- MUST: שם כל צד, "נגד" כמפריד
- MUST NOT: תוכן מהותי, תיאור הערר
- Dependencies: none
**Weight:** 1% (קבוע)
**Processing:**
- Generation type: template-fill
- Temperature: 0 | Thinking: off | Effort: min | Model: script
### Block ד: כותרת "החלטה" / Decision Title
**ID:** `block-dalet`
**Akoma Ntoso:** `body > judgment > header`
**CREAC role:** none
**Functional purpose (JWM):** Orientation — סימון פורמלי של תחילת ההחלטה.
**Content model:**
- Types: template-field
- Elements: מילה אחת: "החלטה"
- Sources: none
**Constraints:**
- MUST: David 16pt, bold, מרכז
- Dependencies: none
**Weight:** 0% (שורה אחת)
**Processing:**
- Generation type: template-fill
- Temperature: 0 | Thinking: off | Effort: min | Model: script
### Block ה: פתיחה / Opening
**ID:** `block-he`
**Akoma Ntoso:** `body > judgment > introduction`
**CREAC role:** C (מסקנה ראשונית — הצגת מה לפנינו)
**Functional purpose (JWM):** Orientation — מכוון את הקורא למהות הערר במשפט אחד. מגדיר "להלן" מרכזיים.
**Content model:**
- Types: narrative (1-2 סעיפים)
- Elements: numbered-para עם הגדרות "להלן"
- Sources: כתב ערר, החלטת ועדה מקומית
**Constraints:**
- MUST: "לפנינו...", הגדרת הוועדה המקומית, הגדרת התכנית/הבקשה, הגדרת המגרש
- MUST NOT: ניתוח, ערכי שיפוט, ציטוטים מצדדים
- Dependencies: block-gimel (שמות צדדים להגדרות)
**Weight:** 1% (קבוע — 1-2 סעיפים)
**Processing:**
- Generation type: paraphrase
- Temperature: 0.2 | Thinking: low | Effort: low | Model: sonnet
### Block ו: רקע עובדתי / Factual Background ("פתח דבר")
**ID:** `block-vav`
**Akoma Ntoso:** `body > judgment > background`
**CREAC role:** none (עובדות בלבד, לא ניתוח)
**Functional purpose (JWM):** Framing — מספק את התשתית העובדתית שעליה נבנה הדיון. השופט חייב להבין את המציאות בשטח לפני שקורא טענות.
**Content model:**
- Types: narrative, citation-block, image-placeholder
- Elements: numbered-para, blockquote (ציטוט מפרוטוקול), image-box
- Sources: כתבי טענות, תשריטים, פרוטוקולים, החלטות קודמות, GIS
**סדר תוכן פנימי:**
1. מקרקעין — מיקום, שטח, מאפיינים
2. סביבת מקרקעין — בנייה סמוכה, אופי
3. 📷 תמונה: מיקום GIS
4. היסטוריה תכנונית — תכניות, החלטות (עובדות יבשות בלבד)
5. מהות הבקשה/תכנית
6. 📷 תמונה: תשריט
7. ציטוט מפרוטוקול ועדה מקומית
8. החלטת הוועדה + תנאים
9. 📷 תמונה: צילום אוויר (אופציונלי)
10. הגשת הערר
**Constraints:**
- MUST: מקרקעין, מהות הבקשה, החלטת הוועדה, הגשת הערר
- MUST: לפחות 2 תמונות (מיקום + תשריט)
- MUST: ציטוט מפרוטוקול הוועדה המקומית
- ⚠️ **MUST NOT ("רקע ניטרלי"):** ציטוטים ישירים מצדדים, מילות ערך/שיפוט ("חריג", "חטא", "בעייתי"). החלטות קודמות = עובדה יבשה ("ביום X נדחתה תכנית Y"), ללא נימוקים וציטוטים מהן.
- Dependencies: block-he (הגדרות "להלן")
**Weight:**
| סוג ערר | משקל | הערות |
|---------|------|-------|
| רישוי — דחייה | 15-25% | רקע מפורט עם הקשר תכנוני |
| רישוי — קבלה | 30-40% | כולל ציטוט מפרוטוקול |
| רישוי — קבלה חלקית | 25-35% | כולל ציטוט מפרוטוקול |
| היטל השבחה | 6-18% | רקע מצומצם |
**Weight methodology:**
- Communicative weight (40%): גבוה — מספק את "התמונה" לשופט שלא מכיר את התיק
- Reader attention (20%): בינוני-גבוה — primacy effect, הקורא קשוב בהתחלה
- Judicial review (25%): גבוה — שופט בודק שהעובדות מלאות ומדויקות
- Empirical (15%): מבוסס על מדידת החלטות דפנה (3.2 ב-SKILL.md)
**Processing:**
- Generation type: reproduction (העתקה נאמנה ממקורות)
- Cognitive complexity: lookup (ארגון, לא ניתוח)
- Accuracy: high-precision
- Temperature: 0 | Thinking: off | Effort: low | Model: sonnet
### Block ז: טענות הצדדים / Parties' Claims
**ID:** `block-zayin`
**Akoma Ntoso:** `body > judgment > arguments`
**CREAC role:** none (הצגת טענות, לא ניתוח)
**Functional purpose (JWM):** Argumentation — מציג את עמדות הצדדים בנאמנות, כך שהקורא יבין את המחלוקת לפני שקורא את ההכרעה.
**Content model:**
- Types: narrative
- Elements: section-heading ("תמצית טענות הצדדים"), sub-headings (לכל צד), numbered-para
- Sources: כתב ערר, כתב תשובה — **כתבי טענות מקוריים בלבד** (לא השלמות טיעון)
**סדר קבוע:**
1. כותרת: "תמצית טענות הצדדים"
2. "טענות העוררים" (אם כמה עוררים — תתי-כותרות לכל אחד)
3. "עמדת הוועדה המקומית"
4. "עמדת מבקשי ההיתר" / "עמדת מגישי התכנית"
**Constraints:**
- MUST: כל טענה בסעיף נפרד, גוף שלישי ("העורר טוען כי...")
- MUST: כל צד בפרק נפרד, סדר קבוע
- MUST NOT: ניתוח, מסקנות, הערכת הוועדה ("טענה זו חלשה...")
- MUST NOT: תוכן מהשלמות טיעון (→ block-chet)
- Dependencies: block-vav (מספור רציף)
**Weight:**
| סוג ערר | משקל | הערות |
|---------|------|-------|
| רישוי — דחייה | 30-40% | טענות מפורטות |
| רישוי — קבלה | 20-30% | כולל השלמות |
| רישוי — קבלה חלקית | 25-30% | |
| היטל השבחה | 13-25% | |
**Weight methodology:**
- Communicative weight (40%): בינוני — הצגה, לא הכרעה
- Reader attention (20%): נמוך-בינוני — scanning attention, הקורא מחפש טענות ספציפיות
- Judicial review (25%): גבוה — שופט בודק ש"נשמעו כל הצדדים"
- Empirical (15%): מבוסס על מדידת החלטות דפנה
**Processing:**
- Generation type: paraphrase (סיכום נאמן בשפה של דפנה)
- Cognitive complexity: medium-synthesis (קיבוץ וסידור טענות)
- Accuracy: high-precision (לא לפספס טענה, לא לעוות)
- Temperature: 0.1 | Thinking: low | Effort: medium | Model: sonnet
### Block ח: הליכים בפני ועדת הערר / Proceedings
**ID:** `block-chet`
**Akoma Ntoso:** `body > judgment > proceedings` (custom extension)
**CREAC role:** none (תיעוד, לא ניתוח)
**Functional purpose (JWM):** Procedural record — מתעד שהוועדה פעלה כדין ונתנה מלוא יום בבית דין. קריטי ל"מבחן השופט" — שופט בעתמ"ם בודק שהצדדים קיבלו הזדמנות הוגנת.
**Content model:**
- Types: narrative, image-placeholder
- Elements: section-heading ("ההליכים בפני ועדת הערר"), numbered-para, image-box
- Sources: פרוטוקול דיון, תמונות סיור, החלטות ביניים, השלמות טיעון
**סדר כרונולוגי:**
1. דיון — תאריך, נוכחים
2. סיור — תאריך, תיאור
3. 📷 תמונה: צילומים מהסיור
4. השלמות טיעון — עם תוכן מפורט (כל השלמה = סעיף נפרד)
5. החלטות ביניים
6. תגובות לתגובות — כרונולוגי
7. 📷 תמונה: הדמיות/חתכים (אם צורפו)
8. עררים מקבילים (אם יש)
**Constraints:**
- MUST: תאריכים מדויקים, כרונולוגיה ברורה
- MUST: תוכן השלמות טיעון מפורט — כל השלמה בסעיף נפרד עם תמצית תוכן
- MUST NOT: ניתוח או הערכה של ההשלמות ("טענה חזקה/חלשה")
- Dependencies: block-zayin (מספור רציף)
- References: block-zayin (הפניה לטענות מקוריות כשיש חפיפה)
**Weight:**
| סוג ערר | משקל | הערות |
|---------|------|-------|
| ערר פשוט (ללא השלמות) | 3-5% | דיון + סיור בלבד |
| ערר מורכב (השלמות רבות) | 8-15% | כמו אריאלי: 31 סעיפים |
| היטל השבחה | 2-4% | בדרך כלל מינימלי |
**Weight methodology:**
- Communicative weight (40%): נמוך-בינוני — תיעוד, לא הכרעה
- Reader attention (20%): נמוך — scanning, אלא אם יש ממצאים חדשים מסיור/השלמות
- Judicial review (25%): **גבוה מאוד** — שופט בודק שנתנו procedural fairness
- Empirical (15%): מגוון רחב — תלוי בכמות ההשלמות
**Processing:**
- Generation type: reproduction + paraphrase (תאריכים מדויקים + תמצית תוכן)
- Cognitive complexity: low (סידור כרונולוגי)
- Accuracy: high-precision (תאריכים, שמות מסמכים)
- Temperature: 0 | Thinking: off | Effort: low | Model: sonnet
### Block ט: תכניות חלות / Applicable Plans (אופציונלי)
**ID:** `block-tet`
**Akoma Ntoso:** `body > judgment > motivation > background` (extended)
**CREAC role:** R (Rule — הצגת הכללים המשפטיים/תכנוניים)
**Functional purpose (JWM):** Deliberation (preliminary) — מציג את המסגרת הנורמטיבית שלאורה ייבחנו הטענות. בלוק גשר בין עובדות לניתוח.
**Content model:**
- Types: narrative, citation-block
- Elements: section-heading, numbered-para, blockquote (ציטוט מהוראות תכנית)
- Sources: הוראות תכנית (PDF), נספחי בינוי, החלטות מרכזות
**Constraints:**
- MUST: ציטוט ישיר מהוראות תכנית עם הדגשת (bold) מילים מכריעות
- MUST NOT: ניתוח מעמיק (→ block-yod), הכרעה בין פרשנויות
- Dependencies: block-chet (מספור), block-vav (הגדרות תכניות)
- Condition: **אופציונלי** — רק כשיש מורכבות תכנונית (תכניות סותרות, תמ"א 38 + שימור, פרשנות)
**Weight:**
| מתי קיים | משקל |
|----------|------|
| תמ"א 38 + שימור | 8-12% |
| פרשנות תכנית | 5-10% |
| לא קיים | 0% |
**Weight methodology:**
- Communicative weight (40%): בינוני — הנחת תשתית נורמטיבית
- Reader attention (20%): נמוך — טכני, אלא אם פרשנות שנויה במחלוקת
- Judicial review (25%): בינוני — שופט בודק שהוועדה הבינה את הדין
- Empirical (15%): אריאלי — 14 סעיפים; בית הכרם — משולב בדיון
**Processing:**
- Generation type: guided-synthesis (ציטוט + ניתוח ראשוני)
- Cognitive complexity: medium (פרשנות טקסט משפטי)
- Accuracy: precision + interpretation
- Temperature: 0.2 | Thinking: medium | Effort: medium | Model: opus
### Block י: דיון והכרעה / Discussion and Decision
**ID:** `block-yod`
**Akoma Ntoso:** `body > judgment > motivation`
**CREAC role:** **full-CREAC** — C (מסקנה בפתיחה) → R (כלל משפטי) → E (ציטוט פסיקה) → A (יישום על העובדות) → C (מסקנת ביניים)
**Functional purpose (JWM):** Deliberation — ליבת ההחלטה. כאן הוועדה מנתחת, מאזנת, ומכריעה. זהו ה-ratio decidendi.
**Content model:**
- Types: narrative, citation-block, image-placeholder
- Elements: numbered-para (אסה רציפה ללא כותרות משנה), blockquote (ציטוטי פסיקה ותכנית), image-box
- Sources: **כל** הבלוקים הקודמים + פסיקה + skill
**מבנה פנימי (לפי סוג ערר — ראה SKILL.md סעיף 7.3):**
- דחייה: שכבות הגנה (concentric circles)
- קבלה: נימוק-נימוק
- קבלה חלקית: מיפוי מתחים + ניתוח נושאי
- היטל השבחה: פתיחה ישירה עם מסקנה
**Constraints:**
- MUST: מסקנה בפתיחת הדיון (לא בסוף)
- MUST: מענה לכל טענה שהוצגה בבלוק ז
- MUST: ציטוט פסיקה בבלוקים ארוכים (200-600 מילים)
- ⚠️ **MUST NOT ("ללא כפילות"):** חזרה על עובדות/טענות מבלוקים קודמים. השתמש בהפניות: "כאמור בסעיף X לעיל", "כפי שפורט", "כפי שציינו"
- MUST NOT: כותרות משנה (חריג: נושאים נפרדים לחלוטין)
- Dependencies: **ALL** previous blocks (ה-ט)
**Weight:**
| סוג ערר | משקל | הערות |
|---------|------|-------|
| רישוי — דחייה | 37-50% | פתיחה רחבה + שכבות |
| רישוי — קבלה | 35-45% | נימוק-נימוק |
| רישוי — קבלה חלקית | 40-47% | מיפוי מתחים + ניתוח נושאי |
| היטל השבחה | 32-48% | ציטוטי פסיקה מרובים |
**Weight methodology:**
- Communicative weight (40%): **מקסימלי** — זהו ה-ratio decidendi, תכלית ההחלטה
- Reader attention (20%): **גבוה** — deep reading, הקורא מחפש את הנימוקים
- Judicial review (25%): **מקסימלי** — שופט בוחן סבירות, מידתיות, התייחסות לטענות
- Empirical (15%): 35-50% באופן עקבי בכל החלטות דפנה
**Processing:**
- Generation type: **rhetorical-construction** (בניית טיעון, איזון, רטוריקה)
- Cognitive complexity: **high-reasoning** (CREAC מלא, שכבות, חידוד)
- Accuracy: **precision + creativity** (ניתוח מדויק + ביטוי אלגנטי)
- Temperature: **0.4** | Thinking: **max (budget 16K+)** | Effort: **max** | Model: **opus בלבד**
### Block יא: סיכום / סוף דבר / Summary
**ID:** `block-yod-alef`
**Akoma Ntoso:** `body > judgment > decision`
**CREAC role:** C (Conclusion אחרון — תמצית אופרטיבית)
**Functional purpose (JWM):** Disposition — ההוראה האופרטיבית שמבצעים. זה מה שהצדדים צריכים לדעת "מה עכשיו."
**Content model:**
- Types: narrative
- Elements: section-heading ("סיכום"/"סוף דבר"), numbered-para, sub-items (א. ב. ג.)
- Sources: block-yod (מסקנות)
**מבנה לפי תוצאה (ראה SKILL.md סעיף 8):**
- דחייה: "הערר נדחה" + תתי-סעיפים + פסקה חמה (רישוי בלבד)
- קבלה: "הערר מתקבל בכפוף ל..." + פרוזה
- קבלה חלקית: "הערר מתקבל באופן חלקי" + 2-3 הוראות אופרטיביות
- היטל השבחה: יבש
**Constraints:**
- MUST: תוצאה ברורה (נדחה/מתקבל/מתקבל חלקית)
- MUST NOT (בקבלה חלקית): חזרה על נימוקים — ההנמקה כבר בדיון
- Dependencies: block-yod (מסקנות)
**Weight:**
| סוג ערר | משקל |
|---------|------|
| דחייה | 2-9% |
| קבלה | 3-5% |
| קבלה חלקית | 2-3% |
| היטל השבחה | 3-4% |
**Processing:**
- Generation type: paraphrase (עיבוד מסקנות בלוק י)
- Cognitive complexity: low
- Accuracy: high-precision (הוראות חייבות להיות חד-משמעיות)
- Temperature: 0.1 | Thinking: low | Effort: low | Model: sonnet
### Block יב: חתימות / Signatures
**ID:** `block-yod-bet`
**Akoma Ntoso:** `conclusions > signature`
**CREAC role:** none
**Functional purpose (JWM):** Authentication — אישור פורמלי של ההחלטה.
**Content model:**
- Types: template-field
- Elements: "ניתנה פה אחד" + תאריך עברי/לועזי + טבלת חתימות
- Sources: none
**Constraints:**
- MUST: "ניתנה פה אחד", תאריך, יו"ר + מזכיר/ה
- Dependencies: none
**Weight:** 1% (קבוע)
**Processing:**
- Generation type: template-fill
- Temperature: 0 | Thinking: off | Effort: min | Model: script
---
## 3. כללי גזירת פרמטרים
פרמטרי העיבוד נגזרים ממאפייני התוכן, לא נקבעים שרירותית:
### Temperature — נגזר מסוג הייצור
| Generation type | Temperature | נימוק |
|----------------|-------------|-------|
| template-fill | 0 | אין צורך בשפה — מילוי שדות |
| reproduction | 0 | נאמנות מוחלטת למקור. אפס יצירתיות |
| paraphrase | 0.1 | מרווח מינימלי לניסוח בשפה של דפנה |
| guided-synthesis | 0.2 | גמישות בארגון וחיבור מקורות, לא בתוכן |
| analytical-reasoning | 0.3-0.4 | צריך ליצור קשרים בין עקרונות משפטיים |
| rhetorical-construction | 0.4-0.5 | טווח ביטוי רחב לכתיבה משכנעת ואלגנטית |
### Thinking budget — נגזר ממורכבות קוגניטיבית
| Cognitive task | Budget | נימוק |
|---------------|--------|-------|
| template-fill / lookup | off | אין צורך בחשיבה |
| sequential-extraction | low | חילוץ מידע חד-שלבי |
| multi-source-integration | medium | צריך להצליב מקורות |
| legal-analysis-with-CREAC | max (16K+) | חשיבה רב-שלבית: מסקנה → כלל → הסבר → יישום |
### Model — נגזר מדרישת דיוק
| Accuracy profile | Model | נימוק |
|-----------------|-------|-------|
| factual-precision | sonnet | מהיר, מדויק לחילוץ עובדות |
| precision + interpretation | opus | נדרש לפרשנות תכנית / ציטוט מובנה |
| precision + creativity | opus | נדרש לניתוח משפטי מורכב ורטוריקה |
---
## 4. מתודולוגיית משקלות
משקל כל בלוק נקבע על ידי שקלול 4 גורמים:
### 4.1 Communicative Weight (40%)
מה חלקו של הבלוק בתכלית ההחלטה? ההחלטה באה לעשות דבר אחד: להכריע במחלוקת ולנמק. בלוק י (דיון) הוא ליבת התכלית. בלוקים א-ד (כותרות) הם עטיפה.
### 4.2 Reader Attention Distribution (20%)
מבוסס על מחקרי F-pattern ו-primacy/recency:
- **פתיחה** (בלוקים ה-ו): קשב גבוה (primacy effect)
- **אמצע** (בלוקים ז-ח): scanning — הקורא מחפש טענות ספציפיות
- **דיון** (בלוק י): deep reading — הקורא מחפש נימוקים
- **סיום** (בלוק יא): קשב גבוה (recency effect)
### 4.3 Judicial Review Requirement (25%)
מה שופט בבית משפט לעניינים מנהליים יבדוק ("מבחן השופט"):
- **תשתית עובדתית** (בלוק ו): מלאה ומדויקת?
- **שמיעת צדדים** (בלוקים ז-ח): נתנו מלוא יום בבית דין?
- **סבירות ומידתיות** (בלוק י): ההכרעה מנומקת ומאוזנת?
- **התייחסות לטענות** (בלוק י): כל טענה קיבלה מענה?
### 4.4 Empirical Basis (15%)
מבוסס על מדידה מהחלטות שפורסמו:
- הכט 1180-1181 (דחייה, 02.2026)
- בית הכרם 1126/25 (קבלה חלקית, 03.2026)
- אריאלי 1078+1083 (קבלה, 03.2026)
המשקלות ב-SKILL.md סעיף 3.2 (יחסי הזהב) משמשים כבסיס אמפירי שאומת על ידי שלושת הגורמים האנליטיים.
---
## 5. כללי ולידציה
### 5.1 סדר בלוקים
- בלוקים חייבים להופיע בסדר א עד יב
- בלוקים א-ה ויב נדרשים בכל החלטה
- בלוק ט אופציונלי (רק כשיש מורכבות תכנונית)
### 5.2 Content Constraints
- **רקע ניטרלי (בלוק ו):** אם סעיף מכיל ציטוט ישיר מצד או מילת שיפוט → לא שייך כאן
- **טענות מקוריות בלבד (בלוק ז):** רק מכתבי ערר/תשובה. השלמות → בלוק ח
- **ללא כפילות (בלוק י):** הפניה לבלוקים קודמים, לא חזרה. חריג: "נשוב על כך כי..." (חזרה מכוונת עם שכבה חדשה)
- **הליכים ללא הערכה (בלוק ח):** תיעוד מה הוגש, לא הערכה של חוזק הטענות
### 5.3 Weight Compliance
- משקל כל בלוק (ספירת מילים / סה"כ) צריך להיות בטווח המוגדר **±10%**
- אם בלוק י < 30% → flag: דיון לא מפותח מספיק
- אם בלוק ו > 35% → flag: רקע מנופח, בדוק שאין תוכן טענתי
### 5.4 Structural Integrity
- מספור סעיפים רציף מ-1 עד הסוף, ללא איפוס בין בלוקים
- כל הגדרת "להלן" חייבת להופיע לפני השימוש הראשון בה
- כל טענה בבלוק ז חייבת לקבל מענה בבלוק י (ישיר או "למעלה מן הצורך")
- כותרות פרקים: David 14pt, bold, קו תחתון, מרכז
- כותרות משנה: David 12pt, bold, מרכז, ללא קו תחתון
---
## 6. גרף תלויות בין בלוקים
```
א (כותרת) → עצמאי
ב (הרכב) → עצמאי
ג (צדדים) → עצמאי
ד (כותרת) → עצמאי
ה (פתיחה) → תלוי ב: ג (שמות צדדים להגדרות "להלן")
ו (רקע) → תלוי ב: ה (הגדרות). מספור ממשיך מ-ה.
ז (טענות) → תלוי ב: ו (מספור). מפנה ל: ה, ו (הגדרות)
ח (הליכים) → תלוי ב: ז (מספור). מפנה ל: ז (טענות מקוריות)
ט (תכניות) → תלוי ב: ח (מספור). אופציונלי. מפנה ל: ו (הגדרות תכניות)
י (דיון) → תלוי ב: **כל** הבלוקים ה-ט. מפנה ל: כולם.
יא (סיכום) → תלוי ב: י (מסקנות). מפנה ל: י בלבד.
יב (חתימות) → עצמאי
```

View File

@@ -0,0 +1 @@
../../../docs/block-schema.md

View File

@@ -22,7 +22,6 @@ import zipfile
from fastapi import FastAPI, File, Form, HTTPException, UploadFile from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel from pydantic import BaseModel
import asyncpg import asyncpg
@@ -63,20 +62,12 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="העלאת מסמכים משפטיים", lifespan=lifespan) app = FastAPI(title="העלאת מסמכים משפטיים", lifespan=lifespan)
STATIC_DIR = Path(__file__).parent / "static"
# ── API Endpoints ────────────────────────────────────────────────── # ── API Endpoints ──────────────────────────────────────────────────
@app.get("/") @app.get("/")
async def index(): async def index():
return FileResponse(STATIC_DIR / "index.html") return {"status": "ok", "frontend": "https://legal-ai-next.nautilus.marcusgroup.org"}
@app.get("/design-system.css")
async def design_system_css():
return FileResponse(STATIC_DIR / "design-system.css", media_type="text/css")
@app.post("/api/upload") @app.post("/api/upload")

View File

@@ -1,369 +0,0 @@
/* ════════════════════════════════════════════════════════════
* Ezer Mishpati — Design System
* Editorial/Judicial aesthetic for a Hebrew RTL judicial tool.
*
* Typography: Frank Ruhl Libre (display) + Assistant (body)
* Palette: Navy #0f172a + Cream #f5f1e8 + Gold #a97d3a
* ════════════════════════════════════════════════════════════ */
@import url('https://fonts.googleapis.com/css2?family=Heebo:wght@300;400;500;600;700;800;900&display=swap');
:root {
/* ── Colors ─────────────────────────────────────────── */
--color-navy: #0f172a;
--color-navy-soft: #1e293b;
--color-navy-dim: #334155;
--color-cream: #f5f1e8;
--color-cream-deep: #ede8d8;
--color-parchment: #fbf8f0;
--color-gold: #a97d3a;
--color-gold-deep: #8b6428;
--color-gold-soft: #c89a56;
--color-gold-wash: #fdf6e8;
--color-ink: #1a1a2e;
--color-ink-soft: #3a3a52;
--color-ink-muted: #6b7280;
--color-ink-light: #9ca3af;
--color-rule: #e5dfd0; /* cream-toned hairline */
--color-rule-soft: #f0ead8;
--color-surface: #ffffff;
--color-surface-raised: #fbf8f0;
--color-bg: var(--color-cream);
/* Status colors — tuned to the palette */
--color-success: #4a7c59;
--color-success-bg: #e8efe7;
--color-warn: #b8894a;
--color-warn-bg: #faf0dc;
--color-danger: #a54242;
--color-danger-bg: #f5e6e6;
--color-info: #4e6a8c;
--color-info-bg: #e6ecf3;
/* ── Typography — Heebo (Google's primary Hebrew font) ─── */
--font-display: 'Heebo', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-body: 'Heebo', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: ui-monospace, 'Cascadia Code', 'SF Mono', Menlo, monospace;
--text-xs: 0.75rem;
--text-sm: 0.85rem;
--text-base: 0.95rem;
--text-md: 1.05rem;
--text-lg: 1.2rem;
--text-xl: 1.45rem;
--text-2xl: 1.8rem;
--text-3xl: 2.3rem;
--text-4xl: 2.9rem;
--leading-tight: 1.25;
--leading-snug: 1.4;
--leading-body: 1.65;
--leading-prose: 1.8;
--weight-light: 300;
--weight-normal: 400;
--weight-medium: 500;
--weight-semi: 600;
--weight-bold: 700;
--weight-display: 900;
/* ── Spacing scale (8px grid) ───────────────────────── */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-7: 32px;
--space-8: 40px;
--space-9: 56px;
--space-10: 72px;
/* ── Radii ──────────────────────────────────────────── */
--radius-sm: 4px;
--radius: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-pill: 999px;
/* ── Shadows — soft, editorial ──────────────────────── */
--shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.05);
--shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow: 0 2px 6px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow-md: 0 4px 12px rgba(15, 23, 42, 0.08), 0 2px 4px rgba(15, 23, 42, 0.04);
--shadow-lg: 0 10px 30px rgba(15, 23, 42, 0.12), 0 2px 6px rgba(15, 23, 42, 0.05);
--shadow-gold: 0 0 0 3px var(--color-gold-wash);
/* ── Transitions ────────────────────────────────────── */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--t-fast: 120ms var(--ease-out);
--t: 180ms var(--ease-out);
--t-slow: 280ms var(--ease-out);
}
/* ── Dark theme overrides ────────────────────────────── */
body.dark {
--color-navy: #f5f1e8;
--color-navy-soft: #e8e0c8;
--color-navy-dim: #c7bc9a;
--color-cream: #0a0f1c;
--color-cream-deep: #121a2e;
--color-parchment: #161f36;
--color-gold: #d4a55a;
--color-gold-deep: #e8bc6f;
--color-gold-soft: #c89a56;
--color-gold-wash: rgba(212, 165, 90, 0.08);
--color-ink: #f5f1e8;
--color-ink-soft: #d8d2c0;
--color-ink-muted: #9a9380;
--color-ink-light: #6a6458;
--color-rule: #2a3352;
--color-rule-soft: #1e2a45;
--color-surface: #141b2f;
--color-surface-raised: #1a2238;
--color-bg: #0a0f1c;
--color-success: #5a9a6a;
--color-success-bg: rgba(90, 154, 106, 0.12);
--color-warn: #c79956;
--color-warn-bg: rgba(199, 153, 86, 0.12);
--color-danger: #c16565;
--color-danger-bg: rgba(193, 101, 101, 0.12);
--color-info: #6d8bab;
--color-info-bg: rgba(109, 139, 171, 0.12);
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.35), 0 1px 2px rgba(0, 0, 0, 0.25);
--shadow: 0 2px 6px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45), 0 2px 4px rgba(0, 0, 0, 0.25);
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
}
body.dark header {
background: #060a18;
border-bottom-color: var(--color-gold);
}
/* ── Base overrides ──────────────────────────────────── */
html { font-size: 16px; }
body {
font-family: var(--font-body);
font-weight: var(--weight-normal);
font-size: var(--text-base);
line-height: var(--leading-body);
color: var(--color-ink);
background: var(--color-bg);
direction: rtl;
text-align: right;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: "kern", "liga", "clig", "calt";
}
/* Display typography — serif for headings */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-display);
font-weight: var(--weight-bold);
line-height: var(--leading-tight);
color: var(--color-navy);
letter-spacing: -0.01em;
}
h1 { font-size: var(--text-3xl); font-weight: var(--weight-display); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-xl); }
h4 { font-size: var(--text-lg); }
h5 { font-size: var(--text-md); }
h6 { font-size: var(--text-base); }
/* Prose paragraphs — justify both sides for Hebrew legal text */
p,
.prose {
text-align: justify;
text-justify: inter-word;
hyphens: auto;
line-height: var(--leading-body);
}
/* Text that should NOT justify (short labels, meta) */
.no-justify, .meta, .label, .caption,
th, td, button, input, select, label, nav {
text-align: right;
}
/* Links */
a {
color: var(--color-gold-deep);
text-decoration: none;
transition: color var(--t-fast);
}
a:hover { color: var(--color-gold); }
/* Focus rings — gold, subtle */
*:focus-visible {
outline: 2px solid var(--color-gold);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
/* Selection */
::selection {
background: var(--color-gold-wash);
color: var(--color-navy);
}
/* ── Utility classes ─────────────────────────────────── */
.text-display { font-family: var(--font-display); }
.text-body { font-family: var(--font-body); }
.text-mono { font-family: var(--font-mono); }
.text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); }
.text-base { font-size: var(--text-base); }
.text-md { font-size: var(--text-md); }
.text-lg { font-size: var(--text-lg); }
.text-xl { font-size: var(--text-xl); }
.text-2xl { font-size: var(--text-2xl); }
.text-3xl { font-size: var(--text-3xl); }
.text-muted { color: var(--color-ink-muted); }
.text-light { color: var(--color-ink-light); }
.text-gold { color: var(--color-gold-deep); }
.text-navy { color: var(--color-navy); }
.weight-light { font-weight: var(--weight-light); }
.weight-normal { font-weight: var(--weight-normal); }
.weight-medium { font-weight: var(--weight-medium); }
.weight-bold { font-weight: var(--weight-bold); }
.justify { text-align: justify; text-justify: inter-word; }
.start { text-align: right; } /* RTL start */
.end { text-align: left; } /* RTL end */
.center { text-align: center; }
.ornament {
display: block;
text-align: center;
color: var(--color-gold);
font-family: var(--font-display);
letter-spacing: 0.3em;
margin: var(--space-6) 0;
}
.ornament::before { content: "❦"; font-size: 1.3em; }
.divider {
border: 0;
height: 1px;
background: linear-gradient(
to left,
transparent 0%,
var(--color-rule) 20%,
var(--color-rule) 80%,
transparent 100%
);
margin: var(--space-6) 0;
}
.divider-gold {
border: 0;
height: 2px;
background: linear-gradient(
to left,
transparent 0%,
var(--color-gold) 50%,
transparent 100%
);
margin: var(--space-6) 0;
}
/* ── Loading skeleton ───────────────────────────────── */
.skeleton {
background: linear-gradient(
100deg,
var(--color-cream-deep) 30%,
var(--color-parchment) 50%,
var(--color-cream-deep) 70%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.4s linear infinite;
border-radius: var(--radius);
color: transparent;
user-select: none;
}
@keyframes skeleton-shimmer {
from { background-position: 100% 0; }
to { background-position: -100% 0; }
}
.skeleton-line {
height: 0.9em;
margin: 4px 0;
border-radius: var(--radius-sm);
}
.skeleton-line.short { width: 40%; }
.skeleton-line.medium { width: 70%; }
/* ── Print — optimized for Dafna printing the portrait ─ */
@media print {
:root {
--color-bg: #fff;
--color-surface: #fff;
--color-navy: #000;
--color-ink: #000;
--color-ink-muted: #444;
}
body { background: #fff; color: #000; font-size: 11pt; }
header, .status-bar, .process-panel, .toast, .btn, nav,
#navDiagnostics, .home-sidebar, .home-hero-actions,
#processPanel, #trainingAnalysisCard, #trainingTasksCard {
display: none !important;
}
.main { max-width: 100% !important; padding: 0 !important; }
.page { display: none !important; }
.page.active { display: block !important; }
.portrait-card, .card {
box-shadow: none !important;
border: 1px solid #ccc !important;
page-break-inside: avoid;
margin-bottom: 16px !important;
}
.portrait-headline {
background: #fafafa !important;
border-right: 3px solid #000 !important;
color: #000 !important;
}
h1, h2, h3 { color: #000 !important; page-break-after: avoid; }
.growth-curve, .donut, .hero-timeline { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.phrase-filters, .btn, button { display: none !important; }
/* Force expand all details */
details { display: block !important; }
summary::marker, summary::-webkit-details-marker { display: none; }
}
/* ── Responsive (desktop-first, minimal mobile) ────── */
@media (max-width: 900px) {
.main { padding: var(--space-5) var(--space-4); }
header { padding: 14px 20px; flex-wrap: wrap; gap: 10px; }
header nav { gap: 2px; }
header nav a { padding: 6px 10px; font-size: 0.82em; }
.home-hero-title { font-size: 2em; }
.style-report-header h1 { font-size: 2em; }
.portrait-card { padding: var(--space-6) var(--space-5); }
.portrait-hero .hero-body { grid-template-columns: 1fr; }
.hero-donut-wrap { justify-content: center; }
.process-panel { width: 280px; }
}

File diff suppressed because it is too large Load Diff