chore: fix .gitignore inline comments + add untracked code files
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 <noreply@anthropic.com>
This commit is contained in:
12
.env.example
Normal file
12
.env.example
Normal file
@@ -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_...
|
||||||
31
.gitignore
vendored
31
.gitignore
vendored
@@ -6,7 +6,8 @@ data/backups/
|
|||||||
data/precedent-library/
|
data/precedent-library/
|
||||||
data/.auto-sync.log
|
data/.auto-sync.log
|
||||||
data/*.db
|
data/*.db
|
||||||
data/checkpoints/ # X16 durable-pipeline SQLite checkpoints (runtime artifact)
|
# X16 durable-pipeline SQLite checkpoints (runtime artifact)
|
||||||
|
data/checkpoints/
|
||||||
*.bak-pre-*
|
*.bak-pre-*
|
||||||
mcp-server/.venv/
|
mcp-server/.venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@@ -18,6 +19,30 @@ kiryat-yearim/
|
|||||||
continuation-prompt.md
|
continuation-prompt.md
|
||||||
node_modules/
|
node_modules/
|
||||||
data/eval/eval-report-*
|
data/eval/eval-report-*
|
||||||
data/adapter-migration-state.json # revert snapshot for migrate_agent_adapter.py (runtime state)
|
# revert snapshot for migrate_agent_adapter.py (runtime state)
|
||||||
.claude/agents/.generated/ # frontmatter-stripped instruction copies for content_arg adapters (generated)
|
data/adapter-migration-state.json
|
||||||
|
# frontmatter-stripped instruction copies for content_arg adapters (generated)
|
||||||
|
.claude/agents/.generated/
|
||||||
.claude/worktrees/
|
.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/
|
||||||
|
|||||||
47
.taskmaster/templates/example_prd.txt
Normal file
47
.taskmaster/templates/example_prd.txt
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<context>
|
||||||
|
# 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]
|
||||||
|
</context>
|
||||||
|
<PRD>
|
||||||
|
# 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]
|
||||||
|
</PRD>
|
||||||
511
.taskmaster/templates/example_prd_rpg.txt
Normal file
511
.taskmaster/templates/example_prd_rpg.txt
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
<rpg-method>
|
||||||
|
# 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 `<instruction>` block
|
||||||
|
- Look at `<example>` 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.
|
||||||
|
</rpg-method>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<overview>
|
||||||
|
<instruction>
|
||||||
|
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.
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## 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"]
|
||||||
|
|
||||||
|
</overview>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<functional-decomposition>
|
||||||
|
<instruction>
|
||||||
|
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
|
||||||
|
|
||||||
|
<example type="good">
|
||||||
|
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
|
||||||
|
</example>
|
||||||
|
|
||||||
|
<example type="bad">
|
||||||
|
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.)
|
||||||
|
</example>
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## 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]
|
||||||
|
...
|
||||||
|
|
||||||
|
</functional-decomposition>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<structural-decomposition>
|
||||||
|
<instruction>
|
||||||
|
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).
|
||||||
|
|
||||||
|
<example type="good">
|
||||||
|
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)
|
||||||
|
</example>
|
||||||
|
|
||||||
|
<example type="bad">
|
||||||
|
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.)
|
||||||
|
</example>
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## 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]
|
||||||
|
|
||||||
|
</structural-decomposition>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<dependency-graph>
|
||||||
|
<instruction>
|
||||||
|
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?"
|
||||||
|
|
||||||
|
<example type="good">
|
||||||
|
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]
|
||||||
|
</example>
|
||||||
|
|
||||||
|
<example type="bad">
|
||||||
|
- 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.)
|
||||||
|
</example>
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## 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...]
|
||||||
|
|
||||||
|
</dependency-graph>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<implementation-roadmap>
|
||||||
|
<instruction>
|
||||||
|
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.
|
||||||
|
|
||||||
|
<example type="good">
|
||||||
|
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
|
||||||
|
</example>
|
||||||
|
|
||||||
|
<example type="bad">
|
||||||
|
Phase 1: Build Everything
|
||||||
|
Tasks:
|
||||||
|
- API
|
||||||
|
- Database
|
||||||
|
- UI
|
||||||
|
- Tests
|
||||||
|
(Problem: No clear focus. Too broad. Dependencies not considered.)
|
||||||
|
</example>
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## 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...]
|
||||||
|
|
||||||
|
</implementation-roadmap>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<test-strategy>
|
||||||
|
<instruction>
|
||||||
|
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.
|
||||||
|
|
||||||
|
<example type="good">
|
||||||
|
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
|
||||||
|
</example>
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## 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]
|
||||||
|
|
||||||
|
</test-strategy>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<architecture>
|
||||||
|
<instruction>
|
||||||
|
Describe technical architecture, data models, and key design decisions.
|
||||||
|
|
||||||
|
Keep this section AFTER functional/structural decomposition - implementation details come after understanding structure.
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## 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]
|
||||||
|
|
||||||
|
</architecture>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<risks>
|
||||||
|
<instruction>
|
||||||
|
Identify risks that could derail development and how to mitigate them.
|
||||||
|
|
||||||
|
Categories:
|
||||||
|
- Technical risks (complexity, unknowns)
|
||||||
|
- Dependency risks (blocking issues)
|
||||||
|
- Scope risks (creep, underestimation)
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
## 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]
|
||||||
|
|
||||||
|
</risks>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<appendix>
|
||||||
|
## References
|
||||||
|
[Papers, documentation, similar systems]
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
[Domain-specific terms]
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
[Things to resolve during development]
|
||||||
|
</appendix>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<task-master-integration>
|
||||||
|
# How Task Master Uses This PRD
|
||||||
|
|
||||||
|
When you run `task-master parse-prd <file>.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
|
||||||
|
</task-master-integration>
|
||||||
43
data/halacha_night_check.sh
Executable file
43
data/halacha_night_check.sh
Executable file
@@ -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"
|
||||||
@@ -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)
|
||||||
@@ -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 — שיעורים ינועו ככל שהדריינר/פאנל רצים.
|
||||||
348
docs/superpowers/plans/2026-05-16-hooks-execution.md
Normal file
348
docs/superpowers/plans/2026-05-16-hooks-execution.md
Normal file
@@ -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<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
"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 |
|
||||||
306
docs/superpowers/plans/2026-05-16-per-agent-claude-md.md
Normal file
306
docs/superpowers/plans/2026-05-16-per-agent-claude-md.md
Normal file
@@ -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` | עובד |
|
||||||
412
docs/superpowers/plans/2026-05-16-scheduled-agents.md
Normal file
412
docs/superpowers/plans/2026-05-16-scheduled-agents.md
Normal file
@@ -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<string, string> = {
|
||||||
|
"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 מועיר |
|
||||||
21
mcp-server/tests/test_adapter_profiles.py
Normal file
21
mcp-server/tests/test_adapter_profiles.py
Normal file
@@ -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")
|
||||||
145
scripts/exec_summary_1043.py
Normal file
145
scripts/exec_summary_1043.py
Normal file
@@ -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()
|
||||||
102
scripts/ingest_incoming_batch.py
Normal file
102
scripts/ingest_incoming_batch.py
Normal file
@@ -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())
|
||||||
392
scripts/ocr_benchmark_mistral.py
Normal file
392
scripts/ocr_benchmark_mistral.py
Normal file
@@ -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()
|
||||||
170
skills/docx/references/advanced-features.md
Normal file
170
skills/docx/references/advanced-features.md
Normal file
@@ -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
|
||||||
|
<w:footnote w:id="1">
|
||||||
|
<w:p>
|
||||||
|
<w:pPr>
|
||||||
|
<w:pStyle w:val="FootnoteText"/>
|
||||||
|
<w:bidi/>
|
||||||
|
<w:jc w:val="start"/>
|
||||||
|
</w:pPr>
|
||||||
|
<w:r>
|
||||||
|
<w:rPr>
|
||||||
|
<w:rStyle w:val="FootnoteReference"/>
|
||||||
|
<w:rtl/>
|
||||||
|
</w:rPr>
|
||||||
|
<w:footnoteRef/>
|
||||||
|
</w:r>
|
||||||
|
</w:p>
|
||||||
|
</w:footnote>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## תוכן עניינים (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` ידנית.
|
||||||
219
skills/docx/references/document-templates.md
Normal file
219
skills/docx/references/document-templates.md
Normal file
@@ -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)
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
```
|
||||||
57
skills/docx/references/tracked-changes.md
Normal file
57
skills/docx/references/tracked-changes.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Tracked Changes — עקוב אחר שינויים
|
||||||
|
|
||||||
|
## שם מחבר בעברית
|
||||||
|
```xml
|
||||||
|
<w:del w:id="10" w:author="עו"ד כהן" w:date="2026-02-06T09:00:00Z">
|
||||||
|
```
|
||||||
|
|
||||||
|
## שינוי ערך (סכום, תאריך, תקופה)
|
||||||
|
פצל את הטקסט ועטוף רק את הערך שמשתנה:
|
||||||
|
```xml
|
||||||
|
<w:r><w:rPr>...RTL PROPS...</w:rPr>
|
||||||
|
<w:t xml:space="preserve">שכר הטרחה יעמוד על סך של </w:t></w:r>
|
||||||
|
<w:del w:id="10" w:author="עו"ד כהן" w:date="...">
|
||||||
|
<w:r><w:rPr>...RTL PROPS...</w:rPr><w:delText>750</w:delText></w:r>
|
||||||
|
</w:del>
|
||||||
|
<w:ins w:id="11" w:author="עו"ד כהן" w:date="...">
|
||||||
|
<w:r><w:rPr>...RTL PROPS...</w:rPr><w:t>850</w:t></w:r>
|
||||||
|
</w:ins>
|
||||||
|
<w:r><w:rPr>...RTL PROPS...</w:rPr>
|
||||||
|
<w:t xml:space="preserve"> ש״ח לשעת עבודה</w:t></w:r>
|
||||||
|
```
|
||||||
|
|
||||||
|
## מחיקת סעיף שלם
|
||||||
|
```xml
|
||||||
|
<w:p>
|
||||||
|
<w:pPr>
|
||||||
|
<w:bidi/>
|
||||||
|
<w:jc w:val="both"/>
|
||||||
|
<w:rPr>
|
||||||
|
<w:del w:id="20" w:author="עו"ד כהן" w:date="..."/>
|
||||||
|
</w:rPr>
|
||||||
|
</w:pPr>
|
||||||
|
<w:del w:id="21" w:author="עו"ד כהן" w:date="...">
|
||||||
|
<w:r><w:rPr>...RTL PROPS...</w:rPr>
|
||||||
|
<w:delText>הסעיף שנמחק</w:delText></w:r>
|
||||||
|
</w:del>
|
||||||
|
</w:p>
|
||||||
|
```
|
||||||
|
|
||||||
|
## RTL PROPS — בלוק rPr מלא לכל run
|
||||||
|
```xml
|
||||||
|
<w:rPr>
|
||||||
|
<w:rFonts w:ascii="David" w:cs="David" w:eastAsia="David" w:hAnsi="David"/>
|
||||||
|
<w:sz w:val="24"/>
|
||||||
|
<w:szCs w:val="24"/>
|
||||||
|
<w:rtl/>
|
||||||
|
</w:rPr>
|
||||||
|
```
|
||||||
|
|
||||||
|
## קבלה/דחייה של שינויים
|
||||||
|
|
||||||
|
| פעולה | לפני | אחרי |
|
||||||
|
|-------|------|------|
|
||||||
|
| קבלת הוספה | `<w:ins ...><w:r>...<w:t>טקסט</w:t></w:r></w:ins>` | `<w:r>...<w:t>טקסט</w:t></w:r>` |
|
||||||
|
| דחיית הוספה | `<w:ins ...><w:r>...</w:r></w:ins>` | *(מחק הכל)* |
|
||||||
|
| קבלת מחיקה | `<w:del ...><w:r>...<w:delText>טקסט</w:delText></w:r></w:del>` | *(מחק הכל)* |
|
||||||
|
| דחיית מחיקה | `<w:del ...><w:r>...<w:delText>טקסט</w:delText></w:r></w:del>` | `<w:r>...<w:t>טקסט</w:t></w:r>` |
|
||||||
Reference in New Issue
Block a user