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/training/
data/exports/
data/backups/
data/.auto-sync.log
mcp-server/.venv/
__pycache__/
*.pyc

View File

@@ -30,7 +30,7 @@
- לקחים מהשוואת טיוטות לגרסאות סופיות
- סקריפט ייצוא 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/legal-decision-lessons.md`](docs/legal-decision-lessons.md) | לקחים מ-3 החלטות — מה עבד, מה השתנה, ביטויי מעבר חדשים | **לפני כל כתיבת החלטה** |
| [`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/memory.md`](docs/memory.md) | הקשר כללי — skills, פרויקטים שהושלמו, מבנה vault | להתמצאות כללית |
| [`skills/decision/SKILL.md`](skills/decision/SKILL.md) | מדריך סגנון מלא של דפנה — טון, מבנה, ביטויים, מתודולוגיה | **לפני כל כתיבת החלטה** |
@@ -82,15 +84,28 @@
│ └── docx/ עיצוב DOCX
├── data/
│ ├── training/ ← 4 החלטות לאימון (DOCX)
│ ├── exports/ ← ייצוא legacy (תיקים ישנים)
│ ├── exports/ ← טיוטות DOCX מיוצאות
│ └── 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
└── scripts/ ← סקריפטים וכלי עזר
└── scripts/ ← סקריפטים וכלי עזר (ראה scripts/SCRIPTS.md)
└── .archive/ ← סקריפטים שהושלמו (לא להריץ)
```
---
## כלל: עדכון `scripts/SCRIPTS.md`
בכל פעם שנוצר, נמחק, או משתנה סקריפט בתיקיית `scripts/`**חובה לעדכן את `scripts/SCRIPTS.md`** בהתאם.
הקובץ מתעד את התפקיד, הסטטוס, וההחלפה (אם יש) של כל סקריפט.
---
## ניהול משימות — TaskMaster AI
הפרויקט משתמש ב-**TaskMaster AI** (MCP server) לניהול משימות מובנה:

View File

@@ -277,13 +277,27 @@ async def case_update(
"""
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)
if not case:
return f"תיק {case_number} לא נמצא."
fields = {}
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:
fields["title"] = title
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.responses import FileResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
import asyncpg
@@ -63,20 +62,12 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="העלאת מסמכים משפטיים", lifespan=lifespan)
STATIC_DIR = Path(__file__).parent / "static"
# ── API Endpoints ──────────────────────────────────────────────────
@app.get("/")
async def index():
return FileResponse(STATIC_DIR / "index.html")
@app.get("/design-system.css")
async def design_system_css():
return FileResponse(STATIC_DIR / "design-system.css", media_type="text/css")
return {"status": "ok", "frontend": "https://legal-ai-next.nautilus.marcusgroup.org"}
@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