Add docs, scripts, skills, commands, and taskmaster config to repo

Includes:
- docs/: architecture, block-schema, migration-plan, product-specification
- scripts/: bidi_table, decompose-decisions, extract-claims, seed-knowledge, etc.
- skill-legal-decision/: SKILL.md + references + block-schema
- skill-legal-assistant/: SKILL.md
- skill-legal-docx/: SKILL.md + references
- .claude/commands/: bidi-table skill
- .taskmaster/: task config + PRDs
- .gitignore: exclude legacy/, kiryat-yearim/, node_modules/, memory/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 14:19:17 +00:00
parent bacb330a2a
commit d5ccf03e4c
41 changed files with 9356 additions and 2 deletions

View File

@@ -0,0 +1,257 @@
#!/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())