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>
258 lines
8.4 KiB
Python
258 lines
8.4 KiB
Python
#!/usr/bin/env python3
|
||
"""Validate a decision against block-schema rules.
|
||
|
||
Usage: python validate-decision.py <case_number>
|
||
|
||
Checks:
|
||
1. Neutral background (block-vav) — no party quotes or value words
|
||
2. Weight compliance — blocks within expected ranges
|
||
3. Structural integrity — all required blocks present
|
||
4. Claims coverage — every claim in block-zayin addressed in block-yod
|
||
"""
|
||
|
||
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
|
||
|
||
|
||
# Value/judgment words that shouldn't appear in neutral background
|
||
VALUE_WORDS = [
|
||
"חריג", "חטא", "בעייתי", "מזעזע", "שערורייתי", "מגוחך",
|
||
"נפשע", "פגום", "חמור", "מקומם", "בלתי סביר", "מופרז",
|
||
"מגונה", "פסול", "נלוז", "מטריד",
|
||
]
|
||
|
||
# Party quote indicators
|
||
QUOTE_INDICATORS = [
|
||
r"לטענת\s+(העוררי|המשיב|מבקשי)",
|
||
r"לדברי\s+(העוררי|המשיב|מבקשי)",
|
||
r"העורר\s+טוען",
|
||
r"המשיבה\s+טוענת",
|
||
r"לשיטת\s+(העוררי|המשיב)",
|
||
]
|
||
|
||
# Expected weight ranges per block type (for רישוי appeals)
|
||
WEIGHT_RANGES_LICENSING = {
|
||
"block-he": (0.5, 5),
|
||
"block-vav": (3, 40),
|
||
"block-zayin": (13, 40),
|
||
"block-chet": (0, 15),
|
||
"block-tet": (0, 15),
|
||
"block-yod": (30, 75),
|
||
"block-yod-alef": (1, 10),
|
||
"block-yod-bet": (0, 2),
|
||
}
|
||
|
||
# Expected weight ranges for היטל השבחה
|
||
WEIGHT_RANGES_LEVY = {
|
||
"block-he": (0, 5),
|
||
"block-vav": (2, 20),
|
||
"block-zayin": (15, 40),
|
||
"block-chet": (0, 25),
|
||
"block-tet": (0, 15),
|
||
"block-yod": (25, 75),
|
||
"block-yod-alef": (1, 10),
|
||
"block-yod-bet": (0, 3),
|
||
}
|
||
|
||
|
||
def check_neutral_background(content: str) -> list[str]:
|
||
"""Check block-vav for neutrality violations."""
|
||
issues = []
|
||
if not content:
|
||
return issues
|
||
|
||
lines = content.split("\n")
|
||
for i, line in enumerate(lines):
|
||
# Check value words
|
||
for word in VALUE_WORDS:
|
||
if word in line:
|
||
issues.append(f"מילת שיפוט ברקע (שורה {i+1}): \"{word}\" — \"{line[:80]}...\"")
|
||
|
||
# Check party quotes
|
||
for pattern in QUOTE_INDICATORS:
|
||
if re.search(pattern, line):
|
||
match = re.search(pattern, line).group()
|
||
issues.append(f"ציטוט מצד ברקע (שורה {i+1}): \"{match}\" — \"{line[:80]}...\"")
|
||
|
||
return issues
|
||
|
||
|
||
def check_weight_compliance(blocks: list[dict], appeal_type: str) -> list[str]:
|
||
"""Check block weights are within expected ranges."""
|
||
issues = []
|
||
ranges = WEIGHT_RANGES_LEVY if appeal_type == "levy" else WEIGHT_RANGES_LICENSING
|
||
|
||
total_words = sum(b["word_count"] for b in blocks)
|
||
if total_words == 0:
|
||
return ["אין תוכן בהחלטה"]
|
||
|
||
for block in blocks:
|
||
bid = block["block_id"]
|
||
if bid in ranges and block["word_count"] > 0:
|
||
weight = block["word_count"] / total_words * 100
|
||
low, high = ranges[bid]
|
||
if weight < low:
|
||
issues.append(f"בלוק {bid} ({block['title']}): משקל {weight:.1f}% — מתחת לטווח ({low}-{high}%)")
|
||
elif weight > high:
|
||
issues.append(f"בלוק {bid} ({block['title']}): משקל {weight:.1f}% — מעל לטווח ({low}-{high}%)")
|
||
|
||
return issues
|
||
|
||
|
||
def check_structural_integrity(blocks: list[dict]) -> list[str]:
|
||
"""Check all required blocks are present."""
|
||
issues = []
|
||
required = ["block-he", "block-zayin", "block-yod"]
|
||
block_ids = {b["block_id"] for b in blocks if b["word_count"] > 0}
|
||
|
||
for req in required:
|
||
if req not in block_ids:
|
||
issues.append(f"בלוק חובה חסר: {req}")
|
||
|
||
# Check discussion is the heaviest block
|
||
yod = next((b for b in blocks if b["block_id"] == "block-yod"), None)
|
||
if yod:
|
||
max_block = max((b for b in blocks if b["block_id"] not in ("block-alef", "block-bet", "block-gimel", "block-dalet")),
|
||
key=lambda x: x["word_count"], default=None)
|
||
if max_block and max_block["block_id"] != "block-yod":
|
||
issues.append(f"בלוק הדיון (י) אינו הבלוק הגדול ביותר — {max_block['title']} ({max_block['word_count']} מילים) גדול יותר")
|
||
|
||
return issues
|
||
|
||
|
||
def check_no_duplication(vav_content: str, yod_content: str) -> list[str]:
|
||
"""Check block-yod doesn't repeat block-vav content."""
|
||
issues = []
|
||
if not vav_content or not yod_content:
|
||
return issues
|
||
|
||
# Find sentences from background that appear verbatim in discussion
|
||
vav_sentences = [s.strip() for s in re.split(r'[.!?]', vav_content) if len(s.strip()) > 30]
|
||
for sent in vav_sentences:
|
||
if sent in yod_content:
|
||
issues.append(f"כפילות: משפט מהרקע חוזר בדיון — \"{sent[:60]}...\"")
|
||
|
||
return issues
|
||
|
||
|
||
async def main():
|
||
if len(sys.argv) < 2:
|
||
print("שימוש: python validate-decision.py <מספר_תיק>")
|
||
sys.exit(1)
|
||
|
||
case_number = sys.argv[1]
|
||
await init_schema()
|
||
pool = await get_pool()
|
||
|
||
async with pool.acquire() as conn:
|
||
case = await conn.fetchrow(
|
||
"SELECT * FROM cases WHERE case_number = $1", case_number
|
||
)
|
||
if not case:
|
||
print(f"תיק {case_number} לא נמצא")
|
||
sys.exit(1)
|
||
|
||
decision = await conn.fetchrow(
|
||
"SELECT * FROM decisions WHERE case_id = $1",
|
||
case["id"],
|
||
)
|
||
if not decision:
|
||
print(f"אין החלטה לתיק {case_number}")
|
||
sys.exit(1)
|
||
|
||
blocks = await conn.fetch(
|
||
"""SELECT block_id, title, content, word_count, weight_percent
|
||
FROM decision_blocks WHERE decision_id = $1
|
||
ORDER BY block_index""",
|
||
decision["id"],
|
||
)
|
||
blocks = [dict(b) for b in blocks]
|
||
|
||
claims_count = await conn.fetchval(
|
||
"SELECT count(*) FROM claims WHERE case_id = $1", case["id"]
|
||
)
|
||
|
||
await close_pool()
|
||
|
||
# Determine appeal type
|
||
num = case_number.split("/")[0].split("+")[0].split("-")[0]
|
||
if num.startswith("8"):
|
||
appeal_type = "levy"
|
||
appeal_type_heb = "היטל השבחה"
|
||
elif num.startswith("9"):
|
||
appeal_type = "compensation"
|
||
appeal_type_heb = "פיצויים"
|
||
else:
|
||
appeal_type = "licensing"
|
||
appeal_type_heb = "רישוי ובנייה"
|
||
|
||
print(f"{'='*60}")
|
||
print(f"ולידציה: {case_number} — {case['title']}")
|
||
print(f"סוג: {appeal_type_heb} | מילים: {decision['total_words']} | טענות: {claims_count}")
|
||
print(f"{'='*60}")
|
||
|
||
all_issues = []
|
||
|
||
# 1. Neutral background
|
||
vav = next((b for b in blocks if b["block_id"] == "block-vav"), None)
|
||
issues = check_neutral_background(vav["content"] if vav else "")
|
||
if issues:
|
||
print(f"\n❌ רקע ניטרלי — {len(issues)} בעיות:")
|
||
for i in issues:
|
||
print(f" • {i}")
|
||
all_issues.extend(issues)
|
||
else:
|
||
print("\n✅ רקע ניטרלי — תקין")
|
||
|
||
# 2. Weight compliance
|
||
issues = check_weight_compliance(blocks, appeal_type)
|
||
if issues:
|
||
print(f"\n⚠ משקלות — {len(issues)} חריגות:")
|
||
for i in issues:
|
||
print(f" • {i}")
|
||
all_issues.extend(issues)
|
||
else:
|
||
print("\n✅ משקלות — בטווח")
|
||
|
||
# 3. Structural integrity
|
||
issues = check_structural_integrity(blocks)
|
||
if issues:
|
||
print(f"\n❌ מבנה — {len(issues)} בעיות:")
|
||
for i in issues:
|
||
print(f" • {i}")
|
||
all_issues.extend(issues)
|
||
else:
|
||
print("\n✅ מבנה — תקין")
|
||
|
||
# 4. No duplication
|
||
yod = next((b for b in blocks if b["block_id"] == "block-yod"), None)
|
||
issues = check_no_duplication(
|
||
vav["content"] if vav else "",
|
||
yod["content"] if yod else "",
|
||
)
|
||
if issues:
|
||
print(f"\n⚠ כפילויות — {len(issues)} נמצאו:")
|
||
for i in issues:
|
||
print(f" • {i}")
|
||
all_issues.extend(issues)
|
||
else:
|
||
print("\n✅ ללא כפילויות — תקין")
|
||
|
||
# Summary
|
||
print(f"\n{'='*60}")
|
||
if all_issues:
|
||
print(f"סה\"כ: {len(all_issues)} בעיות נמצאו")
|
||
else:
|
||
print("✅ ההחלטה עומדת בכל הכללים")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|