From 4de555367d854f6ef88e1352329c21ba45bb5ce1 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sat, 27 Jun 2026 12:46:32 +0000 Subject: [PATCH] chore: fix .gitignore inline comments + add untracked code files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inline # comments in gitignore are not supported — they were silently breaking three patterns (data/checkpoints/, data/adapter-migration-state.json, .claude/agents/.generated/). Moved comments to their own lines and added missing entries for runtime dirs (data/audit/, data/logs/, etc.) and temp files (.interaction_tmp.json, .design-build/, .taskmaster bak files). Also tracks previously untracked legitimate files: scripts, tests, docs, skills references, .env.example, taskmaster templates. Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 12 + .gitignore | 31 +- .taskmaster/templates/example_prd.txt | 47 ++ .taskmaster/templates/example_prd_rpg.txt | 511 ++++++++++++++++++ data/halacha_night_check.sh | 43 ++ .../01-claude-architecture-data-audit.md | 170 ++++++ ...ractor-generosity-committee-application.md | 125 +++++ .../plans/2026-05-16-hooks-execution.md | 348 ++++++++++++ .../plans/2026-05-16-per-agent-claude-md.md | 306 +++++++++++ .../plans/2026-05-16-scheduled-agents.md | 412 ++++++++++++++ mcp-server/tests/test_adapter_profiles.py | 21 + scripts/exec_summary_1043.py | 145 +++++ scripts/ingest_incoming_batch.py | 102 ++++ scripts/ocr_benchmark_mistral.py | 392 ++++++++++++++ skills/docx/references/advanced-features.md | 170 ++++++ skills/docx/references/document-templates.md | 219 ++++++++ skills/docx/references/tracked-changes.md | 57 ++ 17 files changed, 3108 insertions(+), 3 deletions(-) create mode 100644 .env.example create mode 100644 .taskmaster/templates/example_prd.txt create mode 100644 .taskmaster/templates/example_prd_rpg.txt create mode 100755 data/halacha_night_check.sh create mode 100644 docs/precedent-corpus-redesign/01-claude-architecture-data-audit.md create mode 100644 docs/precedent-corpus-redesign/06-extractor-generosity-committee-application.md create mode 100644 docs/superpowers/plans/2026-05-16-hooks-execution.md create mode 100644 docs/superpowers/plans/2026-05-16-per-agent-claude-md.md create mode 100644 docs/superpowers/plans/2026-05-16-scheduled-agents.md create mode 100644 mcp-server/tests/test_adapter_profiles.py create mode 100644 scripts/exec_summary_1043.py create mode 100644 scripts/ingest_incoming_batch.py create mode 100644 scripts/ocr_benchmark_mistral.py create mode 100644 skills/docx/references/advanced-features.md create mode 100644 skills/docx/references/document-templates.md create mode 100644 skills/docx/references/tracked-changes.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..60bd23e --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# API Keys (Required to enable respective provider) +ANTHROPIC_API_KEY="your_anthropic_api_key_here" # Required: Format: sk-ant-api03-... +PERPLEXITY_API_KEY="your_perplexity_api_key_here" # Optional: Format: pplx-... +OPENAI_API_KEY="your_openai_api_key_here" # Optional, for OpenAI models. Format: sk-proj-... +GOOGLE_API_KEY="your_google_api_key_here" # Optional, for Google Gemini models. +MISTRAL_API_KEY="your_mistral_key_here" # Optional, for Mistral AI models. +XAI_API_KEY="YOUR_XAI_KEY_HERE" # Optional, for xAI AI models. +GROQ_API_KEY="YOUR_GROQ_KEY_HERE" # Optional, for Groq models. +OPENROUTER_API_KEY="YOUR_OPENROUTER_KEY_HERE" # Optional, for OpenRouter models. +AZURE_OPENAI_API_KEY="your_azure_key_here" # Optional, for Azure OpenAI models (requires endpoint in .taskmaster/config.json). +OLLAMA_API_KEY="your_ollama_api_key_here" # Optional: For remote Ollama servers that require authentication. +GITHUB_API_KEY="your_github_api_key_here" # Optional: For GitHub import/export features. Format: ghp_... or github_pat_... \ No newline at end of file diff --git a/.gitignore b/.gitignore index 84a79f7..05348be 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,8 @@ data/backups/ data/precedent-library/ data/.auto-sync.log data/*.db -data/checkpoints/ # X16 durable-pipeline SQLite checkpoints (runtime artifact) +# X16 durable-pipeline SQLite checkpoints (runtime artifact) +data/checkpoints/ *.bak-pre-* mcp-server/.venv/ __pycache__/ @@ -18,6 +19,30 @@ kiryat-yearim/ continuation-prompt.md node_modules/ data/eval/eval-report-* -data/adapter-migration-state.json # revert snapshot for migrate_agent_adapter.py (runtime state) -.claude/agents/.generated/ # frontmatter-stripped instruction copies for content_arg adapters (generated) +# revert snapshot for migrate_agent_adapter.py (runtime state) +data/adapter-migration-state.json +# frontmatter-stripped instruction copies for content_arg adapters (generated) +.claude/agents/.generated/ .claude/worktrees/ + +# TaskMaster backups (runtime) +.taskmaster/tasks/tasks.json.bak.* + +# Build artifacts +.design-build/ + +# Temp files +.interaction_tmp.json + +# Runtime eval/ab-test data +data/ab_halacha_*.json +data/ab_run_*.log +data/x11_treatment_run_*.log + +# Runtime data directories +data/audit/ +data/bulletins/ +data/digests/ +data/internal-decisions/ +data/learning/ +data/logs/ diff --git a/.taskmaster/templates/example_prd.txt b/.taskmaster/templates/example_prd.txt new file mode 100644 index 0000000..194114d --- /dev/null +++ b/.taskmaster/templates/example_prd.txt @@ -0,0 +1,47 @@ + +# Overview +[Provide a high-level overview of your product here. Explain what problem it solves, who it's for, and why it's valuable.] + +# Core Features +[List and describe the main features of your product. For each feature, include: +- What it does +- Why it's important +- How it works at a high level] + +# User Experience +[Describe the user journey and experience. Include: +- User personas +- Key user flows +- UI/UX considerations] + + +# Technical Architecture +[Outline the technical implementation details: +- System components +- Data models +- APIs and integrations +- Infrastructure requirements] + +# Development Roadmap +[Break down the development process into phases: +- MVP requirements +- Future enhancements +- Do not think about timelines whatsoever -- all that matters is scope and detailing exactly what needs to be build in each phase so it can later be cut up into tasks] + +# Logical Dependency Chain +[Define the logical order of development: +- Which features need to be built first (foundation) +- Getting as quickly as possible to something usable/visible front end that works +- Properly pacing and scoping each feature so it is atomic but can also be built upon and improved as development approaches] + +# Risks and Mitigations +[Identify potential risks and how they'll be addressed: +- Technical challenges +- Figuring out the MVP that we can build upon +- Resource constraints] + +# Appendix +[Include any additional information: +- Research findings +- Technical specifications] + \ No newline at end of file diff --git a/.taskmaster/templates/example_prd_rpg.txt b/.taskmaster/templates/example_prd_rpg.txt new file mode 100644 index 0000000..5ad908f --- /dev/null +++ b/.taskmaster/templates/example_prd_rpg.txt @@ -0,0 +1,511 @@ + +# Repository Planning Graph (RPG) Method - PRD Template + +This template teaches you (AI or human) how to create structured, dependency-aware PRDs using the RPG methodology from Microsoft Research. The key insight: separate WHAT (functional) from HOW (structural), then connect them with explicit dependencies. + +## Core Principles + +1. **Dual-Semantics**: Think functional (capabilities) AND structural (code organization) separately, then map them +2. **Explicit Dependencies**: Never assume - always state what depends on what +3. **Topological Order**: Build foundation first, then layers on top +4. **Progressive Refinement**: Start broad, refine iteratively + +## How to Use This Template + +- Follow the instructions in each `` block +- Look at `` blocks to see good vs bad patterns +- Fill in the content sections with your project details +- The AI reading this will learn the RPG method by following along +- Task Master will parse the resulting PRD into dependency-aware tasks + +## Recommended Tools for Creating PRDs + +When using this template to **create** a PRD (not parse it), use **code-context-aware AI assistants** for best results: + +**Why?** The AI needs to understand your existing codebase to make good architectural decisions about modules, dependencies, and integration points. + +**Recommended tools:** +- **Claude Code** (claude-code CLI) - Best for structured reasoning and large contexts +- **Cursor/Windsurf** - IDE integration with full codebase context +- **Gemini CLI** (gemini-cli) - Massive context window for large codebases +- **Codex/Grok CLI** - Strong code generation with context awareness + +**Note:** Once your PRD is created, `task-master parse-prd` works with any configured AI model - it just needs to read the PRD text itself, not your codebase. + + +--- + + + +Start with the problem, not the solution. Be specific about: +- What pain point exists? +- Who experiences it? +- Why existing solutions don't work? +- What success looks like (measurable outcomes)? + +Keep this section focused - don't jump into implementation details yet. + + +## Problem Statement +[Describe the core problem. Be concrete about user pain points.] + +## Target Users +[Define personas, their workflows, and what they're trying to achieve.] + +## Success Metrics +[Quantifiable outcomes. Examples: "80% task completion via autopilot", "< 5% manual intervention rate"] + + + +--- + + + +Now think about CAPABILITIES (what the system DOES), not code structure yet. + +Step 1: Identify high-level capability domains +- Think: "What major things does this system do?" +- Examples: Data Management, Core Processing, Presentation Layer + +Step 2: For each capability, enumerate specific features +- Use explore-exploit strategy: + * Exploit: What features are REQUIRED for core value? + * Explore: What features make this domain COMPLETE? + +Step 3: For each feature, define: +- Description: What it does in one sentence +- Inputs: What data/context it needs +- Outputs: What it produces/returns +- Behavior: Key logic or transformations + + +Capability: Data Validation + Feature: Schema validation + - Description: Validate JSON payloads against defined schemas + - Inputs: JSON object, schema definition + - Outputs: Validation result (pass/fail) + error details + - Behavior: Iterate fields, check types, enforce constraints + + Feature: Business rule validation + - Description: Apply domain-specific validation rules + - Inputs: Validated data object, rule set + - Outputs: Boolean + list of violated rules + - Behavior: Execute rules sequentially, short-circuit on failure + + + +Capability: validation.js + (Problem: This is a FILE, not a CAPABILITY. Mixing structure into functional thinking.) + +Capability: Validation + Feature: Make sure data is good + (Problem: Too vague. No inputs/outputs. Not actionable.) + + + +## Capability Tree + +### Capability: [Name] +[Brief description of what this capability domain covers] + +#### Feature: [Name] +- **Description**: [One sentence] +- **Inputs**: [What it needs] +- **Outputs**: [What it produces] +- **Behavior**: [Key logic] + +#### Feature: [Name] +- **Description**: +- **Inputs**: +- **Outputs**: +- **Behavior**: + +### Capability: [Name] +... + + + +--- + + + +NOW think about code organization. Map capabilities to actual file/folder structure. + +Rules: +1. Each capability maps to a module (folder or file) +2. Features within a capability map to functions/classes +3. Use clear module boundaries - each module has ONE responsibility +4. Define what each module exports (public interface) + +The goal: Create a clear mapping between "what it does" (functional) and "where it lives" (structural). + + +Capability: Data Validation + → Maps to: src/validation/ + ├── schema-validator.js (Schema validation feature) + ├── rule-validator.js (Business rule validation feature) + └── index.js (Public exports) + +Exports: + - validateSchema(data, schema) + - validateRules(data, rules) + + + +Capability: Data Validation + → Maps to: src/utils.js + (Problem: "utils" is not a clear module boundary. Where do I find validation logic?) + +Capability: Data Validation + → Maps to: src/validation/everything.js + (Problem: One giant file. Features should map to separate files for maintainability.) + + + +## Repository Structure + +``` +project-root/ +├── src/ +│ ├── [module-name]/ # Maps to: [Capability Name] +│ │ ├── [file].js # Maps to: [Feature Name] +│ │ └── index.js # Public exports +│ └── [module-name]/ +├── tests/ +└── docs/ +``` + +## Module Definitions + +### Module: [Name] +- **Maps to capability**: [Capability from functional decomposition] +- **Responsibility**: [Single clear purpose] +- **File structure**: + ``` + module-name/ + ├── feature1.js + ├── feature2.js + └── index.js + ``` +- **Exports**: + - `functionName()` - [what it does] + - `ClassName` - [what it does] + + + +--- + + + +This is THE CRITICAL SECTION for Task Master parsing. + +Define explicit dependencies between modules. This creates the topological order for task execution. + +Rules: +1. List modules in dependency order (foundation first) +2. For each module, state what it depends on +3. Foundation modules should have NO dependencies +4. Every non-foundation module should depend on at least one other module +5. Think: "What must EXIST before I can build this module?" + + +Foundation Layer (no dependencies): + - error-handling: No dependencies + - config-manager: No dependencies + - base-types: No dependencies + +Data Layer: + - schema-validator: Depends on [base-types, error-handling] + - data-ingestion: Depends on [schema-validator, config-manager] + +Core Layer: + - algorithm-engine: Depends on [base-types, error-handling] + - pipeline-orchestrator: Depends on [algorithm-engine, data-ingestion] + + + +- validation: Depends on API +- API: Depends on validation +(Problem: Circular dependency. This will cause build/runtime issues.) + +- user-auth: Depends on everything +(Problem: Too many dependencies. Should be more focused.) + + + +## Dependency Chain + +### Foundation Layer (Phase 0) +No dependencies - these are built first. + +- **[Module Name]**: [What it provides] +- **[Module Name]**: [What it provides] + +### [Layer Name] (Phase 1) +- **[Module Name]**: Depends on [[module-from-phase-0], [module-from-phase-0]] +- **[Module Name]**: Depends on [[module-from-phase-0]] + +### [Layer Name] (Phase 2) +- **[Module Name]**: Depends on [[module-from-phase-1], [module-from-foundation]] + +[Continue building up layers...] + + + +--- + + + +Turn the dependency graph into concrete development phases. + +Each phase should: +1. Have clear entry criteria (what must exist before starting) +2. Contain tasks that can be parallelized (no inter-dependencies within phase) +3. Have clear exit criteria (how do we know phase is complete?) +4. Build toward something USABLE (not just infrastructure) + +Phase ordering follows topological sort of dependency graph. + + +Phase 0: Foundation + Entry: Clean repository + Tasks: + - Implement error handling utilities + - Create base type definitions + - Setup configuration system + Exit: Other modules can import foundation without errors + +Phase 1: Data Layer + Entry: Phase 0 complete + Tasks: + - Implement schema validator (uses: base types, error handling) + - Build data ingestion pipeline (uses: validator, config) + Exit: End-to-end data flow from input to validated output + + + +Phase 1: Build Everything + Tasks: + - API + - Database + - UI + - Tests + (Problem: No clear focus. Too broad. Dependencies not considered.) + + + +## Development Phases + +### Phase 0: [Foundation Name] +**Goal**: [What foundational capability this establishes] + +**Entry Criteria**: [What must be true before starting] + +**Tasks**: +- [ ] [Task name] (depends on: [none or list]) + - Acceptance criteria: [How we know it's done] + - Test strategy: [What tests prove it works] + +- [ ] [Task name] (depends on: [none or list]) + +**Exit Criteria**: [Observable outcome that proves phase complete] + +**Delivers**: [What can users/developers do after this phase?] + +--- + +### Phase 1: [Layer Name] +**Goal**: + +**Entry Criteria**: Phase 0 complete + +**Tasks**: +- [ ] [Task name] (depends on: [[tasks-from-phase-0]]) +- [ ] [Task name] (depends on: [[tasks-from-phase-0]]) + +**Exit Criteria**: + +**Delivers**: + +--- + +[Continue with more phases...] + + + +--- + + + +Define how testing will be integrated throughout development (TDD approach). + +Specify: +1. Test pyramid ratios (unit vs integration vs e2e) +2. Coverage requirements +3. Critical test scenarios +4. Test generation guidelines for Surgical Test Generator + +This section guides the AI when generating tests during the RED phase of TDD. + + +Critical Test Scenarios for Data Validation module: + - Happy path: Valid data passes all checks + - Edge cases: Empty strings, null values, boundary numbers + - Error cases: Invalid types, missing required fields + - Integration: Validator works with ingestion pipeline + + + +## Test Pyramid + +``` + /\ + /E2E\ ← [X]% (End-to-end, slow, comprehensive) + /------\ + /Integration\ ← [Y]% (Module interactions) + /------------\ + / Unit Tests \ ← [Z]% (Fast, isolated, deterministic) + /----------------\ +``` + +## Coverage Requirements +- Line coverage: [X]% minimum +- Branch coverage: [X]% minimum +- Function coverage: [X]% minimum +- Statement coverage: [X]% minimum + +## Critical Test Scenarios + +### [Module/Feature Name] +**Happy path**: +- [Scenario description] +- Expected: [What should happen] + +**Edge cases**: +- [Scenario description] +- Expected: [What should happen] + +**Error cases**: +- [Scenario description] +- Expected: [How system handles failure] + +**Integration points**: +- [What interactions to test] +- Expected: [End-to-end behavior] + +## Test Generation Guidelines +[Specific instructions for Surgical Test Generator about what to focus on, what patterns to follow, project-specific test conventions] + + + +--- + + + +Describe technical architecture, data models, and key design decisions. + +Keep this section AFTER functional/structural decomposition - implementation details come after understanding structure. + + +## System Components +[Major architectural pieces and their responsibilities] + +## Data Models +[Core data structures, schemas, database design] + +## Technology Stack +[Languages, frameworks, key libraries] + +**Decision: [Technology/Pattern]** +- **Rationale**: [Why chosen] +- **Trade-offs**: [What we're giving up] +- **Alternatives considered**: [What else we looked at] + + + +--- + + + +Identify risks that could derail development and how to mitigate them. + +Categories: +- Technical risks (complexity, unknowns) +- Dependency risks (blocking issues) +- Scope risks (creep, underestimation) + + +## Technical Risks +**Risk**: [Description] +- **Impact**: [High/Medium/Low - effect on project] +- **Likelihood**: [High/Medium/Low] +- **Mitigation**: [How to address] +- **Fallback**: [Plan B if mitigation fails] + +## Dependency Risks +[External dependencies, blocking issues] + +## Scope Risks +[Scope creep, underestimation, unclear requirements] + + + +--- + + +## References +[Papers, documentation, similar systems] + +## Glossary +[Domain-specific terms] + +## Open Questions +[Things to resolve during development] + + +--- + + +# How Task Master Uses This PRD + +When you run `task-master parse-prd .txt`, the parser: + +1. **Extracts capabilities** → Main tasks + - Each `### Capability:` becomes a top-level task + +2. **Extracts features** → Subtasks + - Each `#### Feature:` becomes a subtask under its capability + +3. **Parses dependencies** → Task dependencies + - `Depends on: [X, Y]` sets task.dependencies = ["X", "Y"] + +4. **Orders by phases** → Task priorities + - Phase 0 tasks = highest priority + - Phase N tasks = lower priority, properly sequenced + +5. **Uses test strategy** → Test generation context + - Feeds test scenarios to Surgical Test Generator during implementation + +**Result**: A dependency-aware task graph that can be executed in topological order. + +## Why RPG Structure Matters + +Traditional flat PRDs lead to: +- ❌ Unclear task dependencies +- ❌ Arbitrary task ordering +- ❌ Circular dependencies discovered late +- ❌ Poorly scoped tasks + +RPG-structured PRDs provide: +- ✅ Explicit dependency chains +- ✅ Topological execution order +- ✅ Clear module boundaries +- ✅ Validated task graph before implementation + +## Tips for Best Results + +1. **Spend time on dependency graph** - This is the most valuable section for Task Master +2. **Keep features atomic** - Each feature should be independently testable +3. **Progressive refinement** - Start broad, use `task-master expand` to break down complex tasks +4. **Use research mode** - `task-master parse-prd --research` leverages AI for better task generation + diff --git a/data/halacha_night_check.sh b/data/halacha_night_check.sh new file mode 100755 index 0000000..ca2059d --- /dev/null +++ b/data/halacha_night_check.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# One-shot morning verdict for the halacha night drain (scheduled 2026-06-15 04:30 UTC +# via chaim's crontab; throwaway — lives under data/, not tracked in scripts/). +# Captures whether last night's run (with the PR #251 fix: durable rate-limit +# detection + 05:00–07:00 catch-up window) actually drained the backlog. +# Baseline at install time (2026-06-14 13:xx IDT): pending=96 done=248 halachot=4099. +set -u +export HOME=/home/chaim +REPO=/home/chaim/legal-ai +PY="$REPO/mcp-server/.venv/bin/python" +OUT="$REPO/data/logs/halacha_night_report_$(TZ=Asia/Jerusalem date +%Y%m%d).md" +SUP_LOG=/home/chaim/.pm2/logs/legal-halacha-supervisor-out.log +DRAIN_ERR=/home/chaim/.pm2/logs/legal-halacha-drain-error.log +mkdir -p "$REPO/data/logs" + +{ + echo "# דוח-בוקר: ריצת-הלכות הלילה — $(TZ=Asia/Jerusalem date '+%Y-%m-%d %H:%M %Z')" + echo + echo "בסיס-השוואה (אתמול 13:xx IDT): pending=96 · done=248 · halachot=4099" + echo + echo '## מצב נוכחי (supervisor status)' + echo '```' + cd "$REPO" && "$PY" scripts/halacha_drain_supervisor.py status 2>&1 + echo '```' + echo + echo '## פעולות המתזמר ב-12 השעות האחרונות (modes/actions)' + echo '```' + grep -E 'מצב:|פעולה:|catch-up|rate-limit|נעצר' "$SUP_LOG" 2>/dev/null | tail -40 + echo '```' + echo + echo '## אותות rate-limit בלוג-הדריינר (24ש אחרונות בלוג)' + echo '```' + echo "429 hits (tail 4000): $(tail -4000 "$DRAIN_ERR" 2>/dev/null | grep -c '429')" + echo "session-limit msgs: $(tail -4000 "$DRAIN_ERR" 2>/dev/null | grep -c 'hit your session limit')" + echo "extraction_failed: $(tail -4000 "$DRAIN_ERR" 2>/dev/null | grep -c 'extraction_failed')" + echo "hold-stopped (fix A): $(grep -c 'hold-stopped' "$SUP_LOG" 2>/dev/null)" + echo "catch-up opened (B): $(grep -c 'catch-up בוקר' "$SUP_LOG" 2>/dev/null)" + echo '```' + echo + echo "_(נוצר ע\"י data/halacha_night_check.sh; ניתן למחוק את שורת ה-crontab של 15.6.)_" +} > "$OUT" 2>&1 + +echo "report written: $OUT" diff --git a/docs/precedent-corpus-redesign/01-claude-architecture-data-audit.md b/docs/precedent-corpus-redesign/01-claude-architecture-data-audit.md new file mode 100644 index 0000000..c097281 --- /dev/null +++ b/docs/precedent-corpus-redesign/01-claude-architecture-data-audit.md @@ -0,0 +1,170 @@ +# ממצאי ביקורת — ארכיטקטורת קורפוס־הפסיקה + מצב הדאטה בפועל + +> **מקור:** Claude (Opus 4.8) · **תאריך:** 2026-06-20 · **קונטקסט:** חקירה לקראת תכנון־מחדש של קורפוס־הפסיקה. +> מסמך זה הוא **אחד מכמה** קלטי־סוכנים שחיים אוסף; ייעודו להזין את שלב־הסינתזה. אינו תכנית — הוא **אבחון**. +> +> **שאלת־המוצא של חיים:** "הקורפוס נבנה מראש לא נכון, אני כל הזמן מתעסק בתיקונים. האם כדאי ליצור מחדש את קורפוס־הפסיקה ולהתחיל דף נקי?" +> +> **המודל הרצוי (כפי שחיים תיאר אותו):** מאגר פסקי־דין והחלטות ועדות־ערר; חוקר־התקדימים מזהה בשלב ניתוח־הערר פס"ד/החלטות שדנו במקרה דומה או הלכה דומה; הסוכן־הכותב משייך ומזכיר אותם בפרק הדיון וההכרעה בסגנון דפנה. שלושה מקורות־הזנה: (1) החלטות דפנה עצמה, (2) ועדות־ערר אחרות שמצטטות פסיקה, (3) פס"ד עליון/מחוזי. + +--- + +## 0. תקציר־מנהלים (TL;DR) + +**מה ש"בנוי לא נכון" אינו הסכמה — היא שכבת־הביצוע.** הסכמה כבר תואמת בדיוק את המודל שחיים תיאר: טבלה אחת (`case_law`) ששלושת המקורות נכנסים אליה דרך `source_kind`/`source_type`; שלושת ה־`search_*` הם שלושה *מסננים* על אותו מקור, לא שלושה מאגרים מקבילים (G2 מקוים ברמת־הסכמה). **רֵבילד של הסכמה ייצר בדיוק את אותה סכמה** — ולכן אינו פותר דבר, ומסכן ב־second-system syndrome. + +מה שכן מחולל את "התיקונים האינסופיים" — שלושה כשלי־ביצוע מדידים: +1. **חוזה־קליטה רופף** → 66% מהפסיקה בלי `practice_area`, 31 רשומות ריקות, אכיפת־שלמות (INV-DM1) מופרת בפועל. +2. **צינור הלכות→קנוני מייצר רעש** → 5,472 קנוני, מתוכם 5,456 סינגלטונים, **0 published** → השכבה שאמורה להזין את הכותב (INV-G10) **אינרטית לגמרי**. +3. **כפילות `style_corpus`** → 55 החלטות דפנה חיות בשני נתיבי־אחזור. + +**המלצה:** לא לשרוף את הסכמה. כן לבצע "איפוס שכבות־נגזרות" צר (truncate ל-chunks+halachot+canonical והרצה־מחדש), **אבל רק אחרי תיקון החוזה והסף** — אחרת מחזירים את אותו בלגן (G1: תיקון במקור, לא בקריאה). מסמכי־המקור (363 רשומות, 332 עם full_text) נשמרים; הם יקרים ו/או ניתנים לקליטה־מחדש מ־PDF. + +--- + +## 1. מצב הדאטה בפועל (שאילתות חיות מול legal_ai @ localhost:5433, 2026-06-20) + +``` +┌─────────────────────────────────────┬─────────┬──────────────────────────────────┐ +│ Metric │ Count │ Reading │ +├─────────────────────────────────────┼─────────┼──────────────────────────────────┤ +│ case_law (total precedents) │ 363 │ קטן — re-ingestable │ +│ • external_upload (court rulings) │ 240 │ מקור (3) — עליון/מחוזי │ +│ • internal_committee (ועדות ערר) │ 92 │ מקור (1)+(2) │ +│ • (יתר — ללא source_kind מובהק) │ 31 │ = הרשומות הריקות (ראה למטה) │ +│ style_corpus (החלטות דפנה) │ 55 │ כפילות עם internal_committee │ +│ precedent_chunks │ 11,904 │ נגזר — מתחדש מ-full_text │ +│ halachot (total) │ 5,489 │ נגזר │ +│ • approved/published │ 1,352 │ 25% בלבד │ +│ • pending_review (backlog ידני) │ 2,402 │ 44% — צוואר־בקבוק │ +│ canonical_halachot (V41) │ 5,472 │ כמעט 1:1 עם halachot ⚠️ │ +│ • singletons (instance_count=1) │ 5,456 │ דה־דופ כמעט לא קרה │ +│ • merged (instance_count>=2) │ 16 │ 0.3% מיזוג │ +│ • published (מגיע לסוכן הכותב) │ 0 │ ⚠️ השכבה אינרטית לחלוטין │ +│ case_law w/o practice_area │ 240 │ 66% — חופף-בדיוק לפסיקה החיצונית │ +│ case_law missing summary │ 27 │ │ +│ case_law w/ 0 chunks / no full_text │ 31 │ רשומות שבורות/ריקות │ +│ distinct practice_area │ 4 │ rishuy/betterment/197/(ריק) │ +└─────────────────────────────────────┴─────────┴──────────────────────────────────┘ + +practice_area breakdown: + (ריק) 240 ← כל הפסיקה החיצונית ללא סיווג + rishuy_uvniya 70 + betterment_levy 50 + compensation_197 3 +``` + +**קריאות מפתח:** +- **240 = 240:** מספר הפסיקה־החיצונית שווה־בדיוק למספר חסרי־`practice_area`. כלומר אף פס"ד חיצוני לא סווג לתחום — סינון לפי תחום באחזור פשוט לא עובד עליהם. +- **5,456 / 5,472 סינגלטונים:** מנוע הקנוניזציה (V41) רץ אך לא מאחד. סף 0.85 כנראה הדוק מדי, או שהחילוץ מנסח כל הלכה ייחודית מספיק כדי לא להתלכד. +- **0 published canonical:** לפי INV-G10 רק קנוני `published` מגיע לכתיבה. אפס. **כל מנגנון V41 כרגע מנותק מהכתיבה בפועל.** +- **2,402 pending_review:** צוואר־הבקבוק הוא אישור־אנושי ידני, לא טכנולוגיה. + +--- + +## 2. מפת הארכיטקטורה — קורפוס אחד, שלושה מסננים + +**פיזית: טבלה אחת.** כל שלושת המקורות מתכנסים ל-`case_law`, מתחתיה `precedent_chunks` (FK) ו-`halachot` (FK), ומעל ה-`halachot` שכבת `canonical_halachot` (V41). + +``` + canonical_halachot (עקרונות מאוחדים — V41; כיום אינרטי, 0 published) + ▲ 1:many + halachot (מופע-להלכה per precedent; review_status gate) + ▲ FK + case_law (רישום מרכזי — court rulings + ועדות-ערר) + ├─ source_kind='external_upload' → פס"ד עליון/מחוזי [מקור 3] + └─ source_kind='internal_committee'→ ועדות-ערר + דפנה [מקור 1+2] + ▼ FK + precedent_chunks (chunks + embedding vector(1024)) + + בנפרד: + style_corpus (55 החלטות דפנה — נתיב-אחזור מקביל ל-search_decisions) + document_chunks (מסמכי-תיק + style; FK→documents→cases) +``` + +**שלושת נתיבי־האחזור (לא שלושה מאגרים — שלושה scopes):** + +``` +┌──────────────────────────────┬─────────────────────────┬────────────────────────────┐ +│ Tool │ Table / filter │ Purpose │ +├──────────────────────────────┼─────────────────────────┼────────────────────────────┤ +│ search_decisions │ document_chunks │ סגנון/קול: החלטות דפנה │ +│ │ (scoped case/area) │ + מסמכי-תיק │ +│ search_precedent_library │ case_law + chunks/halach │ source_kind=external_upload│ +│ │ ot, source_kind filter │ → פסיקה חיצונית │ +│ search_internal_decisions │ אותן פונקציות DB, │ source_kind=internal_ │ +│ │ source_kind אחר │ committee → ועדות-ערר │ +└──────────────────────────────┴─────────────────────────┴────────────────────────────┘ +``` + +שתי האחרונות קוראות **לאותן פונקציות DB** (`search_precedent_library_semantic`/`_lexical`) עם `source_kind` שונה. זו הפרדה־בשאילתה, לא קוד מקביל. + +--- + +## 3. שלושת המקורות של חיים → איפה הם נופלים היום + +``` +┌────────────────────────────────────┬───────────────────────────────┬─────────────────────────┐ +│ Source (חיים) │ Stored as │ Retrieval │ +├────────────────────────────────────┼───────────────────────────────┼─────────────────────────┤ +│ (1) החלטות דפנה עצמה │ style_corpus + (מהוגר ל-) │ search_decisions + │ +│ │ case_law internal_committee │ search_internal_decisions│ +│ │ chair_name='דפנה תמיר' │ ← כפילות / נתיב-כפול │ +│ (2) ועדות-ערר אחרות │ case_law internal_committee │ search_internal_decisions│ +│ │ chair_name=<אחר>, district │ │ +│ (3) פס"ד עליון/מחוזי │ case_law external_upload │ search_precedent_library │ +│ │ source_type='court_ruling' │ │ +└────────────────────────────────────┴───────────────────────────────┴─────────────────────────┘ +``` + +**מסקנה:** המודל המנטלי של חיים **כבר ממומש בסכמה**. אין צורך להמציא מבנה חדש — צריך לאכוף את המבנה הקיים בקליטה, ולחבר את שכבת־הקנוני לכתיבה. + +--- + +## 4. הדיאגנוזה — מה "בנוי לא נכון" (3 מחוללי־כאב) + +### 4.1 חוזה־קליטה רופף (root cause #1) +- 66% מהפסיקה ללא `practice_area`; 27 ללא summary; 31 ללא full_text/chunks. +- אין אכיפה ש"`searchable=false` עד שהמטא שלם" → INV-DM1 מופר בפועל. +- **כל העלאה מוסיפה חוב** במקום רשומה שלמה. זה המקור לתיקונים החוזרים. + +### 4.2 צינור הלכות→קנוני מייצר רעש, לא ערך (root cause #2) +- 5,456/5,472 סינגלטונים → דה־דופ לא עובד (סף 0.85? ניסוח־חילוץ?). +- **0 published** → השכבה שאמורה להזין את הכותב (INV-G10) מנותקת. +- 2,402 בתור־אישור־ידני → הצינור מייצר מהר יותר ממה שאדם מאשר. +- **זה בולע את רוב זמן־התחזוקה.** + +### 4.3 כפילות style_corpus (root cause #3) +- 55 החלטות דפנה בשני מקומות + שני נתיבי־אחזור. +- צריך מקור־אמת אחד: או `style_corpus` SoT וה-`case_law` נגזר, או הפוך. + +--- + +## 5. ההמלצה — לא רֵבילד־סכמה; "איפוס שכבות־נגזרות" אחרי תיקון־חוזה + +**אסור** לשרוף ולהעלות־מחדש את `case_law` — תבנה אותה סכמה ותאבד מטא־דאטה ידני. **כן** הגיוני רֵבילד צר של ה**נגזר**, אבל בסדר הזה (G1 — מקור לפני תסמין): + +1. **קודם החוזה:** אכוף ב-`*_upload` שדות־חובה (practice_area, summary, full_text); כשל → `searchable=false`. נקה/מחק את 31 הריקות. +2. **תקן את הקנוניזציה:** כוונן סף 0.85, הגדר מתי `published`, ובדוק ניסוח־החילוץ. בלי זה אין טעם להריץ מחדש. +3. **רק אז** re-derive מהמקור הקיים: chunks → halachot → canonical. +4. **הכרע style_corpus:** מקור־אמת אחד. + +**מתי רֵבילד־מלא כן מוצדק:** רק אם יתגלה שמסמכי־המקור עצמם (PDF/full_text של 363) פגומים/חסרים. המספרים *לא* מראים זאת (332/363 עם full_text תקין). + +--- + +## 6. שאלות פתוחות לשלב־הסינתזה +- **למה 0 published בקנוני?** האם זה סף, ניסוח־חילוץ, או שפשוט אף אחד לא אישר? (קריטי — קובע אם V41 שמיש בכלל.) +- **style_corpus מול case_law:** מי SoT? (משפיע על search_decisions מול search_internal_decisions.) +- **practice_area לפסיקה חיצונית:** לחלץ אוטומטית בקליטה, או להשאיר ידני? +- **תור־האישור (2,402):** האם המנגנון (פאנל/active-learning, #133) יכול לסגור את הפער, או שצריך לחתוך את קצב־החילוץ? + +--- + +## 7. נספח — קבצים מרכזיים (file:line) +- סכמה: `mcp-server/src/legal_mcp/services/db.py` (case_law, precedent_chunks, halachot, canonical_halachot) +- קליטה חיצונית: `mcp-server/src/legal_mcp/tools/precedent_library.py` (`precedent_library_upload`) +- קליטה פנימית: `mcp-server/src/legal_mcp/tools/internal_decisions.py` (`internal_decision_upload`) +- הגירת style→case_law: `mcp-server/src/legal_mcp/services/internal_decisions.py` (`migrate_from_style_corpus`, chair/district hardcoded) +- אחזור: `mcp-server/src/legal_mcp/tools/search.py`, `services/hybrid_search.py`, `services/db.py` (`search_precedent_library_semantic`/`_lexical`) +- ספ: `docs/spec/02-data-model.md` (INV-DM1–7), `docs/spec/03-retrieval.md` (INV-RET1–5), `docs/spec/00-constitution.md` (G2/G10) diff --git a/docs/precedent-corpus-redesign/06-extractor-generosity-committee-application.md b/docs/precedent-corpus-redesign/06-extractor-generosity-committee-application.md new file mode 100644 index 0000000..8eaff78 --- /dev/null +++ b/docs/precedent-corpus-redesign/06-extractor-generosity-committee-application.md @@ -0,0 +1,125 @@ +# 06 — נדיבות-המחלץ: `application` בהחלטות-ועדה (כימות חוצה-קורפוס) + +> **קלט-נתונים** ליוזמה, נמדד חי על **5,489 רשומות `halachot`** (כל הקורפוס, 2026-06-20). +> נולד משאלת-חיים: "8508-03-24 מפיק 71 הלכות ממתינות — האם המחלץ נדיב מדי על החלטות-ועדה, +> או שזה ספציפי לתיק הזה?" התשובה: **שיטתי, לא ספציפי — אבל הנדיבות מוצדקת.** +> +> **מתכתב עם [`00-final-synthesis.md`](00-final-synthesis.md):** הנתונים כאן **מחזקים** את הכרעת-הסינתזה +> ("לא לחתוך") ומוסיפים שני דברים שלא היו לה: (א) כימות חוצה-קורפוס של ה-`application`, (ב) ממצא +> חדש וגדול יותר — `nli_unsupported` על הפסיקה החיצונית. ראה §4 (יישוב-מתח) ו-§5 (דרכי-פעולה). + +--- + +## 1. הממצא המרכזי — `application` הוא תופעה שיטתית של החלטות-ועדה + +הפרדנו את הקורפוס לפי `authority` (נגזר דטרמיניסטית מ-`precedent_level`, [02-data-model §162](../spec/02-data-model.md)): +`binding` = פסיקת-עליון/מנהלי · `persuasive` = ועדת-ערר מחוזית. + +```text +source rows rt=application nli_unsupported any-flag +court (binding) 3,603 0.2% 39.7% 45.4% +committee (persuasive) 1,842 13.7% 25.0% 30.5% +``` + +- **`rule_type='application'` הוא כמעט-בלעדית של ועדות:** 13.7% מול 0.2%. מתוך 258 רשומות-`application` + בכל הקורפוס, **~252 מגיעות מהחלטות-ועדה.** פער של פי-~70. +- **עקבי בין תחומים** (לא עניין-שמאות נקודתי): +```text +committee, rt=application by practice_area: + rishuy_uvniya 13.5% betterment_levy 14.1% compensation_197 18.8% +``` + +**הפרשנות (תואם [02-data-model §163](../spec/02-data-model.md)):** `application` = "החלה תלוית-עובדות — +לרוב לא-הלכה". ועדת-ערר היא גוף מיישֵם: היא לא יוצרת הלכה (רק בית-משפט עושה זאת), אלא **מיישמת +פסיקת-עליון קיימת** (לוסטרניק, דלי-דליה) על עובדות-התיק. לכן חילוץ מהחלטת-ועדה מייצר באופן מובנה +שיעור גבוה של יישומי-דוקטרינה. **זו תכונה של מקור-הנתונים, לא באג של המחלץ.** + +## 2. 8508-03-24 — מייצג בקצה-העליון, לא חריג + +```text +דירוג 8508-03-24: 7 מתוך 45 תיקי-ועדה (≥15 רשומות) · 27% application (rt או flag) +חציון תיקי-הוועדה: 11.9% +מעליו: 1001-02-19 (40%) · 1044-08-22 (37%) · 9002-24 (33%) · 1007-01-25 (30%) +``` + +8508 הוא ~פי-2.3 מהחציון (רבעון-עליון), אבל מה שבולט בו הוא בעיקר ש**הוא התיק הארוך בקורפוס** +(111 רשומות) — אז 27% נותן 30 פריטי-`application` במספר מוחלט, הגבוה בקורפוס. כלומר: **כמות גבוהה, +שיעור גבוה-אך-נורמלי.** אין כאן פתולוגיה ייחודית לתיק. + +## 3. מנגנון-הניתוב הקיים (איך זה כבר מטופל ב-UI) + +הפיצול בתור-ההלכות (`/precedents` → "תור הלכות") אינו לפי `exclude_low_quality`, אלא לפי +`isExtractionFixItem(h) = (quality_flags.length>0) && !panel_round` +([web-ui/.../precedent-library.ts:652](../../web-ui/src/lib/api/precedent-library.ts#L652)): + +```text +8508-03-24, 71 ממתינות (מצב 2026-06-20): + bucket # panel? →"להכרעתך" →"דורש תיקון-חילוץ" + clean (ללא דגל) 40 15/40 40 0 + application 23 0/23 0 23 + nli_unsupported 4 4/4 4 0 + thin_restatement 4 0/4 0 4 +``` + +**משמעות:** פריטי-`application` (חסרי-פאנל) כבר מנותבים ל**"דורש תיקון-חילוץ"** — **מחוץ** לתור-ההכרעה +של היו"ר. כלומר המערכת כבר מסננת אותם מהעומס-הידני. (הפאנל התלת-מודלי, [halacha_panel_approve.py:191](../../scripts/halacha_panel_approve.py#L191), +מטפל רק בדליים `clean`+`nli`; `application` ו-`defect` עוקפים אותו במכוון.) + +## 4. ⚠️ יישוב-המתח מול הסינתזה — `application` ≠ "רעש" + +זו הנקודה הקריטית להעברה. **אסור** לתרגם "13.7% application" ל"13.7% רעש לחיתוך". המבחן בסינתזה +(§2 שם) כבר הוכיח שחיתוך-אגרסיבי על 8508 השמיד את **לוסטרניק** ו-~22 עקרונות-ליבה. רוב פריטי-ה-`application` +הם בדיוק יישומי-הדוקטרינה הללו — **בני-ציטוט שהכותב צריך**. דוגמאות אמיתיות מ-8508 שמסומנות `application`: + +```text +"ציפיות הנובעות אך ממיקומם של המקרקעין... אין לנטרלן" ← לוסטרניק מיושמת — לשמור! +"בחישוב שווי במצב קודם יש לכלול ציפיות כלליות... ולא ספציפיות" ← ליבת חישוב היטל-ההשבחה +"קביעת מקדם מצויה בליבת שיקול-דעת השמאי, והוועדה לא תתערב" ← סטנדרט אי-התערבות מיושם +``` + +לכן: **הנתון הזה תומך ב"שמור-בספק" של עמוד-1/2 בסינתזה.** המסקנה הנכונה אינה "לסנן application" אלא +"`application` מאשר שעקרוני-הוועדה הם persuasive-יישומיים — לדרג אותם נמוך באחזור (רמה B), לא למחוק +אותם (רמה A)". `importance=0` ל-8508 כבר משקיע אותם ממילא. + +## 5. ⭐ ממצא-לוואי גדול יותר — `nli_unsupported` על הפסיקה החיצונית + +```text +nli_unsupported: court (binding) 39.7% ≫ committee 25.0% +any-flag: court 45.4% · committee 30.5% +``` + +**כמעט מחצית מעקרוני-הפסיקה-החיצונית נושאים דגל-איכות, בעיקר `nli_unsupported`** (הכלל אינו נגזר +לוגית מהציטוט התומך שלו, [halacha_quality.py:282](../../mcp-server/src/legal_mcp/services/halacha_quality.py#L282)). +זה **מגמד מספרית** את סוגיית-ה-`application`, ונוגע ישירות ל"רמה A = ניקוי-רעש" של הסינתזה: +**מהו ה"רעש" שמנקים?** הדגל הדומיננטי הוא `nli`, והוא מרוכז בפסיקה, לא בוועדות. + +שתי השערות מתחרות, שצריך להכריע ביניהן **לפני** שמשתמשים ב-`nli` כמסנן-רעש ברמה A: +- **(א) הבודק מחמיר מדי** — סף-ה-NLI חותך יישורים לגיטימיים → 40% הם false-positives, וה"רעש" מדומה. +- **(ב) החילוץ-מהפסיקה לקוי** — ציטוטים תומכים שלא מיישרים לכלל → בעיית-חילוץ אמיתית בקנה-מידה. + +ההכרעה משנה את כל אסטרטגיית רמה-A. **אנו ממליצים לאמת זאת על מדגם-זהב לפני כל שימוש ב-`nli` כסיגנל.** + +--- + +## 6. דרכי-פעולה מוצעות (לסוכן-הקורפוס) + +ממוין מהשמרני לאגרסיבי. ההמלצה שלנו: **B כברירת-מחדל + C כעבודה-מקבילה**; להימנע מ-A ומ-D. + +| # | פעולה | טיעון | סיכון | המלצה | +|---|-------|-------|-------|-------| +| **A** | לכוונן את ה-prompt לדכא `application` מוועדות במקור | מטפל-בשורש (G1); חוסך 14% רשומות | **גבוה** — סותר את מבחן-8508; משמיד יישומי-לוסטרניק בני-ציטוט | ✗ לא | +| **B** | להשאיר את החילוץ; לסמוך על הניתוב הקיים (`application`→"תיקון", מחוץ לתור-היו"ר) + לדרג נמוך ב-RRF | תואם-סינתזה (שמור-בספק + דרג-בזמן-אחזור); אפס סיכון-אובדן | הרעש נשאר ב-DB (אחסון בלבד) | ✓ **כן — ברירת-מחדל** | +| **C** | לחקור קודם את `nli_unsupported` (40% פסיקה): מדגם-זהב, להכריע (א) מחמיר-מדי מול (ב) חילוץ-לקוי | זה הסיגנל הגדול; הכרחי לפני שמגדירים "רעש" ברמה A | דורש מדגם מתויג-ידנית | ✓ **כן — במקביל** | +| **D** | להפסיק חילוץ-עקרונות מוועדות לגמרי | רוב עקרוני-הוועדה הם שכתוב-persuasive של עליון | **קיצוני** — נוגד 07-learning §61 (ועדות ברות-ציטוט במכוון); נוגע INV-LRN | ✗ לא (אלא בהכרעת-יו"ר מפורשת) | + +### ההמלצה המזוקקת +1. **לא לגעת בחילוץ-מוועדות** — הנתון מאשר שהנדיבות מוצדקת; `application`=יישום-בר-ציטוט, לא זבל. עקבי עם + הכרעת-הסינתזה "לא לחתוך". +2. **רמה B עושה את העבודה** — `importance` boost ב-RRF מטביע את עקרוני-הוועדה ה-persuasive מתחת + לפסיקה-המחייבת, בלי למחוק דבר. 8508 (`importance≈0`) שוקע ממילא. +3. **להעביר את ה-`nli` לראש תור-המחקר** — לפני שמשתמשים בו כמסנן-רעש ברמה A, לאמת אם 40% אמיתי. + +--- + +> **מקור-הנתונים:** `GET /api/halachot?limit=100000` (5,489 שורות) + per-case `?case_law_id=…`. +> ניתן לשחזר את כל המספרים מהשאילתות האלו. הקאנון הוא live — שיעורים ינועו ככל שהדריינר/פאנל רצים. diff --git a/docs/superpowers/plans/2026-05-16-hooks-execution.md b/docs/superpowers/plans/2026-05-16-hooks-execution.md new file mode 100644 index 0000000..7e8bad4 --- /dev/null +++ b/docs/superpowers/plans/2026-05-16-hooks-execution.md @@ -0,0 +1,348 @@ +# Hooks: Case Status Webhooks Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** כאשר status של תיק משתנה ב-legal-ai (e.g. `qa_failed`, `exported`), ה-plugin מקבל webhook, מעדכן את ה-issue ב-Paperclip, ומעיר את ה-CEO במקרה הצורך. + +**Architecture:** Legal-ai REST API קורא `pc_request("POST", "/api/plugins/marcusgroup.legal-ai/webhooks/case-status", ...)` אחרי כל שינוי status. ה-plugin מטפל ב-`onWebhook()` ומגיב: מוסיף תגובה לissue, מעיר CEO אם QA נכשל. + +**Tech Stack:** TypeScript (plugin-legal-ai), Python/FastAPI (legal-ai web), `@paperclipai/plugin-sdk@2026.325.0`, `httpx` (Python), `pc_request` helper. + +--- + +## File Map + +| Action | File | +|--------|------| +| Modify | `plugin-legal-ai/src/worker.ts` — add `onWebhook()` to `definePlugin({})` | +| Modify | `plugin-legal-ai/plugin.json` — add `"webhooks.receive"` capability | +| Modify | `plugin-legal-ai/src/manifest.ts` — add webhook capability | +| Modify | `legal-ai/web/app.py` — emit webhook after `PUT /api/cases/{case_number}` | +| Modify | `legal-ai/web/paperclip_api.py` — add `emit_webhook()` helper | + +--- + +## Task 1: Add `emit_webhook` helper ב-Python + +**Files:** +- Modify: `legal-ai/web/paperclip_api.py` + +- [ ] **Step 1: קרא את הקובץ הקיים** + +```bash +head -90 /home/chaim/legal-ai/web/paperclip_api.py +``` + +- [ ] **Step 2: הוסף את ה-helper בסוף הקובץ** + +פתח `/home/chaim/legal-ai/web/paperclip_api.py` והוסף אחרי הפונקציה `pc_request`: + +```python +async def emit_case_status_webhook( + case_number: str, + old_status: str, + new_status: str, + company_id: str | None = None, + run_id: str | None = None, +) -> None: + """Notify the Paperclip plugin that a case status changed. + + Fire-and-forget: logs errors but never raises, so callers aren't blocked. + """ + try: + await pc_request( + "POST", + "/api/plugins/marcusgroup.legal-ai/webhooks/case-status", + json={ + "caseNumber": case_number, + "oldStatus": old_status, + "newStatus": new_status, + "companyId": company_id, + "timestamp": datetime.utcnow().isoformat() + "Z", + }, + run_id=run_id, + timeout=5.0, + ) + except Exception as exc: + logger.warning("emit_case_status_webhook failed: %s", exc) +``` + +> **הערה:** `datetime` ו-`logger` כבר מיובאים ב-`app.py`. בדוק שהם מיובאים גם ב-`paperclip_api.py` — אם לא, הוסף `from datetime import datetime` ו-`import logging; logger = logging.getLogger(__name__)` בראש הקובץ. + +- [ ] **Step 3: Commit** + +```bash +cd /home/chaim/legal-ai +git add web/paperclip_api.py +git commit -m "feat: add emit_case_status_webhook helper" +``` + +--- + +## Task 2: צרף webhook לendpoint `PUT /api/cases/{case_number}` + +**Files:** +- Modify: `legal-ai/web/app.py` + +- [ ] **Step 1: מצא את ה-endpoint** + +```bash +grep -n "PUT\|case_number\|update_case" /home/chaim/legal-ai/web/app.py | head -30 +``` + +- [ ] **Step 2: קרא את הenable endpoint המלא** + +זהה את הסקציה המלאה של ה-endpoint ואת הייבוא הקיים. + +- [ ] **Step 3: הוסף import ל-emit_webhook** + +בראש `app.py`, בסקציית ה-imports מ-`paperclip_api`: +```python +from .paperclip_api import pc_request, emit_case_status_webhook +``` + +- [ ] **Step 4: הוסף webhook emit בתוך הendpoint** + +אחרי שהקוד מעדכן את ה-case (לפני ה-`return`), הוסף: + +```python +# Notify plugin about status change (fire-and-forget) +if updates.get("status") and old_status != updates["status"]: + background_tasks.add_task( + emit_case_status_webhook, + case_number=case_number, + old_status=old_status, + new_status=updates["status"], + company_id=str(case.get("company_id")), + ) +``` + +> אם ה-endpoint כבר מקבל `background_tasks: BackgroundTasks` — השתמש בו. אם לא, הוסף `background_tasks: BackgroundTasks` לחתימת הפונקציה. הוסף `from fastapi import BackgroundTasks` ל-imports. + +- [ ] **Step 5: שמור את ה-`old_status` לפני ה-update** + +בתחילת ה-endpoint handler, לפני קריאת ה-DB update: +```python +old_status = (await db.get_case(case_number) or {}).get("status", "") +``` + +- [ ] **Step 6: בדיקה בסיסית — שלח PUT ידני** + +```bash +curl -s -X PUT https://legal-ai.nautilus.marcusgroup.org/api/cases/1130-25 \ + -H "Content-Type: application/json" \ + -d '{"status": "in_progress"}' | jq .status +``` + +בדוק ב-Paperclip logs שהwebhook נשלח (עדיין לא מטופל בצד ה-plugin): +```bash +pm2 logs paperclip --lines 20 +``` + +- [ ] **Step 7: Commit** + +```bash +cd /home/chaim/legal-ai +git add web/app.py +git commit -m "feat: emit case-status webhook on PUT /api/cases/:case" +``` + +--- + +## Task 3: הוסף `onWebhook()` ל-plugin + +**Files:** +- Modify: `plugin-legal-ai/src/worker.ts` + +- [ ] **Step 1: קרא את `definePlugin({})` הקיים** + +```bash +grep -n "definePlugin\|onWebhook\|onHealth\|onShutdown" /home/chaim/plugin-legal-ai/src/worker.ts +``` + +- [ ] **Step 2: הוסף את `onWebhook` handler** + +בתוך הobject שמועבר ל-`definePlugin({})`, אחרי `setup(ctx)`: + +```typescript +onWebhook: async (input) => { + const { endpointKey, payload, companyId } = input as { + endpointKey: string; + payload: { + caseNumber: string; + oldStatus: string; + newStatus: string; + companyId: string; + timestamp: string; + }; + companyId: string; + }; + + if (endpointKey !== "case-status") return; + + const { caseNumber, oldStatus, newStatus } = payload; + ctx.logger.info(`Webhook: ${caseNumber} ${oldStatus} → ${newStatus}`); + + // Find the Paperclip issue linked to this case + const stateKey = `case:${caseNumber}`; + const issueId = await ctx.state.get({ companyId }, stateKey); + if (!issueId) { + ctx.logger.warn(`No issue found for case ${caseNumber}`); + return; + } + + const statusLabels: Record = { + in_progress: "🔄 בעבודה", + drafted: "✍️ טיוטה מוכנה", + qa_failed: "❌ QA נכשל", + exported: "📄 יוצא ל-DOCX", + reviewed: "✅ נבדק", + final: "🎯 סופי", + }; + + const label = statusLabels[newStatus] ?? newStatus; + + // Post a status comment on the issue + await ctx.issues.createComment({ + issueId: issueId as string, + body: `**עדכון סטטוס:** ${label} (היה: ${oldStatus})`, + }); + + // Wake CEO if QA failed + if (newStatus === "qa_failed") { + const companies = await ctx.companies.list(); + const company = companies.find((c) => c.id === companyId); + if (!company) return; + + const CEO_IDS: Record = { + "42a7acd0-30c5-4cbd-ac97-7424f65df294": "752cebdd-6748-4a04-aacd-c7ab0294ef33", + "8639e837-4c9d-47fa-a76b-95788d651896": "cdbfa8bc-3d61-41a4-a2e7-677ec7d34562", + }; + const ceoId = CEO_IDS[companyId]; + if (ceoId) { + await ctx.agents.invoke(ceoId, companyId, { + prompt: `תיק ${caseNumber} נכשל בבדיקת QA. עיין בתוצאות QA ותקן את הבעיות.`, + reason: "qa_failed webhook", + }); + } + } +}, +``` + +> **הערה:** `ctx` חייב להיות נגיש ב-`onWebhook`. אם `ctx` מוגדר בתוך `setup()` בלבד — הוצא אותו ל-closure חיצוני של ה-plugin object (ראה את הדפוס הקיים ב-`worker.ts`). + +- [ ] **Step 3: בדק TypeScript** + +```bash +cd /home/chaim/plugin-legal-ai && npx tsc --noEmit +``` + +Expected: 0 errors. + +- [ ] **Step 4: Build** + +```bash +cd /home/chaim/plugin-legal-ai && npm run build +``` + +Expected: `dist/worker.js` נוצר ללא שגיאות. + +- [ ] **Step 5: Commit** + +```bash +cd /home/chaim/plugin-legal-ai +git add src/worker.ts +git commit -m "feat: add onWebhook handler for case-status events" +``` + +--- + +## Task 4: הוסף capability ל-`plugin.json` ול-`manifest.ts` + +**Files:** +- Modify: `plugin-legal-ai/plugin.json` +- Modify: `plugin-legal-ai/src/manifest.ts` + +- [ ] **Step 1: הוסף `"webhooks.receive"` ל-capabilities** + +ב-`plugin-legal-ai/plugin.json`, בarray `"capabilities"`, הוסף: +```json +"webhooks.receive" +``` + +- [ ] **Step 2: הוסף גם ב-`manifest.ts`** + +```bash +grep -n "capabilities\|webhooks" /home/chaim/plugin-legal-ai/src/manifest.ts +``` + +הוסף `"webhooks.receive"` לarray שם. + +- [ ] **Step 3: Re-install plugin** + +```bash +cd /home/chaim/plugin-legal-ai && npm run build +npx paperclipai plugin uninstall marcusgroup.legal-ai \ + --api-base http://localhost:3100 --api-key pcapi_legal_install_key_2026 +npx paperclipai plugin install /home/chaim/plugin-legal-ai \ + --api-base http://localhost:3100 --api-key pcapi_legal_install_key_2026 +pm2 restart paperclip +``` + +- [ ] **Step 4: Commit** + +```bash +cd /home/chaim/plugin-legal-ai +git add plugin.json src/manifest.ts +git commit -m "feat: add webhooks.receive capability to plugin manifest" +``` + +--- + +## Task 5: בדיקה end-to-end + +- [ ] **Step 1: Deploy legal-ai** + +```bash +cd /home/chaim/legal-ai +git push origin main +# המתן לבנייה (~2-4 דקות) +``` + +- [ ] **Step 2: שנה סטטוס תיק** + +```bash +curl -s -X PUT https://legal-ai.nautilus.marcusgroup.org/api/cases/1130-25 \ + -H "Content-Type: application/json" \ + -d '{"status": "qa_failed"}' | jq .status +``` + +- [ ] **Step 3: בדוק שהתגובה הוספה ל-Paperclip issue** + +```bash +# מצא את ה-issue הקשור לתיק 1130-25 +pm2 logs paperclip --lines 30 | grep "1130-25\|webhook\|qa_failed" +``` + +Expected: תגובה "❌ QA נכשל" הוספה לissue. CEO הועיר. + +- [ ] **Step 4: בדוק שינוי סטטוס שגרתי (לא QA)** + +```bash +curl -s -X PUT https://legal-ai.nautilus.marcusgroup.org/api/cases/1130-25 \ + -H "Content-Type: application/json" \ + -d '{"status": "drafted"}' | jq .status +``` + +Expected: תגובה "✍️ טיוטה מוכנה" בissue. CEO **לא** הועיר. + +--- + +## אימות סופי + +| בדיקה | פקודה | תוצאה מצופה | +|-------|-------|-------------| +| QA נכשל → CEO מועיר | `PUT status=qa_failed` | תגובה + agent invocation | +| exported → תגובה בלבד | `PUT status=exported` | תגובה בלבד | +| שינוי ללא status | `PUT title=...` | שום webhook | +| תיק ללא issue | webhook לתיק חדש | לוג warning, ללא crash | diff --git a/docs/superpowers/plans/2026-05-16-per-agent-claude-md.md b/docs/superpowers/plans/2026-05-16-per-agent-claude-md.md new file mode 100644 index 0000000..f012aaa --- /dev/null +++ b/docs/superpowers/plans/2026-05-16-per-agent-claude-md.md @@ -0,0 +1,306 @@ +# Per-Agent CLAUDE.md Versioning & Validation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** הוסף validation ו-version tracking לקבצי הוראות הסוכנים. כעת, לפני שה-sync script מחיל שינויים, הוא מאמת שכל `instructionsFilePath` קיים. בנוסף, metadata של הסוכן ב-DB יכיל `claude_md_mtime` — השינוי האחרון בקובץ — כדי לזהות drift. + +**Architecture:** `sync_agents_across_companies.py` מקבל `--check-instructions` flag שסורק את כל הסוכנים ומדווח על קבצים חסרים/ישנים. ב-`--apply`, מתווספת בדיקת pre-flight שמבטלת את הsync אם קובץ חסר. `agents.metadata` מקבל `claude_md_mtime` עם ה-mtime בפועל של הקובץ. + +**Tech Stack:** Python 3.10+, asyncpg, httpx, `os.path.getmtime()`, Paperclip REST API (`PATCH /api/agents/{id}`). + +--- + +## File Map + +| Action | File | +|--------|------| +| Modify | `legal-ai/scripts/sync_agents_across_companies.py` — `--check-instructions` flag, pre-flight, metadata update | + +זה הקובץ היחיד שצריך לגעת בו. כל שאר הלוגיקה קיימת כבר. + +--- + +## Task 1: הוסף `--check-instructions` flag + +**Files:** +- Modify: `legal-ai/scripts/sync_agents_across_companies.py` + +- [ ] **Step 1: קרא את החלק של `argparse` בסקריפט** + +```bash +grep -n "argparse\|add_argument\|--verify\|--dry-run\|--apply" \ + /home/chaim/legal-ai/scripts/sync_agents_across_companies.py | head -20 +``` + +- [ ] **Step 2: הוסף את הargument** + +מצא את הסקציה שמגדירה `args` והוסף: + +```python +parser.add_argument( + "--check-instructions", + action="store_true", + help="Scan all agents' instructionsFilePath and report missing/outdated files", +) +``` + +- [ ] **Step 3: הוסף את הפונקציה `check_instructions()`** + +הוסף לפני `async def main()`: + +```python +async def check_instructions(agents: list[dict]) -> bool: + """Print a report of all agents' instruction files. Returns True if all OK.""" + all_ok = True + print(f"\n{'Agent':<30} {'File':<60} {'Status':<15} {'Size':>8} {'Modified'}") + print("-" * 120) + + for agent in agents: + adapter_cfg = agent.get("adapter_config") or {} + if isinstance(adapter_cfg, str): + import json as _json + adapter_cfg = _json.loads(adapter_cfg) + + file_path = adapter_cfg.get("instructionsFilePath", "") + name = agent.get("name", agent.get("id", "?"))[:29] + + if not file_path: + print(f"{name:<30} {'(none)':<60} {'⚠️ NOT SET':<15}") + continue + + if not os.path.exists(file_path): + print(f"{name:<30} {file_path[-59:]:<60} {'❌ MISSING':<15}") + all_ok = False + continue + + stat = os.stat(file_path) + size_kb = stat.st_size // 1024 + mtime = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M") + + # Compare with DB metadata + metadata = agent.get("metadata") or {} + if isinstance(metadata, str): + import json as _json + metadata = _json.loads(metadata) + + db_mtime = metadata.get("claude_md_mtime", "") + actual_mtime = str(int(stat.st_mtime)) + drift = " ⚠️ DRIFT" if db_mtime and db_mtime != actual_mtime else "" + + print(f"{name:<30} {file_path[-59:]:<60} {'✅ OK':<15} {size_kb:>6}KB {mtime}{drift}") + + print() + return all_ok +``` + +> `from datetime import datetime` ו-`import os` — בדוק שמיובאים בראש הסקריפט. אם לא, הוסף. + +- [ ] **Step 4: הוסף קריאה ל-`check_instructions()` ב-`main()`** + +בתוך `async def main()`, אחרי שloading הagents מה-DB: + +```python +if args.check_instructions: + all_ok = await check_instructions(master_agents + mirror_agents) + sys.exit(0 if all_ok else 1) +``` + +- [ ] **Step 5: בדיקה** + +```bash +cd /home/chaim/legal-ai +python scripts/sync_agents_across_companies.py --check-instructions +``` + +Expected: טבלה עם כל הסוכנים, paths, סטטוס ✅/❌. + +- [ ] **Step 6: Commit** + +```bash +cd /home/chaim/legal-ai +git add scripts/sync_agents_across_companies.py +git commit -m "feat: add --check-instructions flag to sync script" +``` + +--- + +## Task 2: הוסף pre-flight validation לפני `--apply` + +**Files:** +- Modify: `legal-ai/scripts/sync_agents_across_companies.py` + +- [ ] **Step 1: מצא את נקודת הכניסה של `--apply`** + +```bash +grep -n "args.apply\|if.*apply\|apply.*mode" \ + /home/chaim/legal-ai/scripts/sync_agents_across_companies.py | head -10 +``` + +- [ ] **Step 2: הוסף pre-flight לפני apply** + +בתחילת בלוק `--apply`, לפני כל שינוי: + +```python +if args.apply: + # Pre-flight: abort if any agent is missing its instructions file + print("🔍 Pre-flight: checking instruction files...") + all_ok = await check_instructions(master_agents + mirror_agents) + if not all_ok: + print("❌ Abort: one or more instruction files are missing. Fix paths before --apply.") + sys.exit(1) + print("✅ Pre-flight passed.\n") + # ... rest of apply logic ... +``` + +- [ ] **Step 3: בדיקה — הפעל עם קובץ חסר (סימולציה)** + +```bash +# שנה זמנית path לקובץ שלא קיים +cd /home/chaim/legal-ai +python scripts/sync_agents_across_companies.py --dry-run 2>&1 | head -5 +``` + +Expected: dry-run עובר. אם תנסה `--apply` עם agent שhis file חסר — הsync יבוטל. + +- [ ] **Step 4: Commit** + +```bash +cd /home/chaim/legal-ai +git add scripts/sync_agents_across_companies.py +git commit -m "feat: add pre-flight instruction file validation before --apply" +``` + +--- + +## Task 3: עדכן `agents.metadata` עם `claude_md_mtime` + +**Files:** +- Modify: `legal-ai/scripts/sync_agents_across_companies.py` + +- [ ] **Step 1: מצא את `compute_diff()` או `apply_diff()`** + +```bash +grep -n "def compute_diff\|def apply_diff\|def build_patch\|metadata" \ + /home/chaim/legal-ai/scripts/sync_agents_across_companies.py | head -20 +``` + +- [ ] **Step 2: הוסף פונקציה `get_claude_md_mtime()`** + +```python +def get_claude_md_mtime(adapter_config: dict) -> str | None: + """Return the Unix mtime of the agent's instructionsFilePath, or None if missing.""" + path = adapter_config.get("instructionsFilePath", "") + if not path or not os.path.exists(path): + return None + return str(int(os.path.getmtime(path))) +``` + +- [ ] **Step 3: שלב mtime בbuild של metadata patch** + +מצא את המקום שמכין את ה-`metadata` לsync. הוסף: + +```python +# Build metadata patch with claude_md_mtime +current_metadata = master_agent.get("metadata") or {} +if isinstance(current_metadata, str): + import json as _json + current_metadata = _json.loads(current_metadata) + +adapter_cfg = master_agent.get("adapter_config") or {} +if isinstance(adapter_cfg, str): + import json as _json + adapter_cfg = _json.loads(adapter_cfg) + +mtime = get_claude_md_mtime(adapter_cfg) +if mtime: + current_metadata["claude_md_mtime"] = mtime + current_metadata["claude_md_last_synced"] = datetime.utcnow().isoformat() + "Z" +``` + +כלול את ה-`current_metadata` המעודכן ב-PATCH לAPI. + +- [ ] **Step 4: בדוק שה-metadata מתעדכן** + +הפעל `--dry-run` וחפש `claude_md_mtime` בoutput: +```bash +python scripts/sync_agents_across_companies.py --dry-run 2>&1 | grep -i "mtime\|metadata" | head -10 +``` + +לאחר `--apply`, בדוק ב-DB: +```bash +psql -h localhost -p 54329 -U paperclip -c \ + "SELECT name, metadata->>'claude_md_mtime' AS mtime FROM agents WHERE metadata->>'claude_md_mtime' IS NOT NULL LIMIT 5" \ + paperclip +``` + +- [ ] **Step 5: Commit** + +```bash +cd /home/chaim/legal-ai +git add scripts/sync_agents_across_companies.py +git commit -m "feat: track claude_md_mtime in agents.metadata during sync" +``` + +--- + +## Task 4: הוסף `make check-agents` shortcut + +**Files:** +- Modify: `legal-ai/Makefile` (אם קיים) אחרת הוסף alias + +- [ ] **Step 1: בדוק אם Makefile קיים** + +```bash +ls /home/chaim/legal-ai/Makefile 2>/dev/null && echo "EXISTS" || echo "MISSING" +``` + +- [ ] **Step 2א: אם Makefile קיים — הוסף target** + +```makefile +check-agents: + python scripts/sync_agents_across_companies.py --check-instructions + +sync-agents-dry: + python scripts/sync_agents_across_companies.py --dry-run + +sync-agents: + python scripts/sync_agents_across_companies.py --apply +``` + +- [ ] **Step 2ב: אם Makefile לא קיים — הוסף alias ל-`~/.bashrc`** + +```bash +echo "alias check-agents='cd /home/chaim/legal-ai && python scripts/sync_agents_across_companies.py --check-instructions'" >> ~/.bashrc +source ~/.bashrc +``` + +- [ ] **Step 3: בדוק** + +```bash +# אם Makefile: +make -C /home/chaim/legal-ai check-agents +# אם alias: +check-agents +``` + +Expected: טבלת סוכנים מוצגת. + +- [ ] **Step 4: Commit (אם Makefile)** + +```bash +cd /home/chaim/legal-ai +git add Makefile +git commit -m "feat: add check-agents and sync-agents make targets" +``` + +--- + +## אימות סופי + +| בדיקה | פקודה | תוצאה מצופה | +|-------|-------|-------------| +| `--check-instructions` | `python sync_agents... --check-instructions` | טבלה עם ✅ לכל agent | +| Pre-flight בולם apply | מחק קובץ זמנית + `--apply` | Abort עם הודעה ברורה | +| mtime ב-DB | `SELECT metadata->>'claude_md_mtime' FROM agents` | timestamp לכל agent | +| DRIFT זוהה | שנה קובץ + `--check-instructions` | ⚠️ DRIFT מוצג | +| shortcut | `check-agents` או `make check-agents` | עובד | diff --git a/docs/superpowers/plans/2026-05-16-scheduled-agents.md b/docs/superpowers/plans/2026-05-16-scheduled-agents.md new file mode 100644 index 0000000..2a09ada --- /dev/null +++ b/docs/superpowers/plans/2026-05-16-scheduled-agents.md @@ -0,0 +1,412 @@ +# Scheduled Background Agents Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** הוסף 2 cron jobs לplugin: (1) תזכורת על תיקים תקועים 3+ ימים, (2) ניתוח פידבק יו"ר שבועי עם עדכון `decision-lessons.md` אוטומטי. + +**Architecture:** שני jobs חדשים נרשמים ב-`ctx.jobs.register()`. Job 1 קורא `/api/cases/stale?days=3` (endpoint חדש ב-legal-ai) ומוסיף תגובה לissues תקועים. Job 2 קורא `/api/chair-feedback/weekly-summary` (endpoint חדש), שולח לCEO agent שמעדכן את `decision-lessons.md`. + +**Tech Stack:** TypeScript (plugin-legal-ai jobs), Python/FastAPI (legal-ai web), asyncpg, Paperclip SDK `ctx.jobs.register()`, `ctx.agents.invoke()`. + +--- + +## File Map + +| Action | File | +|--------|------| +| Modify | `plugin-legal-ai/src/worker.ts` — register 2 new jobs | +| Modify | `plugin-legal-ai/plugin.json` — declare 2 new job entries | +| Modify | `plugin-legal-ai/src/manifest.ts` — add to jobs array | +| Modify | `legal-ai/web/app.py` — add `GET /api/cases/stale` + `GET /api/chair-feedback/weekly-summary` | +| Modify | `legal-ai/web/database.py` (or equivalent DB module) — add `get_stale_cases()` + `get_weekly_chair_feedback()` | + +--- + +## Task 1: הוסף endpoint `GET /api/cases/stale` + +**Files:** +- Modify: `legal-ai/web/app.py` +- Modify: `legal-ai/web/database.py` (שם DB queries מנוהלים — בדוק עם `grep -n "async def get_cases\|from.*database\|import.*db" /home/chaim/legal-ai/web/app.py | head -10`) + +- [ ] **Step 1: מצא את module ה-DB** + +```bash +grep -n "^from\|^import\|db\." /home/chaim/legal-ai/web/app.py | head -20 +``` + +זהה את שם הmodule שמכיל את DB queries (בד"כ `database.py` או `db.py`). + +- [ ] **Step 2: הוסף `get_stale_cases()` לmodule ה-DB** + +```python +async def get_stale_cases(days: int = 3) -> list[dict]: + """Return cases whose status is not 'final' and haven't been updated in `days` days.""" + async with get_db_connection() as conn: + rows = await conn.fetch( + """ + SELECT case_number, title, status, company_id, + updated_at, + now() - updated_at AS age + FROM cases + WHERE status NOT IN ('final', 'new') + AND updated_at < now() - ($1 || ' days')::interval + ORDER BY updated_at ASC + """, + str(days), + ) + return [dict(r) for r in rows] +``` + +> `get_db_connection()` — השתמש בדפוס הקיים בקובץ. אם זה `asyncpg.connect()` ישיר, `asyncpg.create_pool()`, או context manager — העתק את הדפוס. + +- [ ] **Step 3: הוסף endpoint ב-`app.py`** + +```python +@app.get("/api/cases/stale") +async def list_stale_cases(days: int = 3): + """Cases stuck in non-final status for more than `days` days.""" + cases = await db.get_stale_cases(days=days) + return { + "cases": [ + { + "case_number": c["case_number"], + "title": c["title"], + "status": c["status"], + "company_id": str(c["company_id"]), + "days_stale": c["age"].days, + } + for c in cases + ], + "total": len(cases), + } +``` + +- [ ] **Step 4: בדיקה** + +```bash +curl -s "https://legal-ai.nautilus.marcusgroup.org/api/cases/stale?days=1" | jq .total +``` + +Expected: JSON עם רשימת תיקים. + +- [ ] **Step 5: Commit** + +```bash +cd /home/chaim/legal-ai +git add web/app.py web/database.py # או השם הנכון +git commit -m "feat: add GET /api/cases/stale endpoint" +``` + +--- + +## Task 2: הוסף endpoint `GET /api/chair-feedback/weekly-summary` + +**Files:** +- Modify: `legal-ai/web/app.py` +- Modify: DB module + +- [ ] **Step 1: בדוק את מבנה טבלת `chair_feedback`** + +```bash +sqlite3 /home/chaim/.paperclip/instances/default/data/app.db \ + ".schema chair_feedback" 2>/dev/null || \ +psql -h localhost -p 5433 -U legal_ai -c "\d chair_feedback" legal_ai 2>/dev/null || \ +grep -rn "chair_feedback" /home/chaim/legal-ai/mcp-server/src/ | head -10 +``` + +- [ ] **Step 2: הוסף `get_weekly_chair_feedback()` לDB module** + +```python +async def get_weekly_chair_feedback(days: int = 7) -> list[dict]: + """Return chair feedback entries from the last `days` days.""" + async with get_db_connection() as conn: + rows = await conn.fetch( + """ + SELECT cf.case_number, cf.feedback_text, cf.created_at, + cf.feedback_type, c.title + FROM chair_feedback cf + JOIN cases c ON c.case_number = cf.case_number + WHERE cf.created_at > now() - ($1 || ' days')::interval + ORDER BY cf.created_at DESC + """, + str(days), + ) + return [dict(r) for r in rows] +``` + +> אם שמות השדות שונים (בדוק ב-Step 1) — התאם. + +- [ ] **Step 3: הוסף endpoint** + +```python +@app.get("/api/chair-feedback/weekly-summary") +async def get_chair_feedback_weekly(days: int = 7): + """Feedback entries from the past week, formatted for the learning agent.""" + entries = await db.get_weekly_chair_feedback(days=days) + if not entries: + return {"summary": "", "entry_count": 0} + + lines = [ + f"- תיק {e['case_number']} ({e['title']}): {e['feedback_text']}" + for e in entries + ] + summary = "\n".join(lines) + return {"summary": summary, "entry_count": len(entries), "entries": entries} +``` + +- [ ] **Step 4: בדיקה** + +```bash +curl -s "https://legal-ai.nautilus.marcusgroup.org/api/chair-feedback/weekly-summary" | jq .entry_count +``` + +- [ ] **Step 5: Commit** + +```bash +cd /home/chaim/legal-ai +git add web/app.py web/database.py +git commit -m "feat: add GET /api/chair-feedback/weekly-summary endpoint" +``` + +--- + +## Task 3: הוסף jobs לplugin + +**Files:** +- Modify: `plugin-legal-ai/plugin.json` +- Modify: `plugin-legal-ai/src/manifest.ts` + +- [ ] **Step 1: קרא את ה-`jobs` הקיים ב-`plugin.json`** + +```bash +cat /home/chaim/plugin-legal-ai/plugin.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps(d['jobs'], indent=2))" +``` + +- [ ] **Step 2: הוסף 2 jobs חדשים לarray `"jobs"` ב-`plugin.json`** + +```json +{ + "jobKey": "stale-case-reminder", + "displayName": "תזכורת תיקים תקועים", + "description": "מזהה תיקים שלא עודכנו 3+ ימים ומוסיף תגובה לissue", + "schedule": "0 8 * * *" +}, +{ + "jobKey": "weekly-feedback-analysis", + "displayName": "ניתוח פידבק שבועי", + "description": "מסכם פידבק יו\"ר מהשבוע האחרון ומעדכן את decision-lessons.md", + "schedule": "0 19 * * 0" +} +``` + +> `"0 8 * * *"` = כל יום בשעה 08:00. `"0 19 * * 0"` = כל ראשון ב-19:00. + +- [ ] **Step 3: עדכן `manifest.ts`** + +```bash +grep -n "jobs\|jobKey\|schedule" /home/chaim/plugin-legal-ai/src/manifest.ts +``` + +הוסף את אותם 2 objects לarray `jobs` ב-`manifest.ts`. + +- [ ] **Step 4: Commit** + +```bash +cd /home/chaim/plugin-legal-ai +git add plugin.json src/manifest.ts +git commit -m "feat: declare stale-case-reminder and weekly-feedback-analysis jobs" +``` + +--- + +## Task 4: Implement job handlers ב-`worker.ts` + +**Files:** +- Modify: `plugin-legal-ai/src/worker.ts` + +- [ ] **Step 1: קרא את handler של `sync-case-status` הקיים** + +```bash +grep -n "sync-case-status\|jobs.register\|jobKey" /home/chaim/plugin-legal-ai/src/worker.ts +``` + +העתק את הדפוס. + +- [ ] **Step 2: הוסף את `stale-case-reminder` handler** + +בתוך `setup(ctx)`, אחרי רישום ה-job הקיים: + +```typescript +ctx.jobs.register("stale-case-reminder", async (job) => { + ctx.logger.info("stale-case-reminder: starting"); + + const config = await ctx.config.get(); + const apiBase = (config.legalApiBaseUrl as string) ?? "http://localhost:8085"; + + const resp = await ctx.http.fetch(`${apiBase}/api/cases/stale?days=3`); + if (!resp.ok) { + ctx.logger.error(`stale-case-reminder: API error ${resp.status}`); + return; + } + + const data = (await resp.json()) as { + cases: Array<{ + case_number: string; + title: string; + status: string; + company_id: string; + days_stale: number; + }>; + }; + + for (const staleCase of data.cases) { + const issueId = await ctx.state.get( + { companyId: staleCase.company_id }, + `case:${staleCase.case_number}` + ); + if (!issueId) continue; + + await ctx.issues.createComment({ + issueId: issueId as string, + body: `⚠️ **תיק תקוע** — ${staleCase.days_stale} ימים ללא עדכון (סטטוס: ${staleCase.status}). האם יש צורך בפעולה?`, + }); + + ctx.logger.info( + `stale-case-reminder: posted reminder for ${staleCase.case_number} (${staleCase.days_stale}d stale)` + ); + } + + ctx.logger.info(`stale-case-reminder: done. ${data.cases.length} cases reminded`); +}); +``` + +- [ ] **Step 3: הוסף את `weekly-feedback-analysis` handler** + +```typescript +ctx.jobs.register("weekly-feedback-analysis", async (job) => { + ctx.logger.info("weekly-feedback-analysis: starting"); + + const config = await ctx.config.get(); + const apiBase = (config.legalApiBaseUrl as string) ?? "http://localhost:8085"; + + const resp = await ctx.http.fetch(`${apiBase}/api/chair-feedback/weekly-summary`); + if (!resp.ok) { + ctx.logger.error(`weekly-feedback-analysis: API error ${resp.status}`); + return; + } + + const data = (await resp.json()) as { + summary: string; + entry_count: number; + }; + + if (data.entry_count === 0) { + ctx.logger.info("weekly-feedback-analysis: no feedback this week, skipping"); + return; + } + + // Invoke the CEO agent to process feedback and update decision-lessons.md + const companies = await ctx.companies.list(); + for (const company of companies) { + // CEO IDs per company + const CEO_IDS: Record = { + "42a7acd0-30c5-4cbd-ac97-7424f65df294": "752cebdd-6748-4a04-aacd-c7ab0294ef33", + "8639e837-4c9d-47fa-a76b-95788d651896": "cdbfa8bc-3d61-41a4-a2e7-677ec7d34562", + }; + const ceoId = CEO_IDS[company.id]; + if (!ceoId) continue; + + await ctx.agents.invoke(ceoId, company.id, { + prompt: `ניתוח פידבק שבועי יו"ר (${data.entry_count} פריטים): + +${data.summary} + +המשימה: עדכן את /home/chaim/legal-ai/docs/legal-decision-lessons.md עם הלקחים החדשים שעולים מהפידבק. הוסף רק לקחים חדשים שלא קיימים כבר. קבץ לפי נושא.`, + reason: "weekly-feedback-analysis scheduled job", + }); + + ctx.logger.info( + `weekly-feedback-analysis: invoked CEO for company ${company.id} (${data.entry_count} feedback entries)` + ); + break; // One CEO is enough — lessons file is shared + } +}); +``` + +- [ ] **Step 4: TypeScript check** + +```bash +cd /home/chaim/plugin-legal-ai && npx tsc --noEmit +``` + +Expected: 0 errors. + +- [ ] **Step 5: Build** + +```bash +cd /home/chaim/plugin-legal-ai && npm run build +``` + +- [ ] **Step 6: Commit** + +```bash +cd /home/chaim/plugin-legal-ai +git add src/worker.ts +git commit -m "feat: implement stale-case-reminder and weekly-feedback-analysis jobs" +``` + +--- + +## Task 5: Re-install plugin + בדיקה + +- [ ] **Step 1: Deploy legal-ai** + +```bash +cd /home/chaim/legal-ai && git push origin main +# המתן ~3 דקות +curl -s https://legal-ai.nautilus.marcusgroup.org/api/health | jq .status +``` + +- [ ] **Step 2: Re-install plugin** + +```bash +cd /home/chaim/plugin-legal-ai && npm run build +npx paperclipai plugin uninstall marcusgroup.legal-ai \ + --api-base http://localhost:3100 --api-key pcapi_legal_install_key_2026 +npx paperclipai plugin install /home/chaim/plugin-legal-ai \ + --api-base http://localhost:3100 --api-key pcapi_legal_install_key_2026 +pm2 restart paperclip +``` + +- [ ] **Step 3: בדוק שה-jobs רשומים** + +```bash +curl -s -H "Authorization: Bearer pcapi_legal_install_key_2026" \ + http://localhost:3100/api/plugins/marcusgroup.legal-ai/jobs | jq .[].jobKey +``` + +Expected: `"sync-case-status"`, `"stale-case-reminder"`, `"weekly-feedback-analysis"`. + +- [ ] **Step 4: הפעל job ידנית לבדיקה** + +```bash +curl -s -X POST -H "Authorization: Bearer pcapi_legal_install_key_2026" \ + http://localhost:3100/api/plugins/marcusgroup.legal-ai/jobs/stale-case-reminder/run | jq . +``` + +Expected: Job הופעל. בדוק logs: +```bash +pm2 logs paperclip --lines 30 | grep "stale-case-reminder" +``` + +--- + +## אימות סופי + +| בדיקה | פקודה | תוצאה מצופה | +|-------|-------|-------------| +| API stale endpoint | `curl .../api/cases/stale?days=1` | JSON עם cases | +| API feedback endpoint | `curl .../api/chair-feedback/weekly-summary` | JSON עם summary | +| Jobs רשומים | `GET .../api/plugins/.../jobs` | 3 jobs רשומים | +| Stale reminder ידני | `POST .../jobs/stale-case-reminder/run` | תגובות בissues | +| Feedback analysis ידני | `POST .../jobs/weekly-feedback-analysis/run` | CEO מועיר | diff --git a/mcp-server/tests/test_adapter_profiles.py b/mcp-server/tests/test_adapter_profiles.py new file mode 100644 index 0000000..988459b --- /dev/null +++ b/mcp-server/tests/test_adapter_profiles.py @@ -0,0 +1,21 @@ +"""Adapter-profile compatibility gates for Paperclip migration.""" +from __future__ import annotations + +import importlib.util +from pathlib import Path + +_SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "adapter_profiles.py" +_spec = importlib.util.spec_from_file_location("adapter_profiles", _SCRIPT) +profiles = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(profiles) + + +def test_codex_local_profile_accepts_openai_model_ids(): + assert profiles.model_matches_provider("gpt-5.3-codex", "codex_local") + assert profiles.model_matches_provider("o4-mini", "codex_local") + assert profiles.model_matches_provider("codex-mini-latest", "codex_local") + + +def test_codex_local_profile_rejects_foreign_model_ids(): + assert not profiles.model_matches_provider("claude-opus-4-8", "codex_local") + assert not profiles.model_matches_provider("gemini-3.1-pro-preview", "codex_local") diff --git a/scripts/exec_summary_1043.py b/scripts/exec_summary_1043.py new file mode 100644 index 0000000..cb8a453 --- /dev/null +++ b/scripts/exec_summary_1043.py @@ -0,0 +1,145 @@ +"""Ad-hoc: executive summary (סיכום מנהלים) DOCX for case 1043-02-26. +Reuses the dafna decision template styles (David font + RTL) via the +analysis_docx_exporter helpers. One-off prep document for chaim's meeting +with the chair — NOT a decision draft. +""" +from pathlib import Path +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "mcp-server" / "src")) + +from docx import Document +from legal_mcp.services.analysis_docx_exporter import ( + TEMPLATE_PATH, + _clear_body, + _add_paragraph, + _add_runs_with_inline_bold, + _mark_paragraph_rtl, + _mark_run_rtl, +) + +CASE = "1043-02-26" +OUT = Path(f"/home/chaim/legal-ai/data/cases/{CASE}/exports/סיכום-מנהלים-v1.docx") + + +def H1(doc, t): + _add_paragraph(doc, t, "Heading 1") + + +def H2(doc, t): + _add_paragraph(doc, t, "Heading 2") + + +def P(doc, t): + p = doc.add_paragraph(style="Normal") + _add_runs_with_inline_bold(p, t) + _mark_paragraph_rtl(p) + return p + + +def BULLET(doc, t): + p = doc.add_paragraph(style="List Paragraph") + _add_runs_with_inline_bold(p, t) + _mark_paragraph_rtl(p) + return p + + +def LABEL(doc, label, value): + p = doc.add_paragraph(style="Normal") + r = p.add_run(label + ": ") + r.bold = True + _mark_run_rtl(r) + r2 = p.add_run(value) + _mark_run_rtl(r2) + _mark_paragraph_rtl(p) + return p + + +def main(): + doc = Document(str(TEMPLATE_PATH)) + _clear_body(doc) + + H1(doc, "סיכום מנהלים — הכנה לדיון") + P(doc, "**ערר 1043-02-26 — הקמת מתקן למיון פסולת (אתר \"קומפוסט דלילה\", תכנית מי/1030)**") + P(doc, "מסמך הכנה פנימי לקראת דיון עם יו\"ר הוועדה. אינו החלטה ואינו טיוטת החלטה.") + + H2(doc, "פרטי התיק") + LABEL(doc, "סוג הערר", "רישוי ובנייה (1xxx) — ערר על סירוב בקשה להיתר") + LABEL(doc, "עוררים (מבקשי ההיתר)", "קיבוץ נחשון; חברת אלקטרה אקו גרין פארק") + LABEL(doc, "משיבים", "הוועדה המקומית מטה יהודה; מושבי גפן/תירוש/כפר הריף; קיבוצי כפר מנחם/רבדים; מועצה אזורית יואב (מתנגדים)") + LABEL(doc, "מושא הערר", "החלטת הוועדה המקומית מטה יהודה מיום 9.2.26 לסרב לבקשה (מס' 20240972)") + LABEL(doc, "המקרקעין", "גוש 5093 ח\"ל 4 מגרש 1, אתר \"דלילה\" (~137 דונם), מצפון לכפר מנחם, ליד כביש 383") + LABEL(doc, "תקן ביקורת", "ועדת הערר כמוסד תכנון בעל סמכות מקורית — שיקול דעת תכנוני עצמאי; ביקורת רחבה יותר בחלק המשפטי-פרשני") + + H2(doc, "מהות המחלוקת") + P(doc, "מבקשי ההיתר מבקשים להקים מתקן מיון פסולת/קומפוסטציה הכולל מבנה קומפוסטציה אחוד (~19,118 מ\"ר עיקרי), משטחי הבשלה פתוחים ובריכה תפעולית, בצירוף שתי הקלות: הגבהת גובה מ-8 ל-20 מ', והגדלת תכסית מ-3.5% ל-22%. הוועדה המקומית סירבה, בקובעה כי השינוי מהותי ומקומו בעדכון התכנית ולא בהליך רישוי. השאלה המרכזית: האם הבקשה תואמת את תכנית מי/1030, או חורגת ממנה באופן המחייב תיקון תכנית.") + + H2(doc, "החלטת הוועדה המקומית (מושא הערר)") + P(doc, "**דחתה** את טענת המתנגדים לפקיעת התכנית (בהסתמך על ע\"א 3213/97 נקר) ואת טענת השימושים האסורים.") + P(doc, "**קיבלה** והפכה לבסיס הסירוב: (1) היקף הבינוי חורג משלב א' לפי הוראת השלביות; (2) הבינוי המונוליטי שונה דרמטית מנספח הבינוי; (3) משטחי ההבשלה הפתוחים מנוגדים לסעיף 6.9 (טיפול במבנים סגורים) — שהפרתו מוגדרת בתכנית כסטייה ניכרת.") + + H2(doc, "טענות סף (לדיון תמציתי)") + BULLET(doc, "**פקיעת תכנית** — סעיף הפקיעה (5 שנים) הושמט מהנוסח המאושר (ראו ממצאי ניתוח-העומק).") + BULLET(doc, "**מיצוי התנגדות / \"מועד ב'\"** — האם טענות שהוכרעו בשלב התכנית מועלות מחדש בשלב הרישוי.") + BULLET(doc, "**זכות עמידה** — מועד חתימת חוזה החכירה מול מועד פתיחת הבקשה (הוכרע: קיבוץ נחשון חוכר רשום).") + BULLET(doc, "**פגמי פרסום / היעדר יידוע** (ס' 149(א)(2א)) — היעדר תשריט, אי-יידוע ועדה מקומית יואב.") + BULLET(doc, "**איחור בהגשת ההתנגדויות** — מול אינטרס ההסתמכות (רכישת קרקע ב-35 מיליון ₪).") + + H2(doc, "חמש הסוגיות המהותיות") + + P(doc, "**סוגיה 1 (מכריעה) — שלביות הביצוע: בינוי מול היקף פעילות**") + BULLET(doc, "השאלה: האם הוראת השלביות (ס' 7.1) חלה על הבינוי הפיזי או רק על היקף ההפעלה?") + BULLET(doc, "עמדות: עוררים — נוגעת להיקף הפעילות; ועדה/משיבים — חלה על הבינוי (4 מבנים ≈ 8,000 מ\"ר מול ~19,118 מבוקשים).") + BULLET(doc, "לאן נוטה: **לטובת הוועדה** — לשון \"הקמת 4 מבני קומפוסט\" מתייחסת לבינוי; נספחים 06, 11 מצביעים על מנגנון פיילוט מדורג מהותי.") + BULLET(doc, "תקדים: עע\"מ 10089/07 אירוס הגלבוע (אין לעקוף תכנון נדרש דרך היתר).") + + P(doc, "**סוגיה 2 (מכריעה) — משטחי ההבשלה וסעיף 6.9: \"טיפול\" מול \"אחסון\"**") + BULLET(doc, "השאלה: האם ההבשלה הפתוחה היא \"טיפול\" החוסה תחת 6.9 (→ סטייה ניכרת החוסמת הקלה, ס' 151), או \"אחסון\"?") + BULLET(doc, "עמדות: עוררים (ד\"ר ענבר, נספח 22) — אחסון מוצר סופי נטול ריח; ועדה — חלק בלתי נפרד מהטיפול.") + BULLET(doc, "לאן נוטה: **נחלשה לוועדה** — נספח 07 §122-127: ועדת המשנה לעררים אישרה הבשלה פתוחה כ\"נכון וסביר\" (אך כינתה זאת \"שלב אחרון של טיפול\"). מתח פרשני אמיתי.") + BULLET(doc, "תקדים: עע\"מ 402/03 עמותת העצמאים (ס' 151 — סטייה ניכרת).") + + P(doc, "**סוגיה 3 — ההקלות בגובה ובתכסית + טענת \"טעות סופר\"**") + BULLET(doc, "השאלה: האם ההקלות בגדרי הקלה או סטייה ניכרת? והאם התכסית 3.5% היא \"טעות סופר\"?") + BULLET(doc, "לאן נוטה: טענת טעות הסופר **התחזקה מאוד** — נספח 12 (מופקד) מראה ש-3.5% נגזרה מתפיסה שהחממות \"אינן שטח לבניה\"; נספחים 06 §14 ו-07 §130 הורו לתקן; הסכם רמ\"י נוקב ב-30,280 מ\"ר.") + BULLET(doc, "תקדים: עמ\"נ 25955-11-22 ברק-רחביה (פרשנות הרמונית); בג\"ץ 2667/17 מטה בנימין (\"פרשנות אפשרית\").") + + P(doc, "**סוגיה 4 — תצורת הבינוי מול נספח בינוי מנחה ונספח נופי מחייב**") + BULLET(doc, "השאלה: האם המבנה המונוליטי + הבריכה חורגים ממרחב הגמישות של נספח מנחה, לנוכח הנספח הנופי המחייב?") + BULLET(doc, "תקדים: **ערר 1033-25 אבו גוש** (תקדים דפנה ישיר — נספח בינוי מנחה אינו המלצה בלבד); בג\"ץ 6525/15 עמק שווה.") + + P(doc, "**סוגיה 5 — שיקולים זרים / NIMBY בהחלטת הסירוב**") + BULLET(doc, "השאלה: האם הסירוב נגוע בשיקולים זרים? (עשוי להתייתר אם הבחינה העצמאית מכריעה).") + BULLET(doc, "לאן נוטה: **נתמך בראיות** — תמליל (נספח 19, \"ועדה פוליטית\") + הצוות המקצועי המליץ לאשר (נספח 17). מנגד — בסיס מהותי לא-NIMBY (נספחים 25, 30).") + + H2(doc, "ממצאי ניתוח-העומק — מה התחדש לאחר מיצוי 22 נספחי הרקע") + BULLET(doc, "**6.9 (ליבת התיק) נחלש לוועדה** — ועדת המשנה לעררים כבר אישרה הבשלה פתוחה.") + BULLET(doc, "**טעות הסופר בתכסית התחזקה** — שתי ערכאות הורו לתקן את חישוב השטח.") + BULLET(doc, "**פקיעה — הוכרע עובדתית** — סעיף הפקיעה היה בנוסח המופקד (נספח 12) והושמט מהמאושר; נותרה מחלוקת משפטית בלבד (נקר/לויתן מול חמדת הגליל).") + BULLET(doc, "**אופי המתקן מטה למשיבים** — תת\"ל 220 + ויתור על 8 מבני קומפוסט לטובת מתקן תרמי (נספחים 25, 30) → טיעון \"פריסת סלאמי\"/עקיפת תכנון.") + BULLET(doc, "**תיקון עובדתי** — פסה\"ד שדחה את העתירה (נספח 10) ניתן נגד תכנית דרך הגישה, לא נגד מי/1030.") + BULLET(doc, "**פער תחבורתי** — 80-92 משאיות/יום (מוסדות התכנון) מול 320-400 (ד\"ר לינק) — טעון יישוב.") + + H2(doc, "פסיקה הדורשת אימות חיצוני (אינה בקורפוס הסמכותי)") + BULLET(doc, "ע\"א 3213/97 **נקר** — \"הדין המתפרסם ברבים מחייב\" (עוגן הוועדה לדחיית הפקיעה).") + BULLET(doc, "עע\"מ 4768/22 **חמדת הגליל** — פקיעת תכנית (עוגן המשיבים).") + BULLET(doc, "ע\"א 482/99 בלפוריה; בג\"ץ 5636/13 מתיישבי תימורים; בג\"ץ 9098/01 גניס; דנ\"א 3993/07 איקאפוד.") + + H2(doc, "שאלות פתוחות להכרעת היו\"ר + סדר דיון מומלץ") + BULLET(doc, "(1) סיווג ההבשלה — טיפול (6.9) או אחסון (4.1.1)?") + BULLET(doc, "(2) דין הפקיעה לאור ההשמטה המוכחת מהנוסח המאושר.") + BULLET(doc, "(3) האם התכסית 3.5% היא טעות סופר הניתנת לתיקון פרשני?") + BULLET(doc, "(4) האם תת\"ל 220 + הוויתור על מבני הקומפוסט הופכים את הבקשה ל\"עקיפת תכנון\" (אירוס הגלבוע)?") + BULLET(doc, "(5) הסעד: סירוב מלא / אישור מותנה בהתאמה לשלב א' / החזרה לוועדה המקומית עם הנחיות.") + P(doc, "**סדר דיון מומלץ:** טענות סף (פקיעה → מיצוי/השתק → עמידה/פרסום) ← סוגיה 1 (שלביות) ← סוגיה 2 (6.9) ← סוגיה 3 (הקלות/טעות סופר) ← סוגיה 4 (תצורת בינוי) ← סוגיה 5 (שיקולים זרים).") + + H2(doc, "הערכת תרחישים") + P(doc, "התמונה שקולה. לטובת הוועדה: סוגיה 1 (שלביות) וטיעון \"עקיפת תכנון/סלאמי\" נוכח תת\"ל 220 — חזקים. לטובת העוררים: סוגיה 2 (6.9) נחלשה, טעות הסופר התחזקה, ו-NIMBY נתמך בראיות. **התרחיש הסביר ביותר:** קבלה חלקית / החזרה מותנית או דחייה — תלוי בעיקר בשאלת היקף הבינוי בשלב א' ובשאלת \"עקיפת התכנון\". ההכרעה במובהק של יו\"ר הוועדה.") + + OUT.parent.mkdir(parents=True, exist_ok=True) + doc.save(str(OUT)) + print(f"saved: {OUT}") + + +if __name__ == "__main__": + main() diff --git a/scripts/ingest_incoming_batch.py b/scripts/ingest_incoming_batch.py new file mode 100644 index 0000000..6a0d35d --- /dev/null +++ b/scripts/ingest_incoming_batch.py @@ -0,0 +1,102 @@ +"""Batch ingest of appeals-committee decisions staged in data/precedents/incoming/. + +Sequential (NOT concurrent — avoids the 2026-05-31 load-spike incident) ingest of +each .doc/.docx via the canonical internal pipeline, followed by metadata extraction +per case (the internal path does NOT auto-queue metadata — INV-ING3). Halacha is +auto-queued by ingest; drain it separately via MCP precedent_process_pending. + +case_number canonical follows the filename/Nevo convention validated against the +corpus + missing_precedents list: + - מרכז/חיפה/ת"א committees number with month: NNNN/MM/YY → NNNN-MM-YY + - ירושלים/צפון committees number without month: NNNN/YY → NNNN-YY +decision_date / summary / subject_tags / appeal_subtype are left empty on purpose — +the metadata extractor fills them from the full text (more reliable than parsing here). + +Run: mcp-server/.venv/bin/python scripts/ingest_incoming_batch.py +Config (POSTGRES_URL, VOYAGE_API_KEY, ANTHROPIC_API_KEY) auto-loads from ~/.env. +""" + +import asyncio +import os +import sys +import traceback +from pathlib import Path + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "mcp-server", "src")) + +from legal_mcp.services import internal_decisions as svc +from legal_mcp.services import precedent_metadata_extractor as meta + +INC = "/home/chaim/legal-ai/data/precedents/incoming" + +# file, case_number(canonical-with-slashes), chair, district, court, practice_area +DECISIONS = [ + ("105-07.doc", "105/07", "דרור לביא-אפרת", "צפון", "rishuy_uvniya"), + ("ARAR-17-105-44.doc", "105/17", "רונית אלפר", "מרכז", "betterment_levy"), + ("ARAR-18-1029.doc", "1029/18", "אליעד וינשל", "ירושלים", "rishuy_uvniya"), + ("ARAR-20-1018-44.doc", "1018/20", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"), + ("ARAR-20-1023-55.doc", "1023/20", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"), + ("ARAR-21-1080-55.doc", "1080/21", "נילי בן משה ידגר", "צפון", "rishuy_uvniya"), + ("ARAR-21-11-1051.doc", "1051/11/21", "רונית אלפר", "מרכז", "rishuy_uvniya"), + ("ARAR-22-01-1015.doc", "1015/01/22", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"), + ("ARAR-22-06-1029.doc", "1029/06/22", "מאיה אשכנזי", "מרכז", "rishuy_uvniya"), + ("ARAR-22-08-1044.doc", "1044/08/22", "מאיה אשכנזי", "מרכז", "rishuy_uvniya"), + ("ARAR-22-10-1050.doc", "1050/10/22", "מאיה אשכנזי", "מרכז", "rishuy_uvniya"), + ("ARAR-22-1079.doc", "1079/22", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"), + ("ARAR-23-04-1010.doc", "1010/04/23", "מאיה אשכנזי", "מרכז", "rishuy_uvniya"), + ("ARAR-23-08-1074-9.doc", "1074/08/23", "מיכל הלברשטם דגני", "חיפה", "rishuy_uvniya"), + ("ARAR-23-1034.docx", "1006/23", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"), + ("ARAR-23-1073.doc", "1073/23", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"), + ("ARAR-23-1085.doc", "1085/23", "נילי בן משה ידגר", "צפון", "rishuy_uvniya"), + ("ARAR-24-01-1009-5.docx","1009/01/24", "מיכל דגני הלברשטם", "תל אביב", "rishuy_uvniya"), + ("ARAR-24-05-1044.doc", "1044/05/24", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"), + ("ARAR-25-01-10072.docx", "1007/01/25", "יפעת בן אריה שטיינברג", "תל אביב", "rishuy_uvniya"), +] + + +async def main(): + results = [] + for fname, case_number, chair, district, parea in DECISIONS: + fp = Path(INC) / fname + rec = {"file": fname, "case_number": case_number} + if not fp.exists(): + rec["error"] = "file-missing" + print(f"✗ {fname}: file missing", flush=True) + results.append(rec) + continue + try: + out = await svc.ingest_internal_decision( + file_path=fp, + case_number=case_number, + chair_name=chair, + district=district, + court=f"ועדת הערר לתכנון ובנייה — מחוז {district}", + practice_area=parea, + proceeding_type="ערר", + is_binding=False, + ) + cid = out.get("case_law_id") + rec["case_law_id"] = cid + rec["chunks"] = out.get("chunks") + print(f"✓ ingest {case_number}: id={cid} chunks={out.get('chunks')}", flush=True) + # metadata (internal path does not auto-queue it) + m = await meta.extract_and_apply(cid) + rec["meta_status"] = m.get("status") + sug = m.get("suggested") or {} + rec["suggested_case_number"] = sug.get("case_number_clean") + rec["citation_formatted"] = sug.get("citation_formatted") + rec["meta_date"] = sug.get("decision_date_iso") + print(f" meta {case_number}: {m.get('status')} | clean={sug.get('case_number_clean')} | {sug.get('citation_formatted')}", flush=True) + except Exception as e: + rec["error"] = f"{type(e).__name__}: {e}" + print(f"✗ {fname} ({case_number}): {e}", flush=True) + traceback.print_exc() + results.append(rec) + + print("\n===SUMMARY===", flush=True) + for r in results: + print(r, flush=True) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/ocr_benchmark_mistral.py b/scripts/ocr_benchmark_mistral.py new file mode 100644 index 0000000..4ce89b6 --- /dev/null +++ b/scripts/ocr_benchmark_mistral.py @@ -0,0 +1,392 @@ +#!/usr/bin/env python3 +"""OCR Benchmark: Current system (PyMuPDF + Google Vision) vs Mistral OCR 4.0 + +Usage: + python scripts/ocr_benchmark_mistral.py [--docs N] [--output PATH] + +Downloads PDFs from MinIO, calls Mistral OCR API, compares against +already-extracted text stored in the DB, and writes a Markdown report. +""" + +from __future__ import annotations + +import argparse +import asyncio +import base64 +import json +import re +import subprocess +import sys +import tempfile +import time +from pathlib import Path +from typing import TypedDict + +import httpx + +# ── Config ─────────────────────────────────────────────────────────────────── + +MISTRAL_API_KEY = "UsZjLCX30ev6pox0KgvXyuFP3ktsPYpN" +MISTRAL_OCR_MODEL = "mistral-ocr-latest" +MISTRAL_OCR_URL = "https://api.mistral.ai/v1/ocr" +MINIO_ALIAS = "legalminio" +MINIO_BUCKET = "legal-documents" + +# Documents to benchmark — selected for diversity: +# - main appeal (40 pp, 4.4 MB, large digital doc) +# - permit (9 pp, 1.7 MB, likely partially scanned) +# - protocol (5 pp, 178 KB, administrative typed) +# - response (12 pp, ~390 KB, digital legal brief) +DOCS_TO_BENCHMARK = [ + { + "title": "כתב ערר", + "minio_key": "cases/1027-04-26/documents/originals/כתב ערר - מושב נחם נ׳ ועדה מקומית בית שמש.pdf", + "doc_type": "appeal", + "pages": 40, + }, + { + "title": "נספח 1 — היתר הבנייה", + "minio_key": "cases/1027-04-26/documents/originals/נספח 1 - היתר הבנייה (פורסם 17.03.26).pdf", + "doc_type": "permit", + "pages": 9, + }, + { + "title": "נספח 14 — פרוטוקול ועדה מחוזית", + "minio_key": "cases/1027-04-26/documents/originals/נספח 14 - פרוטוקול דיון ועדה מחוזית - 06.06.23.pdf", + "doc_type": "protocol", + "pages": 5, + }, + { + "title": "תגובת המשיבה 3", + "minio_key": "cases/1027-04-26/documents/originals/תגובת המשיבה 3 לערר ולבקשה להתליית היתר בנייה.pdf", + "doc_type": "response", + "pages": 12, + }, +] + +# ── DB access (read-only: fetch already-extracted text) ────────────────────── + +def _fetch_extracted_texts(case_number: str) -> dict[str, str]: + """Pull extracted_text from DB for all docs in the case.""" + import psycopg2 # type: ignore + conn = psycopg2.connect( + host="localhost", port=5433, dbname="legal_ai", + user="legal_ai", password="od0ASJZFYibOlWK59krLvvETmgqwlXe8", + ) + cur = conn.cursor() + cur.execute( + """ + SELECT d.title, d.extracted_text, d.file_path, d.page_count + FROM documents d + JOIN cases c ON c.id = d.case_id + WHERE c.case_number = %s AND d.extracted_text IS NOT NULL + """, + (case_number,), + ) + rows = cur.fetchall() + cur.close() + conn.close() + return {row[0]: {"text": row[1], "file_path": row[2], "pages": row[3]} for row in rows} + + +# ── MinIO download ──────────────────────────────────────────────────────────── + +def download_from_minio(minio_key: str, dest: Path) -> None: + """Download a file from MinIO using mcli.""" + src = f"{MINIO_ALIAS}/{MINIO_BUCKET}/{minio_key}" + result = subprocess.run( + ["mcli", "cp", src, str(dest)], + capture_output=True, text=True, + ) + if result.returncode != 0: + raise RuntimeError(f"mcli cp failed: {result.stderr}") + + +# ── Text quality metrics ───────────────────────────────────────────────────── + +_HEBREW_RE = re.compile(r'[֐-׿]') +_WORD_RE = re.compile(r'\S+') + + +def compute_metrics(text: str) -> dict: + """Compute quality metrics for extracted text.""" + if not text: + return {"chars": 0, "words": 0, "avg_word_len": 0, + "single_char_pct": 0, "words_per_line": 0, + "hebrew_pct": 0, "quality_ok": False} + + words = _WORD_RE.findall(text) + n_words = len(words) + if n_words == 0: + return {"chars": len(text), "words": 0, "avg_word_len": 0, + "single_char_pct": 0, "words_per_line": 0, + "hebrew_pct": 0, "quality_ok": False} + + avg_len = sum(len(w) for w in words) / n_words + single_char_pct = sum(1 for w in words if len(w) == 1) / n_words + + lines = [ln for ln in text.split("\n") if ln.strip()] + words_per_line = n_words / len(lines) if lines else 0 + + letters = re.findall(r'[a-zA-Z֐-׿]', text) + hebrew_pct = ( + sum(1 for c in letters if _HEBREW_RE.match(c)) / len(letters) + if letters else 0 + ) + + quality_ok = ( + n_words >= 10 + and avg_len >= 2.5 + and single_char_pct <= 0.4 + and words_per_line >= 3.0 + and hebrew_pct >= 0.5 + ) + + return { + "chars": len(text), + "words": n_words, + "avg_word_len": round(avg_len, 2), + "single_char_pct": round(single_char_pct * 100, 1), + "words_per_line": round(words_per_line, 1), + "hebrew_pct": round(hebrew_pct * 100, 1), + "quality_ok": quality_ok, + } + + +def count_known_abbrev_errors(text: str) -> int: + """Count common Hebrew abbreviation OCR errors (pre-fix indicators).""" + patterns = ['עוהייד', 'עוייד', 'הנייל', 'ביהמייש', 'עייי', 'בייכ', 'תמייא'] + return sum(text.count(p) for p in patterns) + + +def count_correct_abbrevs(text: str) -> int: + """Count correctly rendered Hebrew abbreviations.""" + correct = ['עו"ד', 'הנ"ל', 'ביהמ"ש', 'ע"י', 'ב"כ', 'תמ"א', 'ס"ק'] + return sum(text.count(p) for p in correct) + + +# ── Mistral OCR ─────────────────────────────────────────────────────────────── + +def call_mistral_ocr(pdf_path: Path) -> tuple[str, float]: + """Call Mistral OCR API on a PDF. Returns (extracted_text, elapsed_seconds).""" + with open(pdf_path, "rb") as f: + pdf_b64 = base64.b64encode(f.read()).decode() + + payload = { + "model": MISTRAL_OCR_MODEL, + "document": { + "type": "document_url", + "document_url": f"data:application/pdf;base64,{pdf_b64}", + }, + "include_image_base64": False, + } + + t0 = time.time() + with httpx.Client(timeout=300.0) as client: + resp = client.post( + MISTRAL_OCR_URL, + headers={ + "Authorization": f"Bearer {MISTRAL_API_KEY}", + "Content-Type": "application/json", + }, + json=payload, + ) + elapsed = time.time() - t0 + + if resp.status_code != 200: + raise RuntimeError(f"Mistral OCR error {resp.status_code}: {resp.text[:500]}") + + data = resp.json() + # Response: {"pages": [{"index": 0, "markdown": "..."}, ...]} + pages = data.get("pages", []) + text = "\n\n".join(p.get("markdown", "") for p in pages) + return text, elapsed + + +# ── Report ──────────────────────────────────────────────────────────────────── + +def render_metric_table(current: dict, mistral: dict) -> str: + rows = [ + ("תווים", f"{current['chars']:,}", f"{mistral['chars']:,}"), + ("מילים", f"{current['words']:,}", f"{mistral['words']:,}"), + ("אורך מילה ממוצע", str(current['avg_word_len']), str(mistral['avg_word_len'])), + ("% מילים חד-תוויות", f"{current['single_char_pct']}%", f"{mistral['single_char_pct']}%"), + ("מילים לשורה", str(current['words_per_line']), str(mistral['words_per_line'])), + ("% תווים עבריים", f"{current['hebrew_pct']}%", f"{mistral['hebrew_pct']}%"), + ("איכות כוללת", "✅" if current['quality_ok'] else "❌", "✅" if mistral['quality_ok'] else "❌"), + ] + lines = ["| מדד | OCR נוכחי | Mistral OCR |", "|-----|-----------|-------------|"] + for label, cur_val, mis_val in rows: + lines.append(f"| {label} | {cur_val} | {mis_val} |") + return "\n".join(lines) + + +def build_report(results: list[dict], output_path: Path) -> None: + lines = [ + "# השוואת OCR: מערכת נוכחית מול Mistral OCR", + f"\n**תיק:** 1027-04-26 — בל\"מ מושב נחם מפעל בטון בית שמש ", + f"**תאריך:** {time.strftime('%Y-%m-%d %H:%M')} ", + f"**מודל Mistral:** `{MISTRAL_OCR_MODEL}` ", + f"**מערכת נוכחית:** PyMuPDF (born-digital) + Google Cloud Vision (scanned) ", + "\n---\n", + "## סיכום מנהלים\n", + ] + + # Summary table + sum_lines = ["| מסמך | עמודים | נוכחי תווים | Mistral תווים | Mistral זמן | עדיפות |", + "|------|--------|-------------|---------------|-------------|--------|"] + for r in results: + if "error" in r: + sum_lines.append(f"| {r['title']} | {r['pages']} | — | שגיאה | — | — |") + continue + cur_chars = r["current_metrics"]["chars"] + mis_chars = r["mistral_metrics"]["chars"] + winner = "🔵 נוכחי" if cur_chars > mis_chars * 1.05 else ( + "🟢 Mistral" if mis_chars > cur_chars * 1.05 else "⚖️ שקול") + sum_lines.append( + f"| {r['title']} | {r['pages']} | {cur_chars:,} | {mis_chars:,} | " + f"{r['mistral_elapsed']:.1f}s | {winner} |" + ) + lines.extend(sum_lines) + lines.append("\n---\n") + + # Per-document detail + for r in results: + lines.append(f"## {r['title']} ({r['pages']} עמודים)\n") + if "error" in r: + lines.append(f"**שגיאה ב-Mistral OCR:** `{r['error']}`\n") + continue + + lines.append(render_metric_table(r["current_metrics"], r["mistral_metrics"])) + lines.append("") + + cur_abbr_err = count_known_abbrev_errors(r["current_text"]) + mis_abbr_err = count_known_abbrev_errors(r["mistral_text"]) + cur_abbr_ok = count_correct_abbrevs(r["current_text"]) + mis_abbr_ok = count_correct_abbrevs(r["mistral_text"]) + + lines.append(f"\n**קיצורים עבריים:**") + lines.append(f"- נוכחי: {cur_abbr_ok} נכונים, {cur_abbr_err} שגויים") + lines.append(f"- Mistral: {mis_abbr_ok} נכונים, {mis_abbr_err} שגויים") + lines.append(f"\n**זמן Mistral:** {r['mistral_elapsed']:.1f} שניות\n") + + # Side-by-side first 600 chars + lines.append("### דוגמת טקסט — 600 תווים ראשונים\n") + lines.append("**מערכת נוכחית:**") + lines.append("```") + lines.append((r["current_text"] or "")[:600].replace("```", "'''")) + lines.append("```\n") + lines.append("**Mistral OCR:**") + lines.append("```") + lines.append((r["mistral_text"] or "")[:600].replace("```", "'''")) + lines.append("```\n") + lines.append("---\n") + + output_path.write_text("\n".join(lines), encoding="utf-8") + print(f"\n✅ דוח נשמר: {output_path}") + + +# ── Main ────────────────────────────────────────────────────────────────────── + +def main() -> None: + parser = argparse.ArgumentParser(description="OCR benchmark: current vs Mistral") + parser.add_argument("--docs", type=int, default=4, + help="כמה מסמכים לבדוק (ברירת מחדל: 4)") + parser.add_argument("--output", type=str, + default="/home/chaim/legal-ai/data/audit/ocr-benchmark-mistral.md", + help="נתיב לדוח הפלט") + args = parser.parse_args() + + docs = DOCS_TO_BENCHMARK[: args.docs] + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + + print(f"🔍 שולף טקסטים קיימים מה-DB עבור תיק 1027-04-26...") + db_texts = _fetch_extracted_texts("1027-04-26") + print(f" נמצאו {len(db_texts)} מסמכים עם טקסט מחולץ") + + results = [] + + with tempfile.TemporaryDirectory() as tmp_dir: + for doc in docs: + print(f"\n📄 מעבד: {doc['title']} ({doc['pages']} עמודים)") + + # --- Current OCR text from DB --- + current_text = "" + for title, info in db_texts.items(): + if doc["title"].split("—")[0].strip() in title or doc["minio_key"].split("/")[-1] in (info.get("file_path") or ""): + current_text = info["text"] or "" + break + if not current_text: + # Try by file_path suffix match + key_name = doc["minio_key"].split("/")[-1] + for title, info in db_texts.items(): + if key_name in (info.get("file_path") or ""): + current_text = info["text"] or "" + break + + if not current_text: + print(f" ⚠️ לא נמצא טקסט ב-DB, מחפש לפי שם מסמך...") + # fallback: match by doc_type + rough title + for title, info in db_texts.items(): + if doc["doc_type"] in title.lower() or doc["title"][:8] in title: + current_text = info["text"] or "" + break + + print(f" נוכחי: {len(current_text):,} תווים") + + # --- Download PDF from MinIO --- + pdf_name = doc["minio_key"].split("/")[-1] + pdf_path = Path(tmp_dir) / pdf_name + print(f" מוריד מ-MinIO...") + try: + download_from_minio(doc["minio_key"], pdf_path) + print(f" הורד: {pdf_path.stat().st_size:,} bytes") + except Exception as e: + print(f" ❌ שגיאה בהורדה: {e}") + results.append({"title": doc["title"], "pages": doc["pages"], "error": str(e)}) + continue + + # --- Mistral OCR --- + print(f" קורא Mistral OCR API...") + try: + mistral_text, elapsed = call_mistral_ocr(pdf_path) + print(f" Mistral: {len(mistral_text):,} תווים ({elapsed:.1f}s)") + except Exception as e: + print(f" ❌ שגיאת Mistral API: {e}") + results.append({ + "title": doc["title"], "pages": doc["pages"], + "current_text": current_text, + "current_metrics": compute_metrics(current_text), + "mistral_text": "", "mistral_metrics": compute_metrics(""), + "mistral_elapsed": 0, "error": str(e), + }) + continue + + results.append({ + "title": doc["title"], + "pages": doc["pages"], + "current_text": current_text, + "current_metrics": compute_metrics(current_text), + "mistral_text": mistral_text, + "mistral_metrics": compute_metrics(mistral_text), + "mistral_elapsed": elapsed, + }) + + print("\n📊 בונה דוח...") + build_report(results, output_path) + + # Also save raw texts for manual inspection + raw_dir = output_path.parent / "ocr-benchmark-raw" + raw_dir.mkdir(exist_ok=True) + for r in results: + safe = r["title"].replace("/", "-").replace(" ", "_")[:40] + if "current_text" in r: + (raw_dir / f"{safe}_current.txt").write_text(r["current_text"], encoding="utf-8") + if "mistral_text" in r: + (raw_dir / f"{safe}_mistral.txt").write_text(r["mistral_text"], encoding="utf-8") + print(f"💾 טקסטים גולמיים נשמרו: {raw_dir}") + + +if __name__ == "__main__": + main() diff --git a/skills/docx/references/advanced-features.md b/skills/docx/references/advanced-features.md new file mode 100644 index 0000000..aeaef6d --- /dev/null +++ b/skills/docx/references/advanced-features.md @@ -0,0 +1,170 @@ +# Advanced Features — פיצ'רים מתקדמים + +## הערות שוליים (Footnotes) + +**שימוש מרכזי:** הפניות לחקיקה ופסיקה. + +```javascript +const { FootnoteReferenceRun } = require('docx'); + +const doc = new Document({ + footnotes: { + 1: { children: [new Paragraph({ + bidirectional: true, alignment: AlignmentType.START, + children: [new TextRun({ + text: "חוק החוזים (חלק כללי), התשל״ג-1973, סעיף 12.", + font: "David", size: 20, rightToLeft: true + })] + })] }, + }, + // ...sections +}); + +// הפניה בגוף הטקסט: +new Paragraph({ + bidirectional: true, alignment: AlignmentType.BOTH, + children: [ + new TextRun({ text: "חובת תום הלב", font: "David", size: 24, rightToLeft: true }), + new FootnoteReferenceRun(1), + new TextRun({ text: " חלה על כל שלבי המשא ומתן.", font: "David", size: 24, rightToLeft: true }), + ] +}) +``` + +### תיקון RTL בהערות שוליים (post-unpack) +docx-js לא מגדיר RTL מלא. אחרי unpack, תקן ב-`word/footnotes.xml`: +```xml + + + + + + + + + + + + + + + + +``` + +--- + +## תוכן עניינים (TOC) + +**⚠️ TOC ידני בלבד** — `TableOfContents` של docx-js מאבד הגדרות RTL בעדכון Word. + +```javascript +const { Tab, TabStopType, LeaderType, LineRuleType } = require('docx'); + +const tocEntry = (text, pageNum, opts = {}) => new Paragraph({ + bidirectional: true, + spacing: { after: 60, line: 276, lineRule: LineRuleType.AUTO }, + ...(opts.indent ? { indent: { right: opts.indent } } : {}), + tabStops: [{ type: TabStopType.RIGHT, position: 9026, leader: LeaderType.DOT }], + children: [ + new TextRun({ text, font: "David", size: 24, rightToLeft: true, bold: opts.bold || false }), + new TextRun({ children: [new Tab()], font: "David", rightToLeft: true }), + new TextRun({ text: String(pageNum), font: "David", size: 24, rightToLeft: true }), + ] +}); + +// שימוש: +tocEntry("פרק א׳ — הגדרות כלליות", 2, { bold: true }), +tocEntry("1. הגדרות יסוד", 2, { indent: 400 }), +``` + +--- + +## מספר סקשנים (Multiple Sections) + +**שימוש:** כותרות שונות לנספחים, שוליים שונים. + +```javascript +const doc = new Document({ + sections: [ + { + properties: { + page: { size: { width: 11906, height: 16838 }, margin: { top: 1417, right: 1417, bottom: 1417, left: 1417 } }, + bidi: true, + }, + headers: { + default: new Header({ children: [new Paragraph({ + bidirectional: true, alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "הסכם שירותים", font: "David", size: 20, bold: true, rightToLeft: true })] + })] }) + }, + children: [ /* גוף ההסכם */ ] + }, + { + properties: { + page: { size: { width: 11906, height: 16838 }, margin: { top: 1417, right: 1417, bottom: 1417, left: 1417 } }, + bidi: true, + }, + headers: { + default: new Header({ children: [new Paragraph({ + bidirectional: true, alignment: AlignmentType.START, + children: [new TextRun({ text: "נספח א׳ — לוח תעריפים", font: "David", size: 20, bold: true, rightToLeft: true })] + })] }) + }, + children: [ /* הנספח */ ] + } + ] +}); +``` + +--- + +## לוגו/תמונה בכותרת (Letterhead) + +```javascript +const { ImageRun } = require('docx'); +const logoBuffer = fs.readFileSync('/path/to/logo.png'); + +headers: { + default: new Header({ + children: [ + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new ImageRun({ data: logoBuffer, transformation: { width: 200, height: 60 }, type: "png" })], + }), + new Paragraph({ + bidirectional: true, alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "משרד עורכי דין ישראלי ושות׳", font: "David", size: 20, bold: true, rightToLeft: true })], + }), + ], + }), +} +``` + +**הערה:** תמונה חייבת להיות קובץ אמיתי — לבקש מהמשתמש אם אין. + +--- + +## היפרלינקים + +```javascript +const { ExternalHyperlink, UnderlineType } = require('docx'); + +new Paragraph({ + bidirectional: true, + children: [ + new TextRun({ text: "ראה: ", font: "David", size: 24, rightToLeft: true }), + new ExternalHyperlink({ + link: "https://www.nevo.co.il/law_html/law01/073_002.htm", + children: [new TextRun({ + text: "חוק החוזים באתר נבו", + font: "David", size: 24, rightToLeft: true, + color: "0563C1", + underline: { type: UnderlineType.SINGLE }, + })], + }), + ] +}) +``` + +**⚠️ אל תשתמש ב-`style: "Hyperlink"`** — מפריע ל-RTL. הגדר `color` + `underline` ידנית. diff --git a/skills/docx/references/document-templates.md b/skills/docx/references/document-templates.md new file mode 100644 index 0000000..ebe8434 --- /dev/null +++ b/skills/docx/references/document-templates.md @@ -0,0 +1,219 @@ +# Document Templates — תבניות מסמכים משפטיים + +## תבנית 1: כתב טענות (בקשה, תביעה, הגנה, ערעור) + +```javascript +const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, + AlignmentType, LevelFormat, BorderStyle, WidthType } = require('docx'); + +const PAGE_WIDTH = 11906; +const MARGINS = { top: 1134, right: 1134, bottom: 1134, left: 1134 }; +const CONTENT_WIDTH = PAGE_WIDTH - MARGINS.left - MARGINS.right; + +const noBorder = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }; +const noBorders = { top: noBorder, bottom: noBorder, left: noBorder, right: noBorder }; + +// Header בית משפט — טבלה עם שם בית המשפט (ימין) ומספר תיק (שמאל) +function courtHeader(courtName, caseNumber) { + return new Table({ + width: { size: CONTENT_WIDTH, type: WidthType.DXA }, + columnWidths: [CONTENT_WIDTH / 2, CONTENT_WIDTH / 2], + visuallyRightToLeft: true, + rows: [ + new TableRow({ + children: [ + new TableCell({ + width: { size: CONTENT_WIDTH / 2, type: WidthType.DXA }, + borders: noBorders, + children: [new Paragraph({ + bidirectional: true, alignment: AlignmentType.START, + children: [new TextRun({ text: courtName, bold: true, font: "David", size: 26, rightToLeft: true })] + })] + }), + new TableCell({ + width: { size: CONTENT_WIDTH / 2, type: WidthType.DXA }, + borders: noBorders, + children: [new Paragraph({ + bidirectional: true, alignment: AlignmentType.END, + children: [new TextRun({ text: caseNumber, bold: true, font: "David", size: 26, rightToLeft: true })] + })] + }) + ] + }) + ] + }); +} + +function mainTitle(text) { + return new Paragraph({ + bidirectional: true, alignment: AlignmentType.CENTER, + spacing: { before: 300, after: 300 }, + children: [new TextRun({ text, bold: true, font: "David", size: 28, rightToLeft: true, underline: {} })] + }); +} + +function subHeading(text) { + return new Paragraph({ + bidirectional: true, alignment: AlignmentType.START, + spacing: { before: 240, after: 120 }, + children: [new TextRun({ text, bold: true, font: "David", size: 24, rightToLeft: true, underline: {} })] + }); +} + +const doc = new Document({ + numbering: { + config: [{ + reference: "legal-clauses", + levels: [{ + level: 0, format: LevelFormat.DECIMAL, text: "%1.", + alignment: AlignmentType.START, suffix: "tab", + style: { paragraph: { indent: { left: 360, hanging: 360 } } } + }] + }] + }, + sections: [{ + properties: { + page: { size: { width: PAGE_WIDTH, height: 16838 }, margin: MARGINS }, + bidi: true + }, + children: [ + courtHeader("בית המשפט המחוזי בתל אביב", "ת\"א 12345-01-26"), + mainTitle("כתב תביעה"), + // ... פרטי צדדים, סעיפים, חתימה + ] + }] +}); +``` + +--- + +## תבנית 2: מכתב התראה + +```javascript +function letterHeader(firmName, address, phone, email) { + return [ + new Paragraph({ + bidirectional: true, alignment: AlignmentType.START, + children: [new TextRun({ text: firmName, bold: true, font: "David", size: 28, rightToLeft: true })] + }), + new Paragraph({ + bidirectional: true, alignment: AlignmentType.START, + children: [new TextRun({ text: address, font: "David", size: 22, rightToLeft: true })] + }), + new Paragraph({ + bidirectional: true, alignment: AlignmentType.START, + spacing: { after: 300 }, + children: [new TextRun({ text: `טל': ${phone} | ${email}`, font: "David", size: 22, rightToLeft: true })] + }), + ]; +} + +function subjectLine(text) { + return new Paragraph({ + bidirectional: true, alignment: AlignmentType.CENTER, + spacing: { before: 200, after: 200 }, + children: [ + new TextRun({ text: "הנדון: ", bold: true, font: "David", size: 24, rightToLeft: true }), + new TextRun({ text, bold: true, font: "David", size: 24, rightToLeft: true, underline: {} }) + ] + }); +} + +// שימוש: +sections: [{ + properties: { page: { size: { width: 11906, height: 16838 }, margin: { top: 1417, right: 1417, bottom: 1417, left: 1417 } }, bidi: true }, + children: [ + ...letterHeader("משרד עו\"ד כהן ושות'", "רח' הרצל 1, תל אביב", "03-1234567", "office@cohen-law.co.il"), + new Paragraph({ + bidirectional: true, alignment: AlignmentType.START, + children: [new TextRun({ text: "תאריך: 10.2.2026", font: "David", size: 24, rightToLeft: true })] + }), + new Paragraph({ + bidirectional: true, alignment: AlignmentType.START, + spacing: { before: 200 }, + children: [new TextRun({ text: "לכבוד: [שם הנמען]", font: "David", size: 24, rightToLeft: true })] + }), + subjectLine("התראה בטרם נקיטת הליכים משפטיים"), + ] +}] +``` + +--- + +## תבנית 3: הסכם/חוזה + +```javascript +const CONTENT_WIDTH = 9638; // A4 עם שוליים 2.5 ס"מ +const noBorders = { /* ראה תבנית 1 */ }; + +function contractTitle(text) { + return new Paragraph({ + bidirectional: true, alignment: AlignmentType.CENTER, + spacing: { after: 300 }, + children: [new TextRun({ text, bold: true, font: "David", size: 32, rightToLeft: true })] + }); +} + +function partyClause(label, name, id, address, alias) { + return new Paragraph({ + bidirectional: true, alignment: AlignmentType.BOTH, + spacing: { after: 120 }, + children: [ + new TextRun({ text: `${label}: `, bold: true, font: "David", size: 24, rightToLeft: true }), + new TextRun({ text: `${name}, ח.פ./ת.ז. ${id}, מ${address} (להלן: "`, font: "David", size: 24, rightToLeft: true }), + new TextRun({ text: alias, bold: true, font: "David", size: 24, rightToLeft: true }), + new TextRun({ text: '")', font: "David", size: 24, rightToLeft: true }), + ] + }); +} + +function signatureTable(contentWidth) { + return new Table({ + width: { size: contentWidth, type: WidthType.DXA }, + columnWidths: [contentWidth / 2, contentWidth / 2], + visuallyRightToLeft: true, + rows: [new TableRow({ + children: [ + new TableCell({ + borders: noBorders, + children: [ + new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "_________________", font: "David", size: 24, rightToLeft: true })] }), + new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "צד א'", font: "David", size: 24, rightToLeft: true })] }) + ] + }), + new TableCell({ + borders: noBorders, + children: [ + new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "_________________", font: "David", size: 24, rightToLeft: true })] }), + new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "צד ב'", font: "David", size: 24, rightToLeft: true })] }) + ] + }) + ] + })] + }); +} + +sections: [{ + properties: { page: { size: { width: 11906, height: 16838 }, margin: { top: 1417, right: 1417, bottom: 1417, left: 1417 } }, bidi: true }, + children: [ + contractTitle("הסכם שירותים"), + new Paragraph({ + bidirectional: true, alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "נערך ונחתם בתל אביב ביום __________", font: "David", size: 24, rightToLeft: true })] + }), + partyClause("מצד אחד", "[שם]", "[מספר]", "[כתובת]", "המזמין"), + partyClause("מצד שני", "[שם]", "[מספר]", "[כתובת]", "הספק"), + // הואילים + סעיפים... + new Paragraph({ + bidirectional: true, alignment: AlignmentType.CENTER, + spacing: { before: 400, after: 300 }, + children: [new TextRun({ text: "ולראיה באו הצדדים על החתום:", bold: true, font: "David", size: 24, rightToLeft: true })] + }), + signatureTable(CONTENT_WIDTH) + ] +}] +``` diff --git a/skills/docx/references/tracked-changes.md b/skills/docx/references/tracked-changes.md new file mode 100644 index 0000000..7d8ade5 --- /dev/null +++ b/skills/docx/references/tracked-changes.md @@ -0,0 +1,57 @@ +# Tracked Changes — עקוב אחר שינויים + +## שם מחבר בעברית +```xml + +``` + +## שינוי ערך (סכום, תאריך, תקופה) +פצל את הטקסט ועטוף רק את הערך שמשתנה: +```xml +...RTL PROPS... + שכר הטרחה יעמוד על סך של + + ...RTL PROPS...750 + + + ...RTL PROPS...850 + +...RTL PROPS... + ש״ח לשעת עבודה +``` + +## מחיקת סעיף שלם +```xml + + + + + + + + + + ...RTL PROPS... + הסעיף שנמחק + + +``` + +## RTL PROPS — בלוק rPr מלא לכל run +```xml + + + + + + +``` + +## קבלה/דחייה של שינויים + +| פעולה | לפני | אחרי | +|-------|------|------| +| קבלת הוספה | `...טקסט` | `...טקסט` | +| דחיית הוספה | `...` | *(מחק הכל)* | +| קבלת מחיקה | `...טקסט` | *(מחק הכל)* | +| דחיית מחיקה | `...טקסט` | `...טקסט` |