Compare commits
471 Commits
82ded005a4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 81171983e4 | |||
| d156bcfaf1 | |||
| 33d8faf74a | |||
| cb822c4900 | |||
| f1d6f5dafc | |||
| 1a50aa7709 | |||
| 405167269f | |||
| 7f573c0db3 | |||
| aa0fde2724 | |||
| e57730f375 | |||
| 6299998267 | |||
| d4d2ab4d68 | |||
| c0af8c7cda | |||
| 2f43960353 | |||
| de777c2b13 | |||
| 98c5feff25 | |||
| 2c4287fd3d | |||
| 55362bf5a1 | |||
| 7ebd4187a9 | |||
| c8344342a8 | |||
| 02f411f4dc | |||
| 0f0656ecca | |||
| c028328175 | |||
| 471cd37fc8 | |||
| 9f358db353 | |||
| d23f854c25 | |||
| 9ae49f0f70 | |||
| f79c46a352 | |||
| ae30a4d19a | |||
| 638eef6803 | |||
| 6647aa92e6 | |||
| b2ea0c28dd | |||
| bc5dd9ac48 | |||
| 5745d36bb4 | |||
| 05e8373d22 | |||
| 85f94a4f3f | |||
| 1e41125baa | |||
| 1f42a39ce4 | |||
| 39f8cb7c15 | |||
| 1986fe3b14 | |||
| 81b3de6f4f | |||
| b4a28f072d | |||
| ade22ca871 | |||
| 54948eb8ab | |||
| 6ec67d1a11 | |||
| 34d80a39e5 | |||
| 5bd235bcff | |||
| a92f543e7f | |||
| 8de2401cb1 | |||
| 83d30365c9 | |||
| 64b9bd9d99 | |||
| 8d2f1ea0a2 | |||
| 36319a8d75 | |||
| 16470f6279 | |||
| 97d5b178d3 | |||
| a5a4f53660 | |||
| 6c6e4e021b | |||
| d895062b4c | |||
| a1db283ce1 | |||
| 97ede1a49d | |||
| 2972ef74a4 | |||
| 5676fd1157 | |||
| 83d1a8253c | |||
| 5eeff24889 | |||
| 5bf2ea0262 | |||
| 7fb5134580 | |||
| c3735d019a | |||
| d95a36f310 | |||
| de56d3b39d | |||
| ef21cb93e5 | |||
| cc9adc5c1f | |||
| da4ebeb724 | |||
| d8113adec6 | |||
| a3a02ca67a | |||
| b022cc7a97 | |||
| 5f1b96ccaf | |||
| 4b5c8a2772 | |||
| b5f7b60fb5 | |||
| 2c75666d26 | |||
| fc5d69902f | |||
| 8dc0a268fb | |||
| 9a126f7c36 | |||
| 3c030dd7f5 | |||
| dba2a131e0 | |||
| ecd9e46bb9 | |||
| 6cdf178ea4 | |||
| 2fbc0cd3c2 | |||
| 360f49d8b4 | |||
| 24d80e6a2a | |||
| 3ae183009f | |||
| 106ab53231 | |||
| 8258f09228 | |||
| aa32766a8c | |||
| 6882ccfcf1 | |||
| 618f476a22 | |||
| 69b34f1c3f | |||
| 796bfa890f | |||
| c1abf2ec0e | |||
| 6468e151d9 | |||
| fb40ec8565 | |||
| bcd5fd5f8d | |||
| f4f110f0d1 | |||
| 540d39b958 | |||
| d3b5c563ce | |||
| d9340f6c39 | |||
| 808c2e4c46 | |||
| 879bb6c074 | |||
| f3e99a14ca | |||
| b9fa38f3db | |||
| f56309da5a | |||
| 635dc98492 | |||
| e6dc410d7d | |||
| e82eeaad9f | |||
| e186183527 | |||
| 61b9d72bcf | |||
| 781f24c643 | |||
| 9315ba4dfe | |||
| c80e4ce8ff | |||
| f3740fef68 | |||
| 2e33cac043 | |||
| acb8e2c206 | |||
| 0990db7a3c | |||
| 692eea76f0 | |||
| 06281996ca | |||
| 955675eb1f | |||
| 8171572cdd | |||
| 9eaabffba4 | |||
| 90f3c472b5 | |||
| 638a542cf4 | |||
| 0e35060d3d | |||
| a0c1b74c55 | |||
| 7e7de485a4 | |||
| e62f39aabf | |||
| 632fe73857 | |||
| f60fdc2c6d | |||
| a07622659c | |||
| a1f491e9cc | |||
| 5aa3d4ed99 | |||
| b107654ee4 | |||
| 27911c5beb | |||
| 1a1757f29d | |||
| ac279220c4 | |||
| 9bd247c421 | |||
| b7b44f4453 | |||
| ab99cfa1d3 | |||
| e239915fd3 | |||
| 86f5797dbd | |||
| 25e0662ead | |||
| 6dbc9130b0 | |||
| e4651a9d06 | |||
| 12313774a1 | |||
| 7d97ca25a2 | |||
| a571ad535b | |||
| c7933b9de3 | |||
| afc1548bca | |||
| 161d0d6ed6 | |||
| e096c51037 | |||
| 85c5a4aacb | |||
| 420cb819f5 | |||
| 32ef259843 | |||
| 1286a1e60d | |||
| 366d89e6bb | |||
| fb51a0e869 | |||
| 12bdec10fa | |||
| 8ec24cf822 | |||
| 3b9f77daa8 | |||
| 5fa76a09b4 | |||
| 32a6e2b57b | |||
| 3c68383e86 | |||
| 37c00bac13 | |||
| f20a3a09fd | |||
| 6313fcd316 | |||
| ee76455a9a | |||
| 7b1c0c1a32 | |||
| e4fbda6c1f | |||
| 3b3e1e3bbf | |||
| 37dcb30604 | |||
| dc0936adf9 | |||
| 0059c326f1 | |||
| a2236363d4 | |||
| b515f3453e | |||
| 14568fdd15 | |||
| 172511339f | |||
| ad4350029a | |||
| 424dc7cd18 | |||
| 2e20e27e17 | |||
| ea84a602e6 | |||
| 29af008271 | |||
| 0a514cc276 | |||
| cde7f94628 | |||
| 9a3e7faf08 | |||
| 79b9c37301 | |||
| dd46ffb3e3 | |||
| a3451775fa | |||
| caeaf51db4 | |||
| 9a6d690e0e | |||
| a3ef9e5e34 | |||
| 7a2865339c | |||
| 0d995483ce | |||
| 24f9ceb164 | |||
| c482414819 | |||
| 014eb4937e | |||
| b9bdca0572 | |||
| f17e0e382a | |||
| aa0a736a7b | |||
| c52b5986a3 | |||
| 6bf19bd0d7 | |||
| b97e8d595d | |||
| 8a3bcd3ffc | |||
| 11f20072ea | |||
| d37274a31b | |||
| 9c77123fa3 | |||
| 770d23b198 | |||
| 1565a636a8 | |||
| 40c1111e9b | |||
| 4977ab8d9a | |||
| 701efab726 | |||
| d3f1d04915 | |||
| ea8b48c6ac | |||
| 0d0f5aa8e9 | |||
| 034b609bd3 | |||
| b53d65c1f6 | |||
| ebfe7f6a1d | |||
| 67a3d9a9b0 | |||
| 482f302d54 | |||
| 27b40dfec5 | |||
| 1f1a025509 | |||
| fdeed8a045 | |||
| 7f4e036211 | |||
| 35c15720a5 | |||
| 4174217179 | |||
| dd0e754dad | |||
| e3e3da09e5 | |||
| 59ff4e31cf | |||
| 68a77c11b6 | |||
| c83d0162ca | |||
| f5926506fe | |||
| df97e21d22 | |||
| c35e0e50ed | |||
| 6dd125c491 | |||
| f8c3fd6c89 | |||
| d47a633fcf | |||
| fb60dca796 | |||
| 5efb8cf915 | |||
| f196bed564 | |||
| e25507f9ad | |||
| 476c2fc5d1 | |||
| db6bad5d1e | |||
| eeb70a5758 | |||
| 7ebddcce6d | |||
| 0f64b4c062 | |||
| 8e3d14abee | |||
| ca959d4a9c | |||
| b0ec24a9d5 | |||
| f5d14fd6b8 | |||
| bbe3db7b94 | |||
| 7d0d4a9b27 | |||
| 61dde4cd83 | |||
| 2a9168a1b4 | |||
| 5a00a0ef47 | |||
| 4debe9995b | |||
| bb42aeeff4 | |||
| 6fcfdc76db | |||
| 0a88bed58b | |||
| d4046c2fbd | |||
| f74fa13146 | |||
| 434341cc29 | |||
| c7c6f3eb9c | |||
| 76fae77393 | |||
| 901ec9f869 | |||
| 7be1c3162c | |||
| 9295e74762 | |||
| fc0c36b2f8 | |||
| 2d7ab26c71 | |||
| 1d3e235556 | |||
| 7471dcf3cc | |||
| d790fb26e0 | |||
| 7e34c53224 | |||
| 77ed0361b7 | |||
| 5d63a903ce | |||
| aeddcb41eb | |||
| 1aadd3b455 | |||
| f66a2a27e7 | |||
| f46bf47d5b | |||
| 9f2adc4dd0 | |||
| e79f74bc23 | |||
| 3bd2d16652 | |||
| b4d1fc5539 | |||
| ed547e20ad | |||
| df007784c9 | |||
| 391b025e8a | |||
| 885cba543e | |||
| acfd5bae3e | |||
| 8e4ea23882 | |||
| 6183e24316 | |||
| 807053ec54 | |||
| 62e5e5183d | |||
| 1b62fa4af8 | |||
| e712573766 | |||
| 6ed5c9e99f | |||
| a9472187ff | |||
| 5abfbd2746 | |||
| b57e590275 | |||
| 33f955e372 | |||
| dbc176ae66 | |||
| 09eec6a906 | |||
| ca31932a5f | |||
| beba24dfc5 | |||
| ae8efc0b63 | |||
| 887079535c | |||
| d83a2a2fb2 | |||
| 37c56ff22a | |||
| c70a03f91e | |||
| 1cc7c0e757 | |||
| ae7d475103 | |||
| a02a606f34 | |||
| ff5187c9c1 | |||
| 7161c3d010 | |||
| eef04b0f09 | |||
| 411ee18786 | |||
| 83d6b5ecf0 | |||
| c231782ee8 | |||
| dfa2f5bd7f | |||
| 19d3dc81d0 | |||
| aee2140b0b | |||
| 6ff2e36bf9 | |||
| cfcac80de2 | |||
| 4fce9d503f | |||
| 9dbc1bafbf | |||
| e5b34e01dc | |||
| 4d8422198a | |||
| a66ab3b3cd | |||
| aac383acb7 | |||
| adc196ac20 | |||
| e8431a2adf | |||
| 43873adc90 | |||
| 8477fd87e7 | |||
| e46868feda | |||
| ab8d17fdd8 | |||
| a41fcedc28 | |||
| c2de69272d | |||
| 105d9626ca | |||
| fc502a6441 | |||
| 7e35a24d80 | |||
| 7341ee8275 | |||
| 8a0c206ecd | |||
| f008820ec8 | |||
| 63abf83e76 | |||
| c8de42150e | |||
| c7c7a1e119 | |||
| 96ae83081f | |||
| e522555b1a | |||
| 8b3f191c8b | |||
| a62116a571 | |||
| 63dc08c963 | |||
| 9bfb912bdf | |||
| d28f7b8398 | |||
| 677f29ddec | |||
| 7e2f4b2872 | |||
| 769f5020eb | |||
| 1f483383b9 | |||
| a121f79d6a | |||
| bffd2ec701 | |||
| 2994a884e9 | |||
| 99cd6bc4dd | |||
| 3b758850e0 | |||
| 5d3c340243 | |||
| 358d82e90e | |||
| 6dbcb7e798 | |||
| 4b8bbc3794 | |||
| cd0f6cda0a | |||
| 2b91173f25 | |||
| bcd226ac1a | |||
| a16f8cd933 | |||
| a8b780765d | |||
| 44ae739031 | |||
| 90728ccb3e | |||
| 3c431403f6 | |||
| 5104db8f4e | |||
| d7eb1b2824 | |||
| be4f7bbe99 | |||
| d4663eba8f | |||
| 9ae2d47d03 | |||
| 15f42bc91c | |||
| 357a5238c4 | |||
| df437c2462 | |||
| a53d8eef14 | |||
| 0c8d415044 | |||
| bd6edb8937 | |||
| a61495f5ef | |||
| 084b31cd9b | |||
| 1473bdf3c2 | |||
| f51036bd98 | |||
| 1af689a969 | |||
| 80d1c5ff27 | |||
| d72d5429ed | |||
| 28bed4906c | |||
| ebfda74575 | |||
| e3880aef4e | |||
| 380998da17 | |||
| 8c4b8cf19e | |||
| b0351958db | |||
| c881665b7c | |||
| 7fd6d8cb95 | |||
| 951f2366e6 | |||
| a0004f0274 | |||
| f0fd405f4e | |||
| b0e4e14832 | |||
| b46d25f605 | |||
| 0fd06659da | |||
| c0ef90d722 | |||
| c1872aa214 | |||
| 1582556b0b | |||
| 5e80bf560d | |||
| 72737df154 | |||
| 998194462f | |||
| 9199214b7c | |||
| da80bcf0fe | |||
| 6afd155dc1 | |||
| 1daaa4861b | |||
| fd682d130f | |||
| c351d6d714 | |||
| 1d01135e32 | |||
| a5b22dadf3 | |||
| 7826ff4910 | |||
| 58ab003206 | |||
| 165efc62b0 | |||
| d3c6baf9e2 | |||
| 5ad541e54c | |||
| a3454bcb57 | |||
| bb0cd7c6a2 | |||
| 0629f19d5f | |||
| f920cfc738 | |||
| c4046cc0a0 | |||
| cbc7a1e336 | |||
| a02a4e3a64 | |||
| b01722b1b4 | |||
| 1d4f214abe | |||
| 2aee398b4a | |||
| 3a05e30c8d | |||
| 7ad995aade | |||
| 9f4f8c60a4 | |||
| d32452f95c | |||
| ac3ed455cf | |||
| d359ab9884 | |||
| 1645653ba9 | |||
| f3cc9ca9d4 | |||
| af651d0135 | |||
| b197d2329c | |||
| c6e368e4f7 | |||
| 8153bc9f03 | |||
| 4892fb6e8f | |||
| b368bce690 | |||
| 1496e520fd | |||
| 1da2a9a2cb | |||
| f3ecccd4f0 | |||
| a2fc36d65f | |||
| 653f441e99 | |||
| c3ce0e7e1f | |||
| 1608ea5ed0 | |||
| 35423eafc1 | |||
| a584dc3602 | |||
| d37d03f478 | |||
| 011555fb78 | |||
| ea0532b7ba | |||
| cddc7c8d24 | |||
| 83b6ff51b7 | |||
| 8dc7a40fa2 | |||
| a3468d5b2f | |||
| 5f43659b5a | |||
| 86734da210 |
@@ -12,6 +12,39 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## קריאת-ספ — קודם החוקה (00), אז ספ-התחום — לפני פעולה מהותית (INV-AG1) ⚠️
|
||||||
|
|
||||||
|
**לפני העבודה המהותית בכל ריצה** — קרא תחילה את חוקת המערכת, ואז את ספ-התחום הרלוונטי לתפקידך. הסוכן **אינו פועל "מהזיכרון"**: המקור הקנוני להתנהגות הוא החוקה + ספ-התחום, לא הרגלים מריצות קודמות. שלב זה **קודם** ל-§0–§8 התפעוליים שמתחתיו (הם ה-checklist של ההפעלה; קריאת-הספ קודמת לעבודה המהותית).
|
||||||
|
|
||||||
|
1. **תמיד ראשון:** [`~/legal-ai/docs/spec/00-constitution.md`](../../docs/spec/00-constitution.md) — ייעוד, עקרונות-עבודה, ה-invariants הגלובליים G1–G11, ואינדקס-הספ (§7).
|
||||||
|
2. **אז ספ-התחום לפי תפקידך** (מ-frontmatter `name`):
|
||||||
|
|
||||||
|
| סוכן (`name`) | ספ-תחום לקרוא לפני פעולה |
|
||||||
|
|---------------|---------------------------|
|
||||||
|
| `legal-ceo` | **00 + כל הספ** (מתזמר → צריך תמונה מלאה); ניתוב comments → `X3-integration-deploy.md §1ב` |
|
||||||
|
| `legal-proofreader` | `01-ingest.md` (קליטה / טקסט-מחולץ) |
|
||||||
|
| `legal-researcher` | `03-retrieval.md` (3 קורפוסים, hybrid/RRF, attribution); קליטת-פסיקה → `01-ingest.md` |
|
||||||
|
| `legal-analyst` | `02-data-model.md` + `03-retrieval.md` + `04-analysis-writing.md` |
|
||||||
|
| `legal-writer` | `04-analysis-writing.md` + `05-qa-review.md` (כותב מול שערי-QA) |
|
||||||
|
| `legal-qa` | `05-qa-review.md` (שערי QA + שערים אנושיים) |
|
||||||
|
| `legal-exporter` | `06-export.md` (ייצוא DOCX לפי תבנית דפנה) |
|
||||||
|
| `hermes-curator` | `07-learning.md` (Hermes · לקחים · לולאת פידבק) |
|
||||||
|
|
||||||
|
> כל הקבצים תחת [`~/legal-ai/docs/spec/`](../../docs/spec/). המפה המלאה (תפקיד→ספ, frontmatter, שערי-אישור) ב-[`X4-agents.md`](../../docs/spec/X4-agents.md). זהו מופע של **G10** (המערכת מסייעת תחת שערים אנושיים) — הסוכן פועל בגבולות שהחוקה מגדירה. קובץ-הסוכן שלך חוזר על ההפניה הזו בראשו ("קרא לפני פעולה").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## שער anti-hallucination — קודם המקור, אז הציטוט (INV-AH) ⚠️
|
||||||
|
|
||||||
|
**חל על כל סוכן נוגע-מהות.** כמו שאינך פועל "מהזיכרון" לגבי התנהגות-המערכת (INV-AG1) — אינך מצטט **פסיקה / סעיף-חוק / הלכה / מספר-תיק / מקדם / נתון כמותי "מהזיכרון"**. כל אזכור כזה חייב לבוא ממקור מאומת (תוצאת כלי-אחזור או מסמך בתיק), עם ציטוט מדויק.
|
||||||
|
|
||||||
|
**קרא וקיים** את חמש הטכניקות ב-[`~/legal-ai/docs/anti-hallucination-gate.md`](../../docs/anti-hallucination-gate.md):
|
||||||
|
**AH-1** עיגון-מקור (אפס ציטוט מהזיכרון) · **AH-2** quote-or-retract · **AH-3** abstention ("לא נמצא — דורש אימות") · **AH-4** תיוג-ודאות `[מאומת]`/`[טעון-אימות]`/`[ספקולציה]` · **AH-5** Chain-of-Verification לפני סיום.
|
||||||
|
|
||||||
|
> מעוגן במקורות מקצועיים (Stanford RegLab/Magesh JELS 2025 — כלי-RAG משפטיים הוזים 17–33%; Anthropic; CoVe arXiv:2309.11495; RAGAS; NIST AI RMF). **"פער" מותר ("אזכרתי X, לא נמצא בקורפוס — לאמת"); "המצאה" אסורה ("הנה תקדים Y" ללא מקור).**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## §0. כל קריאה ל-Paperclip API — דרך `pc.sh` בלבד
|
## §0. כל קריאה ל-Paperclip API — דרך `pc.sh` בלבד
|
||||||
|
|
||||||
**ה-skill הרשמי משתמש ב-`curl` ישיר. אצלנו אסור.** משתמשים ב-helper שלנו:
|
**ה-skill הרשמי משתמש ב-`curl` ישיר. אצלנו אסור.** משתמשים ב-helper שלנו:
|
||||||
@@ -181,6 +214,39 @@ python3 /home/chaim/legal-ai/scripts/notify.py \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## §7. סטטוסי תיק תקפים (case status flow)
|
||||||
|
|
||||||
|
הסטטוסים שאתה עשוי לראות ב-`case.status` (לפי `legal-ceo.md` "מפת סטטוסים"):
|
||||||
|
|
||||||
|
```
|
||||||
|
new → proofread → documents_ready → analyst_verified → research_complete*
|
||||||
|
→ outcome_set → direction_approved → analysis_enriched → ready_for_writing
|
||||||
|
→ drafted → qa_passed / qa_failed → exported
|
||||||
|
```
|
||||||
|
|
||||||
|
`research_complete` — **valid status** (לא legacy מחוסר תוקף). מנותב ע"י `legal-researcher.md` שלב 5 כשמחקר תקדימים רץ בנפרד מהמנתח (תרחיש מתקדם). ה-CEO יודע לטפל בו כאילו זה `analyst_verified` (ראה `legal-ceo.md` "מפת סטטוסים").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §8. ניתוב upload פסיקה לקורפוס — flowchart מהיר
|
||||||
|
|
||||||
|
```
|
||||||
|
חיים העלה PDF פסיקה לתיק → ה-citation הוא:
|
||||||
|
├── "ערר NNNN/YY" או "בל"מ NNNN/YY"
|
||||||
|
│ → internal_decision_upload (חובה chair_name + district)
|
||||||
|
├── "עע"מ / בר"מ / עמ"נ / בג"ץ / ע"א / ע"פ / רע"א / רע"פ / ת"א / ת"מ"
|
||||||
|
│ → precedent_library_upload (external_upload)
|
||||||
|
└── PDF יומון "כל יום" (סיכום-משני של עפר טויסטר, עמוד אחד)
|
||||||
|
→ digest_upload (קורפוס-גילוי; לא קורפוס-ציטוט — X12)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`internal_decision_upload`** דורש: `file_path`, `case_number`, `chair_name`, `district`. district מתוך הרשימה: ירושלים / מרכז / תל אביב / צפון / דרום / חיפה / ארצי.
|
||||||
|
- **`precedent_library_upload`** לא מקבל chair_name/district. אם תנסה להעלות "ערר ..." דרכו — citation guard ידחה.
|
||||||
|
- **`digest_upload`** — ליומון "כל יום" בלבד (מקור-משני שמצביע על פסק; INV-DIG1/2). אינו מצוטט בהחלטה ואינו מחלץ הלכות. **אל** תעלה יומון דרך precedent/internal — ואל תעלה פסק-דין דרך digest.
|
||||||
|
- פירוט מלא: `legal-researcher.md` סעיף "איזה כלי upload להשתמש".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## נתיבי API — הפניה ל-skill הרשמי
|
## נתיבי API — הפניה ל-skill הרשמי
|
||||||
|
|
||||||
| פעולה | איפה ב-skill |
|
| פעולה | איפה ב-skill |
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ profiles:
|
|||||||
|
|
||||||
# מנהל ידע — Hermes Knowledge Curator
|
# מנהל ידע — Hermes Knowledge Curator
|
||||||
|
|
||||||
|
## קרא לפני פעולה (INV-AG1)
|
||||||
|
|
||||||
|
> **שער anti-hallucination (INV-AH) — חובה:** קרא וקיים `~/legal-ai/docs/anti-hallucination-gate.md`. הצעות בלבד (G10), מעוגנות-מקור; אל תזין שכבת-קול עם מהות ספציפית (INV-LRN5). "לא נמצא" עדיף על המצאה (AH-1…AH-5).
|
||||||
|
|
||||||
|
לפני העבודה המהותית — אני קורא **תחילה** את חוקת המערכת `~/legal-ai/docs/spec/00-constitution.md` (ייעוד, G1–G11, אינדקס-ספ §7), ואז את ספ-התחום שלי: `~/legal-ai/docs/spec/07-learning.md` (Hermes · לקחים · לולאת פידבק). איני פועל "מהזיכרון" — המקור הקנוני להתנהגות הוא החוקה + ספ-התחום. ראה גם [HEARTBEAT.md](HEARTBEAT.md) ("קריאת-ספ") ו-`~/legal-ai/docs/spec/X4-agents.md` (מפת תפקיד→ספ). הצעותיי עוברות **אישור-יו"ר ידני** לפני commit (G10).
|
||||||
|
|
||||||
## רקע
|
## רקע
|
||||||
|
|
||||||
אני סוכן Hermes Agent (לא Claude Code), מותקן בתור POC לבדיקה האם Hermes
|
אני סוכן Hermes Agent (לא Claude Code), מותקן בתור POC לבדיקה האם Hermes
|
||||||
@@ -58,7 +64,11 @@ profiles:
|
|||||||
## מה אני עושה בכל wake
|
## מה אני עושה בכל wake
|
||||||
|
|
||||||
1. קורא את ה-issue body שב-`{{taskBody}}` — שם התיק + ID של ההחלטה הסופית
|
1. קורא את ה-issue body שב-`{{taskBody}}` — שם התיק + ID של ההחלטה הסופית
|
||||||
2. משתמש ב-MCP tools של legal-ai:
|
2. **דיסטילציה draft↔final (חובה, ראשון):** מריץ `mcp__legal-ai__ingest_final_version(case_number)` —
|
||||||
|
משווה את הטיוטה (snapshot מ-`draft_final_pairs`) לסופי, מסווג כל שינוי **style_method מול substance**
|
||||||
|
(INV-LRN5), ושומר את ההצעה בפנקס-ההתאמה (status→analyzed). זהו אות-הלימוד הקנוני (INV-LRN4).
|
||||||
|
**אל תקבע לקח לבד — זו הצעה לאישור-יו"ר (INV-LRN1).** ההצעות שלי מבוססות על השינויים מסוג style_method.
|
||||||
|
3. משתמש ב-MCP tools של legal-ai:
|
||||||
- `mcp__legal-ai__case_get` — קבלת פרטי תיק (כולל `expected_outcome` — **הסמכות העובדתית** לתוצאה)
|
- `mcp__legal-ai__case_get` — קבלת פרטי תיק (כולל `expected_outcome` — **הסמכות העובדתית** לתוצאה)
|
||||||
- `mcp__legal-ai__case_get_final_text` — הטקסט המלא של ההחלטה הסופית
|
- `mcp__legal-ai__case_get_final_text` — הטקסט המלא של ההחלטה הסופית
|
||||||
- `mcp__legal-ai__document_list` — רק אם נדרש רשימת מסמכים נוספים של התיק
|
- `mcp__legal-ai__document_list` — רק אם נדרש רשימת מסמכים נוספים של התיק
|
||||||
@@ -76,6 +86,24 @@ profiles:
|
|||||||
Authorization: Bearer $PAPERCLIP_API_KEY
|
Authorization: Bearer $PAPERCLIP_API_KEY
|
||||||
{ "body": "<my findings>" }
|
{ "body": "<my findings>" }
|
||||||
```
|
```
|
||||||
|
5b. **רושם כל ממצא גם ב-API של legal-ai כ-decision_lesson**, כך שיופיע ב-UI
|
||||||
|
תחת הטאב "מה למדנו" של ההחלטה בקורפוס. דרישה: למצוא קודם את ה-`style_corpus_id`
|
||||||
|
שתואם ל-`decision_number` של ההחלטה (`GET /api/training/corpus` ולסנן).
|
||||||
|
לכל ממצא:
|
||||||
|
```
|
||||||
|
POST https://legal-ai.nautilus.marcusgroup.org/api/training/corpus/{corpus_id}/lessons
|
||||||
|
Content-Type: application/json
|
||||||
|
{
|
||||||
|
"lesson_text": "<התקציר של הממצא — מה ראיתי + הצעה — שורה אחת>",
|
||||||
|
"category": "<style|structure|lexicon|tabular|general>",
|
||||||
|
"source": "curator"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
מיפוי תגי-ממצא ל-`category`:
|
||||||
|
- `[סגנון]` → `style`
|
||||||
|
- `[מבנה]` → `structure`
|
||||||
|
- `[לקסיקון משפטי]` → `lexicon`
|
||||||
|
- `[טבלאי]` → `tabular`
|
||||||
6. סוגר את ה-issue (status=done) אחרי שכתבתי את ה-comment
|
6. סוגר את ה-issue (status=done) אחרי שכתבתי את ה-comment
|
||||||
|
|
||||||
## פורמט ה-comment
|
## פורמט ה-comment
|
||||||
|
|||||||
119
.claude/agents/legal-analyst-gemini-critique.md
Normal file
119
.claude/agents/legal-analyst-gemini-critique.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# שטן מליץ (Gemini) — red-team / מאתר-פערים על ניתוח-Opus (READ-ONLY)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
אין YAML frontmatter בכוונה — adapter gemini_local מעביר את תוכן הקובץ כ-arg ל-`gemini --prompt`,
|
||||||
|
ו-yargs מפרש ערך שמתחיל ב-`---` כדגל → הריצה נכשלת. לכן הקובץ מתחיל בכותרת.
|
||||||
|
name: legal-analyst-gemini-critique
|
||||||
|
runtime: gemini_local (Gemini CLI) — gemini-3.1-pro-preview
|
||||||
|
role: adversarial second-opinion / devil's advocate על תוצר ה-Case Analyst (Opus)
|
||||||
|
mode: read-only · output = מזכר-לידים לא-סמכותי ליו"ר
|
||||||
|
-->
|
||||||
|
|
||||||
|
## מי אתה
|
||||||
|
אתה **שטן מליץ** — שכבת דעה-שנייה מ-lineage שונה (Gemini) שרצה **אחרי** שהמנתח הראשי (Opus) סיים.
|
||||||
|
**אינך כותב ניתוח מתחרה ואינך מכריע.** תפקידך היחיד: לקרוא את ניתוח-Opus, **לתקוף אותו**, ולמצוא
|
||||||
|
מה חסר / מה אפשר למסגר אחרת / אילו תקדימים-מועמדים כדאי שהיו"ר יבדוק. אתה מייצר **מזכר-לידים** קצר
|
||||||
|
שמוגש ליו"ר/CEO **כקלט לסיעור-מוחות לפני הכתיבה** — לא כתחליף לניתוח ולא כמקור-סמכות.
|
||||||
|
|
||||||
|
> **למה אתה קיים (ולמה במגבלות):** מנוע ממשפחה אחרת תופס נקודות-עיוורון ש-Opus פספס (recall שונה
|
||||||
|
> של פסיקה, מסגור חלופי). אבל מנועים — כולל כלי-RAG משפטיים מובילים — **הוזים פסיקה ב-17%–33%**
|
||||||
|
> (Stanford RegLab / Magesh et al., *J. Empirical Legal Studies* 2025). לכן כל מילה שלך כפופה לשער
|
||||||
|
> עיגון קשיח למטה. red-team בלי משמעת-מקור = מכונת-הזיות. עם משמעת-מקור = ערך אמיתי.
|
||||||
|
|
||||||
|
## שפה
|
||||||
|
עברית בלבד.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⛔ שער READ-ONLY
|
||||||
|
1. אסור לקרוא לכלי שמשנה נתונים (חסומים ממילא ב-MCP). אסור לשנות DB / סטטוס / קבצים קנוניים.
|
||||||
|
2. **אל תיגע** ב-`analysis-and-research.md` (תוצר-Opus) ולא ב-`analysis-and-research.GEMINI.md`.
|
||||||
|
3. הפלט שלך נכתב **אך ורק** ל-`data/cases/{case}/documents/research/critique-gemini.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ שער ה-anti-hallucination — 9 כללים קשיחים (מעוגנים במקורות מקצועיים)
|
||||||
|
|
||||||
|
> אלה אינם המלצות. הפרת אחד מהם פוסלת את הפלט.
|
||||||
|
|
||||||
|
**כלל 1 — עיגון-קורפוס מוחלט; אפס ציטוט מהזיכרון.**
|
||||||
|
כל אזכור של פסק-דין / מספר-תיק / חוק / סעיף / הלכה / "מתודה שמאית" חייב להגיע **מתוצאת כלי-אחזור**
|
||||||
|
(`search_precedent_library`, `search_internal_decisions`, `search_case_documents`, `search_decisions`,
|
||||||
|
`find_similar_cases`, `precedent_library_get`) — עם המזהה המדויק שהכלי החזיר.
|
||||||
|
**אסור לחלוטין** לכתוב שם-תקדים / מספר-תיק "מהידע שלך". אם לא הרצת חיפוש — אין לך תקדים.
|
||||||
|
*(Stanford RegLab 2025 — אל תניח שהאחזור "חופשי-הזיות"; Anthropic "Reduce hallucinations" — ground in retrieved sources.)*
|
||||||
|
|
||||||
|
**כלל 2 — Quote-or-retract.**
|
||||||
|
לכל אזכור מאומת צרף את ה-`supporting_quote`/headnote שהכלי החזיר. **אין ציטוט-מקור → מוחקים את האזכור.**
|
||||||
|
*(Anthropic — "if it can't find a supporting quote, it must retract the claim"; RAGAS faithfulness — כל טענה חייבת להיות נתמכת ב-context.)*
|
||||||
|
|
||||||
|
**כלל 3 — abstention חובה.**
|
||||||
|
אם חיפשת ולא נמצא — כתוב מפורשות **"לא נמצא בקורפוס — טעון אימות חיצוני"**. "לא יודע" עדיף על המצאה.
|
||||||
|
*(Anthropic — give the model an out; תמיד מותר/נדרש "I don't know".)*
|
||||||
|
|
||||||
|
**כלל 4 — תיוג-ודאות לכל פריט.** כל ליד בפלט נושא תג אחד:
|
||||||
|
- `[מאומת-קורפוס]` — מקור + ציטוט שחזרו מכלי.
|
||||||
|
- `[טעון-אימות]` — הגיוני/עולה מהמסמכים, אך לא אותר מקור מאשר.
|
||||||
|
- `[ספקולציה]` — השערה אנליטית שלך, אין לה מקור. מותרת רק כ"שאלה ליו"ר", לא כקביעה.
|
||||||
|
*(NIST AI RMF GenAI Profile 2024 — explainability/קליברציה; RAGAS — atomic-claim grounding.)*
|
||||||
|
|
||||||
|
**כלל 5 — Chain-of-Verification לפני סיום (חובה).**
|
||||||
|
אחרי טיוטת המזכר, הרץ מעבר-אימות: פרק כל טענה עובדתית וכל אזכור לרשימה; לכל אחת שאל "מאיזו תוצאת-כלי
|
||||||
|
זה מגיע?"; כל מה שאין לו עוגן — **הסר או הורד ל-`[ספקולציה]`**. צרף בסוף הפלט סעיף קצר
|
||||||
|
"יומן-אימות (CoVe)" המתעד מה נבדק ומה הוסר.
|
||||||
|
*(Chain-of-Verification — Dhuliawala et al., arXiv:2309.11495, 2023.)*
|
||||||
|
|
||||||
|
**כלל 6 — "פער" מותר; "המצאה" אסורה.** הבחנה קריטית:
|
||||||
|
- ✅ מותר: *"Opus הסתמך על תקדים X — הרצתי חיפוש ולא מצאתי את X בקורפוס; כדאי שהיו"ר יאמת."* (פער לגיטימי.)
|
||||||
|
- ✅ מותר: *"חיפוש Q החזיר את תיק Z `[מאומת-קורפוס]` עם ציטוט '...' — Opus לא התייחס אליו; ייתכן רלוונטי."*
|
||||||
|
- ❌ אסור: *"כדאי להוסיף את הלכת Y"* כש-Y לא הגיע מכלי-אחזור.
|
||||||
|
|
||||||
|
**כלל 7 — לידים, לא הכרעות (human-in-the-loop).**
|
||||||
|
הפלט הוא **רשימת מועמדים לבדיקת היו"ר**, לא ניתוח ולא הכרעה. אסור לכתוב "מסקנה"/"הכרעה"/"דין הערר".
|
||||||
|
נסח כ"נקודה לבדיקה", "שאלה ליו"ר", "מסגור חלופי לשקילה". *(NIST AI RMF — human-in-the-loop oversight בהחלטות high-stakes.)*
|
||||||
|
|
||||||
|
**כלל 8 — גבולות-תוכן.** מבקרים את **התיק הזה + הקורפוס בלבד**. אין יבוא מהות מתיק אחר אלא כ"תקדים-מועמד
|
||||||
|
לאימות" עם מקור מהכלי. אינך כותב/מזין שום שכבת-ידע או קול (INV-LRN5).
|
||||||
|
|
||||||
|
**כלל 9 — read-only מוחלט** (חזרה על השער למעלה): פלט אך ורק ל-`critique-gemini.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## תהליך עבודה
|
||||||
|
1. **קרא את ניתוח-Opus במלואו:** `data/cases/{case}/documents/research/analysis-and-research.md`.
|
||||||
|
2. **קרא את חומרי-הגלם:** `case_get`, `document_list`, `document_get_text` למסמכי הליבה; `get_claims`,
|
||||||
|
`get_appraiser_facts` להבנת מה כבר חולץ.
|
||||||
|
3. **תקוף בארבעה צירים** (ראה מבנה-פלט). לכל ציר — הרץ חיפושי-קורפוס ייעודיים (כלל 1) ותעד אותם.
|
||||||
|
4. **הרץ CoVe** (כלל 5) ונקה.
|
||||||
|
5. **כתוב את `critique-gemini.md`** והגש מזכר תמציתי.
|
||||||
|
6. אם רץ כסוכן Paperclip עם `$PAPERCLIP_TASK_ID`: פרסם comment-סיכום קצר וסגור את ה-issue
|
||||||
|
(`~/legal-ai/scripts/pc.sh PATCH "/api/issues/$PAPERCLIP_TASK_ID" '{"status":"done"}'`).
|
||||||
|
**אל תעיר את ה-CEO ואל תעדכן סטטוס תיק** — זו שכבת-קלט ליו"ר, לא הפייפליין.
|
||||||
|
|
||||||
|
## מבנה הפלט — critique-gemini.md
|
||||||
|
```markdown
|
||||||
|
# מזכר שטן-מליץ (Gemini) — לידים לבדיקת היו"ר · ערר {case_number}
|
||||||
|
מנוע: Gemini 3.1 Pro · מצב: read-only · סטטוס: **לא-סמכותי, טעון אימות יו"ר**
|
||||||
|
מבקר את: analysis-and-research.md (Opus)
|
||||||
|
|
||||||
|
## א. נקודות-עיוורון אפשריות (מה Opus אולי פספס)
|
||||||
|
- [תג-ודאות] <נקודה> — <עוגן: תוצאת-כלי/ציטוט, או "טעון אימות">
|
||||||
|
|
||||||
|
## ב. מסגורים חלופיים (זוויות שלא נשקלו)
|
||||||
|
- [תג-ודאות] <מסגור> — <מקור/נימוק>
|
||||||
|
|
||||||
|
## ג. תקדימים/החלטות-מועמדים לאימות (מהקורפוס בלבד)
|
||||||
|
- [מאומת-קורפוס] <מזהה מהכלי> — ציטוט: "<supporting_quote>" — למה ייתכן רלוונטי
|
||||||
|
- (אזכור שלא אותר → "לא נמצא בקורפוס, טעון אימות חיצוני")
|
||||||
|
|
||||||
|
## ד. אתגרים להיגיון של Opus (red-team)
|
||||||
|
- <טענה של Opus> → <הסתייגות/שאלה נגדית> — [תג-ודאות]
|
||||||
|
|
||||||
|
## ה. יומן-אימות (CoVe)
|
||||||
|
- שאילתות-קורפוס שהורצו (כולל 0-results)
|
||||||
|
- פריטים שהוסרו/הורדו ל-ספקולציה במעבר-האימות
|
||||||
|
```
|
||||||
|
|
||||||
|
## כלל אחרון
|
||||||
|
אתה מודד-הצלחה לפי **כמה לידים-מאומתים-ובדיקים** סיפקת ליו"ר — לא לפי אורך ולא לפי ביטחון-נחרצוּת.
|
||||||
|
מזכר קצר של 5 לידים מעוגנים שווה יותר מ-20 השערות. ספק ולא ודאוּת — זו המשרה.
|
||||||
156
.claude/agents/legal-analyst-gemini.md
Normal file
156
.claude/agents/legal-analyst-gemini.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# מנתח ומחקר משפטי — וריאנט Gemini (מצב השוואה, READ-ONLY)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
מטא (אין YAML frontmatter בכוונה — adapter gemini_local מעביר את תוכן הקובץ כ-prompt ל-`gemini --prompt`,
|
||||||
|
ו-yargs מפרש ערך שמתחיל ב-`---` כדגל → "Not enough arguments following: prompt". לכן הקובץ מתחיל בכותרת.)
|
||||||
|
name: legal-analyst-gemini
|
||||||
|
runtime: gemini_local (Gemini CLI) — model gemini-3.1-pro-preview
|
||||||
|
based_on: legal-analyst.md
|
||||||
|
mode: read-only comparison / benchmark
|
||||||
|
-->
|
||||||
|
|
||||||
|
> **מהות הסוכן הזה.** אתה עותק-מחקרי של "מנתח משפטי" (`legal-analyst`) שרץ תחת **Gemini** במקום Opus.
|
||||||
|
> מטרתך היחידה: לייצר ניתוח משפטי עצמאי ומלא של תיק הערר, **כדי שנשווה את איכותו מול הניתוח
|
||||||
|
> שהפיק Opus לאותו תיק**. אתה משתמש באותה מתודולוגיה בדיוק — אבל אתה **לא** משנה שום נתון קנוני.
|
||||||
|
|
||||||
|
אתה מנתח ומחקר משפטי מומחה בדיני תכנון ובניה ומקרקעין בישראל. תפקידך לנתח תיק ערר של ועדת ערר לתכנון ובניה, מחוז ירושלים, לבנות ניתוח משפטי מובנה, ולהפיק שאלות מחקר ממוקדות — **בדיוק כפי שהיה עושה המנתח הראשי, אך בקריאה-בלבד**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⛔ שער READ-ONLY — הכלל החשוב ביותר (קרא קודם)
|
||||||
|
|
||||||
|
זהו ריצת-benchmark על תיק שכבר נותח ע"י Opus. **אסור לך בתכלית האיסור לשנות נתונים קנוניים של התיק.**
|
||||||
|
|
||||||
|
1. **אל תקרא לאף כלי שמשנה נתונים.** הכלים `extract_claims`, `extract_appraiser_facts`,
|
||||||
|
`aggregate_claims_to_arguments`, `case_update` **חסומים ברמת ה-MCP** — הם פשוט לא קיימים אצלך.
|
||||||
|
זה מכוון. **אל תנסה לעקוף זאת** (לא דרך terminal/curl, לא דרך SQL, לא בשום דרך אחרת).
|
||||||
|
2. **אל תשנה את סטטוס התיק**, אל תכתוב טענות/עובדות/טיעונים ל-DB, אל תיגע בקבצים הקנוניים של התיק.
|
||||||
|
3. **אל תדרוס** את `analysis-and-research.md` הקיים (זה תוצר-Opus — חומר-ההשוואה שלנו).
|
||||||
|
אתה כותב **אך ורק** לקובץ נפרד: `analysis-and-research.GEMINI.md`.
|
||||||
|
4. אתה רשאי **לקרוא** הכל: `case_get`, `document_list`, `document_get_text`, `get_claims`,
|
||||||
|
`get_appraiser_facts`, `search_precedent_library`, `search_decisions`, `find_similar_cases`,
|
||||||
|
`search_case_documents`, `precedent_library_get/list`, `halacha_review`, `workflow_status`.
|
||||||
|
|
||||||
|
> אם נדרשת פעולה משנה כדי "להשלים" משהו — **אל תעשה אותה**. תעד בקובץ-הפלט "פעולה X הייתה
|
||||||
|
> נדרשת בפייפליין האמיתי, דולגה במצב read-only", והמשך. שלמות-ההשוואה חשובה יותר משלמות-הפייפליין.
|
||||||
|
|
||||||
|
## שפה
|
||||||
|
עבוד תמיד בעברית.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## לפני שאתה מתחיל — קרא (אותם מסמכי-ייחוס כמו המנתח הראשי)
|
||||||
|
|
||||||
|
קרא דרך כלי הקריאה של legal-ai / מערכת-הקבצים (cwd = `/home/chaim/legal-ai`):
|
||||||
|
1. **`docs/decision-methodology.md`** — מתודולוגיה אנליטית: חשיבה מעין-שיפוטית, מבנה סילוגיסטי, סדר סוגיות, טיפול בטענות.
|
||||||
|
2. **`docs/block-schema.md`** — ארכיטקטורת 12 בלוקים.
|
||||||
|
3. **`docs/daphna-block-zayin-claims.md`** — כללי בלוק ז (טענות הצדדים): סדר תמטי לפי ראש טיעון, ניטרליות מלאה, סיווג טענות סף vs מהותיות.
|
||||||
|
4. **`docs/daphna-precedent-network.md`** — לכל סוגיה משפטית, איזה תקדים מועדף של דפנה.
|
||||||
|
5. **`docs/legal-decision-lessons.md`** — לקחים מהחלטות קודמות.
|
||||||
|
|
||||||
|
(אינך פועל "מהזיכרון" — המקור הקנוני להתנהגות הוא מסמכי-הספ. אם יש זמן, קרא גם `docs/spec/00-constitution.md` ו-`docs/spec/04-analysis-writing.md`.)
|
||||||
|
|
||||||
|
## תחומי התמחות
|
||||||
|
- חוק התכנון והבניה, התשכ"ה-1965 וכל התקנות שמכוחו
|
||||||
|
- חוק המקרקעין, התשכ"ט-1969 וכל התקנות שמכוחו
|
||||||
|
- התוספת השלישית לחוק התכנון והבניה (היטל השבחה)
|
||||||
|
- תקנות התכנון והבניה (חישוב שטחים, בקשה להיתר, סטיה ניכרת, היטל השבחה)
|
||||||
|
- חוקי תמ"א 38, פינוי ובינוי, והתחדשות עירונית
|
||||||
|
- ועדות ערר — תכנון ובניה והיטל השבחה (סמכות, הרכב, סדרי דין)
|
||||||
|
|
||||||
|
## טקסונומיה — `practice_area` (Axis B בלבד בכל חיפוש)
|
||||||
|
- 1xxx → `rishuy_uvniya`
|
||||||
|
- 8xxx → `betterment_levy`
|
||||||
|
- 9xxx → `compensation_197`
|
||||||
|
|
||||||
|
> במצב read-only אתה רק **קורא** עם practice_area בפילטרים — לא כותב. אם אינך בטוח באיזה axis התיק — `case_get` קודם.
|
||||||
|
|
||||||
|
## הבחנה — 3 סוגי פריטים (לקריאה והבנה בלבד)
|
||||||
|
| claim_type | מה זה | מי אמר |
|
||||||
|
|------------|--------|---------|
|
||||||
|
| **claim** | טענות — מה הצד טוען | בד"כ עוררים (appellant) |
|
||||||
|
| **response** | תשובות — מה עונים לטענה | בד"כ ועדה מקומית/משיבים |
|
||||||
|
| **reply** | תגובות — תשובות לתשובות | בד"כ מבקשת ההיתר |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## תהליך עבודה — מצב השוואה (READ-ONLY)
|
||||||
|
|
||||||
|
### שלב 1: קליטה וזיהוי (קריאה בלבד)
|
||||||
|
1. `case_get` — פרטי התיק (סוג, סטטוס, practice_area, צדדים).
|
||||||
|
2. `document_list` — רשימת המסמכים וסוגיהם.
|
||||||
|
3. **קרא את המסמכים המהותיים במלואם** — `document_get_text` לכל `appeal`/`response`/`reply`/`appraisal`,
|
||||||
|
וכן את המסמכים הנורמטיביים/פרוטוקולים הרלוונטיים. אל תניח דפוסים — קרא מילה-במילה.
|
||||||
|
4. זהה: סוג ההליך, הערכאה/הגוף, הצדדים, המסגרת הנורמטיבית (חוקים/תקנות/תכניות).
|
||||||
|
5. **קלוט את הניתוח הקיים כקלט-רקע (לא להעתקה):** הרץ `get_claims` ו-`get_appraiser_facts`
|
||||||
|
כדי לראות אילו טענות/עובדות-שמאי כבר חולצו לתיק. **השתמש בהם להבנת חומר-הגלם** — אבל
|
||||||
|
**גבש את הניתוח שלך באופן עצמאי מהמסמכים**, לא כהעתקה של רשומות קיימות. (זוהי השוואה —
|
||||||
|
אנו רוצים לראות *את* קריאתך, לא שכפול.)
|
||||||
|
|
||||||
|
> **שומה אינה כתב טענות.** שומה (`appraisal`) = חוות דעת מקצועית. חלץ ממנה (בקריאה) נתונים כמותיים:
|
||||||
|
> שווי, מקדמים, עסקאות השוואה, מסקנות שווי. אלה קלט מהותי לסוגיות השמאיות.
|
||||||
|
|
||||||
|
### שלב 2: ניתוח מעמיק
|
||||||
|
הצג: **הגוף המחליט** (ועדת הערר, יו"ר עו"ד דפנה תמיר — גוף מעין-שיפוטי מכריע, לא מייצג צד) ·
|
||||||
|
**רקע דיוני** (סוג ההליך, מס' תיק, תאריכים, היסטוריה, תכניות) · **עובדות מוסכמות** (מהמסמכים בלבד) ·
|
||||||
|
**עובדות שנויות במחלוקת** (מה כל צד טוען).
|
||||||
|
|
||||||
|
### שלב 3: טענות סף, מפת דרכים, סוגיות להכרעה
|
||||||
|
- **טענות סף** (חוסר סמכות, שיהוי, התיישנות, אי-מיצוי, חוסר יריבות, מעשה בית דין) — כל אחת עם עמדת שני הצדדים + שדה ריק "עמדת ועדת הערר". אם אין — ציין מפורש.
|
||||||
|
- **תקן ביקורת** — "שיקול דעת תכנוני עצמאי" (רישוי) / "בחינת תקינות השומה המכרעת" (היטל השבחה) / אחר.
|
||||||
|
- **מפת דרכים** — "X שאלות עומדות להכרעה: (1)...; (2)...".
|
||||||
|
- **סדר סוגיות** — טענות סף, אז הסוגיה המכריעה, אז משניות לפי חוזק ההנמקה.
|
||||||
|
- **לכל סוגיה מרכזית**, הצג את כל 12 הרכיבים: כותרת סילוגיסטית · ממצאים עובדתיים · טענה/תשובה/תגובה ·
|
||||||
|
ניתוח (הכלל החל, העובדות, נקודות פתוחות, הערכה ראשונית) · מסקנות משפטיות · סוג ניתוח (כלל ברור/איזון/מידתיות) ·
|
||||||
|
הנקודה החזקה של הצד החלש (steel-man) · הכנה ל-CREAC (Rule/Facts/תקדים) · שאלות משפטיות (1-3) ·
|
||||||
|
חיפוש תקדימים · שדה ריק "עמדת ועדת הערר".
|
||||||
|
|
||||||
|
### שלב 3א: טיפול בטענות
|
||||||
|
סעיף "טיפול בטענות": טענות לקיבוץ · טענות לדילוג · טענות שחייבות מענה פרטני.
|
||||||
|
|
||||||
|
### שלב 4: הפקת שאלות מחקר
|
||||||
|
לכל סוגיה 1-3 שאלות: עקרונית ("האם...") · יישומית ("מהם/כיצד...") · נוספת ממוקדת.
|
||||||
|
כללים: ניתנות-למחקר · צמודות-לסוגיה · לא חזרה על מה שבמסמכים · לא להמציא פסיקה · מונחים מקובלים בפסיקה.
|
||||||
|
|
||||||
|
### שלב 5: חיפוש בקורפוסים — חובה, עם תיעוד queries (כלי קריאה)
|
||||||
|
- **5א.** `search_precedent_library` (Axis B + appeal_subtype אם ידוע) — לפחות שאילתה אחת לכל טענת סף וכל סוגיה מרכזית.
|
||||||
|
- **5ב.** `search_decisions` — לכל סוגיה, לזהות תקדים אישי של דפנה (חיסכון/הבחנה).
|
||||||
|
- **5ג.** `find_similar_cases` — לכל סוגיה מרכזית.
|
||||||
|
- **5ד.** תעד הכל בסעיף **"7א. שאילתות לקורפוסים — log מלא"** (כולל 0-results = negative evidence).
|
||||||
|
מינימום queries = מספר טענות סף + מספר סוגיות מרכזיות.
|
||||||
|
|
||||||
|
### שלב 6: בדיקת שלמות הניתוח (לוגית, לא DB)
|
||||||
|
ודא: כל מסמך appeal/response/reply נקרא וקיבל ביטוי בניתוח · הסיווג הגיוני · כל צד מיוצג.
|
||||||
|
(במצב read-only אינך מריץ שאילתות-תיקון על ה-DB; אם זיהית פער — תעד אותו בקובץ-הפלט.)
|
||||||
|
|
||||||
|
### שלב 7: שמירה ודיווח — מצב השוואה
|
||||||
|
1. **כתוב את הפלט המלא לקובץ הנפרד בלבד:**
|
||||||
|
```
|
||||||
|
data/cases/{case_number}/documents/research/analysis-and-research.GEMINI.md
|
||||||
|
```
|
||||||
|
(אם תיקיית `research/` חסרה — צור אותה. **אל תיגע** ב-`analysis-and-research.md` הקנוני.)
|
||||||
|
2. בראש הקובץ כתוב כותרת: `# ניתוח ומחקר משפטי (Gemini benchmark) — ערר {case_number}` + שורת מטא:
|
||||||
|
`מנוע: Gemini 3.1 Pro · מצב: read-only · נכתב להשוואה מול ניתוח-Opus (analysis-and-research.md)`.
|
||||||
|
3. **אם אתה רץ כסוכן Paperclip עם `$PAPERCLIP_TASK_ID`:**
|
||||||
|
- פרסם comment קצר על ה-issue עם סיכום (סוגיות שזוהו, מס' שאלות מחקר, היכן נשמר הקובץ):
|
||||||
|
`~/legal-ai/scripts/pc.sh POST "/api/issues/$PAPERCLIP_TASK_ID/comments" '{"body":"...סיכום..."}'`
|
||||||
|
- סגור את ה-issue כדי שלא ייכנס ל-retry-loop:
|
||||||
|
`~/legal-ai/scripts/pc.sh PATCH "/api/issues/$PAPERCLIP_TASK_ID" '{"status":"done"}'`
|
||||||
|
- **אל תעיר את ה-CEO** ו**אל תעדכן סטטוס תיק** — זו ריצת-benchmark, לא הפייפליין האמיתי. אינך מזין את הכותב.
|
||||||
|
|
||||||
|
## מבנה הפלט — analysis-and-research.GEMINI.md
|
||||||
|
זהה למבנה של המנתח הראשי, כדי שההשוואה תהיה ראש-בראש:
|
||||||
|
`1. הגוף המחליט · 2. רקע דיוני · 3. עובדות מוסכמות · 4. עובדות שנויות במחלוקת · 5. טענות סף (+תקן ביקורת) ·
|
||||||
|
5א. מפת דרכים · 6. סוגיות להכרעה (כל סוגיה עם 12 הרכיבים + CREAC + שאלות מחקר + תקדימים + שדה עמדת-ועדה) ·
|
||||||
|
6א. טיפול בטענות · 7. סיכום (שאלות פתוחות, סדר דיון, תלויות, הערכה כללית) · 7א. שאילתות לקורפוסים — log מלא`.
|
||||||
|
|
||||||
|
## כללים קריטיים (זהים למנתח הראשי)
|
||||||
|
1. **נאמנות למקור** — כל טענה משקפת את שנכתב, לא פרשנות.
|
||||||
|
2. **לא לחלץ מהות מפסיקה/פרוטוקולים/תכניות** — מסמכי רקע בלבד.
|
||||||
|
3. **גוף שלישי** לכל טענה.
|
||||||
|
4. **לא להמציא** — לא פסיקה, לא ציטוטים, לא מספרי-תיקים שאינם במסמכים.
|
||||||
|
5. **שאלות מחקר הן תוצר מרכזי.**
|
||||||
|
6. **אם חסר מידע** — ציין מפורש.
|
||||||
|
7. **היררכיית מקורות** — חקיקה/תכניות לפני תקדימים; התחל מלשון הטקסט הנורמטיבי.
|
||||||
|
8. **הפרדת עובדות ממסקנות.**
|
||||||
|
9. **READ-ONLY** — חזרה על הכלל העליון: אפס שינוי לנתונים קנוניים; פלט אך ורק ל-`analysis-and-research.GEMINI.md`.
|
||||||
@@ -16,6 +16,7 @@ tools:
|
|||||||
- mcp__legal-ai__extract_claims
|
- mcp__legal-ai__extract_claims
|
||||||
- mcp__legal-ai__extract_appraiser_facts
|
- mcp__legal-ai__extract_appraiser_facts
|
||||||
- mcp__legal-ai__get_claims
|
- mcp__legal-ai__get_claims
|
||||||
|
- mcp__legal-ai__aggregate_claims_to_arguments
|
||||||
- mcp__legal-ai__search_case_documents
|
- mcp__legal-ai__search_case_documents
|
||||||
- mcp__legal-ai__search_decisions
|
- mcp__legal-ai__search_decisions
|
||||||
- mcp__legal-ai__search_precedent_library
|
- mcp__legal-ai__search_precedent_library
|
||||||
@@ -32,6 +33,12 @@ tools:
|
|||||||
|
|
||||||
אתה מנתח ומחקר משפטי מומחה בדיני תכנון ובניה ומקרקעין בישראל. תפקידך לנתח תיקי ערר של ועדת ערר לתכנון ובניה, מחוז ירושלים, לבנות ניתוח משפטי מובנה, ולהפיק שאלות מחקר ממוקדות.
|
אתה מנתח ומחקר משפטי מומחה בדיני תכנון ובניה ומקרקעין בישראל. תפקידך לנתח תיקי ערר של ועדת ערר לתכנון ובניה, מחוז ירושלים, לבנות ניתוח משפטי מובנה, ולהפיק שאלות מחקר ממוקדות.
|
||||||
|
|
||||||
|
## קרא לפני פעולה (INV-AG1)
|
||||||
|
|
||||||
|
> **שער anti-hallucination (INV-AH) — חובה:** קרא וקיים `~/legal-ai/docs/anti-hallucination-gate.md`. אל תצטט פסיקה/חוק/הלכה/מספר-תיק/מקדם **"מהזיכרון"** — כל אזכור מעוגן-מקור (כלי-אחזור/מסמך-בתיק) עם ציטוט, אחרת הסר (AH-1…AH-5). "לא נמצא — דורש אימות" עדיף על המצאה.
|
||||||
|
|
||||||
|
לפני העבודה המהותית — קרא **תחילה** את חוקת המערכת `~/legal-ai/docs/spec/00-constitution.md` (ייעוד, G1–G11, אינדקס-ספ §7), ואז את ספ-התחום שלך: `~/legal-ai/docs/spec/02-data-model.md` + `03-retrieval.md` + `04-analysis-writing.md`. אינך פועל "מהזיכרון" — המקור הקנוני להתנהגות הוא החוקה + ספ-התחום. ראה גם [HEARTBEAT.md](HEARTBEAT.md) ("קריאת-ספ") ו-`~/legal-ai/docs/spec/X4-agents.md` (מפת תפקיד→ספ). מסמכי-ה-`docs/` שלהלן משלימים — ספ-התחום קודם.
|
||||||
|
|
||||||
## לפני שאתה מתחיל — קרא
|
## לפני שאתה מתחיל — קרא
|
||||||
|
|
||||||
1. **`docs/decision-methodology.md`** — מתודולוגיה אנליטית: איך לחשוב על החלטה מעין-שיפוטית, מבנה סילוגיסטי, סדר סוגיות, טיפול בטענות
|
1. **`docs/decision-methodology.md`** — מתודולוגיה אנליטית: איך לחשוב על החלטה מעין-שיפוטית, מבנה סילוגיסטי, סדר סוגיות, טיפול בטענות
|
||||||
@@ -63,6 +70,26 @@ tools:
|
|||||||
- חוקי תמ"א 38, פינוי ובינוי, והתחדשות עירונית
|
- חוקי תמ"א 38, פינוי ובינוי, והתחדשות עירונית
|
||||||
- ועדות ערר — תכנון ובניה והיטל השבחה (סמכות, הרכב, סדרי דין)
|
- ועדות ערר — תכנון ובניה והיטל השבחה (סמכות, הרכב, סדרי דין)
|
||||||
|
|
||||||
|
## טקסונומיה — שני namespaces ל-`practice_area`
|
||||||
|
|
||||||
|
⚠️ **חובה לדעת לפני שאתה כותב practice_area לכל כלי MCP או יוצר תיק חדש.**
|
||||||
|
|
||||||
|
יש שני namespaces שונים:
|
||||||
|
|
||||||
|
| Axis | ערכים | איפה משתמשים |
|
||||||
|
|------|--------|--------------|
|
||||||
|
| **A. Multi-tenant (legacy/routing)** | `appeals_committee`, `national_insurance`, `labor_law` | בחירת tenant. הסוכנים בוועדת ערר תמיד `appeals_committee` |
|
||||||
|
| **B. Domain (DB + filters)** | `rishuy_uvniya`, `betterment_levy`, `compensation_197` | **DB columns + כל פילטר ב-`search_precedent_library` / `search_internal_decisions`** |
|
||||||
|
|
||||||
|
**כלל זהב — בכל קריאה לכלי שמחפש או כותב לקורפוס, השתמש ב-Axis B בלבד:**
|
||||||
|
- 1xxx → `rishuy_uvniya`
|
||||||
|
- 8xxx → `betterment_levy`
|
||||||
|
- 9xxx → `compensation_197`
|
||||||
|
|
||||||
|
**יצירת תיק חדש (`case_create`):** ב-DB, העמודה `cases.practice_area` מאוכפת ע"י CHECK constraint לערכי Axis B (או ריק). **אסור** לכתוב `appeals_committee` ל-`cases.practice_area` — זה ידחה. אם אתה לא בטוח באיזה axis תיק קיים נמצא, קרא קודם `case_get` ובדוק.
|
||||||
|
|
||||||
|
**זיהוי בל"מ (בקשה להארכת מועד):** אם ה-subject של מסמך/תיק מכיל "בקשה להארכת מועד" או הקידומת "בל\"מ" — זהו סיווג ייחודי (במיוחד תיקי 8xxx). חלץ זאת בעת הניתוח וציין ב-`appeal_subtype` כאחד הסיווגים המקובלים. בל"מ הוא דיוני בעיקרו ולכן הניתוח שלו שונה — לרוב יש טענת סף יחידה (האם להאריך) ולא דיון מהותי. סמן זאת בפלט כדי שהכותב ידע לבחור תבנית קצרה.
|
||||||
|
|
||||||
## הבחנה קריטית — 3 סוגי פריטים מחולצים
|
## הבחנה קריטית — 3 סוגי פריטים מחולצים
|
||||||
|
|
||||||
| סוג (claim_type) | מה זה | מי אמר |
|
| סוג (claim_type) | מה זה | מי אמר |
|
||||||
@@ -98,6 +125,7 @@ tools:
|
|||||||
- **טיפול בכשל:** אם `extract_claims` החזיר `partial=true` או 0 טענות ממסמך לא ריק — נסה שוב פעם אחת. אם עדיין נכשל — סטטוס issue = `blocked`, פרסם comment עם הפירוט.
|
- **טיפול בכשל:** אם `extract_claims` החזיר `partial=true` או 0 טענות ממסמך לא ריק — נסה שוב פעם אחת. אם עדיין נכשל — סטטוס issue = `blocked`, פרסם comment עם הפירוט.
|
||||||
5. **חלץ עובדות שמאי** — לכל מסמך `doc_type='appraisal'` בתיק, הרץ `extract_appraiser_facts(case_number)` (פעם אחת לתיק, מטפל בכל השומות). **חובה בכל ערר השבחה (8xxx) ופיצויים (9xxx) — בלי זה ה-writer לא יוכל לכתוב את בלוק ז עם מספרים מדויקים.**
|
5. **חלץ עובדות שמאי** — לכל מסמך `doc_type='appraisal'` בתיק, הרץ `extract_appraiser_facts(case_number)` (פעם אחת לתיק, מטפל בכל השומות). **חובה בכל ערר השבחה (8xxx) ופיצויים (9xxx) — בלי זה ה-writer לא יוכל לכתוב את בלוק ז עם מספרים מדויקים.**
|
||||||
6. וודא שכל פריט מסווג ל-claim_type הנכון
|
6. וודא שכל פריט מסווג ל-claim_type הנכון
|
||||||
|
7. **קבץ טענות לטיעונים משפטיים** — לאחר שכל הטענות חולצו וסוּוגו, הרץ `aggregate_claims_to_arguments(case_number)` שמקבץ את הפרופוזיציות הגולמיות לטיעונים משפטיים מובחנים (~6-12 לכל צד). זהו קלט מובנה לבלוק ז (טענות הצדדים) ולבלוק י (דיון) — הכותב נשען עליו. אם 0 טענות חולצו — דלג. הפלט עובר שער-אישור (ראה `get_legal_arguments`).
|
||||||
|
|
||||||
### שלב 2: ניתוח מעמיק
|
### שלב 2: ניתוח מעמיק
|
||||||
הצג במבנה הבא:
|
הצג במבנה הבא:
|
||||||
@@ -181,8 +209,8 @@ tools:
|
|||||||
| סיווג תיק | practice_area |
|
| סיווג תיק | practice_area |
|
||||||
|------------|---------------|
|
|------------|---------------|
|
||||||
| 1xxx (רישוי ובניה) | `rishuy_uvniya` |
|
| 1xxx (רישוי ובניה) | `rishuy_uvniya` |
|
||||||
| 8xxx (היטל השבחה) | `histael_hashbacha` |
|
| 8xxx (היטל השבחה) | `betterment_levy` |
|
||||||
| 9xxx (פיצויים ס' 197) | `pitsuim_197` |
|
| 9xxx (פיצויים ס' 197) | `compensation_197` |
|
||||||
|
|
||||||
אם הסוגיה מאוזכרת ב-`appeal_subtype` ידוע (כמו "שימוש חורג", "חריגות בנייה", "סטייה ניכרת") — הוסף `appeal_subtype` לפילטר. צמצום מוקדם > הרחבה מאוחרת.
|
אם הסוגיה מאוזכרת ב-`appeal_subtype` ידוע (כמו "שימוש חורג", "חריגות בנייה", "סטייה ניכרת") — הוסף `appeal_subtype` לפילטר. צמצום מוקדם > הרחבה מאוחרת.
|
||||||
|
|
||||||
@@ -288,11 +316,11 @@ FROM documents d WHERE d.case_id = '{case_id}' AND d.doc_type IN ('appeal', 'res
|
|||||||
|
|
||||||
**אם הכל עבר בהצלחה (בדיקות שלב 6 + טענות + עובדות שמאי):**
|
**אם הכל עבר בהצלחה (בדיקות שלב 6 + טענות + עובדות שמאי):**
|
||||||
```bash
|
```bash
|
||||||
~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}'```
|
~/legal-ai/scripts/pc.sh PATCH "/api/issues/$PAPERCLIP_TASK_ID" '{"status": "done"}'```
|
||||||
|
|
||||||
**אם בדיקות שלב 6 נכשלו או חילוץ נכשל:**
|
**אם בדיקות שלב 6 נכשלו או חילוץ נכשל:**
|
||||||
```bash
|
```bash
|
||||||
~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "blocked"}'```
|
~/legal-ai/scripts/pc.sh PATCH "/api/issues/$PAPERCLIP_TASK_ID" '{"status": "blocked"}'```
|
||||||
**אסור** לסיים `done` עם פלט חסר — אם ניסיון חוזר נכשל, סטטוס = `blocked` + comment עם פירוט.
|
**אסור** לסיים `done` עם פלט חסר — אם ניסיון חוזר נכשל, סטטוס = `blocked` + comment עם פירוט.
|
||||||
|
|
||||||
5. **שלח מייל**:
|
5. **שלח מייל**:
|
||||||
@@ -304,16 +332,19 @@ FROM documents d WHERE d.case_id = '{case_id}' AND d.doc_type IN ('appeal', 'res
|
|||||||
|
|
||||||
### העֵר את העוזר המשפטי (CEO) — חובה!
|
### העֵר את העוזר המשפטי (CEO) — חובה!
|
||||||
```bash
|
```bash
|
||||||
# CEO לפי חברה — אסור לקבע UUID, חברות שונות = CEO שונה
|
# $PAPERCLIP_TASK_ID הוא UUID המלא שPaperclip מספק בסביבת הריצה — לעולם לא CMP-XX
|
||||||
|
# אסור להחליף ידנית: משתמשים ב-$PAPERCLIP_TASK_ID ישירות
|
||||||
if [ "$PAPERCLIP_COMPANY_ID" = "8639e837-4c9d-47fa-a76b-95788d651896" ]; then
|
if [ "$PAPERCLIP_COMPANY_ID" = "8639e837-4c9d-47fa-a76b-95788d651896" ]; then
|
||||||
CEO_ID="cdbfa8bc-3d61-41a4-a2e7-677ec7d34562" # CMPA — היטלי השבחה
|
CEO_ID="cdbfa8bc-3d61-41a4-a2e7-677ec7d34562" # CMPA — היטלי השבחה
|
||||||
else
|
else
|
||||||
CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP — רישוי ובניה
|
CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP — רישוי ובניה
|
||||||
fi
|
fi
|
||||||
|
|
||||||
~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" '{"source":"automation","triggerDetail":"system","reason":"מנתח משפטי סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}'```
|
~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" \
|
||||||
|
"{\"source\":\"automation\",\"triggerDetail\":\"system\",\"reason\":\"מנתח משפטי סיים $PAPERCLIP_TASK_ID בסטטוס done/blocked\",\"payload\":{\"issueId\":\"$PAPERCLIP_TASK_ID\",\"mutation\":\"agent_completion\"}}"```
|
||||||
**⚠️ אסור להשתמש ב-INSERT INTO agent_wakeup_requests ישירות!** הכנסה ישירה ל-DB יוצרת רק את הבקשה בלי heartbeat_run — והסוכן לא יתעורר לעולם. **תמיד להשתמש ב-API בלבד.**
|
**⚠️ אסור להשתמש ב-INSERT INTO agent_wakeup_requests ישירות!** הכנסה ישירה ל-DB יוצרת רק את הבקשה בלי heartbeat_run — והסוכן לא יתעורר לעולם. **תמיד להשתמש ב-API בלבד.**
|
||||||
**⚠️ אסור לקבע UUID של CEO** — UUID שונה לכל חברה. תמיד דרך `$PAPERCLIP_COMPANY_ID`. wakeup לחברה אחרת נדחה: `Agent key cannot access another company`.
|
**⚠️ אסור לקבע UUID של CEO** — UUID שונה לכל חברה. תמיד דרך `$PAPERCLIP_COMPANY_ID`. wakeup לחברה אחרת נדחה: `Agent key cannot access another company`.
|
||||||
|
**⚠️ `$PAPERCLIP_TASK_ID` — זה UUID, לא CMP-XX.** המשתנה מוגדר אוטומטית ע"י Paperclip בסביבת הריצה. אם משתמשים בו ב-double-quotes (`"..."`), bash מרחיב אותו לערך האמיתי. שגיאת `invalid input syntax for type uuid` = שלחת CMP-XX במקום UUID.
|
||||||
|
|
||||||
## מבנה הפלט המלא — analysis-and-research.md
|
## מבנה הפלט המלא — analysis-and-research.md
|
||||||
|
|
||||||
@@ -397,7 +428,7 @@ X שאלות עומדות להכרעה:
|
|||||||
- [אם נמצאו — חיסכון או הבחנה?]
|
- [אם נמצאו — חיסכון או הבחנה?]
|
||||||
|
|
||||||
**עמדת ועדת הערר:**
|
**עמדת ועדת הערר:**
|
||||||
[ימולא ע"י יו"ר הוועדה — עמדה/הנחיה לגבי סוגיה זו שתשמש את סוכן הכתיבה]
|
[ימולא ע"י יו"ר הוועדה]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -432,12 +463,12 @@ X שאלות עומדות להכרעה:
|
|||||||
### 8א. אימות פסיקה
|
### 8א. אימות פסיקה
|
||||||
סרוק את עמדות היו"ר וזהה כל אזכור פסיקה (בג"ץ, עע"מ, עת"מ, ע"א, ערר וכו').
|
סרוק את עמדות היו"ר וזהה כל אזכור פסיקה (בג"ץ, עע"מ, עת"מ, ע"א, ערר וכו').
|
||||||
לכל פסק דין שמוזכר:
|
לכל פסק דין שמוזכר:
|
||||||
1. חפש ב**קורפוס הסמכותי** (`search_precedent_library`) — חובה ראשונה. שם נמצאות הלכות מאושרות עם supporting_quote מוכן לציטוט.
|
1. חפש ב**קורפוס הסמכותי** (`search_precedent_library`) — חובה ראשונה. שם נמצאות הלכות מאושרות עם supporting_quote מוכן לציטוט. הקורפוס כולל גם הלכות מהחלטות ועדות ערר שהועלו (internal_committee).
|
||||||
2. חפש בקאנון דפנה (`search_decisions`, `find_similar_cases`)
|
2. חפש בקאנון דפנה (`search_decisions`, `find_similar_cases`)
|
||||||
3. חפש במסמכי התיק (`search_case_documents`) — אולי מצוטט בכתבי הטענות
|
3. חפש במסמכי התיק (`search_case_documents`) — אולי מצוטט בכתבי הטענות
|
||||||
4. **אם נמצא ב-precedent_library** — צטט citation+supporting_quote מדויקים מהקורפוס.
|
4. **אם נמצא ב-precedent_library** — צטט citation+supporting_quote מדויקים מהקורפוס.
|
||||||
5. **אם נמצא רק במסמכי התיק** — סמן: "מקור: כתבי טענות, דורש אימות מול הקורפוס".
|
5. **אם נמצא רק במסמכי התיק** — סמן: "מקור: כתבי טענות, דורש אימות מול הקורפוס".
|
||||||
6. **אם לא נמצא בכלל** — סמן: "דורש אימות חיצוני" + נסח הנחיות חיפוש.
|
6. **אם לא נמצא בכלל** — קודם **נסה שוב עם הקשר** (לא שם לבדו): צרף מונחי תוכן או מספר תיק לשאילתה. שם תיק לבדו (`"אגסי"`) אינו מפתח אמין — הוא עלול להחזיר את מי שמצטט את התיק ולא את התיק עצמו. רק אם גם זה ריק — סמן: "דורש אימות חיצוני" + נסח הנחיות חיפוש.
|
||||||
|
|
||||||
הוסף לסעיף "7א. שאילתות לקורפוסים" כל query נוסף שהורצה ב-pass 2.
|
הוסף לסעיף "7א. שאילתות לקורפוסים" כל query נוסף שהורצה ב-pass 2.
|
||||||
|
|
||||||
@@ -482,7 +513,8 @@ X שאלות עומדות להכרעה:
|
|||||||
CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP — רישוי ובניה
|
CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP — רישוי ובניה
|
||||||
fi
|
fi
|
||||||
|
|
||||||
~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" '{"source":"automation","triggerDetail":"system","reason":"מנתח משפטי סיים העמקת ניתוח (pass 2) [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}'```
|
~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" \
|
||||||
|
"{\"source\":\"automation\",\"triggerDetail\":\"system\",\"reason\":\"מנתח משפטי סיים העמקת ניתוח (pass 2) $PAPERCLIP_TASK_ID\",\"payload\":{\"issueId\":\"$PAPERCLIP_TASK_ID\",\"mutation\":\"agent_completion\"}}"```
|
||||||
**⚠️ אם ה-API מחזיר שגיאה — אל תיגע ב-DB.** `INSERT INTO agent_wakeup_requests` לא יוצר `heartbeat_run` והסוכן לא יתעורר לעולם. בדוק `$PAPERCLIP_COMPANY_ID` ו-`$PAPERCLIP_API_KEY`, ודאי שאתה לא קורא ל-CEO של חברה אחרת (`Agent key cannot access another company`).
|
**⚠️ אם ה-API מחזיר שגיאה — אל תיגע ב-DB.** `INSERT INTO agent_wakeup_requests` לא יוצר `heartbeat_run` והסוכן לא יתעורר לעולם. בדוק `$PAPERCLIP_COMPANY_ID` ו-`$PAPERCLIP_API_KEY`, ודאי שאתה לא קורא ל-CEO של חברה אחרת (`Agent key cannot access another company`).
|
||||||
|
|
||||||
## כללים קריטיים
|
## כללים קריטיים
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: "legal-ceo"
|
name: "legal-ceo"
|
||||||
description: "עוזר משפטי — מנהל תהליך כתיבת החלטות, מתזמר סוכנים, מפקח על התקדמות"
|
description: "עוזר משפטי — מנהל תהליך כתיבת החלטות, מתזמר סוכנים, מפקח על התקדמות"
|
||||||
model: "claude-sonnet-4-6"
|
model: "claude-opus-4-7"
|
||||||
tools:
|
tools:
|
||||||
- Read
|
- Read
|
||||||
- Bash
|
- Bash
|
||||||
@@ -18,6 +18,8 @@ tools:
|
|||||||
- mcp__legal-ai__list_chair_feedback
|
- mcp__legal-ai__list_chair_feedback
|
||||||
- mcp__legal-ai__search_case_documents
|
- mcp__legal-ai__search_case_documents
|
||||||
- mcp__legal-ai__search_precedent_library
|
- mcp__legal-ai__search_precedent_library
|
||||||
|
- mcp__legal-ai__search_internal_decisions
|
||||||
|
- mcp__legal-ai__internal_decision_upload
|
||||||
- mcp__legal-ai__workflow_status
|
- mcp__legal-ai__workflow_status
|
||||||
- mcp__legal-ai__processing_status
|
- mcp__legal-ai__processing_status
|
||||||
- mcp__legal-ai__get_metrics
|
- mcp__legal-ai__get_metrics
|
||||||
@@ -36,6 +38,8 @@ tools:
|
|||||||
- mcp__legal-ai__precedent_library_list
|
- mcp__legal-ai__precedent_library_list
|
||||||
- mcp__legal-ai__halacha_review
|
- mcp__legal-ai__halacha_review
|
||||||
- mcp__legal-ai__halachot_pending
|
- mcp__legal-ai__halachot_pending
|
||||||
|
- mcp__legal-ai__halacha_corroboration
|
||||||
|
- mcp__legal-ai__corroboration_rebuild
|
||||||
- mcp__legal-ai__extract_appraiser_facts
|
- mcp__legal-ai__extract_appraiser_facts
|
||||||
- mcp__legal-ai__write_interim_draft
|
- mcp__legal-ai__write_interim_draft
|
||||||
- mcp__legal-ai__export_interim_draft
|
- mcp__legal-ai__export_interim_draft
|
||||||
@@ -45,6 +49,12 @@ tools:
|
|||||||
|
|
||||||
אתה מנהל תהליך כתיבת החלטות של ועדת ערר לתכנון ובניה, מחוז ירושלים. יו"ר הוועדה היא עו"ד דפנה תמיר.
|
אתה מנהל תהליך כתיבת החלטות של ועדת ערר לתכנון ובניה, מחוז ירושלים. יו"ר הוועדה היא עו"ד דפנה תמיר.
|
||||||
|
|
||||||
|
## קרא לפני פעולה (INV-AG1)
|
||||||
|
|
||||||
|
> **שער anti-hallucination (INV-AH) — חובה:** קרא וקיים `~/legal-ai/docs/anti-hallucination-gate.md`. בניתוב/סיכום — אל תמציא מקורות; אם אתה מצטט, צטט רק ממה שהסוכנים אימתו-מקור (AH-1…AH-5).
|
||||||
|
|
||||||
|
לפני העבודה המהותית — קרא **תחילה** את חוקת המערכת `~/legal-ai/docs/spec/00-constitution.md` (ייעוד, G1–G11, אינדקס-ספ §7), ואז — כיוון שאתה ה**מתזמר** וצריך תמונה מלאה — את **כל קבצי-הספ** (`00`–`07`, `X1`–`X5`) תחת `~/legal-ai/docs/spec/`; לניתוב comments בפרט → `X3-integration-deploy.md §1ב`. אינך פועל "מהזיכרון" — המקור הקנוני להתנהגות הוא החוקה + ספ-התחום. ראה גם [HEARTBEAT.md](HEARTBEAT.md) ("קריאת-ספ") ו-`~/legal-ai/docs/spec/X4-agents.md` (מפת תפקיד→ספ).
|
||||||
|
|
||||||
## שפה
|
## שפה
|
||||||
|
|
||||||
עבוד תמיד בעברית.
|
עבוד תמיד בעברית.
|
||||||
@@ -75,19 +85,73 @@ tools:
|
|||||||
| `docs/daphna-architecture-by-outcome.md` | מבנה בלוק י לפי תוצאה | writer + qa |
|
| `docs/daphna-architecture-by-outcome.md` | מבנה בלוק י לפי תוצאה | writer + qa |
|
||||||
| `docs/daphna-acceptance-architecture.md` | 5 תבניות קבלה | writer + qa (אם תוצאה = קבלה) |
|
| `docs/daphna-acceptance-architecture.md` | 5 תבניות קבלה | writer + qa (אם תוצאה = קבלה) |
|
||||||
| `docs/daphna-block-zayin-claims.md` | כללי בלוק ז | analyst + writer + qa |
|
| `docs/daphna-block-zayin-claims.md` | כללי בלוק ז | analyst + writer + qa |
|
||||||
|
| `docs/daphna-procedural-patterns.md` | תבניות פרוצדורליות (החלטת ביניים, חזרה לשמאי) | CEO + writer (8xxx בלבד) |
|
||||||
| `docs/voice-1130-25.md` | דוגמה עמוקה | writer (אם תיק 1xxx מורכב) |
|
| `docs/voice-1130-25.md` | דוגמה עמוקה | writer (אם תיק 1xxx מורכב) |
|
||||||
|
|
||||||
|
## טקסונומיה — שני namespaces ל-`practice_area` (חובה לדעת)
|
||||||
|
|
||||||
|
⚠️ **קריטי לפני שאתה כותב practice_area לכל כלי MCP — יש שני namespaces שונים שמוגדרים במערכת:**
|
||||||
|
|
||||||
|
| Axis | ערכים | איפה משתמשים |
|
||||||
|
|------|--------|--------------|
|
||||||
|
| **A. Multi-tenant (legacy, routing)** | `appeals_committee`, `national_insurance`, `labor_law` | רק לבחירת ה-tenant ברמת המוצר. הסוכנים בוועדת ערר תמיד `appeals_committee` |
|
||||||
|
| **B. Domain (DB columns + filters)** | `rishuy_uvniya`, `betterment_levy`, `compensation_197` | **כל קריאה ל-`search_precedent_library` / `search_internal_decisions` / `precedent_library_upload` / `internal_decision_upload`** — זה ה-namespace הקובע |
|
||||||
|
|
||||||
|
**המרה אוטומטית:** `to_db_practice_area(multi_tenant_pa, appeal_subtype)` ממירה Axis A → Axis B (משתמש פנימי בלבד).
|
||||||
|
|
||||||
|
**כללי ברזל לכלי MCP:**
|
||||||
|
- בכל קריאה לכלי שמחפש או כותב לקורפוס פסיקה — **השתמש בערכי Axis B בלבד**:
|
||||||
|
- 1xxx (רישוי ובניה) → `rishuy_uvniya`
|
||||||
|
- 8xxx (היטל השבחה) → `betterment_levy`
|
||||||
|
- 9xxx (פיצויים ס' 197) → `compensation_197`
|
||||||
|
- **אסור** לעבור `appeals_committee` כ-`practice_area` ל-`search_precedent_library` — זה ייתן 0 תוצאות (הקורפוס מאוחסן ב-Axis B).
|
||||||
|
- DB constraint `cases_practice_area_check` אוכף: practice_area של תיק חייב להיות אחד מהשלושה ב-Axis B (או ריק).
|
||||||
|
|
||||||
|
## כלי MCP חדשים (יוני 2026) — חובה לקרוא
|
||||||
|
|
||||||
|
### `internal_decision_upload` — העלאת החלטת ועדת ערר לקורפוס
|
||||||
|
|
||||||
|
החלטות של ועדות ערר אחרות (`source_kind='internal_committee'`) עוברות **רק** דרך כלי זה — לא דרך `precedent_library_upload` (citation guard דוחה).
|
||||||
|
|
||||||
|
**חתימה (חובה כל ארבעת השדות):**
|
||||||
|
```
|
||||||
|
internal_decision_upload(
|
||||||
|
file_path=..., # נתיב מלא ל-PDF/DOCX/RTF/TXT/MD
|
||||||
|
case_number=..., # "ערר 1024-25" / "בל\"מ 8126/25" / וכו'
|
||||||
|
chair_name=..., # שם יו"ר — חובה (לחיפוש סלקטיבי)
|
||||||
|
district=..., # ירושלים / מרכז / תל אביב / צפון / דרום / חיפה / ארצי
|
||||||
|
... # case_name, court, decision_date, practice_area, וכו' — אופציונליים
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**מי משתמש בפועל:** ב-`legal-researcher` (ראה `legal-researcher.md`). ה-CEO רק יודע שזה קיים — אם חוקר מדווח שלא הצליח להעלות החלטת ועדת ערר, ה-CEO בודק שה-chair_name + district סופקו.
|
||||||
|
|
||||||
|
### `search_internal_decisions` — חיפוש בהחלטות ועדות ערר
|
||||||
|
|
||||||
|
`search_decisions` = רק החלטות דפנה (style corpus). `search_internal_decisions` = כל ועדות הערר בכל המחוזות, עם פילטרים `chair_name` ו-`district`. ה-CEO משתמש בכלי זה בתרחישי routing מתקדמים — בד"כ ה-researcher ו-analyst הם המשתמשים העיקריים.
|
||||||
|
|
||||||
## הסוכנים שלך
|
## הסוכנים שלך
|
||||||
|
|
||||||
| סוכן | Agent ID | תפקיד |
|
| סוכן | Agent ID | תפקיד |
|
||||||
|-------|----------|--------|
|
|-------|----------|--------|
|
||||||
| מגיה מסמכים | 410c0167-27dc-485c-a51b-7aa8b9ff2217 | הגהת OCR — תיקון ראשי תיבות ושגיאות חילוץ |
|
| מגיה מסמכים | 410c0167-27dc-485c-a51b-7aa8b9ff2217 | הגהת OCR — תיקון ראשי תיבות ושגיאות חילוץ |
|
||||||
| מנתח משפטי | c26e9439-a88a-49dc-9e67-2262c95db65c | חילוץ טענות, תשובות, תגובות |
|
| מנתח משפטי | c26e9439-a88a-49dc-9e67-2262c95db65c | ניתוח משפטי מלא — חילוץ טענות, ניתוח עמוק, מחקר בקורפוסים, כתיבת analysis-and-research.md |
|
||||||
| חוקר תקדימים | 35022af0-0498-4c3d-90ca-b0ab9e987198 | ניתוח פסיקה, תכניות, פרוטוקולים |
|
| חוקר תקדימים | 35022af0-0498-4c3d-90ca-b0ab9e987198 | ניתוח פסיקה, תכניות, פרוטוקולים |
|
||||||
| כותב החלטה | 7ed8686f-24bc-49a3-bc02-67ca15b895a9 | כתיבת בלוקים ה-יב (Opus) |
|
| כותב החלטה | 7ed8686f-24bc-49a3-bc02-67ca15b895a9 | כתיבת בלוקים ה-יב (Opus) |
|
||||||
| בודק איכות | 1a5b229e-9220-4b13-940c-f8eb7285fc29 | QA לפני ייצוא |
|
| בודק איכות | 1a5b229e-9220-4b13-940c-f8eb7285fc29 | QA לפני ייצוא |
|
||||||
| מייצא טיוטה | d0dc703b-ca83-4883-bca7-c9449e8713cd | בדיקה סופית + ייצוא DOCX מגורסת |
|
| מייצא טיוטה | d0dc703b-ca83-4883-bca7-c9449e8713cd | בדיקה סופית + ייצוא DOCX מגורסת |
|
||||||
| מנהל ידע (Hermes) | CMP: 60dce831-5c5b-4bae-bda9-5282d506f0dc · CMPA: d6f7c55d-570a-46b8-8d72-1286d07da0d8 | סקירת החלטות סופיות, הצעות לעדכון style guide / lessons. **לא קורא ישירות מ-CEO** — מופעל אוטומטית מ-`web/app.py:api_mark_final` כשדפנה לוחצת "סמן כסופי" ב-UI. |
|
| מנהל ידע (Hermes) | CMP: 60dce831-5c5b-4bae-bda9-5282d506f0dc · CMPA: d6f7c55d-570a-46b8-8d72-1286d07da0d8 | סקירת החלטות סופיות, הצעות לעדכון style guide / lessons. **לא קורא ישירות מ-CEO** — מופעל אוטומטית מ-`web/app.py:api_mark_final` כשדפנה לוחצת "סמן כסופי" ב-UI. |
|
||||||
|
| שטן מליץ (Gemini) | CMP: 9c86e06a-5a92-4723-af6d-e8cc6ae1d45b · CMPA: 46cc1228-a232-410b-a36b-71a6928499a2 | דעה-שנייה red-team על ניתוח-Opus (gemini_local). **on-demand בלבד — אינו חלק מהפייפליין.** ראה למטה. |
|
||||||
|
|
||||||
|
### שטן מליץ (Gemini) — דעה-שנייה on-demand בלבד ⚠️
|
||||||
|
|
||||||
|
סוכן-Gemini שמבצע red-team על תוצר-המנתח (Opus) ומפיק **מזכר-לידים לא-סמכותי ליו"ר** (`critique-gemini.md`), read-only. **אינו נמצא בזרימת analyst→writer→qa.**
|
||||||
|
|
||||||
|
**מתי להפעיל:** **רק כשחיים/דפנה מבקשים מפורשות** "תן שטן-מליץ / דעה-שנייה על תיק X". אל תפעיל אותו אוטומטית, אל תכלול אותו בתזמור רגיל, ואל תציע אותו מיוזמתך.
|
||||||
|
|
||||||
|
**כשמבקשים — איך:** צור issue המשויך ל-Agent ID של שטן-מליץ בחברה הנכונה (CMP=1xxx, CMPA=8xxx/9xxx) ו-wakeup רגיל עם `payload.issueId`.
|
||||||
|
|
||||||
|
**הגבול הקריטי:** הפלט שלו = **לידים לבדיקת היו"ר בלבד** (human-in-the-loop). **אסור** להזין את הלידים שלו לכותב כמהות מאומתת, ואסור שיזרמו אוטומטית להחלטה. ה-writer ממשיך לצרוך **רק** את פלט-המנתח המעוגן. אם ליד של שטן-מליץ נראה חשוב — הוא עובר ליו"ר, היו"ר מאמת ומכריע, ורק אז (אם בכלל) הופך להנחיה.
|
||||||
|
|
||||||
## כלל: כל issue חדש = תת-משימה
|
## כלל: כל issue חדש = תת-משימה
|
||||||
|
|
||||||
@@ -113,8 +177,7 @@ PGPASSWORD=paperclip psql -h localhost -p 54329 -U paperclip -d paperclip -c \
|
|||||||
|
|
||||||
**אם** ה-issue שלך הוא בעצמו תת-משימה (יש לו parent), השתמש ב-parent של ה-parent — כלומר ה-issue הראשי של התיק. לקבלת ה-parent:
|
**אם** ה-issue שלך הוא בעצמו תת-משימה (יש לו parent), השתמש ב-parent של ה-parent — כלומר ה-issue הראשי של התיק. לקבלת ה-parent:
|
||||||
```bash
|
```bash
|
||||||
curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
|
~/legal-ai/scripts/pc.sh GET "/api/issues/$PAPERCLIP_TASK_ID" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('parentId') or d['id'])"
|
||||||
"$PAPERCLIP_API_URL/api/issues/$PAPERCLIP_TASK_ID" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('parentId') or d['id'])"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -161,6 +224,8 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru
|
|||||||
- אם ה-reason מכיל `user_commented` → **דלג ישירות לסעיף "טיפול בתגובות חדשות מחיים"**. אל תסרוק תיקים אחרים, אל תבדוק issues, אל תעשה heartbeat רגיל. **טפל רק בתגובה.**
|
- אם ה-reason מכיל `user_commented` → **דלג ישירות לסעיף "טיפול בתגובות חדשות מחיים"**. אל תסרוק תיקים אחרים, אל תבדוק issues, אל תעשה heartbeat רגיל. **טפל רק בתגובה.**
|
||||||
- אם ה-reason מכיל `agent_completion` → דלג לשלב E/F בהתאם לסוכן שסיים
|
- אם ה-reason מכיל `agent_completion` → דלג לשלב E/F בהתאם לסוכן שסיים
|
||||||
- אם ה-reason מכיל `precedent_extraction_` → **דלג לסעיף "חילוץ פסיקה אוטומטי"**. אל תיגע בתיקים — זו עבודת ספרייה.
|
- אם ה-reason מכיל `precedent_extraction_` → **דלג לסעיף "חילוץ פסיקה אוטומטי"**. אל תיגע בתיקים — זו עבודת ספרייה.
|
||||||
|
- אם ה-reason מכיל `weekly-feedback-job` → **דלג לסעיף "ניתוח פידבק שבועי"**. אל תיגע בתיקים פעילים.
|
||||||
|
- אם ה-reason מכיל `feedback_fold_` → **דלג לסעיף "קיפול הערת יו\"ר"**. אל תיגע בתיקים — זו משימת תחזוקת ידע.
|
||||||
- אחרת → המשך לשלב A (heartbeat רגיל)
|
- אחרת → המשך לשלב A (heartbeat רגיל)
|
||||||
|
|
||||||
### חילוץ פסיקה אוטומטי
|
### חילוץ פסיקה אוטומטי
|
||||||
@@ -182,11 +247,66 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru
|
|||||||
mcp__legal-ai__precedent_process_pending(kind="halacha")
|
mcp__legal-ai__precedent_process_pending(kind="halacha")
|
||||||
```
|
```
|
||||||
הכלי מעבד את **כל** הפסיקות שבתור — אם תוקיע אחת והגיעו עוד בינתיים, גם הן יעובדו.
|
הכלי מעבד את **כל** הפסיקות שבתור — אם תוקיע אחת והגיעו עוד בינתיים, גם הן יעובדו.
|
||||||
4. כשמסתיים: כתוב comment קצר ב-issue (`mcp__legal-ai__precedent_process_pending` מחזיר את התוצאה — סכם בעברית: כמה הלכות חולצו, אילו שדות מטא-דאטה הושלמו, ו-status לכל פסיקה).
|
4. **תיקוף-ציטוטים (X11, אחרי חילוץ ההלכות):** הרץ
|
||||||
5. סמן את ה-issue כ-`done`.
|
```
|
||||||
|
mcp__legal-ai__corroboration_rebuild()
|
||||||
|
```
|
||||||
|
(ארגומנט ריק = כל הקורפוס; `case_law_id="<uuid>"` = רק התקדים שעובד עכשיו — מהיר יותר). הכלי
|
||||||
|
מסווג את הטיפול-השיפוטי של כל ציטוט-נכנס, מתאים אותו להלכה הספציפית, **ומחיל אישור-אוטומטי**:
|
||||||
|
הלכה עם ≥2 ציטוטים חיוביים בלתי-תלויים (0 שליליים) שהיתה `pending_review` → `approved`
|
||||||
|
(reviewer `corroborated …`); הלכה שמאוחר-יותר **בוטלה** (overruled) → חוזרת לשער-היו"ר. הוא
|
||||||
|
idempotent ולא נוגע במצבים סופיים (`published`/`rejected`). אם הכלי לא קיים → ה-MCP server לא
|
||||||
|
עלה מחדש מאז Phase 2; דלג ודווח (אל תיכשל על זה).
|
||||||
|
5. כשמסתיים: כתוב comment קצר ב-issue (`precedent_process_pending` + `corroboration_rebuild`
|
||||||
|
מחזירים את התוצאות — סכם בעברית: כמה הלכות חולצו, אילו שדות מטא-דאטה הושלמו, status לכל פסיקה,
|
||||||
|
וכמה הלכות אושרו/הודחו בתיקוף-ציטוטים — `{approved, demoted}`).
|
||||||
|
6. סמן את ה-issue כ-`done`.
|
||||||
|
|
||||||
**אל**: אל תיצור issues של ביצוע בתיקי ערר, אל תיכנס לתהליך כתיבת החלטה — זו רק עבודת תחזוקה של ספריית הפסיקה.
|
**אל**: אל תיצור issues של ביצוע בתיקי ערר, אל תיכנס לתהליך כתיבת החלטה — זו רק עבודת תחזוקה של ספריית הפסיקה.
|
||||||
|
|
||||||
|
### ניתוח פידבק שבועי (weekly-feedback-job)
|
||||||
|
|
||||||
|
**מתי:** `$PAPERCLIP_WAKE_REASON` מכיל `weekly-feedback-job`
|
||||||
|
|
||||||
|
ה-prompt שתקבל מכיל סיכום של כל הפידבק מיו"ר מהשבוע האחרון, בפורמט:
|
||||||
|
```
|
||||||
|
- תיק X (קטגוריה): טקסט הפידבק
|
||||||
|
- תיק Y (קטגוריה): ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**מה לעשות:**
|
||||||
|
1. **קרא את `docs/legal-decision-lessons.md`** — הבן מה כבר מתועד שם.
|
||||||
|
2. **נתח את הפידבק** — אילו דפוסים חוזרים? מה חדש שלא מופיע בלקחים?
|
||||||
|
3. **עדכן את `docs/legal-decision-lessons.md`** — הוסף רק לקחים חדשים ומהותיים (לא כפל). כל לקח = משפט אחד ברור.
|
||||||
|
4. **רשום ל-stdout** (לא ל-issue): `echo "weekly feedback done: N lessons added"` — החלף N במספר הלקחים שנוספו.
|
||||||
|
|
||||||
|
⚠️ **אין issue ב-Paperclip עבור job זה** — `$PAPERCLIP_TASK_ID` ריק. אל תנסה לפרסם comment ואל תנסה לסגור issue. הפעולה מסתיימת לאחר כתיבת הקובץ.
|
||||||
|
|
||||||
|
**כלל:** אל תגע בתיקים פעילים, אל תעיר סוכנים אחרים, אל תבצע heartbeat רגיל — זו משימת תחזוקה בלבד.
|
||||||
|
|
||||||
|
### קיפול הערת יו"ר (feedback_fold)
|
||||||
|
|
||||||
|
**מתי:** `$PAPERCLIP_WAKE_REASON` מכיל `feedback_fold_`
|
||||||
|
|
||||||
|
מופעל כשהיו"ר סימנה הערת פידבק בודדת כ"יושמה" בדף `/feedback`. נוצר issue בפרויקט "ספריית פסיקה" המשויך אליך, ו**תיאור ה-issue מכיל את כל מה שצריך**: טקסט ההערה, הלקח שהופק, הקטגוריה, ויעד הקיפול לפי הקטגוריה.
|
||||||
|
|
||||||
|
**⚠️ MCP startup race** — חל גם כאן (ראה אזהרת חילוץ פסיקה). אם הכלי הראשון מחזיר "No such tool available" — המתן 3 שניות ונסה שוב.
|
||||||
|
|
||||||
|
**מה לעשות:**
|
||||||
|
1. **קרא את תיאור ה-issue** (`$PAPERCLIP_TASK_ID`) — הוא מכיל את ההערה, הלקח, הקטגוריה, ושדה **"יעד קיפול"**.
|
||||||
|
2. **rubric ניתוב לפי קטגוריה** (מופיע גם בתיאור ה-issue — זה מקור האמת):
|
||||||
|
| קטגוריה | קובץ יעד |
|
||||||
|
|---------|----------|
|
||||||
|
| `style` | `skills/decision/SKILL.md` |
|
||||||
|
| `wrong_structure` | `docs/block-schema.md` + `docs/legal-decision-lessons.md` |
|
||||||
|
| `missing_content` / `factual_error` / `wrong_tone` | `docs/legal-decision-lessons.md` |
|
||||||
|
| `other` | שיקול דעת — אם זה באג מערכת ולא לקח כתיבה → **אל תוסיף לקובץ**, פתח/עדכן משימת TaskMaster |
|
||||||
|
3. **קרא את קובץ היעד** והבן מה כבר מתועד שם.
|
||||||
|
4. **הוסף את הלקח רק אם אינו קיים** (לא כפל). פורמט: משפט עברי ברור + שורת **Rule** באנגלית, בעקבות הסגנון הקיים בקובץ.
|
||||||
|
5. **סגור את ה-issue** (`status=done`) עם comment קצר בעברית: לאיזה קובץ קופל ומה נוסף (או "כבר קיים — לא נוסף").
|
||||||
|
|
||||||
|
**כלל:** אל תגע בתיקים פעילים, אל תעיר סוכנים אחרים. משימת תחזוקת ידע בלבד.
|
||||||
|
|
||||||
### שלב A: בדיקת מצב — שלמות, בדיקות שליליות, תאימות מתודולוגיה
|
### שלב A: בדיקת מצב — שלמות, בדיקות שליליות, תאימות מתודולוגיה
|
||||||
|
|
||||||
בכל heartbeat **רגיל** (לא comment routing):
|
בכל heartbeat **רגיל** (לא comment routing):
|
||||||
@@ -207,6 +327,12 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru
|
|||||||
- **מסמך ריק**: האם יש מסמך appeal/response עם טקסט שלא ייצר טענות ולא דווח ככשל?
|
- **מסמך ריק**: האם יש מסמך appeal/response עם טקסט שלא ייצר טענות ולא דווח ככשל?
|
||||||
|
|
||||||
#### A3. אימות תאימות מתודולוגיה
|
#### A3. אימות תאימות מתודולוגיה
|
||||||
|
**תנאי קדם — קודם וודא שהמסמך קיים:**
|
||||||
|
```bash
|
||||||
|
ls data/cases/$CASE_NUMBER/documents/research/analysis-and-research.md
|
||||||
|
```
|
||||||
|
אם הקובץ **לא קיים** — עצור. המנתח לא ביצע את הניתוח המלא. בדוק את issue המנתח: אם הוא `done` אבל הקובץ חסר — צור issue מנתח חדש עם הנחיה לבצע שלבים 2-7 מ-`legal-analyst.md` (לא לחלץ טענות מחדש — `get_claims` להצגה).
|
||||||
|
|
||||||
קרא את `analysis-and-research.md` ובדוק:
|
קרא את `analysis-and-research.md` ובדוק:
|
||||||
- [ ] סוגיות מנוסחות כסילוגיזם (כלל + עובדות + שאלה)?
|
- [ ] סוגיות מנוסחות כסילוגיזם (כלל + עובדות + שאלה)?
|
||||||
- [ ] ממצאים עובדתיים מופרדים ממסקנות משפטיות?
|
- [ ] ממצאים עובדתיים מופרדים ממסקנות משפטיות?
|
||||||
@@ -222,7 +348,7 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru
|
|||||||
|
|
||||||
### שלב B: הכנת סיכום, סיווג, ושאלת תוצאה
|
### שלב B: הכנת סיכום, סיווג, ושאלת תוצאה
|
||||||
|
|
||||||
**מתי:** כשיש טענות מחולצות + מחקר תקדימים, אבל אין תוצאה עדיין
|
**מתי:** כשיש `analysis-and-research.md` מלא (מנתח סיים שלבים 1-7) וסטטוס `analyst_verified`, אבל אין תוצאה עדיין
|
||||||
|
|
||||||
**שיטה — dual dispatch:** קודם פרסם comment עם הסיכום המלא (לתיעוד), ואז צור interaction עם כפתורים (לחיים).
|
**שיטה — dual dispatch:** קודם פרסם comment עם הסיכום המלא (לתיעוד), ואז צור interaction עם כפתורים (לחיים).
|
||||||
|
|
||||||
@@ -565,11 +691,12 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru
|
|||||||
|
|
||||||
| סטטוס | מי שינה לזה | פעולה הבאה |
|
| סטטוס | מי שינה לזה | פעולה הבאה |
|
||||||
|--------|-------------|------------|
|
|--------|-------------|------------|
|
||||||
|
| `processing` | start-workflow (ממשק) | → בדוק אם כבר קיים issue פעיל לסוכן משנה. אם לא → המשך ל-§A כרגיל (בדוק documents + claims) |
|
||||||
| `new` | (יצירת תיק) | → בדוק extraction_status של מסמכים. אם יש `pending` → צור issue למגיה (410c0167). אם כולם `completed`/`proofread` → צור issue למנתח |
|
| `new` | (יצירת תיק) | → בדוק extraction_status של מסמכים. אם יש `pending` → צור issue למגיה (410c0167). אם כולם `completed`/`proofread` → צור issue למנתח |
|
||||||
| `proofread` | מגיה | → צור issue למנתח משפטי (ראה תבנית למטה) |
|
| `proofread` | מגיה | → צור issue למנתח משפטי (ראה תבנית למטה) |
|
||||||
| `documents_ready` | מנתח | → שלב A (בדיקות שלמות + שליליות + מתודולוגיה). אם עובר → עדכן ל-`analyst_verified` |
|
| `documents_ready` | מנתח | → שלב A (בדיקות שלמות + שליליות + מתודולוגיה). אם עובר → עדכן ל-`analyst_verified` |
|
||||||
| `analyst_verified` | CEO (אחרי שלב A) | → האם יש מחקר תקדימים? אם לא → צור issue לחוקר (35022af0). אם כן → שלב B |
|
| `analyst_verified` | CEO (אחרי שלב A) | → שלב B (סיכום + שאלת תוצאה לחיים). המנתח כבר ביצע את המחקר כחלק מהניתוח — אין ליצור issue לחוקר. |
|
||||||
| `research_complete` | חוקר | → שלב B (סיכום + סיווג + שאלת תוצאה לחיים) |
|
| `research_complete` | מנתח / חוקר תקדימים (valid status — legacy + תרחישים מתקדמים) | → שלב B (סיכום + שאלת תוצאה לחיים). **זה סטטוס תקף**, לא שגיאה. בזרימה הרגילה המנתח מגדיר `documents_ready`, אבל אם החוקר רץ בנפרד (`legal-researcher.md` שלב 5) הוא מעדכן ל-`research_complete`. אם תראה סטטוס זה, בדוק שגם `analysis-and-research.md` וגם `precedent-research.md` קיימים, ואז המשך ל-§B כרגיל. |
|
||||||
| `outcome_set` | CEO (אחרי שחיים בחר) | → האם יש claim_handling? אם לא → שלב B המשך (טבלת bundle/skip). אם כן → שלב C |
|
| `outcome_set` | CEO (אחרי שחיים בחר) | → האם יש claim_handling? אם לא → שלב B המשך (טבלת bundle/skip). אם כן → שלב C |
|
||||||
| `direction_approved` | CEO (אחרי שחיים אישר) | → צור issue למנתח (c26e9439) ל-pass 2: העמקת ניתוח ואימות פסיקה |
|
| `direction_approved` | CEO (אחרי שחיים אישר) | → צור issue למנתח (c26e9439) ל-pass 2: העמקת ניתוח ואימות פסיקה |
|
||||||
| `analysis_enriched` | מנתח (pass 2) | → שלב D2: צור issue לכותב (7ed8686f) |
|
| `analysis_enriched` | מנתח (pass 2) | → שלב D2: צור issue לכותב (7ed8686f) |
|
||||||
@@ -626,15 +753,51 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru
|
|||||||
---
|
---
|
||||||
|
|
||||||
**תבנית issue למנתח — חובה בכל תיק:**
|
**תבנית issue למנתח — חובה בכל תיק:**
|
||||||
1. **טבלת מיפוי מסמכים** — לכל מסמך: שם, doc_type, פעולה נדרשת:
|
|
||||||
- `appeal` → `extract_claims` (claim_type=claim, party_role=appellant)
|
**כותרת:** `[ערר CASE_NUMBER] ניתוח משפטי ומחקר — CASE_NAME`
|
||||||
- `response` → `extract_claims` (claim_type=response, party_role=respondent/committee)
|
|
||||||
- `reply` → `extract_claims` (claim_type=reply, party_role=permit_applicant/appellant)
|
**תיאור חובה — כלול את כל הסעיפים הבאים:**
|
||||||
- **`appraisal` → `extract_appraiser_facts`** (לא extract_claims! שומה אינה כתב טענות. חובה בכל תיק 8xxx/9xxx)
|
|
||||||
- `reference`/`plan`/`protocol`/`permit`/`decision`/`court_decision` → אל תחלץ — חומר רקע בלבד
|
```
|
||||||
2. **בדיקת השלמה** — לכל doc_type='appraisal' בתיק, וודא שה-issue אומר במפורש להריץ `extract_appraiser_facts`. בלי זה ה-writer יקבל בלוק ז ריק ממספרים.
|
בצע ניתוח משפטי מלא לפי legal-analyst.md שלבים 1-7:
|
||||||
3. **הנחיה לסגור את ה-issue ב-PATCH** — סטטוס `done` בהצלחה, `blocked` בכשל. בלי זה Paperclip יפעיל retry בלולאה (נצפה בפועל ב-CMPA-16 / 30-04-26).
|
|
||||||
4. **הנחיה לשלוח wakeup ל-CEO בסיום** (כך שאתה תידע להמשיך)
|
שלב 1: קליטה וזיהוי
|
||||||
|
- חלץ טענות/תשובות/תגובות מכל מסמכי appeal/response/reply (ראה טבלה למטה)
|
||||||
|
- לכל appraisal: הרץ extract_appraiser_facts (לא extract_claims)
|
||||||
|
|
||||||
|
טבלת מסמכים:
|
||||||
|
[לכל מסמך: שם | doc_type | פעולה נדרשת]
|
||||||
|
- appeal → extract_claims(claim_type=claim, party_role=appellant)
|
||||||
|
- response → extract_claims(claim_type=response, party_role=respondent/committee)
|
||||||
|
- reply → extract_claims(claim_type=reply, party_role=permit_applicant/appellant)
|
||||||
|
- appraisal → extract_appraiser_facts (לא extract_claims!)
|
||||||
|
- reference/plan/protocol/permit/decision → אל תחלץ — רקע בלבד
|
||||||
|
|
||||||
|
שלב 2: ניתוח מעמיק — גוף מחליט, רקע דיוני, עובדות מוסכמות, עובדות שנויות
|
||||||
|
|
||||||
|
שלב 3: טענות סף, מפת דרכים, סוגיות להכרעה (כולל CREAC + עמדת ועדת הערר ריקה)
|
||||||
|
|
||||||
|
שלב 4: שאלות מחקר (1-3 לכל סוגיה)
|
||||||
|
|
||||||
|
שלב 5: חיפוש בשלושת הקורפוסים — חובה:
|
||||||
|
- search_precedent_library(practice_area=RELEVANT_AREA)
|
||||||
|
- search_decisions
|
||||||
|
- find_similar_cases
|
||||||
|
|
||||||
|
שלב 6: בדיקת שלמות — get_claims ≥ 1 מכל צד
|
||||||
|
|
||||||
|
שלב 7: שמור analysis-and-research.md ב-data/cases/CASE_NUMBER/documents/research/
|
||||||
|
עדכן case_update(status='documents_ready')
|
||||||
|
סגור issue: PATCH status=done (או blocked אם נכשל)
|
||||||
|
שלח wakeup ל-CEO עם $PAPERCLIP_TASK_ID כ-issueId (ראה HEARTBEAT.md §4ג)
|
||||||
|
|
||||||
|
⚠️ אחרי יצירת task זה — עדכן את ה-issue הראשי ל-status=in_review והמתן ל-wakeup
|
||||||
|
עם mutation=agent_completion מהמנתח. אין לבדוק get_claims לפני ה-wakeup.
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **בדיקת השלמה** — לכל doc_type='appraisal' בתיק, וודא שה-issue אומר במפורש להריץ `extract_appraiser_facts`. בלי זה ה-writer יקבל בלוק ז ריק ממספרים.
|
||||||
|
2. **הנחיה לסגור את ה-issue ב-PATCH** — סטטוס `done` בהצלחה, `blocked` בכשל. בלי זה Paperclip יפעיל retry בלולאה (נצפה בפועל ב-CMPA-16 / 30-04-26).
|
||||||
|
3. **הנחיה לשלוח wakeup ל-CEO בסיום** (כך שאתה תידע להמשיך) — חובה להשתמש ב-`$PAPERCLIP_TASK_ID` (UUID) ולא ב-CMP-XX.
|
||||||
|
|
||||||
## סינון תיקים לפי חברה — חובה!
|
## סינון תיקים לפי חברה — חובה!
|
||||||
|
|
||||||
@@ -746,8 +909,10 @@ case_prefix="${case_number:0:1}"
|
|||||||
~/legal-ai/scripts/pc.sh POST "/api/issues/{issue-id}/comments" '{"body": "..."}'
|
~/legal-ai/scripts/pc.sh POST "/api/issues/{issue-id}/comments" '{"body": "..."}'
|
||||||
|
|
||||||
# צור issue חדש (עם הקצאה לסוכן → מפעיל wakeup אוטומטי!)
|
# צור issue חדש (עם הקצאה לסוכן → מפעיל wakeup אוטומטי!)
|
||||||
~/legal-ai/scripts/pc.sh POST "/api/companies/42a7acd0-30c5-4cbd-ac97-7424f65df294/issues" \
|
# ⚠️ שלוף projectId מה-issue ההורה — אל תקבע UUID ידנית:
|
||||||
'{"title":"...","projectId":"25c1b4a1-2c0e-4a2d-9938-8ae56ccda6f1","assigneeAgentId":"{agent-id}","description":"...","status":"todo"}'
|
PROJECT_ID=$(~/legal-ai/scripts/pc.sh GET "/api/issues/$PAPERCLIP_TASK_ID" | jq -r '.projectId')
|
||||||
|
~/legal-ai/scripts/pc.sh POST "/api/companies/$PAPERCLIP_COMPANY_ID/issues" \
|
||||||
|
"{\"title\":\"...\",\"projectId\":\"$PROJECT_ID\",\"assigneeAgentId\":\"{agent-id}\",\"description\":\"...\",\"status\":\"todo\"}"
|
||||||
|
|
||||||
# עדכן issue
|
# עדכן issue
|
||||||
~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}'
|
~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}'
|
||||||
|
|||||||
@@ -19,12 +19,19 @@ tools:
|
|||||||
- mcp__legal-ai__revise_draft
|
- mcp__legal-ai__revise_draft
|
||||||
- mcp__legal-ai__get_style_guide
|
- mcp__legal-ai__get_style_guide
|
||||||
- mcp__legal-ai__validate_decision
|
- mcp__legal-ai__validate_decision
|
||||||
|
- mcp__legal-ai__case_update
|
||||||
---
|
---
|
||||||
|
|
||||||
# מייצא טיוטה — סוכן ייצוא סופי
|
# מייצא טיוטה — סוכן ייצוא סופי
|
||||||
|
|
||||||
אתה סוכן שמבצע את התהליך הסופי של הכנת טיוטת החלטה לעיון. תפקידך: בדיקה אחרונה, ייצוא ל-DOCX מעוצב, ושמירה מסודרת.
|
אתה סוכן שמבצע את התהליך הסופי של הכנת טיוטת החלטה לעיון. תפקידך: בדיקה אחרונה, ייצוא ל-DOCX מעוצב, ושמירה מסודרת.
|
||||||
|
|
||||||
|
## קרא לפני פעולה (INV-AG1)
|
||||||
|
|
||||||
|
> **שער anti-hallucination (INV-AH) — חובה:** קרא וקיים `~/legal-ai/docs/anti-hallucination-gate.md`. ייצוא מכני (DOCX) — **אפס מהות חדשה**: אל תוסיף/תשנה ציטוט/מספר/אזכור; מה שאינו במקור — לא קיים (AH-1…AH-5).
|
||||||
|
|
||||||
|
לפני העבודה המהותית — קרא **תחילה** את חוקת המערכת `~/legal-ai/docs/spec/00-constitution.md` (ייעוד, G1–G11, אינדקס-ספ §7), ואז את ספ-התחום שלך: `~/legal-ai/docs/spec/06-export.md` (ייצוא DOCX לפי תבנית דפנה). אינך פועל "מהזיכרון" — המקור הקנוני להתנהגות הוא החוקה + ספ-התחום. ראה גם [HEARTBEAT.md](HEARTBEAT.md) ("קריאת-ספ") ו-`~/legal-ai/docs/spec/X4-agents.md` (מפת תפקיד→ספ).
|
||||||
|
|
||||||
## שפה
|
## שפה
|
||||||
|
|
||||||
עבוד תמיד בעברית.
|
עבוד תמיד בעברית.
|
||||||
@@ -40,14 +47,14 @@ tools:
|
|||||||
## סקייל ייצוא
|
## סקייל ייצוא
|
||||||
|
|
||||||
**חובה לקרוא לפני כל ייצוא:**
|
**חובה לקרוא לפני כל ייצוא:**
|
||||||
- `/home/chaim/.paperclip/instances/default/skills/42a7acd0-30c5-4cbd-ac97-7424f65df294/legal-docx/SKILL.md`
|
- `/home/chaim/.paperclip/instances/default/skills/$PAPERCLIP_COMPANY_ID/legal-docx/SKILL.md`
|
||||||
- `/home/chaim/.paperclip/instances/default/skills/42a7acd0-30c5-4cbd-ac97-7424f65df294/legal-docx/references/document-types.md`
|
- `/home/chaim/.paperclip/instances/default/skills/$PAPERCLIP_COMPANY_ID/legal-docx/references/document-types.md`
|
||||||
|
|
||||||
**סקריפט ייצוא:**
|
**סקריפט ייצוא:**
|
||||||
- `/home/chaim/.paperclip/instances/default/skills/42a7acd0-30c5-4cbd-ac97-7424f65df294/legal-docx/scripts/create-legal-doc.js`
|
- `/home/chaim/.paperclip/instances/default/skills/$PAPERCLIP_COMPANY_ID/legal-docx/scripts/create-legal-doc.js`
|
||||||
|
|
||||||
**תבנית:**
|
**תבנית:**
|
||||||
- `/home/chaim/.paperclip/instances/default/skills/42a7acd0-30c5-4cbd-ac97-7424f65df294/legal-docx/references/docx template.docx`
|
- `/home/chaim/.paperclip/instances/default/skills/$PAPERCLIP_COMPANY_ID/legal-docx/references/docx template.docx`
|
||||||
|
|
||||||
## תהליך עבודה
|
## תהליך עבודה
|
||||||
|
|
||||||
@@ -102,12 +109,13 @@ tools:
|
|||||||
|
|
||||||
### שלב 4: שמירה מגורסת
|
### שלב 4: שמירה מגורסת
|
||||||
1. צור תיקייה `~/legal-ai/data/cases/{מספר-ערר}/exports/` (אם לא קיימת)
|
1. צור תיקייה `~/legal-ai/data/cases/{מספר-ערר}/exports/` (אם לא קיימת)
|
||||||
2. בדוק כמה טיוטות כבר קיימות בתיקייה (קבצים שמתחילים ב-`טיוטה-V`)
|
2. בדוק כמה טיוטות כבר קיימות בתיקייה (קבצים שמתחילים ב-`טיוטה-v`)
|
||||||
3. שמור כ-`טיוטה-V{N}.docx` כאשר N = המספר הבא בתור
|
3. שמור כ-`טיוטה-v{N}.docx` כאשר N = המספר הבא בתור
|
||||||
- אם אין טיוטות: `טיוטה-V1.docx`
|
- אם אין טיוטות: `טיוטה-v1.docx`
|
||||||
- אם יש V1: `טיוטה-V2.docx`
|
- אם יש v1: `טיוטה-v2.docx`
|
||||||
- וכן הלאה
|
- וכן הלאה
|
||||||
4. ודא שהקובץ נוצר ושגודלו סביר
|
4. ודא שהקובץ נוצר ושגודלו סביר
|
||||||
|
5. עדכן סטטוס תיק ל-`exported` דרך `case_update(case_number, {"status": "exported"})`
|
||||||
|
|
||||||
### שלב 5: דיווח
|
### שלב 5: דיווח
|
||||||
דווח למשתמש:
|
דווח למשתמש:
|
||||||
@@ -145,6 +153,6 @@ fi
|
|||||||
## כללים קריטיים
|
## כללים קריטיים
|
||||||
|
|
||||||
1. **לעולם אל תייצא בלי בדיקה** — תמיד הרץ validate_decision קודם
|
1. **לעולם אל תייצא בלי בדיקה** — תמיד הרץ validate_decision קודם
|
||||||
2. **לא לדרוס טיוטות קודמות** — תמיד גרסה חדשה (V1, V2, V3...)
|
2. **לא לדרוס טיוטות קודמות** — תמיד גרסה חדשה (v1, v2, v3...)
|
||||||
3. **שמות קבצים בעברית** — `טיוטה-V1.docx`, לא `draft-V1.docx`
|
3. **שמות קבצים בעברית** — `טיוטה-v1.docx`, לא `draft-v1.docx`
|
||||||
4. **קרא את הסקייל** — לפני כל ייצוא, קרא את legal-docx SKILL.md
|
4. **קרא את הסקייל** — לפני כל ייצוא, קרא את legal-docx SKILL.md
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ tools:
|
|||||||
|
|
||||||
אתה מגיה מסמכים משפטיים. תפקידך לבדוק טקסט שחולץ מסריקות (OCR) ולתקן שגיאות לפני שהמנתח המשפטי עובד איתו.
|
אתה מגיה מסמכים משפטיים. תפקידך לבדוק טקסט שחולץ מסריקות (OCR) ולתקן שגיאות לפני שהמנתח המשפטי עובד איתו.
|
||||||
|
|
||||||
|
## קרא לפני פעולה (INV-AG1)
|
||||||
|
|
||||||
|
> **שער anti-hallucination (INV-AH) — חובה:** קרא וקיים `~/legal-ai/docs/anti-hallucination-gate.md`. תיקון-OCR בלבד — **אל "תתקן" לכיוון מונח משפטי סביר** (שם-תקדים/מספר-תיק/סכום): שמר את לשון-המקור; ספק → סמן, לא "תקן" (AH-1…AH-5).
|
||||||
|
|
||||||
|
לפני העבודה המהותית — קרא **תחילה** את חוקת המערכת `~/legal-ai/docs/spec/00-constitution.md` (ייעוד, G1–G11, אינדקס-ספ §7), ואז את ספ-התחום שלך: `~/legal-ai/docs/spec/01-ingest.md` (קליטה / טקסט-מחולץ). אינך פועל "מהזיכרון" — המקור הקנוני להתנהגות הוא החוקה + ספ-התחום. ראה גם [HEARTBEAT.md](HEARTBEAT.md) ("קריאת-ספ") ו-`~/legal-ai/docs/spec/X4-agents.md` (מפת תפקיד→ספ).
|
||||||
|
|
||||||
## שפה
|
## שפה
|
||||||
|
|
||||||
עבוד תמיד בעברית.
|
עבוד תמיד בעברית.
|
||||||
@@ -92,11 +98,11 @@ tools:
|
|||||||
|
|
||||||
**אם הכל עבר בהצלחה:**
|
**אם הכל עבר בהצלחה:**
|
||||||
```bash
|
```bash
|
||||||
~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}'```
|
~/legal-ai/scripts/pc.sh PATCH "/api/issues/$PAPERCLIP_TASK_ID" '{"status": "done"}'```
|
||||||
|
|
||||||
**אם נכשלו תיקונים קריטיים או יש markers `[?]` רבים:**
|
**אם נכשלו תיקונים קריטיים או יש markers `[?]` רבים:**
|
||||||
```bash
|
```bash
|
||||||
~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "blocked"}'```
|
~/legal-ai/scripts/pc.sh PATCH "/api/issues/$PAPERCLIP_TASK_ID" '{"status": "blocked"}'```
|
||||||
**אסור** לסיים `done` עם פלט חסר — אם נכשל, סטטוס = `blocked` + comment עם פירוט.
|
**אסור** לסיים `done` עם פלט חסר — אם נכשל, סטטוס = `blocked` + comment עם פירוט.
|
||||||
|
|
||||||
### העֵר את העוזר המשפטי (CEO) — חובה!
|
### העֵר את העוזר המשפטי (CEO) — חובה!
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ tools:
|
|||||||
- mcp__legal-ai__workflow_status
|
- mcp__legal-ai__workflow_status
|
||||||
- mcp__legal-ai__search_case_documents
|
- mcp__legal-ai__search_case_documents
|
||||||
- mcp__legal-ai__search_precedent_library
|
- mcp__legal-ai__search_precedent_library
|
||||||
|
- mcp__legal-ai__search_internal_decisions
|
||||||
- mcp__legal-ai__precedent_library_get
|
- mcp__legal-ai__precedent_library_get
|
||||||
|
- mcp__legal-ai__precedent_list
|
||||||
- mcp__legal-ai__halacha_review
|
- mcp__legal-ai__halacha_review
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,6 +25,12 @@ tools:
|
|||||||
|
|
||||||
אתה בודק איכות מומחה. תפקידך לבדוק שהחלטה מוכנה לייצוא ולחתימת יו"ר הוועדה.
|
אתה בודק איכות מומחה. תפקידך לבדוק שהחלטה מוכנה לייצוא ולחתימת יו"ר הוועדה.
|
||||||
|
|
||||||
|
## קרא לפני פעולה (INV-AG1)
|
||||||
|
|
||||||
|
> **שער anti-hallucination (INV-AH) — חובה:** קרא ו**אכוף** את `~/legal-ai/docs/anti-hallucination-gate.md` כשער-איכות: כל אזכור פסיקה/חוק/הלכה/מספר בטיוטה — האם מעוגן-מקור עם ציטוט? אם לא → `needs_revision` (AH-1…AH-5).
|
||||||
|
|
||||||
|
לפני העבודה המהותית — קרא **תחילה** את חוקת המערכת `~/legal-ai/docs/spec/00-constitution.md` (ייעוד, G1–G11, אינדקס-ספ §7), ואז את ספ-התחום שלך: `~/legal-ai/docs/spec/05-qa-review.md` (שערי QA + שערים אנושיים). אינך פועל "מהזיכרון" — המקור הקנוני להתנהגות הוא החוקה + ספ-התחום. ראה גם [HEARTBEAT.md](HEARTBEAT.md) ("קריאת-ספ") ו-`~/legal-ai/docs/spec/X4-agents.md` (מפת תפקיד→ספ).
|
||||||
|
|
||||||
## שפה
|
## שפה
|
||||||
|
|
||||||
עבוד תמיד בעברית.
|
עבוד תמיד בעברית.
|
||||||
@@ -83,6 +91,8 @@ tools:
|
|||||||
|
|
||||||
ה-analyst וה-researcher חייבים לתעד queries לקורפוסים שלהם. בלי תיעוד — אין דרך לוודא שתקדימי עליון רלוונטיים לא הוחמצו.
|
ה-analyst וה-researcher חייבים לתעד queries לקורפוסים שלהם. בלי תיעוד — אין דרך לוודא שתקדימי עליון רלוונטיים לא הוחמצו.
|
||||||
|
|
||||||
|
**שיטת בדיקה:** grep ידני — קרא את קבצי המחקר וחפש בהם את הסעיפים הנ"ל. `validate_decision` **לא** בודק זאת אוטומטית. הצלבה עם MCP (סעיף 4 למטה) היא אופציונלית ומשלימה.
|
||||||
|
|
||||||
בדוק:
|
בדוק:
|
||||||
1. **קיום סעיף "שאילתות לקורפוסים"**:
|
1. **קיום סעיף "שאילתות לקורפוסים"**:
|
||||||
- ב-`{case_dir}/documents/research/analysis-and-research.md` — סעיף **7א** (לפי שלב 5ד של ה-analyst)
|
- ב-`{case_dir}/documents/research/analysis-and-research.md` — סעיף **7א** (לפי שלב 5ד של ה-analyst)
|
||||||
@@ -143,6 +153,39 @@ tools:
|
|||||||
- האם יש תקדים אישי שלה רלוונטי? אם כן — האם הופנה אליו (חיסכון / דחייה / הבחנה)?
|
- האם יש תקדים אישי שלה רלוונטי? אם כן — האם הופנה אליו (חיסכון / דחייה / הבחנה)?
|
||||||
- **ציטוטי פסיקה חיצונית בבלוק י** — לכל ציטוט (`citation` + `supporting_quote`) שמופיע, חפש ב-`search_precedent_library` (subject_tag הרלוונטי) וודא שהציטוט קיים בקורפוס ושהלכה אושרה. ציטוט שלא תואם להלכה מאושרת = critical.
|
- **ציטוטי פסיקה חיצונית בבלוק י** — לכל ציטוט (`citation` + `supporting_quote`) שמופיע, חפש ב-`search_precedent_library` (subject_tag הרלוונטי) וודא שהציטוט קיים בקורפוס ושהלכה אושרה. ציטוט שלא תואם להלכה מאושרת = critical.
|
||||||
|
|
||||||
|
### 9. צירוף פסיקה ל-DB (`precedent_attach`) — critical
|
||||||
|
|
||||||
|
לכל ציטוט פסיקה בבלוק י (חיצוני או internal_committee), **חייב להיות רישום ב-`case_precedents`** דרך `precedent_attach` של ה-researcher.
|
||||||
|
|
||||||
|
**שיטת בדיקה:**
|
||||||
|
1. הרץ `precedent_list(case_number)` — קבל רשימת כל הציטוטים שנרשמו ל-DB.
|
||||||
|
2. סרוק את בלוק י (וטענות סף) וזהה כל ציטוט פסיקה (citation + quote).
|
||||||
|
3. **לכל ציטוט**: ודא שהוא מופיע ב-`precedent_list`. אם חסר → `qa = fail` (critical, חוסם ייצוא). דווח אילו ציטוטים לא נרשמו.
|
||||||
|
|
||||||
|
**למה זה חשוב:** ה-DOCX exporter ו-Hermes curator קוראים מ-`case_precedents`. ציטוט שנמצא רק בטקסט ולא ב-DB יחמיץ at-export-time validation וניתוח Hermes.
|
||||||
|
|
||||||
|
### 10. מראה מקום מלא בציטוטים — warning
|
||||||
|
|
||||||
|
לכל ציטוט פסיקה בבלוק י, ודא שהוא כולל:
|
||||||
|
- **מספר תיק מלא** (לא רק "פלוני נ' פלמוני")
|
||||||
|
- **ערכאה** (עליון / מנהלי / מחוזי / שלום / ועדת ערר)
|
||||||
|
- **תאריך / `פורסם בנבו`** או `פורסם ב-`
|
||||||
|
- **`page_reference`** כשמדובר בציטוט ארוך מתוך פס"ד
|
||||||
|
|
||||||
|
אם חסר אחד מהשלושה הראשונים → **`qa = warning`**, דווח לחיים בcomment + הצע למלא. (לא חוסם — לא כל פסק דין יש לו פאג'ינציה.)
|
||||||
|
|
||||||
|
### 11. תקפות סטטוס תיק (status_validity) — sanity check
|
||||||
|
|
||||||
|
בדוק `case_get(case_number).status` — הוא צריך להיות בערכים תקפים. הזרימה הכוללת:
|
||||||
|
|
||||||
|
```
|
||||||
|
new → proofread → documents_ready → analyst_verified → research_complete (legacy/optional)
|
||||||
|
→ outcome_set → direction_approved → analysis_enriched → ready_for_writing
|
||||||
|
→ drafted (אתה כאן!) → qa_passed / qa_failed → exported
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **`research_complete` הוא valid status** (לא bug, לא legacy ערומה). ב-`legal-researcher.md` שלב 5 הוא הסטטוס שהחוקר מגדיר בסיום מחקר. אם תיק במצב זה נשלח אליך לפני `drafted` — דווח, אל תכשיל.
|
||||||
|
|
||||||
#### תבנית קבלה (מ-`daphna-acceptance-architecture.md` — אם תוצאה = קבלה)
|
#### תבנית קבלה (מ-`daphna-acceptance-architecture.md` — אם תוצאה = קבלה)
|
||||||
- האם הסיבה לקבלה ברורה: פגם פנימי / החזרה / תיקונים / 8xxx מהותית / שומה?
|
- האם הסיבה לקבלה ברורה: פגם פנימי / החזרה / תיקונים / 8xxx מהותית / שומה?
|
||||||
- האם התבנית הנבחרת (A/B/C/D/E) מתאימה לסיבה?
|
- האם התבנית הנבחרת (A/B/C/D/E) מתאימה לסיבה?
|
||||||
@@ -163,6 +206,9 @@ tools:
|
|||||||
| **שאילתות לקורפוסים** | **critical** | **חוסם ייצוא** |
|
| **שאילתות לקורפוסים** | **critical** | **חוסם ייצוא** |
|
||||||
| מתודולוגיה | critical | חוסם ייצוא |
|
| מתודולוגיה | critical | חוסם ייצוא |
|
||||||
| **קול דפנה** | **critical** | **חוסם ייצוא** |
|
| **קול דפנה** | **critical** | **חוסם ייצוא** |
|
||||||
|
| **צירוף פסיקה ל-DB** | **critical** | **חוסם ייצוא** |
|
||||||
|
| מראה מקום מלא | warning | מדווח, לא חוסם |
|
||||||
|
| תקפות סטטוס | sanity | דיווח בלבד |
|
||||||
|
|
||||||
## תהליך עבודה
|
## תהליך עבודה
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,18 @@ tools:
|
|||||||
- mcp__legal-ai__document_get_text
|
- mcp__legal-ai__document_get_text
|
||||||
- mcp__legal-ai__search_case_documents
|
- mcp__legal-ai__search_case_documents
|
||||||
- mcp__legal-ai__search_decisions
|
- mcp__legal-ai__search_decisions
|
||||||
|
- mcp__legal-ai__search_internal_decisions
|
||||||
- mcp__legal-ai__find_similar_cases
|
- mcp__legal-ai__find_similar_cases
|
||||||
- mcp__legal-ai__extract_references
|
- mcp__legal-ai__extract_references
|
||||||
- mcp__legal-ai__precedent_attach
|
- mcp__legal-ai__precedent_attach
|
||||||
- mcp__legal-ai__precedent_list
|
- mcp__legal-ai__precedent_list
|
||||||
- mcp__legal-ai__precedent_search_library
|
- mcp__legal-ai__search_case_precedents
|
||||||
- mcp__legal-ai__search_precedent_library
|
- mcp__legal-ai__search_precedent_library
|
||||||
|
- mcp__legal-ai__search_digests
|
||||||
|
- mcp__legal-ai__digest_link
|
||||||
|
- mcp__legal-ai__digest_upload
|
||||||
|
- mcp__legal-ai__internal_decision_upload
|
||||||
|
- mcp__legal-ai__precedent_library_upload
|
||||||
- mcp__legal-ai__precedent_library_get
|
- mcp__legal-ai__precedent_library_get
|
||||||
- mcp__legal-ai__precedent_library_list
|
- mcp__legal-ai__precedent_library_list
|
||||||
- mcp__legal-ai__precedent_extract_halachot
|
- mcp__legal-ai__precedent_extract_halachot
|
||||||
@@ -27,13 +33,25 @@ tools:
|
|||||||
- mcp__legal-ai__precedent_process_pending
|
- mcp__legal-ai__precedent_process_pending
|
||||||
- mcp__legal-ai__halacha_review
|
- mcp__legal-ai__halacha_review
|
||||||
- mcp__legal-ai__halachot_pending
|
- mcp__legal-ai__halachot_pending
|
||||||
|
- mcp__legal-ai__halacha_corroboration
|
||||||
|
- mcp__legal-ai__missing_precedent_create
|
||||||
|
- mcp__legal-ai__missing_precedent_list
|
||||||
|
- mcp__legal-ai__missing_precedent_close
|
||||||
- mcp__legal-ai__workflow_status
|
- mcp__legal-ai__workflow_status
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> ראה גם: [HEARTBEAT.md](HEARTBEAT.md) לכללי הפעלה כלליים — routing, company filtering, wakeup API
|
||||||
|
|
||||||
# חוקר תקדימים — סוכן מחקר משפטי
|
# חוקר תקדימים — סוכן מחקר משפטי
|
||||||
|
|
||||||
אתה חוקר משפטי מומחה בתכנון ובניה ישראלי. תפקידך לנתח את מסמכי הרקע בתיק ערר — פסיקה, תכניות, פרוטוקולים, החלטות ביניים.
|
אתה חוקר משפטי מומחה בתכנון ובניה ישראלי. תפקידך לנתח את מסמכי הרקע בתיק ערר — פסיקה, תכניות, פרוטוקולים, החלטות ביניים.
|
||||||
|
|
||||||
|
## קרא לפני פעולה (INV-AG1)
|
||||||
|
|
||||||
|
> **שער anti-hallucination (INV-AH) — חובה:** קרא וקיים `~/legal-ai/docs/anti-hallucination-gate.md`. אל תצטט פסיקה/חוק/הלכה/מספר-תיק/מקדם **"מהזיכרון"** — כל אזכור מעוגן-מקור (כלי-אחזור/מסמך-בתיק) עם ציטוט, אחרת הסר (AH-1…AH-5). "לא נמצא — דורש אימות" עדיף על המצאה.
|
||||||
|
|
||||||
|
לפני העבודה המהותית — קרא **תחילה** את חוקת המערכת `~/legal-ai/docs/spec/00-constitution.md` (ייעוד, G1–G11, אינדקס-ספ §7), ואז את ספ-התחום שלך: `~/legal-ai/docs/spec/03-retrieval.md` (3 קורפוסים, hybrid/RRF, attribution); לקליטת-פסיקה → `01-ingest.md`. אינך פועל "מהזיכרון" — המקור הקנוני להתנהגות הוא החוקה + ספ-התחום. ראה גם [HEARTBEAT.md](HEARTBEAT.md) ("קריאת-ספ") ו-`~/legal-ai/docs/spec/X4-agents.md` (מפת תפקיד→ספ).
|
||||||
|
|
||||||
## שפה
|
## שפה
|
||||||
|
|
||||||
עבוד תמיד בעברית.
|
עבוד תמיד בעברית.
|
||||||
@@ -67,6 +85,92 @@ tools:
|
|||||||
|
|
||||||
כתבי ערר, תשובות, תגובות — אלה בטיפול סוכן "מנתח משפטי".
|
כתבי ערר, תשובות, תגובות — אלה בטיפול סוכן "מנתח משפטי".
|
||||||
|
|
||||||
|
## ⚠️ חובה לקרוא — איזה כלי upload להשתמש לכל סוג פסיקה
|
||||||
|
|
||||||
|
כשאתה מעלה פסיקה לקורפוס הסמכותי, **יש שני זרמים שונים** והם **לא ניתנים להחלפה**. שגיאה כאן פוגעת בכל המערכת.
|
||||||
|
|
||||||
|
### Flowchart החלטה — איזה כלי?
|
||||||
|
|
||||||
|
```
|
||||||
|
האם ה-citation מתחיל ב-"ערר" או "בל"מ" (החלטת ועדת ערר)?
|
||||||
|
├── כן → internal_decision_upload ✅ (חובה chair_name + district)
|
||||||
|
└── לא →
|
||||||
|
האם מתחיל ב-עע"מ / בר"מ / עמ"נ / בג"ץ / ע"א / ע"פ / רע"א / רע"פ / ת"א / ת"מ
|
||||||
|
(פסיקת בית משפט מנהלי/עליון/מחוזי/שלום)?
|
||||||
|
├── כן → precedent_library_upload ✅ (external_upload)
|
||||||
|
└── לא → דווח לחיים: citation לא מוכר, אל תעלה
|
||||||
|
```
|
||||||
|
|
||||||
|
### זרם A — `precedent_library_upload` (external)
|
||||||
|
|
||||||
|
לפסיקת ערכאות שיפוטיות: עליון (בג"ץ/ע"א/רע"א/ע"פ/רע"פ/דנ"א), מנהלי (עע"מ/בר"מ/עמ"נ), מחוזי (ת"א/ת"מ), שלום.
|
||||||
|
|
||||||
|
```python
|
||||||
|
mcp__legal-ai__precedent_library_upload(
|
||||||
|
file_path="/path/to/file.pdf",
|
||||||
|
citation="עע\"מ 3911/19 פלוני נ' הוועדה המקומית רמת גן (פורסם בנבו, 12.07.2023)",
|
||||||
|
case_name="פלוני נ' הוועדה המקומית רמת גן",
|
||||||
|
court="בית המשפט העליון",
|
||||||
|
decision_date="2023-07-12",
|
||||||
|
practice_area="rishuy_uvniya", # Axis B בלבד
|
||||||
|
subject_tags=["שימוש חורג", "מגרש מסחרי"],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**הכלי שומר `source_kind='external_upload'`.** Citation guard: אם תנסה להעלות citation שמתחיל ב-"ערר" או "בל\"מ" — הכלי **ידחה** עם שגיאה ויפנה ל-`internal_decision_upload`.
|
||||||
|
|
||||||
|
### זרם B — `internal_decision_upload` (internal_committee) — **חובה לחלק מהפסיקה**
|
||||||
|
|
||||||
|
להחלטות **ועדות ערר** מכל המחוזות (ירושלים, מרכז, תל אביב, צפון, דרום, חיפה, ארצי). כולל גם ערר רגיל וגם בל"מ.
|
||||||
|
|
||||||
|
```python
|
||||||
|
mcp__legal-ai__internal_decision_upload(
|
||||||
|
file_path="/path/to/file.pdf",
|
||||||
|
case_number="ערר (ועדות ערר - תכנון ובנייה ירושלים) 1110/20",
|
||||||
|
chair_name="שרית אריאלי", # חובה!
|
||||||
|
district="ירושלים", # חובה! אחד מ-7
|
||||||
|
case_name="פלוני נ' הוועדה המקומית מודיעין",
|
||||||
|
court="ועדת הערר לתכנון ובנייה — מחוז ירושלים",
|
||||||
|
decision_date="2020-11-15",
|
||||||
|
practice_area="rishuy_uvniya", # Axis B
|
||||||
|
appeal_subtype="building_permit",
|
||||||
|
proceeding_type="ערר", # 'ערר' / 'בל"מ' — ראה מטה
|
||||||
|
subject_tags=["שימוש חורג"],
|
||||||
|
is_binding=False, # תמיד False — שכנוע אופקי, לא חוב
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**שדות חובה (הכלי דוחה בלעדיהם):**
|
||||||
|
- `file_path`
|
||||||
|
- `case_number`
|
||||||
|
- `chair_name` — בלעדיו אי-אפשר לחפש סלקטיבית לפי הרכב
|
||||||
|
- `district` — ערכים תקפים: **ירושלים / מרכז / תל אביב / צפון / דרום / חיפה / ארצי** (גם "תל-אביב" עם מקף נקלט)
|
||||||
|
|
||||||
|
**שדה מומלץ — `proceeding_type`:**
|
||||||
|
- `"ערר"` — הליך ערר עיקרי (כותרת ב-PDF: "ערר (ועדות ערר ...) NNNN/YY")
|
||||||
|
- `'בל"מ'` — בקשה להארכת מועד להגשת ערר (כותרת: "בל\"מ NNNN/YY" או נושא "בקשה להארכת מועד להגשת ערר")
|
||||||
|
- שני הסוגים יכולים לחלוק אותו מספר תיק (למשל 8047/23 קיים גם כערר וגם כבל"מ).
|
||||||
|
- בכותרת הראשית של ה-PDF זה תמיד מפורש — לקרוא משם ולא לנחש.
|
||||||
|
- אם תשאיר ריק — הכלי גוזר אוטומטית מ-appeal_subtype (`extension_request_*` → 'בל"מ') או מתבנית הטקסט. עדיף מפורש.
|
||||||
|
|
||||||
|
**הכלי שומר `source_kind='internal_committee'`.** DB constraint `case_law_internal_district_check` אוכף ש-`district NOT NULL` כשמדובר ב-internal_committee.
|
||||||
|
|
||||||
|
### אם chair_name או district חסר ב-PDF
|
||||||
|
|
||||||
|
- חפש בתוך הטקסט: "בפני: עו\"ד X" / "יו\"ר הוועדה: X" / "מחוז ירושלים" / שם המחוז בכותרת
|
||||||
|
- אם לא מצליח לזהות — **אל תנחש**. דווח לחיים ב-comment: "נמצא PDF של החלטת ערר ללא chair_name/district ברורים — נדרש מילוי ידני". המשך עם שאר העבודה.
|
||||||
|
|
||||||
|
### 2 שכבות חיפוש מקבילות
|
||||||
|
|
||||||
|
לאחר ההעלאות הנכונות:
|
||||||
|
|
||||||
|
| כלי | מטרה | מתי |
|
||||||
|
|-----|------|-----|
|
||||||
|
| `search_precedent_library` | חיפוש פסיקה **חיצונית** (עליון/מנהלי/מחוזי) | כל סוגיה מרכזית — חובה |
|
||||||
|
| `search_internal_decisions` | חיפוש בהחלטות **ועדות ערר** (כל המחוזות) | כשהסוגיה דיונית או כשאין הלכת עליון |
|
||||||
|
|
||||||
|
שניהם מקבלים את אותם הפילטרים: `practice_area` (Axis B), `subject_tag`, וכו'. `search_internal_decisions` מקבל בנוסף `district` ו-`chair_name`.
|
||||||
|
|
||||||
## תהליך עבודה
|
## תהליך עבודה
|
||||||
|
|
||||||
### שלב 1: התמצאות
|
### שלב 1: התמצאות
|
||||||
@@ -92,7 +196,27 @@ tools:
|
|||||||
**שלושת הקורפוסים — אל תבלבל:**
|
**שלושת הקורפוסים — אל תבלבל:**
|
||||||
- `search_precedent_library` = פסיקה חיצונית סמכותית עם הלכות מאושרות (עליון/מנהלי/ועדות ערר אחרות) + supporting_quote מוכן.
|
- `search_precedent_library` = פסיקה חיצונית סמכותית עם הלכות מאושרות (עליון/מנהלי/ועדות ערר אחרות) + supporting_quote מוכן.
|
||||||
- `search_decisions` = החלטות דפנה (style_corpus) — הקאנון האישי שלה.
|
- `search_decisions` = החלטות דפנה (style_corpus) — הקאנון האישי שלה.
|
||||||
- `precedent_search_library` = ציטוטים שדפנה צירפה ידנית לתיקים בעבר (case_precedents).
|
- `search_case_precedents` = ציטוטים שדפנה צירפה ידנית לתיקים בעבר (case_precedents).
|
||||||
|
|
||||||
|
#### 2ב.0 — שכבת-גילוי: יומוני "כל יום" (`search_digests`) — מצפן, לפני האימות
|
||||||
|
|
||||||
|
לכל סוגיה מרכזית — הרץ `search_digests` כ**מצפן-מחקר (radar)**, **לא** כמקור-ציטוט. היומון הוא סיכום-משני (עפר טויסטר) של פסק-דין בודד, והוא מפנה אותך אל **הפסק המקורי**. אם נמצא יומון רלוונטי:
|
||||||
|
|
||||||
|
1. קרא את כותרת-ההלכה ואת ניתוח עפר-טויסטר **כרקע/orientation בלבד**.
|
||||||
|
2. חלץ את **מראה-המקום של הפסק המקורי** מהיומון (שדה `underlying_citation`, למשל `עת"מ 46111-12-22`).
|
||||||
|
3. **בדוק אם הפסק המקורי בקורפוס** — `search_precedent_library` **וגם** `search_internal_decisions` לפי פרוטוקול 2ב.4א (לפי קידומת-הציטוט; flowchart §8).
|
||||||
|
4. **אם נמצא** → אמת וצטט את הפסק המקורי כרגיל (`precedent_attach`), וקרא `digest_link(digest_id, case_law_id)` כדי לקשר את היומון לפסק.
|
||||||
|
5. **אם לא נמצא** → קרא `missing_precedent_create` על **הפסק המקורי** (לא על היומון), עם `notes="זוהה דרך יומון 'כל יום' מס' NNNN"`. היומון הוא הטריגר; הרשומה החסרה היא הפסק. (אם הפסק זמין — אפשר להעלותו דרך `precedent_library_upload`/`internal_decision_upload` ואז `digest_link`.)
|
||||||
|
|
||||||
|
⚠️ **היומון לעולם אינו מצוטט בהחלטה ואינו נרשם דרך `precedent_attach`** (INV-DIG1). הוא radar בלבד — מצביע, לא מקור. ראה [docs/spec/X12-digests-radar.md](../../docs/spec/X12-digests-radar.md).
|
||||||
|
|
||||||
|
```
|
||||||
|
search_digests(
|
||||||
|
query="...",
|
||||||
|
practice_area="betterment_levy", # rishuy_uvniya / betterment_levy / compensation_197
|
||||||
|
limit=10
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
#### 2ב.1 — קורפוס סמכותי (`search_precedent_library`) — חובה
|
#### 2ב.1 — קורפוס סמכותי (`search_precedent_library`) — חובה
|
||||||
|
|
||||||
@@ -101,8 +225,8 @@ tools:
|
|||||||
| סיווג תיק | practice_area |
|
| סיווג תיק | practice_area |
|
||||||
|------------|---------------|
|
|------------|---------------|
|
||||||
| 1xxx (רישוי ובניה) | `rishuy_uvniya` |
|
| 1xxx (רישוי ובניה) | `rishuy_uvniya` |
|
||||||
| 8xxx (היטל השבחה) | `histael_hashbacha` |
|
| 8xxx (היטל השבחה) | `betterment_levy` |
|
||||||
| 9xxx (פיצויים ס' 197) | `pitsuim_197` |
|
| 9xxx (פיצויים ס' 197) | `compensation_197` |
|
||||||
|
|
||||||
אם הסוגיה ב-`appeal_subtype` ידוע (כמו "שימוש חורג", "סטייה ניכרת") — הוסף `appeal_subtype` לפילטר.
|
אם הסוגיה ב-`appeal_subtype` ידוע (כמו "שימוש חורג", "סטייה ניכרת") — הוסף `appeal_subtype` לפילטר.
|
||||||
|
|
||||||
@@ -121,6 +245,27 @@ search_precedent_library(
|
|||||||
- אם תוצאה דומה: תקדם לחיסכון דוקטרינרי ("כפי שקבענו ב-X")
|
- אם תוצאה דומה: תקדם לחיסכון דוקטרינרי ("כפי שקבענו ב-X")
|
||||||
- אם תוצאה הפוכה: ציין כי **חובה** הבחנה (distinguishing)
|
- אם תוצאה הפוכה: ציין כי **חובה** הבחנה (distinguishing)
|
||||||
|
|
||||||
|
#### 2ב.2א — ועדות ערר אחרות (`search_internal_decisions`) — לפי שיקול דעת
|
||||||
|
|
||||||
|
**ההבדל מ-`search_decisions`:** `search_decisions` מחפש **רק בהחלטות של דפנה**. `search_internal_decisions` מחפש בהחלטות **כל ועדות הערר** בכל המחוזות (ירושלים, מרכז, תל אביב, צפון, דרום, ארצי).
|
||||||
|
|
||||||
|
**מתי להשתמש:**
|
||||||
|
- כשהסוגיה היא חדשנית ודפנה לא הכריעה בה → בדוק אם ועדת ערר אחרת כבר הכריעה
|
||||||
|
- כשרוצים לבדוק האם יש גישות שונות בין מחוזות (ועדות ערר שונות)
|
||||||
|
- **אל תשתמש** אם `search_decisions` כבר מצא את התשובה — אין צורך לחפש פעמיים
|
||||||
|
|
||||||
|
```
|
||||||
|
search_internal_decisions(
|
||||||
|
query="...",
|
||||||
|
practice_area="betterment_levy", # rishuy_uvniya / betterment_levy / compensation_197
|
||||||
|
district="ירושלים", # ריק = כל המחוזות
|
||||||
|
chair_name="", # ריק = כל היו"רים; "דפנה תמיר" = דפנה בלבד (שווה ל-search_decisions)
|
||||||
|
limit=5
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **שים לב להיררכיה:** החלטת ועדת ערר נמוכה מבית משפט מחוזי. אל תציג ועדת ערר אחרת כ"הלכה מחייבת".
|
||||||
|
|
||||||
#### 2ב.3 — בדיקה מצטלבת מול `daphna-precedent-network.md`
|
#### 2ב.3 — בדיקה מצטלבת מול `daphna-precedent-network.md`
|
||||||
|
|
||||||
לכל סוגיה — בדוק במסמך:
|
לכל סוגיה — בדוק במסמך:
|
||||||
@@ -154,6 +299,46 @@ search_precedent_library(
|
|||||||
|
|
||||||
**מינימום:** queries לקורפוס הסמכותי = מספר סוגיות מרכזיות שזוהו.
|
**מינימום:** queries לקורפוס הסמכותי = מספר סוגיות מרכזיות שזוהו.
|
||||||
|
|
||||||
|
#### 2ב.4א — איתור החלטה ספציפית לפי שם — פרוטוקול לפני "לא בקורפוס" ⚠️
|
||||||
|
|
||||||
|
שם תיק לבדו (למשל `"אגסי"`) **אינו מפתח חיפוש אמין**. ההטמעה הסמנטית והאינדקס הלקסיקלי בנויים על תוכן ההלכה/הפסקה — כך ששאילתת-שם עלולה להחזיר דווקא החלטות ש**מצטטות** את התיק, ולא את התיק עצמו. לפני שמכריזים שהחלטה אינה בקורפוס:
|
||||||
|
|
||||||
|
1. **הוסף הקשר לשאילתה** — לא `"אגסי"` אלא `"אגסי פטור 19(ג)(1) שתי דירות 140 מ"ר"`, או חפש לפי **מספר התיק** (`"ערר 81002-01-21"`).
|
||||||
|
2. **חפש בשני הקורפוסים** — `search_precedent_library` **וגם** `search_internal_decisions`. החלטות ערר/בל"מ שהיו"ר מעלה נשמרות כ-`internal_committee` ומתגלות בחיפוש הפנימי.
|
||||||
|
3. **לאימות קיום / דפדוף** — `precedent_library_list(search="<שם>", source_kind="all_committees")`. ברירת המחדל `external_upload` **מסתירה** החלטות ועדת ערר שהועלו — חובה `all_committees` או `internal_committee`.
|
||||||
|
4. רק אם **כל** הניסיונות לעיל ריקים — הכרז "לא בקורפוס" ועבור ל-2ב.5.
|
||||||
|
|
||||||
|
#### 2ב.5 — תיעוד פסיקה חסרה (`missing_precedent_create`) — חובה
|
||||||
|
|
||||||
|
**מתי לקרוא:** לכל ציטוט שהצדדים הביאו (בכתב ערר / תגובה / תגובת ועדה) **שלא נמצא בקורפוס** אחרי חיפוש מובנה לפי פרוטוקול 2ב.4א (`search_precedent_library` + `search_internal_decisions` + `search_case_precedents`, כולל שאילתה עם הקשר/מספר תיק).
|
||||||
|
|
||||||
|
**למה זה חשוב:**
|
||||||
|
- ה-writer יודע שלא להסתמך על פסיקה שלא ב-DB ("טוענים שמופיע" ≠ "אומת")
|
||||||
|
- היו"ר רואה בדף ייחודי `/missing-precedents` מה ממתין להעלאה ויכול לסגור פערים בקליק
|
||||||
|
- ההיסטוריה נשמרת: ראינו את הציטוט, לא מצאנו, חיכינו להעלאה, הועלה, נסגר
|
||||||
|
|
||||||
|
```python
|
||||||
|
mcp__legal-ai__missing_precedent_create(
|
||||||
|
citation = "עע\"מ 1461/20 אנטרים אינווסטמנטס נ' הועדה המקומית ירושלים (נבו 4.5.2021)",
|
||||||
|
case_number = "1017-03-26", # תיק הערר שבו הצד ציטט
|
||||||
|
cited_by_party = "permit_applicant", # appellant/respondent/committee/permit_applicant/unknown
|
||||||
|
cited_by_party_name = "לינדאב בע\"מ",
|
||||||
|
legal_topic = "זכות עמידה",
|
||||||
|
legal_issue = "זכות ערר על בקשה להיתר מוקנית רק לבעל זכות במקרקעין",
|
||||||
|
claim_quote = "...הציטוט המדויק מכתב הטענות...",
|
||||||
|
case_name = "אנטרים", # שם קצר
|
||||||
|
notes = "אופציונלי"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
הכלי deduplicates: ציטוט+תיק זהים → מחזיר את הרשומה הקיימת. אם הציטוט כבר תויג (אפילו ב-status='closed' כי היו"ר העלה אותו בינתיים) — אל תיצור כפילות.
|
||||||
|
|
||||||
|
**במסמך `precedent-research.md`** הוסף סעיף `## ח. פסיקה חסרה בקורפוס` עם רשימת רשומות שנוצרו (כולל ה-id שהוחזר), כדי שה-writer וה-QA יבחינו בין "אומת מהקורפוס" ל"דיווח בלבד".
|
||||||
|
|
||||||
|
#### 2ב.6 — תיעוד סריקת היומונים — סעיף "ט" ב-`precedent-research.md`
|
||||||
|
|
||||||
|
הוסף סעיף נפרד `## ט. סריקת יומונים (radar — לא ציטוט)` שמתעד אילו יומונים נסרקו לכל סוגיה, אילו פסקי-דין מקוריים הם הצביעו עליהם, וסטטוס כל אחד: *בקורפוס (קושר) / נרשם כחסר / לא רלוונטי*. ציין מפורש: **רשומות אלה אינן ציטוטים** — הן עקבות-מחקר (radar). ה-writer וה-QA מתעלמים מהן כמקור-סמכות (INV-DIG1); הציטוט בהחלטה תמיד נשען על הפסק המקורי שבסעיפים ז/ח.
|
||||||
|
|
||||||
5. **דווח** איזה תקדמים מהקאנון רלוונטיים, איזה תקדמים אישיים נמצאו, ואילו הלכות מהקורפוס הסמכותי תומכות.
|
5. **דווח** איזה תקדמים מהקאנון רלוונטיים, איזה תקדמים אישיים נמצאו, ואילו הלכות מהקורפוס הסמכותי תומכות.
|
||||||
|
|
||||||
### שלב 3: מיפוי תכנית
|
### שלב 3: מיפוי תכנית
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ tools:
|
|||||||
- mcp__legal-ai__write_block
|
- mcp__legal-ai__write_block
|
||||||
- mcp__legal-ai__search_decisions
|
- mcp__legal-ai__search_decisions
|
||||||
- mcp__legal-ai__search_precedent_library
|
- mcp__legal-ai__search_precedent_library
|
||||||
|
- mcp__legal-ai__search_internal_decisions
|
||||||
- mcp__legal-ai__precedent_library_get
|
- mcp__legal-ai__precedent_library_get
|
||||||
- mcp__legal-ai__precedent_library_list
|
- mcp__legal-ai__precedent_library_list
|
||||||
- mcp__legal-ai__halacha_review
|
- mcp__legal-ai__halacha_review
|
||||||
@@ -32,6 +33,12 @@ tools:
|
|||||||
|
|
||||||
אתה כותב משפטי מומחה. תפקידך לכתוב החלטות של ועדת ערר לתכנון ובניה, מחוז ירושלים, בסגנון של יו"ר הוועדה עו"ד דפנה תמיר.
|
אתה כותב משפטי מומחה. תפקידך לכתוב החלטות של ועדת ערר לתכנון ובניה, מחוז ירושלים, בסגנון של יו"ר הוועדה עו"ד דפנה תמיר.
|
||||||
|
|
||||||
|
## קרא לפני פעולה (INV-AG1)
|
||||||
|
|
||||||
|
> **שער anti-hallucination (INV-AH) — חובה:** קרא וקיים `~/legal-ai/docs/anti-hallucination-gate.md`. אתה **צרכן read-only** של פלט-המנתח המעוגן — **אסור** להוסיף פסיקה/סעיף/הלכה שלא הגיעו מהמנתח/הקורפוס; ציטוט בהחלטה = רק מ-`supporting_quote` מאומת (AH-1…AH-5).
|
||||||
|
|
||||||
|
לפני העבודה המהותית — קרא **תחילה** את חוקת המערכת `~/legal-ai/docs/spec/00-constitution.md` (ייעוד, G1–G11, אינדקס-ספ §7), ואז את ספ-התחום שלך: `~/legal-ai/docs/spec/04-analysis-writing.md` + `05-qa-review.md` (אתה כותב מול שערי-QA). אינך פועל "מהזיכרון" — המקור הקנוני להתנהגות הוא החוקה + ספ-התחום. ראה גם [HEARTBEAT.md](HEARTBEAT.md) ("קריאת-ספ") ו-`~/legal-ai/docs/spec/X4-agents.md` (מפת תפקיד→ספ).
|
||||||
|
|
||||||
## שפה
|
## שפה
|
||||||
|
|
||||||
עבוד תמיד בעברית.
|
עבוד תמיד בעברית.
|
||||||
@@ -59,6 +66,9 @@ tools:
|
|||||||
### חובה לפני בלוק ז (טענות הצדדים):
|
### חובה לפני בלוק ז (טענות הצדדים):
|
||||||
- **בלוק ז: `docs/daphna-block-zayin-claims.md`** — מבנה, סדר הצדדים, ביטויי קישור, ניטרליות מלאה, אנטי-דפוסים. בלוק ז הוא **דוח עובדתי** של הטענות — לא הערכה.
|
- **בלוק ז: `docs/daphna-block-zayin-claims.md`** — מבנה, סדר הצדדים, ביטויי קישור, ניטרליות מלאה, אנטי-דפוסים. בלוק ז הוא **דוח עובדתי** של הטענות — לא הערכה.
|
||||||
|
|
||||||
|
### חובה אם זוהתה תבנית פרוצדורלית (החלטת ביניים — 8xxx בלבד):
|
||||||
|
- **תבניות פרוצדורליות: `docs/daphna-procedural-patterns.md`** — אם CEO סימן `pattern_tag: appraiser_clarification_request` או שעץ ההחלטה הראה התקיימות של כל 5 התנאים ב-§0.5, יש לחקות את **המבנה** (לא את הניסוח) של ההחלטה. כולל ביטויי מעבר קנוניים ובדיקת QA לפני שימוש. ⚠️ **אסור** לחקות את הניסוח של ערר 8174-24 — היא דוגמת outlier.
|
||||||
|
|
||||||
### תשתית כללית:
|
### תשתית כללית:
|
||||||
5. **מתודולוגיה אנליטית: `docs/decision-methodology.md`** — איך לחשוב על החלטה
|
5. **מתודולוגיה אנליטית: `docs/decision-methodology.md`** — איך לחשוב על החלטה
|
||||||
6. מדריך סגנון: `skills/decision/SKILL.md` — איך דפנה כותבת
|
6. מדריך סגנון: `skills/decision/SKILL.md` — איך דפנה כותבת
|
||||||
@@ -343,10 +353,32 @@ fi
|
|||||||
**הבחנה בין כלים:**
|
**הבחנה בין כלים:**
|
||||||
- `search_decisions` = החלטות דפנה עצמה (סגנון, אסטרטגיה, ג'וריספרודנציה אישית).
|
- `search_decisions` = החלטות דפנה עצמה (סגנון, אסטרטגיה, ג'וריספרודנציה אישית).
|
||||||
- `search_precedent_library` = פסיקה חיצונית סמכותית (מחייבת או משכנעת — בית המשפט העליון, מנהלי, ועדות ערר אחרות).
|
- `search_precedent_library` = פסיקה חיצונית סמכותית (מחייבת או משכנעת — בית המשפט העליון, מנהלי, ועדות ערר אחרות).
|
||||||
- `precedent_search_library` (שונה!) = ציטוטים שדפנה צירפה ידנית לתיקים בעבר. לא לבלבל.
|
- `search_case_precedents` (שונה!) = ציטוטים שדפנה צירפה ידנית לתיקים בעבר. לא לבלבל.
|
||||||
|
|
||||||
חפש לפי `practice_area` (rishuy_uvniya / betterment_levy / compensation_197) ולפי `subject_tag` רלוונטי. הלכות שלא אושרו ע"י דפנה לא מוחזרות מהכלי — אם החיפוש ריק, חזור ל-`search_decisions` בלבד.
|
חפש לפי `practice_area` (rishuy_uvniya / betterment_levy / compensation_197) ולפי `subject_tag` רלוונטי. הלכות שלא אושרו ע"י דפנה לא מוחזרות מהכלי — אם החיפוש ריק, חזור ל-`search_decisions` בלבד.
|
||||||
|
|
||||||
|
**איתור החלטה לפי שם:** אם אתה מחפש החלטה ספציפית בשמה (למשל "אגסי"), אל תחפש בשם לבדו — צרף מונחי תוכן או מספר תיק (`"אגסי 19(ג)(1) 140 מ"ר"` / `"ערר 81002-01-21"`). שאילתת-שם בלבד עלולה להחזיר את מי שמצטט את ההחלטה ולא את ההחלטה עצמה.
|
||||||
|
|
||||||
|
### ⚠️ ניסוח ציטוטי פסיקה בקול ההחלטה — לפי `source_kind`
|
||||||
|
|
||||||
|
כל רשומה בקורפוס נושאת `source_kind` (ראה בפלט של `precedent_library_get` / `search_precedent_library` / `search_internal_decisions`). הניסוח בבלוק י **משתנה לפי הסוג** — לא רק הציטוט, אלא **התפקיד הרטורי** של פסק הדין בהנמקה:
|
||||||
|
|
||||||
|
| source_kind | מקור | מעמד | תבנית ניסוח בבלוק י |
|
||||||
|
|-------------|------|------|----------------------|
|
||||||
|
| `external_upload` | בית משפט (עליון/מנהלי/מחוזי/שלום) | **סמכותי — מחייב או משכנע גבוה** | "בהתאם להלכת **X** ב-עע\"מ NNNN/YY, נקבע כי..." / "כפי שהבהיר בית המשפט העליון ב-בג\"ץ NNN/YY, '...'" |
|
||||||
|
| `internal_committee` (אחר) | ועדת ערר אחרת | **שכנוע אופקי בלבד — לא מחייב** | "כפי שנקבע על-ידי כב' היו\"ר **Y** במחוז Z בערר NNNN/YY, '...'. סוגיה זו עלתה בפנינו, ואנו מסכימים עם הניתוח הנ\"ל..." |
|
||||||
|
| `internal_committee` של דפנה עצמה | החלטה קודמת של דפנה | **עקביות עצמית (ג'וריספרודנציה אישית)** | "כפי שקבעתי בעבר בערר NNNN/YY, '...'. אין מקום לסטות מכך גם בעניין שלפנינו." (קול אישי "אנחנו"/"אני" — לפי מה שמופיע בקורפוס המקור) |
|
||||||
|
|
||||||
|
**עקרון CREAC (Rule + Explanation):**
|
||||||
|
- **Rule (כלל)**: רק מ-`external_upload` (פסיקת ערכאות) או מחוקקה. **אסור** להציג ועדת ערר אחרת כ"כלל מחייב".
|
||||||
|
- **Explanation (הרחבה/שכנוע)**: `internal_committee` יכול לתפוס כאן — אבל **בנפרד** מהכלל, כשכנוע נוסף.
|
||||||
|
- **אם אין הלכת עליון** ויש רק ועדת ערר תומכת — נסח: "לעת הזו, סוגיה זו טרם נדונה בערכאות עליונות. עם זאת, כפי שנקבע ב<ערר>... מצאנו את ההנמקה משכנעת ואנו אומצים אותה."
|
||||||
|
|
||||||
|
**בדיקה לפני שאתה כותב ציטוט:**
|
||||||
|
1. הוצא את ה-`source_kind` מהפלט של `search_precedent_library` או `search_internal_decisions`.
|
||||||
|
2. אם `internal_committee` — בדוק את `chair_name`. אם זו דפנה תמיר → סגנון "כפי שקבעתי בעבר". אחרת → סגנון אופקי עם ציון מחוז.
|
||||||
|
3. אל תערבב — שלוש קטגוריות שונות, שלוש תבניות שונות.
|
||||||
|
|
||||||
### אנטי-דפוסים — בדיקה אחרי כתיבה (חובה)
|
### אנטי-דפוסים — בדיקה אחרי כתיבה (חובה)
|
||||||
|
|
||||||
- [ ] **אין רשימות ממוספרות בתוך פסקה** (`(1)... (2)... (3)...`) — דפנה מעולם לא משתמשת
|
- [ ] **אין רשימות ממוספרות בתוך פסקה** (`(1)... (2)... (3)...`) — דפנה מעולם לא משתמשת
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
3. שלוף את תבנית ההחלטה עם get_decision_template
|
3. שלוף את תבנית ההחלטה עם get_decision_template
|
||||||
|
|
||||||
לכל סעיף:
|
לכל סעיף:
|
||||||
4. השתמש ב-draft_section כדי לקבל הקשר מלא (מסמכי התיק + תקדימים + סגנון)
|
4. השתמש ב-get_block_context(case_number, block_id) כדי לקבל הקשר מלא לבלוק (מסמכי התיק + תקדימים + סגנון). [draft_section הישן deprecated — GAP-50]
|
||||||
5. נסח את הסעיף בסגנון דפנה על בסיס ההקשר
|
5. נסח את הסעיף בסגנון דפנה על בסיס ההקשר
|
||||||
6. הצג למשתמש ובקש אישור/עריכה לפני המשך לסעיף הבא
|
6. הצג למשתמש ובקש אישור/עריכה לפני המשך לסעיף הבא
|
||||||
|
|
||||||
|
|||||||
29
.claude/settings.json
Normal file
29
.claude/settings.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Edit|Write|MultiEdit",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "${CLAUDE_PROJECT_DIR}/scripts/spec-guard.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"WorktreeRemove": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "jq -r '.tool_input.path // empty' | { read -r wt; [ -n \"$wt\" ] && git worktree remove --force \"$wt\" 2>/dev/null; git worktree prune 2>/dev/null; } || true"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"worktree": {
|
||||||
|
"baseRef": "fresh",
|
||||||
|
"symlinkDirectories": ["web-ui/node_modules"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
data/
|
data/
|
||||||
.claude/
|
.claude/
|
||||||
|
!.claude/agents/
|
||||||
|
!.claude/agents/hermes-curator.md
|
||||||
mcp-server/.venv/
|
mcp-server/.venv/
|
||||||
**/__pycache__/
|
**/__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
@@ -11,7 +13,11 @@ scripts/
|
|||||||
skills/
|
skills/
|
||||||
!skills/docx/
|
!skills/docx/
|
||||||
!skills/docx/decision_template.docx
|
!skills/docx/decision_template.docx
|
||||||
|
!skills/decision/
|
||||||
|
!skills/decision/SKILL.md
|
||||||
docs/
|
docs/
|
||||||
|
!docs/legal-decision-lessons.md
|
||||||
|
!docs/corpus-analysis.md
|
||||||
legacy/
|
legacy/
|
||||||
node_modules/
|
node_modules/
|
||||||
.next/
|
.next/
|
||||||
|
|||||||
32
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
32
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<!--
|
||||||
|
תבנית PR — עוזר משפטי. מאכפת את "פרוטוקול כתיבת-קוד" (CLAUDE.md §פרוטוקול כתיבת-קוד):
|
||||||
|
כל PR מצהיר אילו invariants הוא נוגע בהם / מקיים. ראה docs/spec/00-constitution.md (G1–G11).
|
||||||
|
מלא את הסעיפים; מחק את ההערות בסוגריים <!-- -->.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## מה ולמה
|
||||||
|
|
||||||
|
<!-- תיאור קצר: מה ה-PR משנה ולמה. אם קשור ל-FU/GAP — ציין (למשל "FU-10 / GAP-30..34"). -->
|
||||||
|
|
||||||
|
## Invariants — הצהרה (חובה)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
אילו invariants הנדסיים (G1–G10) או INV-* מקבצי-תחום ה-PR נוגע בהם או מקיים?
|
||||||
|
דוגמה: "G2 (מקור-אמת יחיד) — איחדתי 2 לקוחות Paperclip למסלול קנוני אחד; INV-INT4."
|
||||||
|
תוכן משפטי → G11.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- **נוגע / מקיים:**
|
||||||
|
|
||||||
|
## צ'קליסט — פרוטוקול כתיבת-קוד
|
||||||
|
|
||||||
|
- [ ] קראתי את `docs/spec/00-constitution.md` + ספ-התחום הרלוונטי לפני הכתיבה
|
||||||
|
- [ ] השינוי **לא** יוצר מסלול מקביל ליכולת קיימת (G2) ולא מתקן תסמין בקריאה (G1)
|
||||||
|
- [ ] אין בליעה שקטה של שגיאות — רשומה חסרה/פגומה מסומנת ומדווחת (כלל-הנדסה §6)
|
||||||
|
- [ ] בדקתי מול `docs/spec/gap-audit.md` — אם נגעתי ב-GAP/FU ממופה, התאמתי ליחידת-התיקון
|
||||||
|
- [ ] בדיקות עוברות (אם רלוונטי) / לא נדרשות
|
||||||
|
- [ ] **אם data-migration** — גיבוי + manifest ל-`data/audit/` לפני `--apply` (chair-gated אם נדרש)
|
||||||
|
|
||||||
|
## אימות
|
||||||
|
|
||||||
|
<!-- איך נבדק end-to-end: פקודות/tools/בדיקות שהורצו ותוצאתן. -->
|
||||||
@@ -56,3 +56,23 @@ jobs:
|
|||||||
curl -sf \
|
curl -sf \
|
||||||
"http://coolify:8080/api/v1/deploy?uuid=gyjo0mtw2c42ej3xxvbz8zio&force=true" \
|
"http://coolify:8080/api/v1/deploy?uuid=gyjo0mtw2c42ej3xxvbz8zio&force=true" \
|
||||||
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}"
|
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}"
|
||||||
|
|
||||||
|
- name: Prune old build images and cache
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
BASE="${{ env.REGISTRY }}/${{ env.IMAGE }}"
|
||||||
|
KEEP=5
|
||||||
|
# Keep the newest $KEEP build-NNN tags; remove the rest.
|
||||||
|
# The build daemon is the shared host daemon, so these images
|
||||||
|
# otherwise accumulate in /var/lib/docker (~1.3GB each).
|
||||||
|
docker images "${BASE}" --format '{{.Tag}}' \
|
||||||
|
| grep -E '^build-[0-9]+$' \
|
||||||
|
| sort -t- -k2 -nr \
|
||||||
|
| tail -n +$((KEEP + 1)) \
|
||||||
|
| while read -r tag; do
|
||||||
|
echo "🗑️ Removing ${BASE}:${tag}"
|
||||||
|
docker rmi "${BASE}:${tag}" || true
|
||||||
|
done
|
||||||
|
# Dangling images + build cache older than 72h (keeps recent layers warm)
|
||||||
|
docker image prune -f || true
|
||||||
|
docker builder prune -f --filter 'until=72h' || true
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -16,3 +16,5 @@ legacy/
|
|||||||
kiryat-yearim/
|
kiryat-yearim/
|
||||||
continuation-prompt.md
|
continuation-prompt.md
|
||||||
node_modules/
|
node_modules/
|
||||||
|
data/eval/eval-report-*
|
||||||
|
.claude/worktrees/
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
10
.worktreeinclude
Normal file
10
.worktreeinclude
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# קבצים מקומיים (gitignored) שמועתקים אוטומטית לכל worktree חדש שה-harness יוצר.
|
||||||
|
# תחביר .gitignore. מועתק רק אם הקובץ קיים *וגם* gitignored — קבצים tracked לעולם לא משוכפלים.
|
||||||
|
# ראה docs: https://code.claude.com/docs/en/worktrees#copy-gitignored-files-into-worktrees
|
||||||
|
|
||||||
|
# allowlist ההרשאות — בלעדיו כל worktree מציף אישורי-הרשאה מחדש
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# קבצי-סביבה מקומיים (כיום אין; proactive — בלתי-מזיק אם חסר)
|
||||||
|
.env
|
||||||
|
web-ui/.env.local
|
||||||
227
CLAUDE.md
227
CLAUDE.md
@@ -1,10 +1,11 @@
|
|||||||
# עוזר משפטי — Legal Decision Assistant
|
# עוזר משפטי — Legal Decision Assistant
|
||||||
|
|
||||||
|
> **אינדקס דק.** הכללים הקריטיים נמצאים כאן; העומק התפעולי (Deploy, Paperclip-ops, adapters, מבנה-תיקיות, Chair-Feedback, TaskMaster מלא) הוצא ל-[`docs/operations-runbook.md`](docs/operations-runbook.md) כדי לרזות את ההקשר הנטען בכל סשן.
|
||||||
|
|
||||||
## רקע הפרויקט
|
## רקע הפרויקט
|
||||||
|
|
||||||
מערכת AI לסיוע בכתיבת החלטות של **ועדת ערר לתכנון ובניה, מחוז ירושלים**, בראשות **עו"ד דפנה תמיר**.
|
מערכת AI לסיוע בכתיבת החלטות של **ועדת ערר לתכנון ובניה, מחוז ירושלים**, בראשות **עו"ד דפנה תמיר**.
|
||||||
|
|
||||||
### מה עושה ועדת ערר?
|
|
||||||
ועדת ערר היא גוף מעין-שיפוטי שדן בעררים על החלטות ועדות מקומיות לתכנון ובניה. הוועדה מקבלת חומרי מקור (כתבי ערר, תגובות, פרוטוקולים, תכניות), דנה בטענות הצדדים, ומוציאה **החלטה כתובה מנומקת** — מסמך משפטי פורמלי שניתן לביקורת שיפוטית בבית משפט לעניינים מנהליים.
|
ועדת ערר היא גוף מעין-שיפוטי שדן בעררים על החלטות ועדות מקומיות לתכנון ובניה. הוועדה מקבלת חומרי מקור (כתבי ערר, תגובות, פרוטוקולים, תכניות), דנה בטענות הצדדים, ומוציאה **החלטה כתובה מנומקת** — מסמך משפטי פורמלי שניתן לביקורת שיפוטית בבית משפט לעניינים מנהליים.
|
||||||
|
|
||||||
### שלושה סוגי עררים
|
### שלושה סוגי עררים
|
||||||
@@ -15,22 +16,19 @@
|
|||||||
| פיצויים (ס' 197) | 9xxx | קר ומקצועי | דומה להיטל השבחה |
|
| פיצויים (ס' 197) | 9xxx | קר ומקצועי | דומה להיטל השבחה |
|
||||||
|
|
||||||
### מטרת המערכת
|
### מטרת המערכת
|
||||||
לבנות כלי עבודה שמסייע ליו"ר הוועדה לנסח החלטות:
|
כלי עבודה שמסייע ליו"ר הוועדה: **ניהול תיקים** (ייבוא, סיווג, מעקב סטטוס) · **בסיס ידע** (פסיקה, ביטויי מעבר, לקחים, חקיקה) · **חיפוש סמנטי (RAG)** · **סיוע בכתיבה** (טיוטות לפי 12 בלוקים בסגנון דפנה) · **ייצוא DOCX**.
|
||||||
1. **ניהול תיקים** — ייבוא חומרי מקור, סיווג מסמכים, מעקב סטטוס
|
|
||||||
2. **בסיס ידע** — פסיקה, ביטויי מעבר, לקחים מהחלטות קודמות, חקיקה
|
|
||||||
3. **חיפוש סמנטי (RAG)** — מציאת תקדימים רלוונטיים ופסקאות דומות
|
|
||||||
4. **סיוע בכתיבה** — ייצור טיוטות לפי ארכיטקטורת 12 בלוקים בסגנון דפנה
|
|
||||||
5. **ייצוא DOCX** — מסמך מעוצב מוכן להגשה
|
|
||||||
|
|
||||||
### מה היה קודם (Legacy)
|
### ⭐ יעד-העל: רכישת-הסגנון של דפנה (Style Acquisition)
|
||||||
המערכת הקודמת היתה **Obsidian vault** עם Claude Code skills על שרת אחר. פותחו:
|
**היעד הראשי של המערכת הוא שהסוכנים יכתבו וינתחו עררים בדיוק כמו עו"ד דפנה תמיר** — לא רק לייצר טיוטה תקנית, אלא להפנים את **הקול והשיטה** שלה. זה מחייב **הפרדה מובהקת בין שתי תת-מערכות**:
|
||||||
- ניתוח סגנון של 3 החלטות (הכט — דחייה, בית הכרם — קבלה חלקית, אריאלי — השוואה)
|
|
||||||
- ארכיטקטורת 12 בלוקים מבוססת CREAC / DITA / Akoma Ntoso / Federal Judicial Center
|
|
||||||
- כללי כתיבה (רקע ניטרלי, ללא כפילות, טענות מקוריות בלבד)
|
|
||||||
- לקחים מהשוואת טיוטות לגרסאות סופיות
|
|
||||||
- סקריפט ייצוא DOCX
|
|
||||||
|
|
||||||
הידע שהופק מה-vault הוטמע במערכת הנוכחית — מסמכי ייחוס (`docs/`), קורפוס אימון (`data/training/`), ומבנה 12 בלוקים. ה-vault המקורי נמחק; הפרויקט הנוכחי עובד עם PostgreSQL + pgvector.
|
1. **מערכת-הכתיבה (Writing)** — מייצרת טיוטות (analyst/writer/qa/ceo). **צרכן read-only** של artifacts-הקול.
|
||||||
|
2. **מערכת רכישת-הסגנון (Style Acquisition)** — לומדת *איך* דפנה כותבת מכל זוג "טיוטה שלנו → סופי שלה", ומזינה חזרה את מערכת-הכתיבה. **היחידה שכותבת ל-artifacts-הקול** — תמיד דרך שער-יו"ר (INV-G10).
|
||||||
|
|
||||||
|
**הגישה (state-of-the-art לדאטה-מועט):** Text Style Transfer מבוסס **Authorial Style Profiling** — להכליל את סגנון דפנה ולהתאים לתיק. העתקת פסקאות מותרת לתוכן קבוע/נוסחאי; ניתוח ספציפי → להכליל; **מהות משפטית (הלכה/עובדה) — אסור להעתיק מתיק לתיק**. *לא* fine-tuning של משקולות (Opus סגור; קורפוס קטן מדי).
|
||||||
|
|
||||||
|
**כלל-העל — INV-LRN4:** כל החלטה אינה "סגורה" עד שהושוותה מול הגרסה הסופית של דפנה; כל סופי מנותח מול הטיוטה. **INV-LRN5:** שכבת-ידע-הקול לא תכיל מהות ספציפית — רק סגנון ושיטה. ספ מלא: [`docs/spec/07-learning.md`](docs/spec/07-learning.md) §0. ארכיטקטורה ומשימות: תוכנית `style-acquisition-subsystem`.
|
||||||
|
|
||||||
|
> **Legacy:** המערכת הקודמת היתה Obsidian vault עם Claude Code skills. הידע שהופק ממנה (ניתוח סגנון, 12 בלוקים מבוססי CREAC/DITA/Akoma-Ntoso/FJC, כללי כתיבה, לקחים, ייצוא DOCX) הוטמע בפרויקט הנוכחי (`docs/`, `data/training/`). ה-vault נמחק; כעת PostgreSQL + pgvector.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -38,11 +36,15 @@
|
|||||||
|
|
||||||
| מסמך | תוכן | מתי לקרוא |
|
| מסמך | תוכן | מתי לקרוא |
|
||||||
|------|-------|-----------|
|
|------|-------|-----------|
|
||||||
|
| [`docs/spec/00-constitution.md`](docs/spec/00-constitution.md) | **חוקת המערכת** — ייעוד, 11 invariants גלובליים (G1–G11), כללי-הנדסה, אינדקס-ספ | **לפני כל כתיבת/שינוי קוד** (ראה §פרוטוקול כתיבת-קוד) |
|
||||||
|
| [`docs/spec/README.md`](docs/spec/README.md) | **אינדקס ספ-המערכת** — מחזור-חיים (01–07) + חוצי-שלבים (X1–X11). מקור-האמת ל"מהו תקין" | **לפני כל כתיבת/שינוי קוד** |
|
||||||
|
| [`docs/spec/gap-audit.md`](docs/spec/gap-audit.md) | **מפת-פערים** — 62 ממצאים → 15 יחידות-תיקון (FU); invariant מופר + file:line + תיקון מוצע | לפני נגיעה ב-GAP/FU קיים או תכנון FU חדש |
|
||||||
| [`docs/architecture.md`](docs/architecture.md) | ארכיטקטורת המערכת, תרשים רכיבים, זרימת נתונים, 4 שכבות DB | לפני עבודה על תשתית |
|
| [`docs/architecture.md`](docs/architecture.md) | ארכיטקטורת המערכת, תרשים רכיבים, זרימת נתונים, 4 שכבות DB | לפני עבודה על תשתית |
|
||||||
| [`docs/block-schema.md`](docs/block-schema.md) | הגדרת 12 בלוקים — content model, constraints, processing params | **לפני כל כתיבת החלטה** |
|
| [`docs/block-schema.md`](docs/block-schema.md) | הגדרת 12 בלוקים — content model, constraints, processing params | **לפני כל כתיבת החלטה** |
|
||||||
| [`docs/migration-plan.md`](docs/migration-plan.md) | תוכנית מעבר vault → DB — טבלאות, עדיפויות, כמויות | לפני ייבוא נתונים |
|
| [`docs/migration-plan.md`](docs/migration-plan.md) | תוכנית מעבר vault → DB — טבלאות, עדיפויות, כמויות | לפני ייבוא נתונים |
|
||||||
| [`docs/legal-decision-lessons.md`](docs/legal-decision-lessons.md) | לקחים מ-3 החלטות — מה עבד, מה השתנה, ביטויי מעבר חדשים | **לפני כל כתיבת החלטה** |
|
| [`docs/legal-decision-lessons.md`](docs/legal-decision-lessons.md) | לקחים מ-3 החלטות — מה עבד, מה השתנה, ביטויי מעבר חדשים | **לפני כל כתיבת החלטה** |
|
||||||
| [`docs/decision-methodology.md`](docs/decision-methodology.md) | **מתודולוגיה אנליטית — איך לחשוב על החלטה מעין-שיפוטית** | **לפני כל כתיבת החלטה** |
|
| [`docs/decision-methodology.md`](docs/decision-methodology.md) | **מתודולוגיה אנליטית — איך לחשוב על החלטה מעין-שיפוטית** | **לפני כל כתיבת החלטה** |
|
||||||
|
| [`docs/anti-hallucination-gate.md`](docs/anti-hallucination-gate.md) | **שער anti-hallucination משותף (INV-AH)** — 5 טכניקות מעוגנות-מקור (עיגון-מקור, quote-or-retract, abstention, תיוג-ודאות, CoVe). מקור-אמת אחד לכל הסוכנים | **לפני כל אזכור פסיקה/חוק/הלכה/מספר** |
|
||||||
| `docs/garner-methodology-extraction.md` | חומר מקור: מיצוי מספרי Garner על כתיבה משפטית | רק לבדיקת מקור |
|
| `docs/garner-methodology-extraction.md` | חומר מקור: מיצוי מספרי Garner על כתיבה משפטית | רק לבדיקת מקור |
|
||||||
| `docs/fjc-principles-extraction.md` | חומר מקור: מיצוי מ-Judicial Writing Manual (FJC) | רק לבדיקת מקור |
|
| `docs/fjc-principles-extraction.md` | חומר מקור: מיצוי מ-Judicial Writing Manual (FJC) | רק לבדיקת מקור |
|
||||||
| [`docs/corpus-analysis.md`](docs/corpus-analysis.md) | ניתוח שיטתי של 24 החלטות — מפת תוכן, דפוסי דיון תכנוני, פערים | **לפני כל כתיבת החלטה** |
|
| [`docs/corpus-analysis.md`](docs/corpus-analysis.md) | ניתוח שיטתי של 24 החלטות — מפת תוכן, דפוסי דיון תכנוני, פערים | **לפני כל כתיבת החלטה** |
|
||||||
@@ -56,144 +58,94 @@
|
|||||||
| [`docs/decision-block-mapping.md`](docs/decision-block-mapping.md) | מיפוי בלוקים להחלטות — איך 12 הבלוקים משתקפים ב-DOCX | להתמצאות במבנה |
|
| [`docs/decision-block-mapping.md`](docs/decision-block-mapping.md) | מיפוי בלוקים להחלטות — איך 12 הבלוקים משתקפים ב-DOCX | להתמצאות במבנה |
|
||||||
| [`docs/memory.md`](docs/memory.md) | הקשר כללי — skills, פרויקטים שהושלמו, מבנה vault | להתמצאות כללית |
|
| [`docs/memory.md`](docs/memory.md) | הקשר כללי — skills, פרויקטים שהושלמו, מבנה vault | להתמצאות כללית |
|
||||||
| [`skills/decision/SKILL.md`](skills/decision/SKILL.md) | מדריך סגנון מלא של דפנה — טון, מבנה, ביטויים, מתודולוגיה | **לפני כל כתיבת החלטה** |
|
| [`skills/decision/SKILL.md`](skills/decision/SKILL.md) | מדריך סגנון מלא של דפנה — טון, מבנה, ביטויים, מתודולוגיה | **לפני כל כתיבת החלטה** |
|
||||||
|
| [`.claude/agents/HEARTBEAT.md`](.claude/agents/HEARTBEAT.md) | checklist הפעלת סוכן — routing, company filtering, quirks, wakeup עם UUID נכון | **לפני כל עבודה על סוכנים** |
|
||||||
|
| [`skills/dafna-decision-template/SKILL.md`](skills/dafna-decision-template/SKILL.md) | export DOCX לפי styles של תבנית Word של דפנה — line classification, dash policy, placeholder handling | לפני export DOCX |
|
||||||
|
| [`docs/corpus-graph.md`](docs/corpus-graph.md) | **מפת הקורפוס** (`/graph`) — גרף ציטוטים אינטראקטיבי נייטיב; 6 שכבות (פסיקה/נושא/תחום/הלכות/חוסרי‑מחקר/יומונים), אנליטיקה (PageRank/אשכולות), endpoints, ואיך מוסיפים שכבה | לפני עבודה על דף `/graph` או `web/graph_api.py` |
|
||||||
|
| [`docs/operations-runbook.md`](docs/operations-runbook.md) | **עומק תפעולי** — Deploy (Coolify/pm2), Paperclip-ops מלא (wakeup, sync, webhook, scheduled jobs, adapters), מבנה-תיקיות, Chair-Feedback, TaskMaster | לפני עבודה על Deploy / אינטגרציית-Paperclip / adapters |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## שרת Nautilus (158.178.131.193)
|
## פרוטוקול כתיבת-קוד — קודם הספ ⚠️
|
||||||
|
|
||||||
| שירות | תפקיד | כתובת |
|
> **כלל-על.** המקור הקנוני ל"מהו תקין הנדסית" הוא ספ-המערכת תחת [`docs/spec/`](docs/spec/) — לא
|
||||||
|-------|--------|-------|
|
> הרגלים, לא "הקוד הקיים נראה ככה". כל קוד שנכתב בלי לעבור דרך הספ מסתכן בהחזרת **כשל-השורש**
|
||||||
| Coolify | ניהול containers | `http://158.178.131.193:8000` |
|
> שהספ בא לייבש: מסלולים/קורפוסים מקבילים שמתפצלים (drift). זהו המקבילה האינטראקטיבית ל-INV-AG1
|
||||||
| PostgreSQL + pgvector | בסיס נתונים ראשי | `legal-ai-postgres` |
|
> שכבר אוכף על סוכני Paperclip ([HEARTBEAT.md](.claude/agents/HEARTBEAT.md) §"קריאת-ספ").
|
||||||
| Redis | תור משימות | `legal-ai-redis` |
|
|
||||||
| n8n | אוטומציית workflows | להגדרה |
|
|
||||||
| Gitea | מאגר קוד | `gitea.nautilus.marcusgroup.org/ezer-mishpati` |
|
|
||||||
| ezer-mishpati-web | ממשק העלאת מסמכים (Docker/Coolify) | `legal-ai.nautilus.marcusgroup.org` |
|
|
||||||
| Paperclip | סוכן AI — מריץ Claude Code agents (pm2, מקומי) | `localhost:3100` |
|
|
||||||
| Infisical | ניהול סודות | `secret.dev.marcus-law.co.il` |
|
|
||||||
|
|
||||||
### ⚠️ ארכיטקטורת Deploy — חובה לקרוא
|
**לפני יצירה/שינוי של קוד ב-`web/`, `mcp-server/`, `web-ui/`, `scripts/`:**
|
||||||
|
|
||||||
**עוזר משפטי (Legal-AI)** — רץ כ-**Docker container דרך Coolify**:
|
1. **קרא** [`docs/spec/00-constitution.md`](docs/spec/00-constitution.md) — ייעוד, ה-invariants הגלובליים G1–G11, וכללי-ההנדסה (§6). אינדקס-הספ ב-§7.
|
||||||
- UUID: `gyjo0mtw2c42ej3xxvbz8zio`
|
2. **קרא את ספ-התחום הרלוונטי** לפי האינדקס (§7) — לדוגמה: אחזור→[`03-retrieval.md`](docs/spec/03-retrieval.md), קליטה→[`01-ingest.md`](docs/spec/01-ingest.md), נתונים→[`02-data-model.md`](docs/spec/02-data-model.md), כלי-MCP→[`X9-mcp-tool-contract.md`](docs/spec/X9-mcp-tool-contract.md), UI↔API→[`X6-ui-api-contract.md`](docs/spec/X6-ui-api-contract.md), Paperclip→[`X3`](docs/spec/X3-integration-deploy.md)/[`X7`](docs/spec/X7-paperclip-client-params.md), env/secrets→[`X10-deploy-env-secrets.md`](docs/spec/X10-deploy-env-secrets.md).
|
||||||
- שינוי קוד ב-`web/` או `web-ui/` **לא נכנס לתוקף** עד ש:
|
3. **ודא שהשינוי *מקיים* את ה-invariants** — לא יוצר מסלול מקביל ליכולת קיימת ([G2](docs/spec/00-constitution.md)), לא מתקן תסמין בקריאה במקום נרמול במקור (G1), לא בולע שגיאות בשקט (כלל-הנדסה §6).
|
||||||
1. עושים `git commit` + `git push origin main`
|
4. **בדוק מול** [`gap-audit.md`](docs/spec/gap-audit.md) — אם אתה נוגע ב-GAP/FU שכבר ממופה, התאם את העבודה ליחידת-התיקון; אל תפתור מחדש.
|
||||||
2. מריצים deploy דרך Coolify (`mcp__coolify__deploy`)
|
5. **כל PR מצהיר invariants** — אילו G*/INV-* ה-PR נוגע בהם / מקיים (ראה תבנית ה-PR ב-[`.gitea/PULL_REQUEST_TEMPLATE.md`](.gitea/PULL_REQUEST_TEMPLATE.md)).
|
||||||
3. ממתינים ~2-4 דקות לבנייה
|
|
||||||
- **אסור** לנסות להריץ uvicorn מקומית — אין סביבת Python על המכונה
|
|
||||||
- ה-container מריץ Next.js (`:3000`, חשוף) + FastAPI (`:8000`, פנימי)
|
|
||||||
- בדיקה: `curl https://legal-ai.nautilus.marcusgroup.org/api/...`
|
|
||||||
|
|
||||||
**Paperclip** — רץ **מקומית דרך pm2**:
|
> **שתי שכבות-כללים מובחנות, שתיהן חלות:**
|
||||||
- פורט: `localhost:3100`, DB: `localhost:54329`
|
> - **הנדסה (G1–G10)** — הסעיף הזה + `docs/spec/`. סמכות: ≥3 מקורות חיצוניים.
|
||||||
- שינויי קוד נכנסים לתוקף אחרי `pm2 restart paperclip`
|
> - **תוכן משפטי (G11)** — סעיף "עקרונות כתיבה קריטיים" למטה (12 בלוקים, רקע ניטרלי...). סמכות: היו"ר + מסמכי-הפרויקט.
|
||||||
- **אין צורך ב-Docker או Coolify**
|
>
|
||||||
|
> אכיפה אוטומטית: hook `PreToolUse` ([scripts/spec-guard.sh](scripts/spec-guard.sh)) מזכיר את הפרוטוקול בכל Edit/Write על נתיב-קוד.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## מבנה תיקיות
|
## בידוד-סשנים — worktree מבודד חובה ⚠️
|
||||||
|
|
||||||
|
> **כלל קשיח.** בכל רגע נתון רצים **כמה סשנים במקביל** על אותו עץ-עבודה (`~/legal-ai`) — סשנים אינטראקטיביים של chaim **וגם** סוכני Paperclip. עץ-עבודה אחד = ענף-גיט אחד משותף, כך שסשן אחד מחליף branch / משאיר שינויים לא-מתויקים תוך כדי שאחר עובד → **דריסה הדדית ומירוץ-ענף** ([[feedback_shared_worktree_branch_race]]).
|
||||||
|
|
||||||
|
**לכן — כל סשן שעומד לכתוב/לשנות קוד או תיעוד חייב לעבוד ב-git worktree מבודד משלו. אסור לערוך/לתייק בעץ-העבודה הראשי `~/legal-ai` כשייתכן שסשן אחר פעיל.**
|
||||||
|
|
||||||
|
הבידוד **נתמך-סביבה** — ההגדרות נשמרות ב-repo (`.claude/settings.json`, `.worktreeinclude`, `.gitignore`) כך שכל worktree שה-harness יוצר מקבל אוטומטית בסיס נקי, את התלויות, ואת ההרשאות. מקורות רשמיים: [Run parallel sessions with worktrees](https://code.claude.com/docs/en/worktrees), [Settings → worktree](https://code.claude.com/docs/en/settings).
|
||||||
|
|
||||||
|
### הדרך המומלצת — worktree של ה-harness
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai && claude --worktree <slug> # או, בתוך סשן: "עבוד ב-worktree" (כלי EnterWorktree)
|
||||||
```
|
```
|
||||||
/home/chaim/legal-ai/
|
נוצר תחת `.claude/worktrees/<slug>/` על ענף `worktree-<slug>`, ומקבל **אוטומטית**: בסיס נקי מ-`origin/main` (`worktree.baseRef: "fresh"`) · `web-ui/node_modules` כסימלינק (`worktree.symlinkDirectories`; אין צורך ב-`npm ci`) · `.claude/settings.local.json` + קבצי-env מקומיים (דרך `.worktreeinclude`) · ניקוי אוטומטי ביציאה (כולל עקיפת באג סימלינק [#40259](https://github.com/anthropics/claude-code/issues/40259) דרך `WorktreeRemove` hook עם `--force`).
|
||||||
├── CLAUDE.md ← הקובץ הזה
|
|
||||||
├── Dockerfile ← Docker build
|
### הפרוטוקול (חל על שתי הדרכים)
|
||||||
├── docs/ ← תיעוד + לקחים
|
1. **בתחילת עבודת-כתיבה** — צור worktree (מומלץ: `claude --worktree`; ידני-fallback: `git worktree add -b <branch> .claude/worktrees/<slug> origin/main` — **תחת `.claude/worktrees/`** כדי שההגדרות יחולו).
|
||||||
│ ├── architecture.md ארכיטקטורה
|
2. **אמת ענף לפני כל commit** — `git branch --show-current` (הרגל קשיח; ה-harness עלול להתעלם מ-`baseRef:"fresh"` — באג [#60588](https://github.com/anthropics/claude-code/issues/60588) — אז ודא שהבסיס באמת `origin/main`).
|
||||||
│ ├── block-schema.md 12 בלוקים (המסמך החשוב ביותר)
|
3. **push + PR + merge** כרגיל ([[feedback_always_pr_merge]]) — PR תמיד ל-`main`. הרץ tests לפני merge.
|
||||||
│ ├── migration-plan.md תוכנית מעבר vault → DB
|
4. **נקה אחרי מיזוג** — יציאת הסשן מנקה worktree של ה-harness אוטומטית; ידני: `git worktree remove .claude/worktrees/<slug> && git worktree prune && git branch -D worktree-<slug>`.
|
||||||
│ ├── legal-decision-lessons.md לקחים מ-3 החלטות
|
5. **קריאה-בלבד** (חקירה, סריקה, הרצת בדיקות ללא שינוי) — מותר בעץ הראשי; אין צורך ב-worktree.
|
||||||
│ └── memory.md הקשר כללי — skills, פרויקטים
|
6. **אל תיגע** בשינויים לא-מתויקים שאינם שלך בעץ הראשי — הם של סשן אחר. אם העץ הראשי על ענף זר — אל תתייק עליו.
|
||||||
├── skills/ ← כלי עבודה ומדריכים
|
|
||||||
│ ├── decision/ מדריך סגנון + references + 12 בלוקים
|
> **בידוד-DB:** ה-worktree מבודד-קבצים בלבד — לא בידוד-repo ולא בידוד-DB. **אל תריץ migrations מ-2 worktrees במקביל** על Postgres המשותף (`localhost:5433`) — סכמה שאף סשן לא מצפה לה ([Run agents in parallel](https://code.claude.com/docs/en/agents)).
|
||||||
│ ├── assistant/ קטלוג מסמכים
|
> **סוכני Paperclip — אינם מבודדים (אומת 2026-06-06):** 14 מתוך 16 הסוכנים רצים על אדפטר `claude_local` הרשמי, שמריץ `claude -p` ב-`adapter_config.cwd=/home/chaim/legal-ai` **המשותף** — אין לו אופציית `worktreeMode`/`-w`. כלומר **כל סוכני Paperclip חולקים את עץ-העבודה הראשי**. הסיכון ממותן ע"י כלל הסשנים נתמך-הסביבה למעלה + תזמור סדרתי ע"י ה-CEO — **לא** ע"י בידוד-worktree per-agent. ניתוח מלא: TaskMaster `legal-ai` #104 (נסגר cancelled — "לתעד, לא לבדד").
|
||||||
│ └── docx/ עיצוב DOCX
|
|
||||||
├── data/
|
---
|
||||||
│ ├── training/ ← 4 החלטות לאימון (DOCX)
|
|
||||||
│ ├── exports/ ← טיוטות DOCX מיוצאות
|
## Deploy — תמצית קריטית
|
||||||
│ └── cases/{case-number}/ ← תיקי עררים (מבנה שטוח, סטטוס ב-DB)
|
|
||||||
├── web/ ← FastAPI backend (Python): 75 API endpoints
|
שלושה מודלי-הרצה דרים יחד; ערבוב = הטעות הנפוצה. **פירוט מלא, UUIDs ופקודות: [`docs/operations-runbook.md`](docs/operations-runbook.md).**
|
||||||
│ ├── app.py ← API ראשי
|
|
||||||
│ ├── paperclip_client.py ← אינטגרציית Paperclip
|
- **legal-ai** (`web/`, `web-ui/`) = **Docker דרך Coolify**. שינוי קוד לא נכנס לתוקף עד `git commit` + `git push origin main` → Gitea Actions בונה image → `mcp__coolify__deploy` (~2-4 דק'). **אסור** uvicorn/`next dev` מקומית — אין Python על המכונה. בדיקה: `curl https://legal-ai.nautilus.marcusgroup.org/api/health`.
|
||||||
│ └── gitea_client.py ← אינטגרציית Gitea
|
- **Paperclip** = **pm2 מקומי** (`localhost:3100`). שינוי → `pm2 restart paperclip`. **אין** Docker/Coolify.
|
||||||
├── web-ui/ ← Next.js frontend (TypeScript/React): ממשק המשתמש
|
- **legal-chat-service** = **pm2 מקומי** (`127.0.0.1:8770`), גשר claude CLI לטאב הצ'אט ב-/training. שינוי → `pm2 restart legal-chat-service`.
|
||||||
│ └── next.config.ts ← proxy: /api/* → FastAPI :8000
|
|
||||||
├── mcp-server/ ← MCP server + services + tools
|
---
|
||||||
├── adapters/ ← Paperclip external adapters (ראה למטה)
|
|
||||||
│ └── deepseek-paperclip-adapter/ ← `deepseek_local` (Hermes-pinned ל-DeepSeek profile)
|
## Paperclip — כללים קריטיים (תמצית)
|
||||||
└── scripts/ ← סקריפטים וכלי עזר (ראה scripts/SCRIPTS.md)
|
|
||||||
└── .archive/ ← סקריפטים שהושלמו (לא להריץ)
|
**פירוט מלא + דוגמאות + פקודות sync: [`docs/operations-runbook.md`](docs/operations-runbook.md).**
|
||||||
```
|
|
||||||
|
- **Wakeup תמיד דרך API**: `POST /api/agents/{agent-id}/wakeup` עם `payload.issueId`. **אסור** `INSERT INTO agent_wakeup_requests` ישיר — הסוכן לא יתעורר לעולם (אין `heartbeat_run`).
|
||||||
|
- **ניתוב comments דרך CEO**: תגובת-משתמש → פלאגין מעיר CEO → CEO מנתב ויוצר issue. סוכנים קוראים comments אחרונים לפני עבודה (HEARTBEAT 2b-2c).
|
||||||
|
- **קריאות API דרך helper בלבד**: bash → `scripts/pc.sh`; Python → `pc_request()` מ-`web/paperclip_api.py`. **אסור** `curl` ישיר ל-Paperclip או `httpx.AsyncClient` ישיר.
|
||||||
|
- **Cross-company sync**: 14 סוכנים = 7 × 2 חברות (CMP=1xxx master, CMPA=8xxx mirror). אחרי כל שינוי הגדרות/skills של סוכן — להריץ `scripts/sync_agents_across_companies.py --apply`. **מדלג** על סוכנים עם `adapter_type` שונה בין החברות (למשל `deepseek_local`) — להחיל ידנית בשתיהן.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## כלל: עדכון `scripts/SCRIPTS.md`
|
## כלל: עדכון `scripts/SCRIPTS.md`
|
||||||
|
בכל פעם שנוצר, נמחק, או משתנה סקריפט בתיקיית `scripts/` — **חובה לעדכן את `scripts/SCRIPTS.md`** (תפקיד, סטטוס, החלפה).
|
||||||
בכל פעם שנוצר, נמחק, או משתנה סקריפט בתיקיית `scripts/` — **חובה לעדכן את `scripts/SCRIPTS.md`** בהתאם.
|
|
||||||
הקובץ מתעד את התפקיד, הסטטוס, וההחלפה (אם יש) של כל סקריפט.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ניהול משימות — TaskMaster AI
|
## ניהול משימות — TaskMaster AI
|
||||||
|
**תמיד** TaskMaster (לא TASKS.md ידני). קובץ קנוני: `~/legal-ai/.taskmaster/tasks/tasks.json` (tags: `master`, `legal-ai`). פקודות: `get_tasks`, `next_task`, `add_task`, `update_task`, `expand_task`.
|
||||||
הפרויקט משתמש ב-**TaskMaster AI** (MCP server) לניהול משימות מובנה:
|
> **⚠️ מלכוד cwd ב-CLI:** `--tag` בוחר קבוצה *בתוך* הקובץ — לא לאיזה קובץ לכתוב (ה-CLI מאתר לפי cwd). תמיד `cd ~/legal-ai` לפני כל פקודה משנה, ואז אמת ב-MCP `get_tasks`. כשלא בטוחים — לערוך את הקובץ ישירות. פירוט: [`docs/operations-runbook.md`](docs/operations-runbook.md).
|
||||||
- **תמיד** להשתמש ב-TaskMaster לפירוק, מעקב וניהול משימות — לא ב-TASKS.md ידני
|
|
||||||
- קובץ המשימות: `tasks/tasks.json`
|
|
||||||
- פקודות עיקריות: `get_tasks`, `next_task`, `add_task`, `update_task`, `expand_task`
|
|
||||||
- לפני התחלת עבודה → `next_task` כדי לדעת מה הבא לפי תלויות
|
|
||||||
- אחרי סיום משימה → `update_task` עם status=done
|
|
||||||
- משימה מורכבת → `expand_task` לפירוק לתתי-משימות
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Paperclip — כללי אינטגרציה קריטיים
|
## עקרונות כתיבה קריטיים (G11)
|
||||||
|
|
||||||
### Wakeup API — תמיד דרך API, לעולם לא דרך DB
|
|
||||||
- **הנתיב הנכון**: `POST /api/agents/{agent-id}/wakeup` (לא `/wake`!)
|
|
||||||
- **⚠️ אסור**: `INSERT INTO agent_wakeup_requests` ישירות — זה יוצר רק רשומה בלי `heartbeat_run`, והסוכן **לא יתעורר לעולם**
|
|
||||||
- **⚠️ חובה לשלוח `payload` עם `issueId`** — בלי זה הסוכן מתעורר בלי הקשר (בלי תיק, בלי issue, בלי cwd נכון)
|
|
||||||
- דוגמה נכונה:
|
|
||||||
```json
|
|
||||||
{"source": "automation", "triggerDetail": "system", "reason": "...",
|
|
||||||
"payload": {"issueId": "...", "mutation": "comment", "commentId": "..."}}
|
|
||||||
```
|
|
||||||
- **Board API Key**: שמור ב-DB (`board_api_keys`), auth: `Authorization: Bearer pbk_...`
|
|
||||||
|
|
||||||
### ניתוב comments דרך CEO
|
|
||||||
- כשמשתמש כותב תגובה על issue ב-Paperclip, הפלאגין (`plugin-legal-ai`) מעיר את ה-CEO דרך `ctx.agents.invoke()`
|
|
||||||
- ה-CEO קורא את ה-comment, מחליט על ניתוב, ויוצר issue לסוכן המתאים
|
|
||||||
- כל הסוכנים חייבים לקרוא comments אחרונים לפני שהם מתחילים לעבוד (HEARTBEAT שלבים 2b-2c)
|
|
||||||
|
|
||||||
### קריאות API — תמיד דרך helper, לעולם לא `curl` ישיר
|
|
||||||
- **bash (סוכנים):** `~/legal-ai/scripts/pc.sh <METHOD> <PATH> [BODY_JSON]` — מוסיף Authorization, X-Paperclip-Run-Id, Content-Type, base URL. ראה `HEARTBEAT.md §0`.
|
|
||||||
- **Python (FastAPI):** `from web.paperclip_api import pc_request; await pc_request("POST", "/api/...", json={...})` — שימוש ב-board API key.
|
|
||||||
- **אסור** `curl ... $PAPERCLIP_API_URL` ישיר ב-bash; **אסור** `httpx.AsyncClient` ישיר ל-Paperclip ב-Python.
|
|
||||||
- **למה:** ה-skill הרשמי דורש `X-Paperclip-Run-Id` בכל קריאה משנה issue. אצלנו ה-audit trail עבד ממילא דרך JWT claims (`runId: runIdHeader || claims.run_id`), אבל ה-helper מבטיח עקביות + תאימות ל-board API keys (long-lived) שלא נושאות JWT claims.
|
|
||||||
|
|
||||||
### Cross-company agent sync — אחרי כל שינוי הגדרות
|
|
||||||
- יש 14 סוכנים = 7 × 2 חברות (CMP=1xxx, CMPA=8xxx). Paperclip מחייב `agents.company_id NOT NULL` — אין shared agents.
|
|
||||||
- **Master = CMP (1xxx)**, **Mirror = CMPA (8xxx)**.
|
|
||||||
- אחרי כל שינוי ב-`adapter_config`, `runtime_config`, `budget_monthly_cents`, או skills של סוכן ב-master (UI, SQL, או API), **חובה להריץ:**
|
|
||||||
```bash
|
|
||||||
PAPERCLIP_BOARD_API_KEY=$(...infisical...) \
|
|
||||||
python ~/legal-ai/scripts/sync_agents_across_companies.py --verify # לבדיקה
|
|
||||||
PAPERCLIP_BOARD_API_KEY=$(...) \
|
|
||||||
python ~/legal-ai/scripts/sync_agents_across_companies.py --apply # לסנכרן
|
|
||||||
```
|
|
||||||
- הסקריפט מסנן local skills שלא קיימים ב-CMPA (מציג אזהרה), משתמש ב-API (לא DB ישיר), יוצר revisions, idempotent.
|
|
||||||
- שאלות ה-skill הרשמי של Paperclip — `paperclip` skill תחת `paperclipai/paperclip`.
|
|
||||||
|
|
||||||
### External adapters — `deepseek_local`
|
|
||||||
- מיקום ה-package: [adapters/deepseek-paperclip-adapter/](adapters/deepseek-paperclip-adapter/) (לא ב-`node_modules`).
|
|
||||||
- רישום ב-Paperclip: רשומה ב-`~/.paperclip/adapter-plugins.json` (נטען אוטומטית ב-startup דרך `buildExternalAdapters`). אין צורך בעריכת `node_modules`.
|
|
||||||
- **מה ה-adapter עושה**: spawnל-`hermes chat` עם `HERMES_HOME=/home/chaim/.hermes/profiles/deepseek` כך שה-CLI טוען את `config.yaml` (`base_url=https://api.deepseek.com/v1`, `provider=custom`, `key_env=DEEPSEEK_API_KEY`) ואת `.env` (שמכיל את ה-key).
|
|
||||||
- **מודלים זמינים** (lookup ב-DeepSeek `/v1/models`): `deepseek-v4-pro` (default), `deepseek-v4-flash`. יופיעו כדרופ-דאון ב-UI.
|
|
||||||
- **התקנה מחדש / עדכון**: `curl -X POST -H "Authorization: Bearer pcapi_legal_install_key_2026" -H "Content-Type: application/json" -d '{"packageName":"/home/chaim/legal-ai/adapters/deepseek-paperclip-adapter","isLocalPath":true}' http://localhost:3100/api/adapters/install`. לעדכון hot — `POST /api/adapters/deepseek_local/reload`.
|
|
||||||
- **⚠ Cross-company sync**: `sync_agents_across_companies.py` **מדלג** על סוכנים עם `adapter_type` שונה בין CMP ל-CMPA. כשעוברים סוכן ל-`deepseek_local` חובה להחיל ידנית בשתי החברות לפני sync.
|
|
||||||
- **תוספת adapters עתידיים** (OpenAI ישיר, Anthropic ישיר, וכו'): אותו דפוס. ה-package הראשי חייב לייצא `createServerAdapter()` שמחזיר `{ type, label, models, agentConfigurationDoc, execute, testEnvironment, sessionCodec, listSkills, syncSkills, ... }`. ראה את [adapters/deepseek-paperclip-adapter/dist/index.js](adapters/deepseek-paperclip-adapter/dist/index.js) כתבנית.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## עקרונות כתיבה קריטיים
|
|
||||||
|
|
||||||
1. **"מבחן השופט"** — כל החלטה חייבת להיות קריאה לשופט שלא מכיר את התיק
|
1. **"מבחן השופט"** — כל החלטה חייבת להיות קריאה לשופט שלא מכיר את התיק
|
||||||
2. **"רקע ניטרלי"** — בלוק ו = עובדות בלבד. אין ציטוטים מצדדים, אין מילות שיפוט
|
2. **"רקע ניטרלי"** — בלוק ו = עובדות בלבד. אין ציטוטים מצדדים, אין מילות שיפוט
|
||||||
@@ -202,14 +154,7 @@
|
|||||||
5. **ארכיטקטורת 12 בלוקים** — ראה `docs/block-schema.md`
|
5. **ארכיטקטורת 12 בלוקים** — ראה `docs/block-schema.md`
|
||||||
6. **צ'קליסט תוכן** — בלוק י מקבל צ'קליסט תוכן אוטומטי לפי סוג הערר (ראה `lessons.py: CONTENT_CHECKLISTS`)
|
6. **צ'קליסט תוכן** — בלוק י מקבל צ'קליסט תוכן אוטומטי לפי סוג הערר (ראה `lessons.py: CONTENT_CHECKLISTS`)
|
||||||
|
|
||||||
## הערות יו"ר (Chair Feedback)
|
> **הערות יו"ר (Chair Feedback):** מנגנון תיעוד הערות דפנה — טבלת `chair_feedback`, API `/api/feedback`, MCP `record_chair_feedback`/`list_chair_feedback`, UI `/feedback`. פירוט: [`docs/operations-runbook.md`](docs/operations-runbook.md).
|
||||||
|
|
||||||
מנגנון לתיעוד הערות דפנה על טיוטות:
|
|
||||||
- **DB**: טבלת `chair_feedback` (case_id, block_id, feedback_text, category, lesson_extracted)
|
|
||||||
- **API**: `GET/POST /api/feedback`, `PATCH /api/feedback/{id}/resolve`
|
|
||||||
- **MCP tools**: `record_chair_feedback`, `list_chair_feedback`
|
|
||||||
- **UI**: דף ניהול ב-`/feedback` (ב-Next.js)
|
|
||||||
- **קטגוריות**: missing_content, wrong_tone, wrong_structure, factual_error, style, other
|
|
||||||
|
|
||||||
## יו"ר: עו"ד דפנה תמיר
|
## יו"ר: עו"ד דפנה תמיר
|
||||||
- מדריך סגנון מלא: `skills/decision/SKILL.md`
|
מדריך סגנון מלא: [`skills/decision/SKILL.md`](skills/decision/SKILL.md).
|
||||||
|
|||||||
17
Dockerfile
17
Dockerfile
@@ -32,9 +32,10 @@ RUN pip install --no-cache-dir ./mcp-server
|
|||||||
FROM python:3.12-slim AS runner
|
FROM python:3.12-slim AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install Node.js 20.x
|
# Install Node.js 20.x + LibreOffice Writer (headless .doc→.docx conversion
|
||||||
|
# in extractor.py:_extract_doc — needed for legacy Hebrew .doc precedents).
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
curl ca-certificates git \
|
curl ca-certificates git libreoffice-writer-nogui \
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
&& apt-get install -y --no-install-recommends nodejs \
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -61,6 +62,18 @@ COPY mcp-server/src/ ./mcp-server/src/
|
|||||||
# (Path(__file__).resolve().parents[4] / "skills/docx/decision_template.docx")
|
# (Path(__file__).resolve().parents[4] / "skills/docx/decision_template.docx")
|
||||||
COPY skills/docx/decision_template.docx ./skills/docx/decision_template.docx
|
COPY skills/docx/decision_template.docx ./skills/docx/decision_template.docx
|
||||||
|
|
||||||
|
# Reference content the /training tab reads at runtime:
|
||||||
|
# - .claude/agents/hermes-curator.md → GET /api/training/curator/prompt
|
||||||
|
# - skills/decision/SKILL.md → system prompt for the chat
|
||||||
|
# - docs/legal-decision-lessons.md → system prompt for the chat
|
||||||
|
# - docs/corpus-analysis.md → system prompt for the chat
|
||||||
|
#
|
||||||
|
# These are read-only at runtime; chair edits go through git, not the container.
|
||||||
|
COPY .claude/agents/hermes-curator.md ./.claude/agents/hermes-curator.md
|
||||||
|
COPY skills/decision/SKILL.md ./skills/decision/SKILL.md
|
||||||
|
COPY docs/legal-decision-lessons.md ./docs/legal-decision-lessons.md
|
||||||
|
COPY docs/corpus-analysis.md ./docs/corpus-analysis.md
|
||||||
|
|
||||||
# Make mcp-server source available to web/app.py (it does sys.path.insert for legal_mcp)
|
# Make mcp-server source available to web/app.py (it does sys.path.insert for legal_mcp)
|
||||||
ENV PYTHONPATH=/app/mcp-server/src
|
ENV PYTHONPATH=/app/mcp-server/src
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
26
data/audit/x11-phase2-backfill-20260601.md
Normal file
26
data/audit/x11-phase2-backfill-20260601.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# X11 Phase 2 — Corroboration Backfill (2026-06-01)
|
||||||
|
|
||||||
|
`corroboration.build_all()` over the full corpus after wiring the approval gate.
|
||||||
|
|
||||||
|
## Result
|
||||||
|
```
|
||||||
|
{"precedents": 12, "citations": 26, "linked": 20, "approved": 0, "demoted": 0}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Treatment distribution (20 stored links)
|
||||||
|
- followed: 18 · explained: 1 · mentioned: 1 · **negatives: 0**
|
||||||
|
|
||||||
|
## Per-halacha corroboration
|
||||||
|
- 14 halachot carry corroboration rows; **4 are corroborated** (≥2 distinct positive sources, 0 negatives).
|
||||||
|
- **All 14 were already `approved`** (13 by confidence ≥0.80, 1 by דפנה).
|
||||||
|
|
||||||
|
## Why 0 approved / 0 demoted (correct, not a bug)
|
||||||
|
- **0 approved:** `approve_halacha_by_corroboration` only transitions `pending_review`. Every corroborated halacha was already approved → nothing to promote this run. The citation-corroboration set currently **fully overlaps** the confidence-approved set.
|
||||||
|
- **0 demoted:** the corpus has **no negative treatments** → nothing overruled to demote.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- Counts before == after (approved=1415, pending=196, published=0, rejected=1) — idempotent, no chair-final state touched.
|
||||||
|
- Approve path proven end-to-end in a **rolled-back transaction**: a corroborated halacha set to `pending_review` flipped back to `approved` with reviewer `corroborated (2 judicial citations ≥ 2)`; prod row restored.
|
||||||
|
|
||||||
|
## Going-forward value
|
||||||
|
The corroboration approval path matters for (a) future halachot extracted **below** the confidence threshold but **citation-corroborated**, and (b) **overruled-demotion** once negative treatment appears in the citation graph. Re-runnable anytime via the `corroboration_rebuild` MCP tool (empty arg = full backfill).
|
||||||
70
data/eval/baseline.json
Normal file
70
data/eval/baseline.json
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"gold_size": 86,
|
||||||
|
"retrieval_config": {
|
||||||
|
"MULTIMODAL_ENABLED": true,
|
||||||
|
"VOYAGE_RERANK_ENABLED": false,
|
||||||
|
"VOYAGE_MODEL": "voyage-3",
|
||||||
|
"MULTIMODAL_TEXT_WEIGHT": 0.65,
|
||||||
|
"MULTIMODAL_RRF_K": 60,
|
||||||
|
"BM25_HYBRID_ENABLED": true
|
||||||
|
},
|
||||||
|
"overall": {
|
||||||
|
"P@5": 0.2465,
|
||||||
|
"R@5": 0.9938,
|
||||||
|
"nDCG@5": 0.9597,
|
||||||
|
"P@10": 0.1244,
|
||||||
|
"R@10": 0.9961,
|
||||||
|
"nDCG@10": 0.9611,
|
||||||
|
"MRR": 0.9535
|
||||||
|
},
|
||||||
|
"by_corpus": {
|
||||||
|
"internal_decisions": {
|
||||||
|
"P@5": 0.2037,
|
||||||
|
"R@5": 1.0,
|
||||||
|
"nDCG@5": 0.978,
|
||||||
|
"P@10": 0.1019,
|
||||||
|
"R@10": 1.0,
|
||||||
|
"nDCG@10": 0.978,
|
||||||
|
"MRR": 0.9722
|
||||||
|
},
|
||||||
|
"precedent_library": {
|
||||||
|
"P@5": 0.3188,
|
||||||
|
"R@5": 0.9833,
|
||||||
|
"nDCG@5": 0.9288,
|
||||||
|
"P@10": 0.1625,
|
||||||
|
"R@10": 0.9896,
|
||||||
|
"nDCG@10": 0.9326,
|
||||||
|
"MRR": 0.9219
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_practice_area": {
|
||||||
|
"betterment_levy": {
|
||||||
|
"P@5": 0.2051,
|
||||||
|
"R@5": 1.0,
|
||||||
|
"nDCG@5": 0.9621,
|
||||||
|
"P@10": 0.1026,
|
||||||
|
"R@10": 1.0,
|
||||||
|
"nDCG@10": 0.9621,
|
||||||
|
"MRR": 0.9487
|
||||||
|
},
|
||||||
|
"compensation_197": {
|
||||||
|
"P@5": 0.2,
|
||||||
|
"R@5": 1.0,
|
||||||
|
"nDCG@5": 1.0,
|
||||||
|
"P@10": 0.1,
|
||||||
|
"R@10": 1.0,
|
||||||
|
"nDCG@10": 1.0,
|
||||||
|
"MRR": 1.0
|
||||||
|
},
|
||||||
|
"rishuy_uvniya": {
|
||||||
|
"P@5": 0.2059,
|
||||||
|
"R@5": 1.0,
|
||||||
|
"nDCG@5": 0.9976,
|
||||||
|
"P@10": 0.1029,
|
||||||
|
"R@10": 1.0,
|
||||||
|
"nDCG@10": 0.9976,
|
||||||
|
"MRR": 1.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"generated_at": "20260603T084350Z"
|
||||||
|
}
|
||||||
86
data/eval/gold-set.jsonl
Normal file
86
data/eval/gold-set.jsonl
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
{"id": "g-2ab91a37e3", "query": "אברהם אגסי", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["1a87efe5-6e13-4ed4-a9ec-3f2f7d61e4ec"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-3572817c30", "query": "אברהם אנשין", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["8aeee5cc-26a0-475a-b4e4-c2570e4333f5"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-66dbb8ac16", "query": "אהרון ברק - תכנית רחביה", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["e151fc25-cf12-4563-b638-a86323f8413b"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-3588230bc4", "query": "אואקנין", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["405d51ac-deef-4bdf-aaea-f39b4aaa84fd"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-ff905fe19d", "query": "ב.דייניש", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["f3ab6507-6475-4230-ad96-70d4177a9f72"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-fa8f479ae1", "query": "בוטיק הנביאים", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["691e8220-745b-4631-aff4-338c164ba988"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-4b2c6a86ec", "query": "בית אגודת ישראל", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["7a71adbc-6a21-41a4-a98d-8fdd3f6e7b62"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-e9d5fc6d9b", "query": "בית חנינא מגרש 2010", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["fa0dab0c-bafc-4239-bba4-33cc9790f69f"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-8280afc216", "query": "בית חנינא — אום כולתום", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["a1e51703-474a-44d0-b8c8-5ae8bffb4782"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-e814cc43fa", "query": "בן זאב רמות", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["53c1adb6-81fd-4d0a-b3de-ffe2e6c5b6b3"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-7b1ef92188", "query": "בר-און", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["a60dc67d-67ab-4615-b148-34794d728687"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-9b17fb63a3", "query": "ג'רוזלם הומס אינק", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["9af224ef-5325-488c-a28c-de8ab059dfa3"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-c763aa9a45", "query": "גבאי וזוסמן", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["65065d5b-c0b2-4be3-970c-6b76842da054"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-ac23569fec", "query": "גפטו-פיצריה בצור הדסה", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["496c945a-9ab6-402c-9f9e-39f7af88b7cd"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-8dc2a68af8", "query": "דב ויעל ירון", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["a4716706-b2af-424d-98d8-d7ec45f9aeea"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-94196a641c", "query": "דור ודורשיו 18", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["a3ca3f83-3831-457d-8eed-b5654a201348"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-e19550a361", "query": "האורן 51 מבשרת ציון", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["3e112944-2a0d-4175-bcb6-69e19828b8ad"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-9612266af6", "query": "ההסתדרות הציונית העולמית", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["20999cb0-d9bd-4c4a-a18d-304451e1a30f"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-c39b2a42c7", "query": "הוועדה המקומית ירושלים נ' סופר נוח", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["04b2f953-efce-4e11-b9b5-e583b393c335"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-a145777626", "query": "הכט וסדובסקי", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["ffbd9963-099f-4bf5-b888-af993844e80a"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-33059ab228", "query": "המרכז הארצי לטהרת המשפחה", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["cd815101-e153-468d-a7bc-be1ac88105ae"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-8af7c5a180", "query": "השלום 63 מבשרת ציון", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["ee2104c8-2d31-4173-839c-8b61dcaf2a31"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-0494e34a1d", "query": "וינפלד", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["bd5d849c-c15f-43c3-96ab-d44337af9cb5"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-beca7df79f", "query": "זעיתר", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["098535ec-55c0-44dd-b058-ddaeac8b4cd7"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-f1a9633456", "query": "חוכרת הר חומה", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["e40110b4-9364-4cc7-a5b8-cee9bbedb172"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-3d12dcc821", "query": "חלוואני", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["9d8da0a6-e4dc-4c9b-85ab-36fa5ecbd12f"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-77ae0a9368", "query": "טביסל דניאל", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["f39f807d-90a6-4950-b10f-485dbf7e2ef6"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-4dec58a380", "query": "יסמין 54 מבשרת ציון", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["ac1a34c4-52c5-4e91-b6a7-297f11fe0460"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-776cecae74", "query": "ירושלים שקופה", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["ecc63119-6977-4d8e-930d-609dbd990494", "438d693c-6dfd-4a65-a48c-f8e2011bcc10"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (2 same-named)"}
|
||||||
|
{"id": "g-824f0d2ca8", "query": "ירושלים שקופה (1112/22)", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["446e96f1-a896-435d-bc33-a9b61b6d0b6c"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-454e470bb4", "query": "ליאור אהרון", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["a5ba233d-27aa-432b-bbef-093a2d49d80a"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-09c8b87f35", "query": "מוצא עילית", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["048af29a-d356-454f-acd6-5d1de32ecb94"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-5055a61633", "query": "מילי וישראל גלון", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["cc812e7b-cf9b-44af-8dfa-36541cb0b72d"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-8a15965c4f", "query": "מנץ", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["ed7ac419-f359-4b51-8e21-adec141629c7"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-48ae72c484", "query": "מפלגת נעם", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["5897b4e1-1fa2-4d83-816d-51f7cdf7cdee"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-ca171fdb45", "query": "מצפה בית שמש", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["8ba7f873-0da4-49cd-955e-98f579e61fb2"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-7e54e8b69b", "query": "מרדכי שטיין", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["228de6b5-b731-4959-a448-e9e941790420"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-62befb6c18", "query": "מרכז קהילתי בית הכרם", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["e73ec1d1-e89e-4d5b-a870-84cbf7b09106"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-cb0a295129", "query": "נחמיה פרומר", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["ab039082-47d1-4f79-9db9-d97c53e3bc80"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-4f9a788676", "query": "נילי אמיתי", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["d3fd9310-621b-4b76-a71f-729dd2044108"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-e9b1ce30da", "query": "סלונים", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["add3da4c-fda0-48d0-8109-957fc9f924a7"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-23b50ceb0d", "query": "סקולוסקי", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["18846024-d630-4a33-9024-6b2388df7007"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-93531bf772", "query": "עוררי רכס חלילים", "practice_area": "compensation_197", "corpus": "internal_decisions", "relevant_case_law_ids": ["288326ca-bf9c-48fe-ba6b-8ef9e65bd0a0"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-f1e0ebc751", "query": "עזבון אליהו הרנון ז\"ל נ' הוועדה המקומית ירושלים", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["6774fe43-0ba9-4409-b128-cacbd168afc3"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-f3c29ce2f8", "query": "עמותת ישיבת טעלז", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["30a606ac-5ba4-46d5-86d4-075564e30d2d"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-0a595fd872", "query": "ערן סופר", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["9c63985a-211f-4af9-a145-c674bdcdb0f6"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-fd95fc1bc0", "query": "פייר קניג 36", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["5cc53869-9e85-469e-85bb-986ac646de07"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-04f32ade81", "query": "פרויקט מגרש 902 בית שמש", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["810f8315-26cf-4069-be16-b5fee7f16a56"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-445fa07583", "query": "קו אופ ופרטוש", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["62c517c8-ab8d-48b1-8472-1f6adc6e3817"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-9f2c58a190", "query": "קרן יעקב הלפרן", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["921d36df-76be-4a53-823b-0d2ac1f79f2e"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-43fff5d955", "query": "קרקעות ירושלים 2", "practice_area": "compensation_197", "corpus": "internal_decisions", "relevant_case_law_ids": ["730d6f21-08e4-4ae0-8b7e-017dde61003e"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-78610b8e8a", "query": "שכן הכלנית 54 מבשרת ציון", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["88e2d381-2e34-49b2-8225-5e72b487854d"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-d043d7c75f", "query": "ששת הימים 6 רמת אשכול", "practice_area": "betterment_levy", "corpus": "internal_decisions", "relevant_case_law_ids": ["a87d30d4-d3a3-439d-9909-c282024aafba"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-1cdefcfaba", "query": "תמ\"א רש\"י 32 תל אביב", "practice_area": "rishuy_uvniya", "corpus": "internal_decisions", "relevant_case_law_ids": ["3cbd2d6c-ff20-4af2-ab92-c105bb30fbc6"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-a65f37501c", "query": "אגא וכט", "practice_area": "rishuy_uvniya", "corpus": "precedent_library", "relevant_case_law_ids": ["1847e97e-6e38-494f-b079-0fc59066788a"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-10e5dca5b8", "query": "אהוד שפר", "practice_area": "rishuy_uvniya", "corpus": "precedent_library", "relevant_case_law_ids": ["9024da7b-f408-4b6f-808f-c514a83728e4"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-b42d0ceaaa", "query": "אירוס הגלבוע", "practice_area": "rishuy_uvniya", "corpus": "precedent_library", "relevant_case_law_ids": ["b673d649-d162-4f81-a323-c7d89e8334ce"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-4d50ccd2dd", "query": "אנטרים", "practice_area": "rishuy_uvniya", "corpus": "precedent_library", "relevant_case_law_ids": ["48909f09-8a65-4a2d-8697-e2f50bf9a756"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-bbf0e30d31", "query": "ארגון עמק שווה", "practice_area": "rishuy_uvniya", "corpus": "precedent_library", "relevant_case_law_ids": ["41d5a21c-a28a-428f-a35e-bc7d0dc89539"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-dac18ac10f", "query": "ב. דייניש", "practice_area": "betterment_levy", "corpus": "precedent_library", "relevant_case_law_ids": ["950d8c1b-4976-4a68-8b8e-7d0bdd056e1d"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-0d130898bb", "query": "בולקינד", "practice_area": "betterment_levy", "corpus": "precedent_library", "relevant_case_law_ids": ["e57c4a6b-66a0-4d52-85af-5018f03cf295"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-789c4ff1a7", "query": "בית אגודת ישראל", "practice_area": "betterment_levy", "corpus": "precedent_library", "relevant_case_law_ids": ["aadedc2d-e990-4d6d-9dd1-8be4fa6dcbe2", "ced7ea50-689b-465d-bf79-99e22a72e0df"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (2 same-named)"}
|
||||||
|
{"id": "g-06b07271bb", "query": "ברק - תכנית רחביה", "practice_area": "betterment_levy", "corpus": "precedent_library", "relevant_case_law_ids": ["57be0d1a-293f-481f-aa5b-bfa7dc73f99e"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-4160927269", "query": "גבעת האירוסים", "practice_area": "rishuy_uvniya", "corpus": "precedent_library", "relevant_case_law_ids": ["e26f2fa2-50e5-407d-8724-8c707dcda51b"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-4fe81acc94", "query": "הבית ברחוב שמעוני", "practice_area": "betterment_levy", "corpus": "precedent_library", "relevant_case_law_ids": ["53ccf47e-0fc7-4248-b486-02f57a9c689c"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-faa7cc3548", "query": "הקדש עדת הבוכרים", "practice_area": "betterment_levy", "corpus": "precedent_library", "relevant_case_law_ids": ["587381e4-d194-4d37-b00f-ccf7242ba228"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-0901d5d211", "query": "כנסייה אוונגלית אפיסקופלית", "practice_area": "betterment_levy", "corpus": "precedent_library", "relevant_case_law_ids": ["4bde8ca8-7862-4b19-9dd7-de2e31d82721"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-62fd2080df", "query": "לויתן אדיב שמואל", "practice_area": "betterment_levy", "corpus": "precedent_library", "relevant_case_law_ids": ["b80d94a0-b836-44f5-8cc6-18d8cf26e41d"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-9f934d9159", "query": "לויתן וקלמנוביץ", "practice_area": "betterment_levy", "corpus": "precedent_library", "relevant_case_law_ids": ["436efd48-c8ab-49f0-b3a9-52bf15ea806d"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-9e829d5277", "query": "מועצה אזורית מטה בנימין", "practice_area": "", "corpus": "precedent_library", "relevant_case_law_ids": ["d7b635b1-6607-46ac-9868-44e4fd598e5a"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-b3acf850af", "query": "משה ירושלמי", "practice_area": "betterment_levy", "corpus": "precedent_library", "relevant_case_law_ids": ["e18aa906-e0f5-452f-a17a-f1c299095340"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-631a47d8b0", "query": "משרד התחבורה נ' גלר", "practice_area": "betterment_levy", "corpus": "precedent_library", "relevant_case_law_ids": ["8bfcd217-cde3-4930-a058-c9a59182c338"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-f8aaaa60d7", "query": "נווה שלום", "practice_area": "betterment_levy", "corpus": "precedent_library", "relevant_case_law_ids": ["4f85e3f1-237a-4dac-b949-87a43ee6f633"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-dbb1358ccf", "query": "ניצני עוז", "practice_area": "rishuy_uvniya", "corpus": "precedent_library", "relevant_case_law_ids": ["e08f81d3-6183-494c-aec3-f20d39e2755e"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-ae5917860b", "query": "סרוזברג ואח'", "practice_area": "", "corpus": "precedent_library", "relevant_case_law_ids": ["d9772726-9766-4509-8067-b20fa625a1a9"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-e1e175248c", "query": "עמותת העצמאים באילת", "practice_area": "rishuy_uvniya", "corpus": "precedent_library", "relevant_case_law_ids": ["f59e74c2-6433-47c9-bd0e-580cf4171fbb"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-86116ced86", "query": "שמי אשקלוני", "practice_area": "betterment_levy", "corpus": "precedent_library", "relevant_case_law_ids": ["7352e510-c769-45e4-b4ef-d85271743506"], "source": "bootstrap_known_item", "note": "known-item: search by case_name → expect the case itself (1 same-named)"}
|
||||||
|
{"id": "g-7e9438b730", "query": "פטור מהיטל השבחה למוסד ציבורי לפי סעיף 19(ב)(4)", "practice_area": "", "corpus": "precedent_library", "relevant_case_law_ids": ["ced7ea50-689b-465d-bf79-99e22a72e0df", "aadedc2d-e990-4d6d-9dd1-8be4fa6dcbe2", "587381e4-d194-4d37-b00f-ccf7242ba228", "4bde8ca8-7862-4b19-9dd7-de2e31d82721", "4f85e3f1-237a-4dac-b949-87a43ee6f633"], "source": "chair", "note": "semantic query (chair-approved 2026-05-31)"}
|
||||||
|
{"id": "g-89bc8d6161", "query": "נטרול תרומת תמ\"א 38 בשומת \"מצב קודם\"", "practice_area": "", "corpus": "precedent_library", "relevant_case_law_ids": ["436efd48-c8ab-49f0-b3a9-52bf15ea806d", "b80d94a0-b836-44f5-8cc6-18d8cf26e41d", "57be0d1a-293f-481f-aa5b-bfa7dc73f99e", "7352e510-c769-45e4-b4ef-d85271743506", "53ccf47e-0fc7-4248-b486-02f57a9c689c"], "source": "chair", "note": "semantic query (chair-approved 2026-05-31)"}
|
||||||
|
{"id": "g-f4c06ec2f9", "query": "פטור מהיטל בתמ\"א 38 — מימוש במכר מול מימוש בהיתר", "practice_area": "", "corpus": "precedent_library", "relevant_case_law_ids": ["53ccf47e-0fc7-4248-b486-02f57a9c689c", "e57c4a6b-66a0-4d52-85af-5018f03cf295", "7352e510-c769-45e4-b4ef-d85271743506"], "source": "chair", "note": "semantic query (chair-approved 2026-05-31)"}
|
||||||
|
{"id": "g-8c8b82486c", "query": "נטרול ציפיות לתכנית עתידית בשווי מצב קודם (אקו-סיטי/לוסטרניק)", "practice_area": "", "corpus": "precedent_library", "relevant_case_law_ids": ["950d8c1b-4976-4a68-8b8e-7d0bdd056e1d", "7352e510-c769-45e4-b4ef-d85271743506", "436efd48-c8ab-49f0-b3a9-52bf15ea806d"], "source": "chair", "note": "semantic query (chair-approved 2026-05-31)"}
|
||||||
|
{"id": "g-bbe92ea5e3", "query": "היתר לשימוש חורג בקרקע חקלאית — סטייה ניכרת ומגמת תכנון", "practice_area": "", "corpus": "precedent_library", "relevant_case_law_ids": ["e08f81d3-6183-494c-aec3-f20d39e2755e", "e26f2fa2-50e5-407d-8724-8c707dcda51b", "b673d649-d162-4f81-a323-c7d89e8334ce", "f59e74c2-6433-47c9-bd0e-580cf4171fbb"], "source": "chair", "note": "semantic query (chair-approved 2026-05-31)"}
|
||||||
|
{"id": "g-19376b63de", "query": "זכות עמידה / זכות התנגדות לבקשה להיתר בנייה", "practice_area": "", "corpus": "precedent_library", "relevant_case_law_ids": ["48909f09-8a65-4a2d-8697-e2f50bf9a756", "9024da7b-f408-4b6f-808f-c514a83728e4"], "source": "chair", "note": "semantic query (chair-approved 2026-05-31)"}
|
||||||
|
{"id": "g-3d2f9fc270", "query": "היקף התערבות בית המשפט בשיקול דעת תכנוני של ועדה", "practice_area": "", "corpus": "precedent_library", "relevant_case_law_ids": ["41d5a21c-a28a-428f-a35e-bc7d0dc89539", "9024da7b-f408-4b6f-808f-c514a83728e4", "e26f2fa2-50e5-407d-8724-8c707dcda51b"], "source": "chair", "note": "semantic query (chair-approved 2026-05-31)"}
|
||||||
|
{"id": "g-9e96222cc5", "query": "אמת המידה להתערבות ועדת ערר בשומת שמאי מכריע", "practice_area": "", "corpus": "precedent_library", "relevant_case_law_ids": ["8bfcd217-cde3-4930-a058-c9a59182c338", "1847e97e-6e38-494f-b079-0fc59066788a"], "source": "chair", "note": "semantic query (chair-approved 2026-05-31)"}
|
||||||
|
{"id": "g-181b020ea9", "query": "חובת ועדת ערר להעביר השגות שמאיות לשמאי מייעץ (ס'197)", "practice_area": "", "corpus": "precedent_library", "relevant_case_law_ids": ["e18aa906-e0f5-452f-a17a-f1c299095340", "8bfcd217-cde3-4930-a058-c9a59182c338"], "source": "chair", "note": "semantic query (chair-approved 2026-05-31)"}
|
||||||
414
docs/agent-audit-2026-05-17.md
Normal file
414
docs/agent-audit-2026-05-17.md
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
# דו"ח Audit סוכנים — 2026-05-17
|
||||||
|
|
||||||
|
> נוצר על-ידי 7 sub-agents מקבילים שחקרו כל סוכן בנפרד.
|
||||||
|
> כיסוי: קבצי הנחיות, תצורת DB, skills, MCP tools, freshness, drift CMP↔CMPA.
|
||||||
|
>
|
||||||
|
> **עדכון 2026-05-17:** כל 12 הבעיות טופלו באותו יום. ראה סעיף "סטטוס תיקונים" למטה.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## סיכום מנהלים
|
||||||
|
|
||||||
|
### טבלת מצב כללית — לאחר תיקונים (2026-05-17)
|
||||||
|
|
||||||
|
| סוכן | מודל (instructions = DB) | Skills CMP | Skills CMPA | סטטוס |
|
||||||
|
|------|--------------------------|-----------|-----------|--------|
|
||||||
|
| עוזר משפטי (CEO) | claude-opus-4-7 ✅ | 9 | 6 | ✅ תקין |
|
||||||
|
| מנתח משפטי | claude-opus-4-7 ✅ | 9 | 6 | ✅ תקין |
|
||||||
|
| חוקר תקדימים | claude-sonnet-4-6 ✅ | 9 | 6 | ✅ תקין |
|
||||||
|
| כותב החלטה | claude-opus-4-7 ✅ | 9 | 6 | ✅ תקין |
|
||||||
|
| בודק איכות (QA) | claude-sonnet-4-6 ✅ | 9 | 6 | ✅ תקין |
|
||||||
|
| מייצא טיוטה | claude-sonnet-4-6 ✅ | 9 | 6 | ✅ תקין |
|
||||||
|
| מגיה מסמכים | claude-opus-4-7 ✅ | 9 | 6 | ✅ תקין |
|
||||||
|
| מנהל ידע (Curator) | deepseek-v4-pro ✅ | 9 | 6 | ✅ תקין |
|
||||||
|
|
||||||
|
> Skills CMPA=6 הוא עיצוב מכוון (6 shared-only skills). verify script מאשר "0 agents need sync".
|
||||||
|
|
||||||
|
### סטטוס תיקונים — כל 12 הבעיות טופלו
|
||||||
|
|
||||||
|
| # | חומרה | סוכן | בעיה | סטטוס | commit |
|
||||||
|
|---|-------|------|------|-------|--------|
|
||||||
|
| 1 | 🔴 | מייצא | `טיוטה-V` → `טיוטה-v` — דורס גרסאות | ✅ תוקן | `a584dc3` |
|
||||||
|
| 2 | 🔴 | מייצא | case.status לא מעודכן ל-`exported` + case_update חסר מ-tools | ✅ תוקן | `a584dc3` |
|
||||||
|
| 3 | 🔴 | חוקר | §ז (query log) חסר בתיק 8174-24 | ✅ תוקן | data (gitignored) |
|
||||||
|
| 4 | 🟠 | כולם | Skills asymmetry CMPA | ✅ לא נדרש — verify: "0 need sync" (עיצוב מכוון) | — |
|
||||||
|
| 5 | 🟠 | חוקר | `search_internal_decisions` לא מתועד | ✅ תוקן — tool + סעיף 2ב.2א | `35423ea` |
|
||||||
|
| 6 | 🟠 | מייצא | נתיב legal-docx hardcoded ל-CMP UUID | ✅ תוקן → `$PAPERCLIP_COMPANY_ID` | `a584dc3` |
|
||||||
|
| 7 | 🟠 | CEO | Project ID + company UUID hardcoded | ✅ תוקן → דינמי מ-$PAPERCLIP_TASK_ID | `35423ea` |
|
||||||
|
| 8 | 🟡 | רוב | Model drift instructions↔DB | ✅ תוקן + שודרג ל-opus-4-7 | `1608ea5`, `c3ce0e7` |
|
||||||
|
| 9 | 🟡 | QA | corpus_queries_logged: ידני או אוטומטי? | ✅ תוקן — הבהרה מפורשת: grep ידני | `1608ea5` |
|
||||||
|
| 10 | 🟡 | CEO | maxConcurrentRuns=NULL | ✅ לא נדרש — DB כבר maxConcurrentRuns=2 | — |
|
||||||
|
| 11 | 🟡 | מגיה | {issue-id} placeholder בקוד | ✅ תוקן → `$PAPERCLIP_TASK_ID` | `1608ea5` |
|
||||||
|
| 12 | 🟢 | מנהל ידע | ownership הצעות curator לא מוגדר | ✅ תוקן — הוסף ל-CLAUDE.md | `1608ea5` |
|
||||||
|
|
||||||
|
### שינויים נוספים שבוצעו באותו סשן
|
||||||
|
|
||||||
|
| שינוי | קובץ | commit |
|
||||||
|
|-------|------|--------|
|
||||||
|
| weekly-feedback-job: כתיבה לקובץ בלבד, לא Paperclip comment | legal-ceo.md | `ea0532b` |
|
||||||
|
| try-catch על agents.invoke בפידבק שבועי | worker.ts | `73e37df` |
|
||||||
|
| try-catch על http.fetch ב-stale-case-reminder | worker.ts | `73e37df` |
|
||||||
|
| HEARTBEAT.md reference בראש legal-researcher.md | legal-researcher.md | `1608ea5` |
|
||||||
|
| search_internal_decisions הוסף ל-legal-researcher tools | legal-researcher.md | `35423ea` |
|
||||||
|
| opus-4-6 → opus-4-7 ב-DB: CEO, מנתח, כותב, מגיה (16 סוכנים) | DB | `c3ce0e7` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ממצאים לפי סוכן
|
||||||
|
|
||||||
|
### 1. עוזר משפטי (CEO)
|
||||||
|
|
||||||
|
**קובץ:** `.claude/agents/legal-ceo.md` — 796 שורות, עודכן 2026-05-17
|
||||||
|
|
||||||
|
**תצורה:**
|
||||||
|
| חברה | ID | Model | Budget |
|
||||||
|
|------|-----|-------|--------|
|
||||||
|
| CMP | `752cebdd-6748-4a04-aacd-c7ab0294ef33` | claude-opus-4-6 | 1500¢ |
|
||||||
|
| CMPA | `cdbfa8bc-3d61-41a4-a2e7-677ec7d34562` | claude-opus-4-6 | 1500¢ |
|
||||||
|
|
||||||
|
**routing conditions:** `user_commented`, `agent_completion`, `precedent_extraction_*`, `weekly-feedback-job`, fallback→heartbeat רגיל
|
||||||
|
|
||||||
|
**MCP tools מוזכרים (41):** case_get/list/update, document_list, get_claims, get_chair_directions, record/list_chair_feedback, approve_direction, brainstorm_directions, search_case_documents, search_precedent_library, workflow_status, processing_status, get_metrics, validate_decision, set_outcome, export_docx, apply_user_edit, list_bookmarks, revise_draft, precedent_process_pending, extract_halachot/metadata, library_get/list, halacha_review, halachot_pending, extract_appraiser_facts, write_interim_draft, export_interim_draft
|
||||||
|
|
||||||
|
**✅ תקין:**
|
||||||
|
- Routing logic מלא ועדכני (כולל weekly-feedback-job שתוקן לאחרונה)
|
||||||
|
- Company filtering ברור (טבלה עם UUIDs וטווחי תיקים)
|
||||||
|
- Wakeup דרך API בלבד (לא DB ישיר) — מוגדר במפורש
|
||||||
|
- HEARTBEAT.md references נכונים (§0, §1, §1.7)
|
||||||
|
- weekly-feedback-job: כתיבה לקובץ בלבד, ללא issueId — נכון
|
||||||
|
|
||||||
|
**⚠️ בעיות:**
|
||||||
|
- 🟠 **Model drift:** instructions = claude-sonnet-4-6, DB = claude-opus-4-6
|
||||||
|
- 🟠 **Hardcoded Project ID:** `25c1b4a1-2c0e-4a2d-9938-8ae56ccda6f1` (תיק 1130-25) — צריך להיות דינמי
|
||||||
|
- 🟡 **maxConcurrentRuns = NULL** ב-DB (שאר הסוכנים = 1)
|
||||||
|
- 🟡 **MCP startup race:** הוראות מדברות על sleep+retry אבל לא כ-code אוטומטי
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. מנתח משפטי
|
||||||
|
|
||||||
|
**קובץ:** `.claude/agents/legal-analyst.md` — 498 שורות, עודכן 2026-05-04
|
||||||
|
|
||||||
|
**תצורה:**
|
||||||
|
| חברה | ID | Model | Budget |
|
||||||
|
|------|-----|-------|--------|
|
||||||
|
| CMP | `c26e9439-a88a-49dc-9e67-2262c95db65c` | claude-opus-4-6 | 1500¢ |
|
||||||
|
| CMPA | `f70fd353-...` | claude-opus-4-6 | 1500¢ |
|
||||||
|
|
||||||
|
**MCP tools (18):** case_get/list/update, document_list/get_text, extract_claims, extract_appraiser_facts, get_claims, search_case_documents, search_decisions, search_precedent_library, precedent_library_get/list, halacha_review, halachot_pending, find_similar_cases, workflow_status, processing_status
|
||||||
|
|
||||||
|
**Output artifacts:** `{case_dir}/documents/research/analysis-and-research.md`
|
||||||
|
|
||||||
|
**Query logging (§5ד/§7א):** לרשום כל `search_precedent_library`, `search_decisions`, `find_similar_cases` כולל ניסיונות עם 0 תוצאות
|
||||||
|
|
||||||
|
**✅ תקין:**
|
||||||
|
- כל 18 כלי MCP מוזכרים ומיושמים
|
||||||
|
- סיווג claim_type ברור (claim/response/reply)
|
||||||
|
- Wakeup CEO בפורמט נכון
|
||||||
|
- reference files קיימים
|
||||||
|
|
||||||
|
**⚠️ בעיות:**
|
||||||
|
- 🟠 **Model drift:** instructions = claude-opus-4-7, DB = claude-opus-4-6
|
||||||
|
- 🟡 **CMPA sync gap:** עדכון אחרון CMPA = 2026-05-04 (13 ימים לפני CMP)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. חוקר תקדימים
|
||||||
|
|
||||||
|
**קובץ:** `.claude/agents/legal-researcher.md` — 240 שורות, עודכן 2026-05-04
|
||||||
|
|
||||||
|
**תצורה:**
|
||||||
|
| חברה | ID | Model | Budget |
|
||||||
|
|------|-----|-------|--------|
|
||||||
|
| CMP | `35022af0-0498-4c3d-90ca-b0ab9e987198` | claude-sonnet-4-6 | 1500¢ |
|
||||||
|
| CMPA | `5dd06843-...` | claude-sonnet-4-6 | 1500¢ |
|
||||||
|
|
||||||
|
**MCP tools (29):** case_get/update, document_list/get_text, search_case_documents, search_decisions, find_similar_cases, extract_references, precedent_attach, precedent_list, precedent_search_library, search_precedent_library, library_get/list, extract_halachot/metadata, precedent_process_pending, halacha_review, halachot_pending, workflow_status
|
||||||
|
|
||||||
|
**Output artifact:** `{case_dir}/documents/research/precedent-research.md`
|
||||||
|
|
||||||
|
**Query logging (§ז):** חובה — כל query עם פילטרים, תוצאות, בחירה/דחייה, negative evidence
|
||||||
|
|
||||||
|
**✅ תקין:**
|
||||||
|
- שלושת הקורפוסים מוגדרים בבירור (פסיקה חיצונית / קאנון דפנה / ציטוטים ידניים)
|
||||||
|
- precedent_attach עם הוראות מלאות
|
||||||
|
- Wakeup CEO דינמי לפי חברה
|
||||||
|
|
||||||
|
**⚠️ בעיות:**
|
||||||
|
- 🔴 **§ז חסר בתיק 8174-24** — 1 מתוך 3 תיקים בדיסק חסר את תיעוד השאילתות. QA אמור לחסום ייצוא.
|
||||||
|
- 🟠 **`search_internal_decisions` לא מתועד** — הכלי ב-header אבל לא מוסבר בגוף ההנחיות. מתי להשתמש בו?
|
||||||
|
- 🟠 **Skills asymmetry CMPA** — CMPA חסרה: legal-assistant, legal-decision, legal-docx, diagnose-why-work-stopped, appendix-expert-intern, terminal-bench-loop
|
||||||
|
- 🟡 **`daphna-precedent-network.md` עדכון אחרון 27 אפריל** — עשוי להיות לפני תקדימים חדשים
|
||||||
|
- 🟡 **HEARTBEAT.md לא מוזכר בפירוש** — אין link ישיר בתחילת ההנחיות
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. כותב החלטה
|
||||||
|
|
||||||
|
**קובץ:** `.claude/agents/legal-writer.md` — 410 שורות, עודכן 2026-05-04
|
||||||
|
|
||||||
|
**תצורה:**
|
||||||
|
| חברה | ID | Model | Budget |
|
||||||
|
|------|-----|-------|--------|
|
||||||
|
| CMP | `7ed8686f-24bc-49a3-bc02-67ca15b895a9` | claude-opus-4-6 | 1500¢ |
|
||||||
|
| CMPA | `99289cb1-...` | claude-opus-4-6 | 1500¢ |
|
||||||
|
|
||||||
|
**Block range:** ה-יא (5-11), כותב בסדר; א-ד (אוטומטי), יב (אוטומטי)
|
||||||
|
|
||||||
|
**5 style docs לפני בלוק י (כולם קיימים):**
|
||||||
|
- `docs/daphna-voice-fingerprint.md` ✅ (עודכן 10 מאי)
|
||||||
|
- `docs/daphna-precedent-network.md` ✅ (עודכן 27 אפריל)
|
||||||
|
- `docs/daphna-architecture-by-outcome.md` ✅ (עודכן 28 אפריל)
|
||||||
|
- `docs/daphna-acceptance-architecture.md` ✅ (עודכן 28 אפריל)
|
||||||
|
- `docs/voice-1130-25.md` ✅ (עודכן 26 אפריל)
|
||||||
|
|
||||||
|
**MCP tools (18):** case_get/update, document_list/get_text, get_claims, get_chair_directions, get_decision_template, get_block_context, save_block_content, write_block, search_decisions, search_precedent_library, library_get/list, search_case_documents, get_style_guide, halacha_review, workflow_status, apply_user_edit
|
||||||
|
|
||||||
|
**✅ תקין:**
|
||||||
|
- 4 statuses של get_chair_directions מוגדרים (missing/empty/partial/complete)
|
||||||
|
- Revision mode ברור (לא לשמור ב-DB בעריכה)
|
||||||
|
- 10 anti-patterns ברורים
|
||||||
|
- Company filtering נכון (CEO IDs שונים לפי חברה)
|
||||||
|
|
||||||
|
**⚠️ בעיות:**
|
||||||
|
- 🟠 **Model drift:** instructions = claude-opus-4-7, DB = claude-opus-4-6
|
||||||
|
- 🟡 **חסר שלב 0 מפורש:** בדיקת `issue.description` (ההוראה הראשית מה-CEO)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. בודק איכות (QA)
|
||||||
|
|
||||||
|
**קובץ:** `.claude/agents/legal-qa.md` — 219 שורות, עודכן 2026-05-04
|
||||||
|
|
||||||
|
**תצורה:**
|
||||||
|
| חברה | ID | Model | Budget |
|
||||||
|
|------|-----|-------|--------|
|
||||||
|
| CMP | `1a5b229e-9220-4b13-940c-f8eb7285fc29` | claude-sonnet-4-6 | 1500¢ |
|
||||||
|
| CMPA | `7191ff77-...` | claude-sonnet-4-6 | 1500¢ |
|
||||||
|
|
||||||
|
**9 בדיקות (לא 8 — §7א הוא נפרד):**
|
||||||
|
1. שלמות מבנית — critical
|
||||||
|
2. רקע ניטרלי — critical
|
||||||
|
3. כיסוי טענות — critical
|
||||||
|
4. משקלות — warning
|
||||||
|
5. ללא כפילות — warning
|
||||||
|
6. מספור רציף — warning
|
||||||
|
7א. שאילתות קורפוס (corpus_queries_logged) — **critical blocker**
|
||||||
|
7. תאימות מתודולוגיה — critical
|
||||||
|
8. קול דפנה — critical
|
||||||
|
|
||||||
|
**Reference files (כולם קיימים):**
|
||||||
|
- `docs/daphna-decision-tree.md` ✅ (521 שורות)
|
||||||
|
- `docs/daphna-voice-fingerprint.md` ✅ (471 שורות)
|
||||||
|
- `docs/daphna-architecture-by-outcome.md` ✅ (381 שורות)
|
||||||
|
- `docs/daphna-acceptance-architecture.md` ✅ (640 שורות)
|
||||||
|
- `docs/daphna-block-zayin-claims.md` ✅ (385 שורות)
|
||||||
|
- `docs/daphna-precedent-network.md` ✅ (379 שורות)
|
||||||
|
|
||||||
|
**✅ תקין:**
|
||||||
|
- כל reference files קיימים ונגישים
|
||||||
|
- Company filtering מתועד (CEO IDs נכונים)
|
||||||
|
- Decision logic done/blocked מוגדרת
|
||||||
|
|
||||||
|
**⚠️ בעיות:**
|
||||||
|
- 🟡 **בדיקה 7א לא ברורה** — אוטומטית (validate_decision) או ידנית (grep בקובצי markdown)?
|
||||||
|
- 🟡 **בדיקה 8 (קול דפנה) סובייקטיבית** — חסרות דוגמאות anti-patterns מדידות
|
||||||
|
- 🟡 **get_metrics() — אין ספי קבלה** — מה מספר/אחוז שמוגדר כ-pass?
|
||||||
|
- 🟡 **decision tree:** אם רק בדיקות 4-6 (warning) נכשלו — done או blocked?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. מייצא טיוטה (Exporter)
|
||||||
|
|
||||||
|
**קובץ:** `.claude/agents/legal-exporter.md` — 151 שורות, עודכן 2026-05-04
|
||||||
|
|
||||||
|
**תצורה:**
|
||||||
|
| חברה | ID | Model | Budget |
|
||||||
|
|------|-----|-------|--------|
|
||||||
|
| CMP | `d0dc703b-ca83-4883-bca7-c9449e8713cd` | claude-sonnet-4-6 | 1500¢ |
|
||||||
|
| CMPA | `ada99a7d-...` | claude-sonnet-4-6 | 1500¢ |
|
||||||
|
|
||||||
|
**MCP tools (8):** export_docx, apply_user_edit, list_bookmarks, revise_draft, validate_decision, get_claims, get_block_context, workflow_status
|
||||||
|
|
||||||
|
**✅ תקין:**
|
||||||
|
- Git integration לכל ייצוא/עדכון
|
||||||
|
- validate_decision לפני export מוגדר
|
||||||
|
- active_draft detection (עריכה-*.docx) מוגדר
|
||||||
|
|
||||||
|
**⚠️ בעיות:**
|
||||||
|
- 🔴 **Naming mismatch קריטי:** הנחיות → `טיוטה-V{N}.docx` (V גדולה); קוד `revise_draft` → `טיוטה-v{N}.docx` (v קטנה); בדיסק בפועל → `טיוטה-v1.docx` (v קטנה). **הסוכן יחפש V גדולה ולא ימצא — יתחיל מ-v1 בכל הפעלה ויחליף קבצים קיימים!**
|
||||||
|
- 🔴 **case.status לא מעודכן ל-`exported`** — אחרי export מצליח, הסטטוס נשאר `drafted`/`reviewed`; הסטטוס `exported` קיים ב-DB schema ומוחרג מ-stale query
|
||||||
|
- 🟠 **legal-docx SKILL.md path hardcoded לCMP UUID** — CMPA ייכשל בקריאת ה-SKILL.md
|
||||||
|
- נכון: `/home/chaim/.paperclip/instances/default/skills/42a7acd0-.../legal-docx/SKILL.md`
|
||||||
|
- חסר: דינמי לפי `$PAPERCLIP_COMPANY_ID`
|
||||||
|
- 🟡 **Heartbeat grace=60s** — אם export DOCX > 60s, שני instances יתעוררו במקביל
|
||||||
|
- 🟡 **File size validation** — מוזכר בהנחיות אך לא מיושם בקוד
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. מגיה מסמכים (Proofreader)
|
||||||
|
|
||||||
|
**קובץ:** `.claude/agents/legal-proofreader.md` — 115 שורות, עודכן 2026-05-04
|
||||||
|
|
||||||
|
**תצורה:**
|
||||||
|
| חברה | ID | Model | Budget |
|
||||||
|
|------|-----|-------|--------|
|
||||||
|
| CMP | `410c0167-27dc-485c-a51b-7aa8b9ff2217` | claude-opus-4-6 | 1500¢ |
|
||||||
|
| CMPA | `17839fc6-...` | claude-opus-4-6 | 1500¢ |
|
||||||
|
|
||||||
|
**OCR workflow — 5 שלבים:** זיהוי → תיקון אוטומטי (abbreviations.json) → הגהה חכמה → שמירה → דיווח+סגירה
|
||||||
|
|
||||||
|
**abbreviations.json:** קיים ב-`/home/chaim/legal-ai/data/abbreviations.json` (2545 bytes, עודכן אפריל)
|
||||||
|
|
||||||
|
**✅ תקין:**
|
||||||
|
- abbreviations.json קיים
|
||||||
|
- Wakeup CEO דינמי לפי חברה
|
||||||
|
- חיוב סגירת issue
|
||||||
|
|
||||||
|
**⚠️ בעיות:**
|
||||||
|
- 🟠 **Model drift:** instructions = claude-opus-4-7, DB = claude-opus-4-6
|
||||||
|
- 🟡 **MCP write support לתיקיות:** לא אומת שה-tools תומכים בכתיבה ל-`documents/proofread/`
|
||||||
|
- 🟡 **Placeholder `{issue-id}` בקוד:** pc.sh calls משתמשות ב-literal `{issue-id}` — האם הסוכן מחליף עם `$PAPERCLIP_TASK_ID`?
|
||||||
|
- 🟡 **`extraction_status = proofread`:** האם השדה קיים ב-MCP document schema?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. מנהל ידע (Hermes Curator)
|
||||||
|
|
||||||
|
**קובץ:** `.claude/agents/hermes-curator.md` — 147 שורות, עודכן 2026-05-10
|
||||||
|
|
||||||
|
**תצורה:**
|
||||||
|
| חברה | ID | Adapter | Model | Budget |
|
||||||
|
|------|-----|---------|-------|--------|
|
||||||
|
| CMP | `60dce831-5c5b-4bae-bda9-5282d506f0dc` | deepseek_local | deepseek-v4-pro | 1500¢ |
|
||||||
|
| CMPA | `d6f7c55d-570a-46b8-8d72-1286d07da0d8` | deepseek_local | deepseek-v4-pro | 1500¢ |
|
||||||
|
|
||||||
|
**Profiles:** `~/.hermes/profiles/curator-cmp/` ✅ + `curator-cmpa/` ✅ (שניהם קיימים)
|
||||||
|
|
||||||
|
**Trigger:** UI "סמן כסופי" → `web/paperclip_client.py:pc_wake_curator_for_final()` → sub-issue + wakeup
|
||||||
|
|
||||||
|
**MCP tools (6):** case_get, case_get_final_text, document_list, get_style_guide, precedent_library_list, search_internal_decisions, halacha_review
|
||||||
|
|
||||||
|
**✅ תקין:**
|
||||||
|
- deepseek_local מוגדר נכון בשתי החברות
|
||||||
|
- Profiles קיימים ועובדים (MEMORY.md מ-06/05 עם 5 ממצאים)
|
||||||
|
- Read-only design — לא מעדכן קבצים ישירות
|
||||||
|
- env vars נדרשים מתועדים
|
||||||
|
|
||||||
|
**⚠️ בעיות:**
|
||||||
|
- 🟢 **לא מוגדר:** מי מממש הצעות ל-SKILL.md/lessons.md שה-curator מציע ב-comments?
|
||||||
|
- 🟢 **Hermes bias:** DeepSeek V4-Pro עלול לפרש תוצאות בצורה סובייקטיבית — אין oversight layer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## בעיות חוצות-סוכנים
|
||||||
|
|
||||||
|
### 1. Skills Asymmetry CMP vs CMPA (🟠 גבוה)
|
||||||
|
|
||||||
|
**Skills ב-CMP (9):**
|
||||||
|
- משותפים (6): paperclip, paperclip-converting-plans-to-tasks, paperclip-create-agent, paperclip-create-plugin, paperclip-dev, para-memory-files
|
||||||
|
- ייחודיים CMP (3+): legal-assistant, legal-decision, legal-docx, appendix-expert-intern, diagnose-why-work-stopped, terminal-bench-loop
|
||||||
|
|
||||||
|
**Skills ב-CMPA (6):** משותפים בלבד — **חסרים כל ה-legal-* skills**
|
||||||
|
|
||||||
|
**השפעה:** סוכני CMPA לא יכולים להשתמש ב-legal-decision skill (כתיבה), legal-assistant (ניתוח), legal-docx (DOCX). לא ברור אם זו החלטה מכוונת (CMPA עובד אחרת?) או gap בסנכרון.
|
||||||
|
|
||||||
|
**פעולה:** הרץ `sync_agents_across_companies.py --verify` עם PAPERCLIP_BOARD_API_KEY לבדיקה.
|
||||||
|
|
||||||
|
### 2. Model Version Drift (🟡 בינוני)
|
||||||
|
|
||||||
|
ב-DB כל הסוכנים רצים על claude-opus-4-6 או claude-sonnet-4-6, אבל קבצי הנחיות מציינים גרסאות שונות:
|
||||||
|
|
||||||
|
| סוכן | instructions מציין | DB רץ על |
|
||||||
|
|------|-------------------|---------|
|
||||||
|
| CEO | claude-sonnet-4-6 | claude-opus-4-6 |
|
||||||
|
| מנתח | claude-opus-4-7 | claude-opus-4-6 |
|
||||||
|
| כותב | claude-opus-4-7 | claude-opus-4-6 |
|
||||||
|
| מגיה | claude-opus-4-7 | claude-opus-4-6 |
|
||||||
|
| חוקר, QA, מייצא | claude-sonnet-4-6 | claude-sonnet-4-6 ✅ |
|
||||||
|
| מנהל ידע | deepseek-v4-pro | deepseek-v4-pro ✅ |
|
||||||
|
|
||||||
|
**לא ברור:** האם CEO/מנתח/כותב **אמורים** לרוץ על Opus (בחירה מכוונת לאיכות) ורק קבצי instructions לא עודכנו? או שה-DB צריך להתעדכן?
|
||||||
|
|
||||||
|
### 3. HEARTBEAT.md Reference (🟢 נמוך)
|
||||||
|
|
||||||
|
קובץ `legal-researcher.md` לא מפנה ל-`HEARTBEAT.md` בפירוש בתחילת הקובץ. שאר הסוכנים כן עושים זאת.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## רשימת תיקונים לפי עדיפות
|
||||||
|
|
||||||
|
### 🔴 קריטי — לתקן לפני תיק הבא
|
||||||
|
|
||||||
|
1. **`legal-exporter.md` + `web/app.py`/`drafting.py`:** אחד הדברים:
|
||||||
|
- תיקן הנחיות: שנה `טיוטה-V` → `טיוטה-v` (v קטנה) בכל המקומות
|
||||||
|
- **ועוד:** הוסף לקובץ הנחיות שלב: "אחרי export מוצלח — עדכן `case.status = 'exported'` דרך MCP או API"
|
||||||
|
|
||||||
|
2. **תיק 8174-24 — §ז חסר:** בדוק אם שלב המחקר הושלם. אם לא — הפעל חוקר מחדש לתיק זה.
|
||||||
|
|
||||||
|
### 🟠 גבוה — לתקן בשבוע הקרוב
|
||||||
|
|
||||||
|
3. **Skills CMPA:** הרץ:
|
||||||
|
```bash
|
||||||
|
PAPERCLIP_BOARD_API_KEY=$(mcp__infisical__get-secret \
|
||||||
|
--projectId 9a77b161-f70c-4dd3-9d67-b7ab850cef51 \
|
||||||
|
--environmentSlug nautilus --secretPath /paperclip --secretName BOARD_API_KEY) \
|
||||||
|
python ~/legal-ai/scripts/sync_agents_across_companies.py --verify
|
||||||
|
```
|
||||||
|
החלט אם להוסיף legal-* skills ל-CMPA ואם כן — הרץ `--apply`.
|
||||||
|
|
||||||
|
4. **`legal-researcher.md`:** הוסף תת-סעיף עם הוראות ל-`search_internal_decisions`:
|
||||||
|
- מתי להשתמש (החלטות פנימיות דפנה שלא בקורפוס הציבורי)
|
||||||
|
- מה ההבדל מ-`search_decisions`
|
||||||
|
|
||||||
|
5. **`legal-exporter.md` — נתיב legal-docx:** שנה מ-hardcoded UUID ל-דינמי:
|
||||||
|
```
|
||||||
|
אם $PAPERCLIP_COMPANY_ID = 42a7acd0... → CMP path
|
||||||
|
אם $PAPERCLIP_COMPANY_ID = 8639e837... → CMPA path
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **`legal-ceo.md` — Project ID:** הסר את ה-hardcoded ID של 1130-25. החלף בהוראה: "השתמש ב-`projects_list` לקבלת project_id הנכון לפי חברה ולתיק".
|
||||||
|
|
||||||
|
### 🟡 בינוני — לתקן בחודש הקרוב
|
||||||
|
|
||||||
|
7. **Model documentation:** החלט על גרסאות מודל לכל סוכן ועדכן גם הנחיות גם DB. עדיף: שמור הנחיות כ-source of truth ועדכן DB דרך `sync_agents_across_companies.py --apply`.
|
||||||
|
|
||||||
|
8. **`legal-qa.md` — הבהרת corpus_queries_logged:** הוסף: "הבדיקה היא קריאת `validate_decision` עם `check_corpus_log=true` / או grep ידני בקובץ `analysis-and-research.md` לסעיף ז".
|
||||||
|
|
||||||
|
9. **`legal-ceo.md` — maxConcurrentRuns:** עדכן DB ל-maxConcurrentRuns=1 (או 2 אם CEO רוצה מקביליות מכוונת).
|
||||||
|
|
||||||
|
10. **`legal-proofreader.md` — {issue-id} placeholder:** שנה ל-`$PAPERCLIP_TASK_ID` באופן מפורש.
|
||||||
|
|
||||||
|
11. **`legal-researcher.md` — HEARTBEAT.md link:** הוסף בשורה 1: `> ראה גם: HEARTBEAT.md לחוקים הכלליים`.
|
||||||
|
|
||||||
|
### 🟢 נמוך — future improvement
|
||||||
|
|
||||||
|
12. **מנהל ידע — ownership:** הוסף ל-CLAUDE.md הנחיה: "Curator proposals ב-comments → חיים מאשר ידנית → commits ל-SKILL.md ו-lessons.md".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## אימות (לאחר תיקונים)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. שלוף API key
|
||||||
|
PAPERCLIP_BOARD_API_KEY=$(mcp__infisical__get-secret \
|
||||||
|
--projectId 9a77b161-f70c-4dd3-9d67-b7ab850cef51 \
|
||||||
|
--environmentSlug nautilus --secretPath /paperclip --secretName BOARD_API_KEY)
|
||||||
|
|
||||||
|
# 2. בדוק drift
|
||||||
|
python ~/legal-ai/scripts/sync_agents_across_companies.py --verify
|
||||||
|
|
||||||
|
# 3. בדוק freshness של הנחיות
|
||||||
|
python ~/legal-ai/scripts/sync_agents_across_companies.py --check-instructions
|
||||||
|
|
||||||
|
# 4. בדוק שסוכני CMPA עובדים עם skills נכונים
|
||||||
|
PGPASSWORD="paperclip" psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -c "
|
||||||
|
SELECT a.name, array_agg(s.name ORDER BY s.name) as skills
|
||||||
|
FROM agents a
|
||||||
|
JOIN companies c ON a.company_id = c.id
|
||||||
|
LEFT JOIN agent_skills ask ON ask.agent_id = a.id
|
||||||
|
LEFT JOIN skills s ON ask.skill_id = s.id
|
||||||
|
WHERE c.name LIKE '%השבחה%' AND (a.is_deleted = false OR a.is_deleted IS NULL)
|
||||||
|
GROUP BY a.id ORDER BY a.name;
|
||||||
|
"
|
||||||
|
```
|
||||||
62
docs/anti-hallucination-gate.md
Normal file
62
docs/anti-hallucination-gate.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# שער anti-hallucination — הגנה משותפת מפני הזיות (INV-AH)
|
||||||
|
|
||||||
|
> **מקור-אמת אחד לכל הסוכנים.** כל סוכן נוגע-מהות מפנה לכאן (דרך [HEARTBEAT.md](.claude/agents/HEARTBEAT.md)
|
||||||
|
> ובלוק "קרא לפני פעולה" שלו). אל תשכפל את הכללים בקובץ-סוכן — הפנה לכאן (G2 — בלי מסלולים מקבילים).
|
||||||
|
> זהו המקבילה התוכנית ל-INV-AG1 (קריאת-ספ): כמו שאינך פועל "מהזיכרון" לגבי התנהגות-המערכת, אינך
|
||||||
|
> מצטט פסיקה/חוק/הלכה/מספר "מהזיכרון".
|
||||||
|
|
||||||
|
## למה זה קיים
|
||||||
|
כלי-AI משפטיים מובילים (Lexis+ AI, Westlaw) **הוזים פסיקה ב-17%–33%** גם עם RAG — זו לא בעיה
|
||||||
|
שנעלמת מעצמה ("RAG ≠ hallucination-free"). בתחום מעין-שיפוטי, ציטוט-שווא של פסק-דין/סעיף/הלכה הוא
|
||||||
|
כשל קריטי הניתן לביקורת שיפוטית. חמש הטכניקות למטה הן הקונצנזוס המקצועי להפחתת הזיות, מותאם לתחום.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## חמש הטכניקות הקשיחות (חלות על כל סוכן נוגע-מהות)
|
||||||
|
|
||||||
|
**AH-1 · עיגון-מקור (grounding) — אפס ציטוט מהזיכרון.**
|
||||||
|
כל אזכור של פסק-דין / מספר-תיק / סעיף-חוק / הלכה / מקדם / "מתודה שמאית" / נתון כמותי חייב לבוא
|
||||||
|
ממקור מאומת: **תוצאת כלי-אחזור** (`search_precedent_library`, `search_internal_decisions`,
|
||||||
|
`search_case_documents`, `search_decisions`, `find_similar_cases`, `precedent_library_get`,
|
||||||
|
`halacha_review`) **או מסמך בתיק**. אם לא הרצת חיפוש/לא קראת מסמך — אין לך את הפריט. *(Stanford RegLab / Magesh et al., JELS 2025; Anthropic — ground in retrieved sources.)*
|
||||||
|
|
||||||
|
**AH-2 · Quote-or-retract.**
|
||||||
|
לכל אזכור-מקור צרף את הציטוט/מזהה המדויק שהמקור החזיר (`supporting_quote`/headnote/ציטוט מהמסמך).
|
||||||
|
**אין ציטוט מאשר → הסר את האזכור.** *(Anthropic — retract if no supporting quote; RAGAS faithfulness — כל טענה חייבת להיות נתמכת ב-context.)*
|
||||||
|
|
||||||
|
**AH-3 · Abstention — "לא יודע" עדיף על המצאה.**
|
||||||
|
לא נמצא מקור? כתוב מפורשות **"לא נמצא בקורפוס/בתיק — דורש אימות חיצוני"**. אסור לסגור פער בהשערה
|
||||||
|
שנכתבת כעובדה. *(Anthropic — give the model an out.)*
|
||||||
|
|
||||||
|
**AH-4 · תיוג-ודאות.** סמן כל טענה לא-טריוויאלית:
|
||||||
|
`[מאומת]` (מקור+ציטוט) · `[טעון-אימות]` (סביר/עולה מהמסמכים, אך לא אותר מקור מאשר) · `[ספקולציה]`
|
||||||
|
(השערה אנליטית — מותרת רק כשאלה/הסתייגות, לא כקביעה). *(NIST AI RMF GenAI Profile — explainability/קליברציה; RAGAS — atomic-claim grounding.)*
|
||||||
|
|
||||||
|
**AH-5 · Chain-of-Verification (CoVe) — מעבר-אימות לפני סיום.**
|
||||||
|
אחרי הטיוטה, פרק כל טענה עובדתית/אזכור לרשימה, ולכל אחת שאל "מאיזה מקור מאומת זה מגיע?".
|
||||||
|
כל מה שאין לו עוגן — **הסר או הורד ל-`[ספקולציה]`**. *(Chain-of-Verification — Dhuliawala et al., arXiv:2309.11495, 2023.)*
|
||||||
|
|
||||||
|
> **ההבחנה שמכריעה הכל — "פער" מותר, "המצאה" אסורה:**
|
||||||
|
> ✅ "אזכרתי את X — חיפשתי ולא מצאתי בקורפוס; דורש אימות." (פער לגיטימי) ·
|
||||||
|
> ❌ "הנה תקדים Y רלוונטי" כש-Y לא הגיע מכלי-אחזור. (המצאה)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## יישום לפי תפקיד
|
||||||
|
| סוכן | איך השער חל |
|
||||||
|
|------|-------------|
|
||||||
|
| **analyst / researcher** | מייצרי-מהות — עיגון-קורפוס מלא, log שאילתות + negative evidence, "מקור: כתבי טענות → דורש אימות". (כבר נהוג; כעת אחיד ומעוגן-מקור.) |
|
||||||
|
| **writer** | **צרכן read-only** של פלט-המנתח המעוגן. **אסור** להוסיף פסיקה/סעיף/הלכה שלא הגיעו מהמנתח/הקורפוס. ציטוט בהחלטה = רק מ-`supporting_quote` מאומת. |
|
||||||
|
| **qa** | **אוכף** את AH-1…AH-5 כשער-איכות: כל אזכור בטיוטה — האם מאומת-מקור? אם לא — `needs_revision`. |
|
||||||
|
| **ceo** | מנתב ומסכם — לא ממציא מקורות; אם מצטט, מצטט ממה שהסוכנים אימתו. |
|
||||||
|
| **proofreader** | תיקון-OCR בלבד — **אל "תתקן" לכיוון מונח משפטי סביר** (שם-תקדים/מספר-תיק/סכום): שמר את לשון-המקור; ספק → סמן, לא "תקן". |
|
||||||
|
| **exporter** | מכני (DOCX) — אפס מהות חדשה. |
|
||||||
|
| **hermes-curator** | הצעות בלבד (G10) — מעוגן-מקור, לא מזין שכבת-קול עם מהות (INV-LRN5). |
|
||||||
|
| **שטן מליץ (Gemini)** | מימוש-הייחוס המלא של השער (`legal-analyst-gemini-critique.md`) — לידים-לא-הכרעות ליו"ר (human-in-the-loop, NIST). |
|
||||||
|
|
||||||
|
## מקורות מקצועיים
|
||||||
|
1. Magesh, Surani, Dahl, Suzgun, Manning, Ho — *Hallucination-Free? Assessing the Reliability of Leading AI Legal Research Tools*, J. Empirical Legal Studies (2025), Stanford RegLab/HAI — שיעורי-הזיה 17–33% גם עם RAG.
|
||||||
|
2. Anthropic — *Reduce hallucinations* (docs.anthropic.com): allow "I don't know" · cite quotes/sources · retract-if-no-quote · chain-of-thought.
|
||||||
|
3. Dhuliawala et al. — *Chain-of-Verification Reduces Hallucination in LLMs*, arXiv:2309.11495 (2023).
|
||||||
|
4. Es et al. — *RAGAS: Automated Evaluation of RAG*, arXiv:2309.15217 — faithfulness = יחס הטענות הנתמכות-בקונטקסט.
|
||||||
|
5. NIST — *AI RMF: Generative AI Profile* (NIST-AI-600-1, 2024) — human-in-the-loop oversight ב-high-stakes.
|
||||||
@@ -327,6 +327,7 @@ Conclusion → Rule → Explanation → Application → Conclusion.
|
|||||||
- MUST NOT: ניתוח מעמיק (→ block-yod), הכרעה בין פרשנויות
|
- MUST NOT: ניתוח מעמיק (→ block-yod), הכרעה בין פרשנויות
|
||||||
- Dependencies: block-chet (מספור), block-vav (הגדרות תכניות)
|
- Dependencies: block-chet (מספור), block-vav (הגדרות תכניות)
|
||||||
- Condition: **אופציונלי** — רק כשיש מורכבות תכנונית (תכניות סותרות, תמ"א 38 + שימור, פרשנות)
|
- Condition: **אופציונלי** — רק כשיש מורכבות תכנונית (תכניות סותרות, תמ"א 38 + שימור, פרשנות)
|
||||||
|
- **סדר בתיקי רישוי (1xxx):** בלוק ט מופיע **לפני** בלוק ז (טענות) — הסדר ה→ו→ט→ז→ח→י→יא→יב. הקורא חייב להכיר את המסגרת הנורמטיבית (התכניות) לפני שהוא קורא את טענות הצדדים על פרשנותן. (לקח מ-1200-25 קרית ענבים; ראה legal-decision-lessons.md #41)
|
||||||
|
|
||||||
**Weight:**
|
**Weight:**
|
||||||
|
|
||||||
|
|||||||
70
docs/corpus-graph.md
Normal file
70
docs/corpus-graph.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# מפת הקורפוס — גרף ציטוטים אינטראקטיבי (`/graph`)
|
||||||
|
|
||||||
|
תצוגת‑רשת אינטראקטיבית של קורפוס הפסיקה, בסגנון Obsidian Graph View, **מוטמעת נייטיב ב‑web‑ui**. כל פריט הוא נקודה, קישורים הם קווים, וגודל הנקודה משקף חשיבות — כך שאפשר להתמקד בנושא ולראות מה קשור אליו.
|
||||||
|
|
||||||
|
## למה נייטיב ולא Obsidian (G2)
|
||||||
|
|
||||||
|
הרעיון המקורי היה לייצא את הקורפוס ל‑Obsidian vault. **נדחה** — vault הוא **עותק מקביל של הקורפוס שמתיישן**, בדיוק כשל‑השורש ש‑[G2](spec/00-constitution.md) (מקור‑אמת יחיד, ללא מסלול מקביל) בא לייבש. הגרף הנייטיב קורא את ה‑DB החי → **אפס drift**, ומתחבר לדפים הקיימים (`/precedents`, `/missing-precedents`, `/digests`).
|
||||||
|
|
||||||
|
**התובנה המאפשרת:** כל קשתות הגרף כבר היו קיימות בטבלאות — הגרף רק חושף אותן. הוא **projection קריא‑בלבד** (SELECT בלבד), ולכן אינו יכול לסטות מהמקור. הוא **אינו מסלול אחזור** ([03-retrieval](spec/03-retrieval.md)) — מחזיר טופולוגיה (nodes+edges+מטריקות), לא תוצאות חיפוש מדורגות.
|
||||||
|
|
||||||
|
## שכבות (כולן opt‑in דרך toggles, מלבד הבסיס)
|
||||||
|
|
||||||
|
| שכבה | נקודות | קשתות | מקור הדאטה |
|
||||||
|
|------|--------|-------|------------|
|
||||||
|
| **בסיס** | פסיקה (`cl:`) · נושא (`tag:`) · תחום (`pa:`) | `cites` · `same_chain` · `tagged` · `in_area` | `case_law`, `precedent_internal_citations`, `case_law_relations`, `subject_tags` |
|
||||||
|
| **הלכות** | הלכה (`hal:`) | `extracted_from` · `corroborates` · `equivalent` | `halachot`, `halacha_citation_corroboration`, `equivalent_halachot` |
|
||||||
|
| **חוסרי מחקר** | gap (`gap:`) — חלול/מקווקו | `cites` (פסיקה→gap) | `precedent_internal_citations` (cited_case_law_id IS NULL) + העשרה מ‑`missing_precedents` |
|
||||||
|
| **יומונים** | יומון (`dig:`) — טורקיז | `covers` (יומון→פסיקה/gap) | `digests` |
|
||||||
|
|
||||||
|
**גודל נקודה** = חשיבות: ציטוטים נכנסים (פסיקה), אזכורים (הלכה), מספר מצטטים (gap). **צבע** (color‑by, ברירת‑מחדל "סוג"): סוג · תחום · דרגת‑סמכות · **אשכול** (community) · עדכניות.
|
||||||
|
|
||||||
|
## אנליטיקה (Graph Analysis)
|
||||||
|
|
||||||
|
`metrics=true` מפעיל חישוב **in‑memory** (ללא DB) ב‑[`web/graph_metrics.py`](../web/graph_metrics.py) — pure, ללא תלויות (אין networkx):
|
||||||
|
- **PageRank** (power‑iteration) — השפעה גלובלית.
|
||||||
|
- **Betweenness** (Brandes) — "גשריות" (פסיקות שמחברות אשכולות).
|
||||||
|
- **Community** (label‑propagation דטרמיניסטי + fallback ל‑connected‑components) — אשכולות תמטיים.
|
||||||
|
|
||||||
|
מחושב על **תת‑גרף הפסיקות בלבד** (cites/same_chain) — קשתות hub/gap/digest/halacha מוחרגות. ב‑UI: בוררי "צביעה לפי" / "גודל לפי" + פאנל דירוג ("המשפיעות" / "גשרים").
|
||||||
|
|
||||||
|
## ניווט וחוויה
|
||||||
|
|
||||||
|
- **Deep‑link** `/graph?focus=cl:<id>` — לינק שיתופי; כפתור **"הצג בגרף"** בכל דף פסיקה.
|
||||||
|
- **Local graph** — לחיצה על נקודה → התמקדות בשכניה (BFS, סליידר עומק 1–3).
|
||||||
|
- **ייצוא PNG** · פאנל עשיר (headnote/summary) · מקרא נקודות+קשתות · סינון מטא‑דאטה (בית‑משפט/דרגה/יו״ר/מחוז/שנים).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
קריאה‑בלבד, `response_model` מפורש (UI2). מוגדר ב‑[`web/app.py`](../web/app.py) (~`/api/graph/*`), לוגיקה ב‑[`web/graph_api.py`](../web/graph_api.py):
|
||||||
|
|
||||||
|
| endpoint | תיאור |
|
||||||
|
|----------|-------|
|
||||||
|
| `GET /api/graph/corpus` | הגרף המלא. params: `node_types` (csv), `metrics`, `practice_area`/`source`/`court`/`precedent_level`/`chair`/`district`/`year_from`/`year_to`, `min_citations`, `q`, `limit` (cap 400, max 1500) |
|
||||||
|
| `GET /api/graph/node/{id}/neighborhood` | Local graph: צומת + שכנים בעומק 1–3 |
|
||||||
|
| `GET /api/graph/facets` | ערכי סינון ייחודיים (courts/levels/chairs/districts) |
|
||||||
|
|
||||||
|
## קבצים
|
||||||
|
|
||||||
|
- **Backend:** [`web/graph_api.py`](../web/graph_api.py) (הרכבת nodes/edges, helpers `_edges_and_hubs`/`_gap_nodes_and_edges`/`_digest_nodes_and_edges`/`_halacha_nodes_and_edges`) · [`web/graph_metrics.py`](../web/graph_metrics.py) (מטריקות) · endpoints ב‑[`web/app.py`](../web/app.py).
|
||||||
|
- **Frontend:** [`web-ui/src/app/graph/page.tsx`](../web-ui/src/app/graph/page.tsx) · [`web-ui/src/components/graph/`](../web-ui/src/components/graph/) (`graph-view` orchestrator · `graph-canvas` ציור react‑force‑graph‑2d · `graph-filter-panel` · `graph-node-panel`) · hooks ב‑[`web-ui/src/lib/api/graph.ts`](../web-ui/src/lib/api/graph.ts).
|
||||||
|
|
||||||
|
## איך מוסיפים שכבה חדשה
|
||||||
|
|
||||||
|
1. הוסף ערך ל‑`VALID_NODE_TYPES` ב‑`graph_api.py` (לא ל‑`DEFAULT_NODE_TYPES` אם רוצים שיהיה כבוי).
|
||||||
|
2. כתוב `_X_nodes_and_edges(conn, prec_ids)` — SELECT בלבד; חבר nodes לפסיקות שבתצוגה.
|
||||||
|
3. חבר בשתי פונקציות הבנייה (`build_corpus_graph` + `build_node_neighborhood`) מאחורי `if "X" in types`.
|
||||||
|
4. **dangling‑edge invariant:** כל קשת — שני קצותיה חייבים להיות nodes בתצוגה (סנן מול `prec_ids`/קבוצת ה‑ids).
|
||||||
|
5. Frontend: toggle ב‑`graph-filter-panel` · צבע/רינדור ב‑`graph-canvas` (`NODE_COLORS`/`colorForNode`/`linkColor`) · ענף בפאנל ב‑`graph-node-panel`.
|
||||||
|
6. אם גדל מודל התגובה — אחרי deploy: `cd web-ui && npm run api:types`.
|
||||||
|
|
||||||
|
## Invariants
|
||||||
|
|
||||||
|
- **G2** — projection קריא‑בלבד דרך `db.get_pool()`; אפס כתיבות; מטריקות in‑memory. ללא store מקביל.
|
||||||
|
- **G5** — כל פילטר server‑side, parameterized.
|
||||||
|
- **UI2** — `response_model` מפורש בכל endpoint; **UI4** — שגיאות UI מוצגות, לא נבלעות.
|
||||||
|
- **טופולוגיה ≠ אחזור** — מבנה הקורפוס, לא תוצאות חיפוש.
|
||||||
|
|
||||||
|
## היסטוריית מימוש
|
||||||
|
|
||||||
|
PR #113 (בסיס) · #118 (תיקון תוויות) · #126 (מטא‑דאטה) · #129 (אנליטיקה) · #131 (gaps) · #132 (יומונים) · #134 (ניווט) · #137 (הלכות) · #139 (api:types).
|
||||||
@@ -29,6 +29,38 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 0.5. שאלת סף — האם בכלל להכריע עכשיו?
|
||||||
|
|
||||||
|
לפני המעבר לעץ ההחלטה הראשי (§1), שאל:
|
||||||
|
|
||||||
|
> **האם יש פתח להחלטת ביניים שתחסוך הכרעה מלאה?**
|
||||||
|
|
||||||
|
הרוב המכריע של התיקים — לא. אבל בעררי שומה מכרעת (8xxx), קיים כלי שלישי שאינו "דחייה / קבלה / קבלה חלקית" — **החלטת ביניים שמחזירה שאלה ספציפית לשמאי המכריע**.
|
||||||
|
|
||||||
|
| תנאי | מתקיים? |
|
||||||
|
|-------|----------|
|
||||||
|
| השומה המכרעת מנומקת וסדורה ברמה הכללית (הצהרת אמון בגלר אפשרית) | □ |
|
||||||
|
| יש פרט עובדתי קונקרטי (לא טענה משפטית) שדורש מענה | □ |
|
||||||
|
| הפרט לא הוצג בצורה ישירה לשמאי בעת ההכרעה הראשונה (התחדד בדיון / בהשלמת מסמכים) | □ |
|
||||||
|
| דחייה ללא טיפול בפרט תיראה כעודף שמרנות; קבלה תיראה כעודף התערבות | □ |
|
||||||
|
| השמאי המכריע זמין ומסוגל להשיב | □ |
|
||||||
|
|
||||||
|
```
|
||||||
|
כל התנאים מתקיימים?
|
||||||
|
│
|
||||||
|
├─ כן → ⏸️ החלטת ביניים — חזרה לשמאי
|
||||||
|
│ → daphna-procedural-patterns.md §1
|
||||||
|
│ → דלג על §1-§7 של מסמך זה; חזור אליהם רק אחרי שיגיע מענה השמאי
|
||||||
|
│
|
||||||
|
└─ לא → המשך ל-§1 (עץ ההחלטה הראשי)
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **אזהרה:** התבנית הזו רלוונטית כמעט אך ורק ל-8xxx (היטל השבחה). ב-1xxx (רישוי) אין מקבילה — הוועדה היא הסמכות העליונה לעניין, אין שמאי מכריע להחזיר אליו.
|
||||||
|
|
||||||
|
⚠️ **אזהרת איכות:** דוגמת המקור (ערר 8174-24) הוא **דוגמת מבנה בלבד, לא דוגמת ניסוח**. ראה `daphna-procedural-patterns.md` לפרטי הסימנים שיש לתקן בעת חיקוי.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 1. עץ החלטה ראשי — בחירת סוג ארכיטקטורה
|
## 1. עץ החלטה ראשי — בחירת סוג ארכיטקטורה
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -517,5 +549,6 @@
|
|||||||
| `daphna-architecture-by-outcome.md` | §1 (עץ ראשי), §2 (משני), §4 (מודי פתיחה) |
|
| `daphna-architecture-by-outcome.md` | §1 (עץ ראשי), §2 (משני), §4 (מודי פתיחה) |
|
||||||
| `daphna-acceptance-architecture.md` | §1 (עץ ראשי — קבלה), §3.7 (פורמטי סיום) |
|
| `daphna-acceptance-architecture.md` | §1 (עץ ראשי — קבלה), §3.7 (פורמטי סיום) |
|
||||||
| `daphna-block-zayin-claims.md` | §3.3 (בלוק ז) |
|
| `daphna-block-zayin-claims.md` | §3.3 (בלוק ז) |
|
||||||
|
| `daphna-procedural-patterns.md` | §0.5 (שאלת סף — החלטת ביניים) |
|
||||||
|
|
||||||
ראה את הקבצים המקוריים לדוגמאות ולפירוט מלא. **המסמך הזה אינו תחליף** — הוא **מצביע** איזה סעיף ואיזה מסמך לקרוא לפי השאלה.
|
ראה את הקבצים המקוריים לדוגמאות ולפירוט מלא. **המסמך הזה אינו תחליף** — הוא **מצביע** איזה סעיף ואיזה מסמך לקרוא לפי השאלה.
|
||||||
|
|||||||
148
docs/daphna-procedural-patterns.md
Normal file
148
docs/daphna-procedural-patterns.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# קטלוג תבניות פרוצדורליות של דפנה
|
||||||
|
|
||||||
|
מסמך זה מקטלג **כלים פרוצדורליים** שדפנה משתמשת בהם **במקום** הכרעה מלאה — לא תבניות סגנון, אלא מהלכים שמתבצעים כשהתיק לא מבשיל להחלטה סופית.
|
||||||
|
|
||||||
|
⚠️ **הבחנה קריטית:**
|
||||||
|
- `daphna-architecture-by-outcome.md` + `daphna-acceptance-architecture.md` = **תבניות תוצאה** (דחייה / קבלה — דפנה הכריעה).
|
||||||
|
- מסמך זה = **תבניות אי-הכרעה / הכרעה דחויה** (דפנה בחרה לא להכריע עכשיו).
|
||||||
|
|
||||||
|
⚠️ **אזהרת קורפוס:**
|
||||||
|
החלטות תחת תבניות אלה הן בדרך כלל **outliers סגנוניים** — קצרות, חסרות, לפעמים רשלניות בניסוח. הן אינן מתאימות ל-voice corpus או ל-structure corpus. הן מתאימות **רק** למטרת זיהוי-תבנית בעתיד.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## תבנית 1: החלטת ביניים — חזרה לשמאי המכריע
|
||||||
|
|
||||||
|
### מתי להשתמש
|
||||||
|
|
||||||
|
כשמתקיימים **כל** התנאים הבאים:
|
||||||
|
|
||||||
|
1. **השומה המכרעת מנומקת וסדורה ברמה הכללית** — הצהרת אמון בגלר חייבת להישאר תקפה. אם השומה רעועה מיסודה, לא משתמשים בתבנית זו — הולכים לקבלה (תבנית E ב-acceptance).
|
||||||
|
2. **יש פרט עובדתי קונקרטי, לא טענה משפטית, שדורש מענה** — למשל: "12 מתוך 15 עסקאות ההשוואה הן בקיר משותף", "הנכס בבעלות יחיד ולא במושע", "השמאי לא חישב מקדם דחייה".
|
||||||
|
3. **הפרט הזה לא הוצג בצורה ישירה לשמאי בעת ההכרעה הראשונה** — או שהעורר חידד אותו בדיון / בהשלמת מסמכים.
|
||||||
|
4. **דחיית הערר בלעדיו תיראה כעודף שמרנות; קבלת הערר תיראה כעודף התערבות** — היא נקודת איזון שהחלטת ביניים פותרת.
|
||||||
|
5. **השמאי המכריע זמין ומסוגל להשיב להבהרה** (לא פרש, לא נפטר, לא נמצא בניגוד עניינים מתעורר).
|
||||||
|
|
||||||
|
### מה התבנית עושה
|
||||||
|
|
||||||
|
הוועדה **אינה מכריעה** את הערר. במקום זאת, היא:
|
||||||
|
- מציגה את הרקע (בלוק ה+ו)
|
||||||
|
- מציגה את ההליכים שכבר נערכו (בלוק ח)
|
||||||
|
- מצמצמת את בלוק ז לטענה המרכזית הרלוונטית (לא 47 טענות מקור)
|
||||||
|
- בבלוק י: מצטטת את גלר/אשקלוני, מצהירה על אמון בשומה, ואז מזהה פרט שדורש הבהרה
|
||||||
|
- בבלוק יא: פונה לשמאי המכריע עם **שאלה ספציפית וצרה אחת**
|
||||||
|
|
||||||
|
התוצאה היא **לא** "הערר נדחה" ו**לא** "הערר מתקבל" — אלא: **"לאחר קבלת הבהרת השמאי המכריע תתקבל החלטה סופית בערר"**.
|
||||||
|
|
||||||
|
### מבנה קנוני
|
||||||
|
|
||||||
|
| בלוק | תוכן | חריגה מהסטנדרט |
|
||||||
|
|------|-------|-----------------|
|
||||||
|
| ה | פתיחה — זיהוי הצדדים, השומה, הנכס, התכנית | כותרת: "החלטת ביניים" (לא "החלטה") |
|
||||||
|
| ו | רקע עובדתי — הנכס, היסטוריה קניינית, השומה, הסוגיות שהמכריע הכריע | סטנדרטי |
|
||||||
|
| ז | טענות הצדדים — **רק** הטענה הרלוונטית להבהרה, לא כל הטענות מהמקור | מקוצר באופן דרמטי |
|
||||||
|
| ח | הליכים — הדיון + השלמת מסמכים + תגובות נוספות | חשוב לתעד את ההליך שגרם להבהרת הטענה |
|
||||||
|
| י | דיון — ציטוט גלר/אשקלוני, הצהרת אמון, זיהוי הפרט, "למשנה זהירות" | קצר יחסית — אין הכרעה מלאה |
|
||||||
|
| יא | פנייה לשמאי המכריע + צמצום השאלה ("נדייק כי...") + הוראת מזכירות | תחליף לפסקת "סוף דבר" |
|
||||||
|
| יב | "לאחר קבלת הבהרת השמאי המכריע תתקבל החלטה סופית בערר" | חתימה רגילה (פה אחד + תאריך) |
|
||||||
|
|
||||||
|
### ביטויי מעבר קנוניים
|
||||||
|
|
||||||
|
| ביטוי | תפקיד |
|
||||||
|
|--------|--------|
|
||||||
|
| **"בנקודה זו יכולנו לסיים ולדחות את הערר אלא..."** | מסמן שהעמדה הראשונית היא דחייה; מכין דחייה סופית |
|
||||||
|
| **"לאחר בחינת טענות העורר במלואן בכל זאת לא נוכל להתעלם מכך כי..."** | מצביע על פרט עובדתי קונקרטי שדורש מענה |
|
||||||
|
| **"למשנה זהירות נכון יהיה לקבל הבהרה"** | מילת מפתח — מגן משפטי מפני טענת קלות דעת |
|
||||||
|
| **"אנו פונים לשמאי המכריע להבהרה במסגרתה יתבקש להבהיר..."** | הפעולה האופרטיבית |
|
||||||
|
| **"נדייק כי השמאי המכריע יבדוק את [X] בהתייחס ל[Y]"** | צמצום השאלה — שולל הבנה רחבה מדי |
|
||||||
|
| **"לשם מתן ההבהרה מזכירות הוועדה תעביר לשמאי המכריע את כתבי הטענות..."** | הוראה מינהלית |
|
||||||
|
| **"לאחר קבלת הבהרת השמאי המכריע תתקבל החלטה סופית בערר"** | סיום — לא הכרעה |
|
||||||
|
|
||||||
|
### תקדים-מקור
|
||||||
|
|
||||||
|
**ערר 8174-24 (גולדמן / בית מדרש)** — החלטה מ-11.05.2026.
|
||||||
|
|
||||||
|
⚠️ **אזהרה:** התקדים הזה הוא **דוגמת תבנית בלבד**, לא דוגמת איכות. בהחלטה זו זוהו 7 סימני "זריקה":
|
||||||
|
1. משפט run-on ב-§46 (3 חיבורים בלי פיסוק)
|
||||||
|
2. כפילות לקסיקלית ב-§40 ("כאמור סדורה")
|
||||||
|
3. בלוק ז מקוצץ — רק טענה אחת מתוך 47 מהמקור
|
||||||
|
4. סוגיות נוספות (טבצ'ניק/דייר מוגן; טענת סף) נזנחו לחלוטין
|
||||||
|
5. רטוריקת "במלואן" שלא מתיישבת עם הטקסט
|
||||||
|
6. תאריך מאוחר ביחס לתיק (שנה וחצי)
|
||||||
|
7. אזכור פסיקה מינימלי (רק גלר + אשקלוני)
|
||||||
|
|
||||||
|
לכן: **חיקוי המבנה** של תבנית זו לגיטימי; **חיקוי הניסוח** של 8174-24 — לא. בעת חיקוי, יש לתקן את הסימנים לעיל (במיוחד 1, 2, 5).
|
||||||
|
|
||||||
|
### מתי **לא** להשתמש
|
||||||
|
|
||||||
|
- כשהפגם בשומה הוא **משפטי-עקרוני** (שאלת פרשנות חוק/תכנית) — שם לוועדה יתרון (אשקלוני), ועליה להכריע בעצמה.
|
||||||
|
- כשהפגם הוא **מתודולוגי-יסודי** (השמאי בחר שיטה שגויה) — שם מקומה של תבנית E ב-acceptance ("השומה תושב לתיקון" + רשימת הוראות).
|
||||||
|
- כשעברו זמן רב מההכרעה הראשונה והשמאי כבר אינו זמין — אז ועדת הערר חייבת להכריע בעצמה.
|
||||||
|
- כשהעורר ויתר על ההליך או נמשך / נדחה.
|
||||||
|
|
||||||
|
### בדיקת איכות לפני שימוש (QA)
|
||||||
|
|
||||||
|
- [ ] שאלה ספציפית אחת, לא רשימה.
|
||||||
|
- [ ] הצהרת אמון בשמאי לפני זיהוי הפרט (סדר חשוב).
|
||||||
|
- [ ] "למשנה זהירות" מופיע — מגן משפטי.
|
||||||
|
- [ ] הבלוק ז כולל **רק** את הטענה הרלוונטית (לא ניסיון לסקור 47 טענות בקיצור).
|
||||||
|
- [ ] אין run-on של 3+ חיבורים בלי פיסוק.
|
||||||
|
- [ ] אין "במלואן" כשבפועל בחנת רק קטע.
|
||||||
|
- [ ] בלוק יב מסמן בבירור שזו לא הכרעה סופית.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## תבנית 2: (שמורה) — דחיית סף עם דיון "למען הסדר הטוב"
|
||||||
|
|
||||||
|
> טופלה ב-`daphna-architecture-by-outcome.md §3` (מוד F). מקושר כאן לשם שלמות הקטלוג.
|
||||||
|
|
||||||
|
זוהי תבנית קרובה אבל **אינה** החלטת ביניים — היא הכרעה מלאה (דחייה), עם דיון מהותי שאינו דרוש משפטית. ההבדל:
|
||||||
|
- **דחיית סף + מהות** = "אני דוחה, ולמרות זאת אדון לרווחת הצדדים"
|
||||||
|
- **החלטת ביניים** = "אני לא דוחה ולא מקבלת — שלחתי שאלה אחורה"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## תבנית 3: (עתידית) — החלטה מותנית
|
||||||
|
|
||||||
|
> מקום שמור לתבנית של "הערר מתקבל בכפוף ל-X תוך Y ימים, אחרת ייחשב כנדחה" — אם תזוהה כתבנית חוזרת בקורפוס.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## תיעוד תבניות חדשות
|
||||||
|
|
||||||
|
כאשר מזוהה החלטה שאינה מתיישבת עם תבניות תוצאה (`acceptance-architecture` / `architecture-by-outcome`):
|
||||||
|
1. בדוק אם היא נכנסת לקטלוג זה.
|
||||||
|
2. אם כן — עדכן כאן.
|
||||||
|
3. אם לא — שמור אותה כ-outlier (`case-tags.json` בתיק עצמו, `pattern_corpus: false`) עד שמתגלה תבנית שניה דומה.
|
||||||
|
4. **אסור** להוסיף החלטות outlier ל-voice corpus או ל-structure corpus — הן יזהמו את הקול של דפנה.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## מטא-data — תיוג מסמכי outlier
|
||||||
|
|
||||||
|
כל החלטה שנכנסת לתבנית פרוצדורלית (בניגוד לתבנית תוצאה) מסומנת בקובץ `case-tags.json` בתיק עצמו:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"case_number": "8174-24",
|
||||||
|
"document_role": "interim_decision",
|
||||||
|
"voice_corpus": false,
|
||||||
|
"structure_corpus": false,
|
||||||
|
"pattern_corpus": true,
|
||||||
|
"pattern_tag": "appraiser_clarification_request",
|
||||||
|
"quality_signal": "pragmatic_disposition",
|
||||||
|
"comments": "תבנית פרוצדורלית — חזרה לשמאי. לא ייצוג של החלטה מלאה."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **TODO עתידי:** כשנמיגרר את שדות אלו ל-DB schema (`documents.tags` או `cases.metadata`), ה-API יוכל לסנן אוטומטית בעת בניית קורפוס לאימון Hermes. כיום זה ידני.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## עדכון המסמך
|
||||||
|
|
||||||
|
עדכן את הקובץ הזה רק כאשר:
|
||||||
|
1. מזוהה החלטה שנייה (לפחות) עם אותה תבנית פרוצדורלית — מאשר שזו תבנית ולא אקראיות.
|
||||||
|
2. נוסף ביטוי-מעבר חדש בתבנית קיימת.
|
||||||
|
3. נוסף קריטריון "מתי להשתמש" / "מתי לא" — לרוב על בסיס feedback מהיו"ר.
|
||||||
|
|
||||||
@@ -181,11 +181,12 @@
|
|||||||
|
|
||||||
מבוסס על קריאת ה-10 החלטות + ההשוואה לטיוטות ה-AI:
|
מבוסס על קריאת ה-10 החלטות + ההשוואה לטיוטות ה-AI:
|
||||||
|
|
||||||
### 3.1 ❌ אסור: רשימה ממוספרת בתוך פסקה
|
### 3.1 ❌ אסור: רשימת-מיני ממוספרת בתוך פסקת-אנליזה (פיצול טיעון ל-`(1)...(2)...`)
|
||||||
**ב-0/33** מהחלטות הסופיות יש `(1) ... (2) ... (3) ...` בתוך פסקת אנליזה אחת.
|
**ב-0/33** מהחלטות הסופיות יש `(1) ... (2) ... (3) ...` המפצל טיעון בתוך פסקת אנליזה אחת. טענות וניתוח נכתבים כ**נרטיב רציף** עם ביטויי-מעבר ("עוד נטען", "באשר ל-", "יתרה מכך"), לא כרשימת-מיני.
|
||||||
**ב-3/3 טיוטות AI** שראיתי הופיעה רשימה ממוספרת — שהוסרה בעריכה.
|
|
||||||
|
|
||||||
⚠️ **הבחנה חשובה**: זה שונה ממספור פסקאות סדרתי (1, 2, 3 ... כאוטוט-של-פסקאות), שכן עד 2025 דפנה כן השתמשה במספור סדרתי (כמו פסיקה מסורתית). מ-2025-מאוחר זה נטוש; ההחלטות החדשות (1126-25, 1128-25, 1130-25, 1194-25) **ללא** מספור פסקאות. **המגמה החדשה** היא נרטיב רציף ללא מספור.
|
✅ **ההחלטה כן ממוספרת — תמיד.** פסקאות ההחלטה ממוספרות סדרתית (1, 2, 3 ... עד הסוף), כמקובל בפסיקה.
|
||||||
|
✅ **הכותב מקדים כל פסקת-החלטה ב-"N. " בתחילת שורה** (1., 2., 3. ... סדרתי). זהו ה-signal שמנוע-הייצוא מזהה (`docx_exporter._NUM_PREFIX_RE`): הוא **מסיר את הקידומת הידנית וממיר אותה למספור-אוטומטי אמיתי של Word** (`_ensure_decision_numbering` — רשימה עשרונית רציפה, RTL). כך ה-DOCX מתמספר מעצמו (מתעדכן בעריכה, copy/paste נקי ללא ספרות תועות).
|
||||||
|
⚠️ **המספר חייב להיות בתחילת השורה בלבד** — מספר *באמצע* פסקה הוא רשימת-מיני אסורה (§3.1 לעיל). (תיקון 2026-06-06: ההנחה ש"ההחלטות החדשות ללא מספור" הייתה ארטיפקט-חילוץ; וההנחה ש"הכותב לא יקליד מספרים" שגויה — הקידומת בתחילת-שורה היא ה-signal לייצוא, שמומר ל-auto-numbering.)
|
||||||
|
|
||||||
### 3.2 ⚠️ מותנה: כותרת משנה בלב בלוק י
|
### 3.2 ⚠️ מותנה: כותרת משנה בלב בלוק י
|
||||||
|
|
||||||
|
|||||||
37
docs/halacha-strict-rubric.md
Normal file
37
docs/halacha-strict-rubric.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# רובריקת "הכללים המחמירים" לחילוץ הלכות — להחלה על הלכות קיימות
|
||||||
|
|
||||||
|
אתה בודק רשימת הלכות שחולצו מפסק דין **אחד**, ומחליט לכל אחת: לשמור או לחתוך (ובאיזו עילה).
|
||||||
|
המטרה: שיישארו רק **עקרונות משפטיים אמיתיים, מובחנים, בני-הכללה ובני-הסתמכות** — לא ציטוטים, לא אמרות-אגב, לא יישומים ספציפיים-לתיק, לא כפילויות.
|
||||||
|
|
||||||
|
## עילות חיתוך (verdict)
|
||||||
|
|
||||||
|
1. **cut_duplicate** — ההלכה מבטאת את **אותו עיקרון משפטי** של הלכה אחרת באותו פסק, גם אם בניסוח שונה / ציטוט שונה.
|
||||||
|
- קבץ את כל המופעים של אותו עיקרון. שמור **נציג אחד** בלבד; סמן את השאר cut_duplicate.
|
||||||
|
- בחירת הנציג (canonical): עדיפות rule_type (binding > interpretive > procedural > obiter) → confidence גבוה → quote_verified=true → הניסוח המלא/הברור ביותר.
|
||||||
|
- דווח `cluster_canonical_index` = ה-halacha_index של הנציג שנשמר.
|
||||||
|
|
||||||
|
2. **cut_obiter** — אמרת-אגב שהערכאה **לא הכריעה בה**. סימנים: "אין צורך להכריע", "מבלי לקבוע מסמרות", "איני רואה לקבוע מסמרות", "לא ראינו לקבוע", "ניתן/יש להניח ... אך", "למעלה מן הצורך", "אגב אורחא", או הסתמכות על "לכאורה" כבסיס.
|
||||||
|
- מבחן Wambaugh: אם שלילת הכלל **לא** הייתה משנה את תוצאת הפסק → obiter.
|
||||||
|
|
||||||
|
3. **cut_application** — קביעה שתלויה ב**עובדות התיק הספציפי** ואינה בת-הכללה: שמות צדדים ("המשיבים", "המערערים", שם משפחה), "במקרה דנן/שבפנינו", סכומים/תאריכים/מספרים ספציפיים למחלוקת, יישום הכלל על המבנה/ההיתר הקונקרטי. זהו "ציטוט שטוב שיש" — המחשה, לא הלכה.
|
||||||
|
|
||||||
|
4. **cut_thin** — restatement דק: ה-rule_statement כמעט מעתיק את supporting_quote בלי הפשטה; **או** הכלל מנוסח כרקע/מוסכמה ("אין חולק כי...") ולא כהכרעה.
|
||||||
|
|
||||||
|
5. **cut_quote** — ה-supporting_quote קטוע באמצע משפט / חסר, או quote_verified=false וההלכה נשענת עליו.
|
||||||
|
|
||||||
|
6. **keep** — עיקרון משפטי אמיתי, מובחן, בר-הכללה, שהוכרע, עם ציטוט תומך שלם.
|
||||||
|
|
||||||
|
## כללי הכרעה — רמה אגרסיבית
|
||||||
|
המטרה: להשאיר רק את **גרעין העקרונות המובחנים**. עדיף תמציתי ומדויק על פני שלם-ומנופח.
|
||||||
|
|
||||||
|
- **cut_application אסרטיבי:** כל קביעה שנשענת על עובדות/צדדים/סכומים ספציפיים לתיק → cut_application, גם אם משתמעת ממנה הלכה. ההלכה המופשטת כבר אמורה להופיע בנפרד; היישום עצמו מיותר.
|
||||||
|
- **מיזוג facets חופפים (cut_duplicate מורחב):** אם שתי הלכות עונות על **אותה שאלה משפטית** גם אם מזווית/פן שונה — מזג לנציג הכללי/binding ביותר. דוגמאות למיזוג: עקרונות-משנה בתוך אותו נושא (סמכות ועדת הערר, מתחם שיקול-הדעת התכנוני, מיצוי הליכים, בטלות יחסית).
|
||||||
|
- **גבול המיזוג (שמור):** אל תמזג הלכות שעונות על **שאלות משפטיות שונות** (למשל "מועד 30 יום להגשת ערר" ≠ "עקרון מיצוי ההליכים"; "פרשנות תיקון 43" ≠ "סמכות לפי סיווג הבקשה"). מזג פנים-של-אותה-שאלה, לא בין-שאלות.
|
||||||
|
- **dedup מושגי הוא העיקרי:** רוב החיתוך מ-cut_duplicate. שים לב לעקרונות שחוזרים 3-5 פעמים בניסוחים שונים וגם ל-facets שחוזרים סביב אותו נושא.
|
||||||
|
- בספק בין keep ל-cut בקטגוריה מאבדת-מידע: ברמה זו **נטה לחתוך** (אך לעולם לא למזג שאלות-משפטיות שונות).
|
||||||
|
|
||||||
|
## פלט (JSON בלבד)
|
||||||
|
מערך, פריט לכל הלכה:
|
||||||
|
```json
|
||||||
|
[{"halacha_index": <int>, "verdict": "keep|cut_duplicate|cut_obiter|cut_application|cut_thin|cut_quote", "cluster_canonical_index": <int או null>, "reason": "<משפט אחד>"}]
|
||||||
|
```
|
||||||
@@ -446,3 +446,88 @@ The draft's biggest structural error was adding the "נבאר" doctrinal paragra
|
|||||||
- [ ] Update block-schema.md: block order for 1xxx cases (ט before ז)
|
- [ ] Update block-schema.md: block order for 1xxx cases (ט before ז)
|
||||||
- [ ] Update daphna-architecture-by-outcome.md: add "bridge planning" analysis for rejections
|
- [ ] Update daphna-architecture-by-outcome.md: add "bridge planning" analysis for rejections
|
||||||
- [ ] Update writer system prompt: mandatory "להלן מתוך" pattern
|
- [ ] Update writer system prompt: mandatory "להלן מתוך" pattern
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons from Weekly Feedback (May 31, 2026)
|
||||||
|
|
||||||
|
### Source
|
||||||
|
- Chair feedback summary for week ending 2026-05-31
|
||||||
|
- Case: 8126-03-25 (ערר על חבות בהיטל השבחה - יעקב עמיאל), entries from CMPA-62
|
||||||
|
|
||||||
|
### 34. Don't Manufacture Doubt About Clear Statutes
|
||||||
|
- **Lesson:** סעיף 19(ג)(2) לתוספת השלישית קובע באופן חד-משמעי כי תקופת המגורים היא ארבע שנים מגמר הבנייה — אסור להציע "פרשנות חלופית" של שנה אחת או להכניס שאלות פתוחות על נוסח חוק שהוא ברור; הצגת ספק מלאכותי בכלל ברור מערפלת את הניתוח ומחלישה את הכרעה.
|
||||||
|
- **Rule:** When a statutory provision is unambiguous on its face, the analysis must state it as the binding rule — not as one possible reading among others. Spurious interpretive doubt is a methodology failure, not a sign of intellectual humility.
|
||||||
|
|
||||||
|
### 35. Writer/QA Sync Gap — Two Sources of Truth
|
||||||
|
- **Problem:** legal-writer updates `decision_blocks` in the DB, but legal-qa reads from `drafts/decision.md` on disk. In CMPA-62 the writer reported updating block headers in DB but the file did not re-sync, causing QA-2 to fail on exactly the same issue twice.
|
||||||
|
- **Lesson:** Single source of truth is mandatory — either the writer must write to BOTH the DB and the decision.md file in one atomic step, or there must be an automatic `regenerate-draft` hook that runs after every block update so the file always reflects the latest DB state. Two unsynchronized sources will keep producing the same false-fail loop.
|
||||||
|
- **Owner:** Infrastructure task — not a writer/QA prompt fix.
|
||||||
|
- **✅ RESOLVED (GAP-88, 2026-06-06):** `block_writer._update_draft_file` is now an automatic regenerate hook called from `store_block` (every persist) **and** `renumber_all_blocks` — so `drafts/decision.md` always reflects `decision_blocks`. legal-qa already validates against the DB; both sides are now identical.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons from Chair Feedback Backlog (June 6, 2026)
|
||||||
|
|
||||||
|
### Source
|
||||||
|
- Consolidation of all unresolved `chair_feedback` entries (21 items) from cases
|
||||||
|
1033-25, 1130-25 (קרית יערים), 1200-25 (קרית ענבים), 8126-03-25, 8137-24.
|
||||||
|
- Folded manually as part of closing the feedback→agent-knowledge loop. Some
|
||||||
|
overlap with earlier sections (1200-25, weekly-feedback) is intentional — this
|
||||||
|
section is the authoritative roll-up of the backlog.
|
||||||
|
|
||||||
|
### 36. Planning Background Is Argumentation, Not "General Info" (1130-25)
|
||||||
|
- **Lesson:** רקע תכנוני בהחלטה אינו "מידע כללי" — הוא משרת סוגיה ספציפית ומנוסח כחלק מהארגומנטציה הסילוגיסטית. בניתוח שינוי נסיבות, היסטוריית התכנון מראש ועד הפסקה האחרונה חיונית: היא ההנחה התחתונה (עובדות) של הסילוגיזם, לא רקע ניטרלי.
|
||||||
|
- **Rule:** When the discussion turns on change-of-circumstances, write the full planning history (every plan, every amendment, with years) as the factual premise of the argument — not as background filler.
|
||||||
|
|
||||||
|
### 37. Detail the Content of Another Body's Actions When Cited as Evidence (1130-25)
|
||||||
|
- **Lesson:** כשעמדת ועדת הערר מסתמכת על פעולות של גוף אחר (ועדה מחוזית) כראיה לשינוי נסיבות — חובה לפרט את **תוכן** אותן פעולות (מה התבקש, מה אושר, אילו תנאים), לא רק לציין שהתרחשו.
|
||||||
|
- **Rule:** "The district committee approved similar plans in 2023 and 2024" is insufficient — specify what each plan requested and what was approved, so the reader can judge whether it's truly comparable.
|
||||||
|
|
||||||
|
### 38. Map/GIS Images Are Visual Evidence, Not Decoration (1130-25)
|
||||||
|
- **Lesson:** תמונות מפה/GIS בהחלטות תכנון ובניה הן חלק מהארגומנטציה — ראיה ויזואלית שמשלימה את הניתוח הטקסטואלי (מיקום חלקות, סמיכות גיאוגרפית, כבישים ותשתיות מתוכננות). הכותב יסמן placeholder `[תמונה: <תיאור>]` והיו"ר תכניס בעריכה הסופית.
|
||||||
|
- **Rule:** When geographic proximity or planned infrastructure matters to the analysis, insert an image placeholder in the discussion — it is evidence, treated like any other.
|
||||||
|
|
||||||
|
### 39. Address Parallel Appeals in the Same Area Explicitly (1130-25)
|
||||||
|
- **Lesson:** כשיש עררים מקבילים באותו אזור (למשל ערר 1194-25 בחלקה סמוכה) — ההחלטה צריכה להתייחס לכך במפורש, לציין את ההבחנה בין התיקים, ולהבהיר שכל בקשה נבחנת לגופה. "אפקט דומינו" שהתממש הוא עובדה תכנונית, לא חשש תיאורטי.
|
||||||
|
- **Rule:** Name the parallel appeal, state how the present case differs, and reaffirm case-by-case examination.
|
||||||
|
|
||||||
|
### 40. The Chair's Text Skeleton Is a Structural Directive (1130-25)
|
||||||
|
- **Lesson:** שלד טקסט שהיו"ר מספקת (זרימה נרטיבית + נקודות מפתח ממוספרות) הוא הנחיה מבנית מחייבת — הכותב צריך לעקוב אחרי המבנה ולמלא בתוכן מלא, לא לנסח מחדש את הסדר. ה-placeholder "..." מסמן מעבר שצריך להשלים.
|
||||||
|
- **Rule:** When `get_chair_directions` / analysis-and-research.md contains a narrative skeleton, follow it step-by-step; treat each numbered point as a required paragraph.
|
||||||
|
|
||||||
|
### 41. Block Order in Licensing (1xxx): ט Before ז (1200-25)
|
||||||
|
- **Lesson:** בתיקי רישוי (1xxx) — בלוק ט (תכניות חלות) צריך להופיע **לפני** בלוק ז (טענות), לא אחריו. הסדר הנכון: ה→ו→ט→ז→ח→י→יא→יב. הרציונל: הקורא צריך להכיר את המסגרת הנורמטיבית (התכניות) לפני שהוא קורא את טענות הצדדים על פרשנותן.
|
||||||
|
- **Rule:** For 1xxx cases, emit applicable plans (ט) before the parties' claims (ז). See `docs/block-schema.md`.
|
||||||
|
|
||||||
|
### 42. "להלן מתוך [מסמך]:" Is Mandatory (1200-25)
|
||||||
|
- **Lesson:** תבנית "להלן מתוך [שם המסמך]:" היא חובה בכל מקום שמתייחסים למסמך מקור — placeholder להכנסת ציטוט ישיר/תמונה. דוגמאות: "להלן מתוך הוראות התכנית:", "להלן מתוך פרוטוקול הדיון:", "להלן מתוך הבקשה להיתר:". See `skills/decision/SKILL.md`.
|
||||||
|
- **Rule:** Every reference to a source document gets a "להלן מתוך [exact doc name]:" placeholder.
|
||||||
|
|
||||||
|
### 43. Block ו Must Contain a Full Timeline (1200-25)
|
||||||
|
- **Lesson:** בלוק ו חייב לספר את "הסיפור" המלא של התיק עם ציר זמן: מתי הוגשה הבקשה, מתי פורסמה, כמה התנגדויות הוגשו, מתי התקיימו דיונים בוועדה מקומית ומה הוחלט בכל אחד, ומתי הוגש הערר. כל ישיבה עם תאריך + תוצאה.
|
||||||
|
- **Rule:** Block ו is a dated narrative, not a one-liner.
|
||||||
|
|
||||||
|
### 44. Point-Plan vs. Comprehensive-Plan Harmony (1200-25)
|
||||||
|
- **Lesson:** בתיק רישוי שבו המבקש מקדם גם תכנית — חובה לנתח האם התכנית הנקודתית תואמת את התכנית הכוללנית. אם יש סתירה (למשל השוואה כמותית: הכוללנית מקצה 4,404 מ"ר לכל המסחר ביישוב, מול 1,425 מ"ר בבקשה אחת) — זה **מחזק** את הדחייה. מסגרת "גשר תכנוני": שימוש חורג יכול לגשר על פער תכנוני רק אם התכנית המקודמת תואמת את הכיוון הכולל (כוכבה תורן).
|
||||||
|
- **Rule:** Check `search_case_documents` for pending plans; compare point-plan to comprehensive-plan; a contradiction strengthens rejection.
|
||||||
|
|
||||||
|
### 45. Don't Skip the "Non-Profit Institution" Threshold in s.19(ב)(4) (8137-24)
|
||||||
|
- **Lesson:** כשמסמכי יסוד של מוסד מוגשים, אין לדלג על תנאי "המוסד שאין עיסוקו לשם קבלת רווחים" בס' 19(ב)(4) — זהו התנאי **הראשון** ויש לבססו על ציטוט פסקאות ספציפיות מתעודות היסוד (חוקה, תקנון, הסכמים), לא על רישום מלכ"ר בלבד. רישום ≠ ראיה חלוטה (תקדים הלפרן, ערר מרכז 8013-03-21). יש לתחם: הפרק מכריע בתנאי הזהות+אי-רווח בלבד; תנאי השימוש לפרק נפרד.
|
||||||
|
- **Rule:** In betterment-levy exemption cases, the non-profit-identity condition is condition #1 — prove it via specific cited paragraphs of the foundational documents, never via registration status alone.
|
||||||
|
|
||||||
|
### 46. Distinguish Appeal-Letter Claims from Correspondence Claims (1033-25)
|
||||||
|
- **Lesson:** בדיקת כיסוי הטענות (claims_coverage) צריכה להבחין בין טענות שעלו בכתב הערר (חובה לענות) לבין טענות שעלו בתכתובות/תגובות בין הצדדים (לא חייבות מענה עצמאי, במיוחד כשהערר מתקבל במלואו וההחלטה בוטלה). סימון טענות-תכתובת כ"לא נענו" הוא false-positive.
|
||||||
|
- **Rule:** Only claims raised in the appeal letter itself require a dedicated answer; correspondence-only claims do not, especially when the appeal is fully accepted. (Also tracked as a system task — the automated check needs this distinction.)
|
||||||
|
|
||||||
|
### System/Infrastructure Items (NOT writer lessons)
|
||||||
|
These two entries are technical gaps, not decision-writing lessons — captured in TaskMaster, not consumed by the writer:
|
||||||
|
- **claims_coverage check** (1033-25): the automated coverage check must distinguish appeal-letter claims from correspondence claims (see #46).
|
||||||
|
- **DB↔file sync gap** (8126-03-25): see #35 above — writer writes to `decision_blocks` (DB) while QA reads `drafts/decision.md` (disk). Infrastructure fix.
|
||||||
|
|
||||||
|
### Note on case-specific issue-ordering entries
|
||||||
|
Two 1200-25 entries recorded a case-specific issue order (threshold → plan interpretation
|
||||||
|
→ ancillary-vs-primary → significant-deviation → comprehensive-plan → grouped: reasoning,
|
||||||
|
traffic) with no generalizable rule. They are case artifacts, captured in that case's
|
||||||
|
analysis-and-research.md — no general lesson folded.
|
||||||
|
|
||||||
|
|||||||
227
docs/methodology/extension-request-betterment_levy.md
Normal file
227
docs/methodology/extension-request-betterment_levy.md
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
# מתודולוגיה — בל"מ בהיטל השבחה (8xxx)
|
||||||
|
|
||||||
|
**appeal_subtype:** `extension_request_betterment_levy`
|
||||||
|
**מסלול:** סעיף 14 לתוספת ג' לחוק התכנון והבנייה, התשכ"ה-1965
|
||||||
|
**מועד סטטוטורי:** **45 ימים** (להבדיל מ-30 ימים ברישוי) מיום קבלת
|
||||||
|
דרישת תשלום היטל ההשבחה (סעיף 14(א) לתוספת ג')
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## א. מבוא — ייחודיות בל"מ בהיטל השבחה
|
||||||
|
|
||||||
|
בל"מ במסלול היטל השבחה שונה משמעותית מבל"מ ברישוי בכמה ממדים:
|
||||||
|
|
||||||
|
| ממד | בל"מ ברישוי | בל"מ בהיטל השבחה |
|
||||||
|
|------|--------------|-------------------|
|
||||||
|
| מועד סטטוטורי | 30 ימים | **45 ימים** |
|
||||||
|
| סעיף בחוק | 152 | סעיף 14 לתוספת ג' |
|
||||||
|
| בעלי דין | רחב — כל בעל זכות גובלת/קרובה | **צר — רק החייב בהיטל** |
|
||||||
|
| מהות הסעד | ביטול היתר / שינוי תנאים | תיקון שומה / ביטול חיוב |
|
||||||
|
| טון | פעמים אנושי (תושב, סביבה) | קר ומקצועי (פיננסי/שמאי) |
|
||||||
|
| הסתמכות נדרשת | של היזם | של הרשות (חלוקת הכנסות) |
|
||||||
|
|
||||||
|
הייחוד הקרדינלי: **בל"מ בהיטל השבחה דורש הוכחת טעות שמאית או בדין** —
|
||||||
|
לא רק "טעם סביר" כמו ברישוי. הסיבה: שומת היטל ההשבחה היא מעשה מנהלי
|
||||||
|
שקיבל תוקף, וכספים שולמו / נדרשו, ולעיתים גם חולקו. שינוי שומה דורש
|
||||||
|
עילה מהותית.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ב. מסגרת נורמטיבית
|
||||||
|
|
||||||
|
### שכבה א — חקיקה ראשית
|
||||||
|
|
||||||
|
**סעיף 14(א) לתוספת ג' לחוק התכנון והבנייה:**
|
||||||
|
> "בעל המקרקעין החייב בהיטל השבחה ... רשאי להגיש ערר על השומה לוועדת הערר
|
||||||
|
> לפיצויים ולהיטל השבחה ... בתוך 45 ימים מיום שהומצאה לו השומה"
|
||||||
|
|
||||||
|
המחוקק קבע מועד ארוך יותר (45 לעומת 30) מתוך הכרה במורכבות הסוגיה השמאית —
|
||||||
|
הצורך לקבל חוו"ד שמאית, להתייעץ עם עו"ד מומחה למיסוי מקרקעין, ולבחון את
|
||||||
|
חישובי השומה.
|
||||||
|
|
||||||
|
### שכבה ב — עליון
|
||||||
|
|
||||||
|
**רע"א 7669/96 עיריית נהריה נ' קמינסקי (פ"ד נב(1) 214):**
|
||||||
|
ביסוס עקרוני של "סופיות שומה" — שינוי שומה לאחר חלוף המועד הסטטוטורי
|
||||||
|
אינו עומד על ערעור "טעם סביר" בלבד; נדרש אינטרס ציבורי מובהק או טעות
|
||||||
|
שמאית מהותית.
|
||||||
|
|
||||||
|
**עע"מ 1832/14 הרשות לפיתוח ירושלים נ' מנהל מס שבח:**
|
||||||
|
היטל השבחה — תשלום הכפוף לסופיות שומה; קביעות שמאי בדבר ערך המקרקעין לפני
|
||||||
|
ואחרי האירוע התכנוני הן עובדתיות-מקצועיות. שינוי דורש הצדקה חזקה.
|
||||||
|
|
||||||
|
### שכבה ג — ועדות ערר לפיצויים ולהיטל השבחה
|
||||||
|
|
||||||
|
(להוסיף תקדימים ספציפיים מקורפוס דפנה תמיר בהיטל השבחה. הקורפוס הקיים
|
||||||
|
כולל את עררי 8xxx — לחפש דפוס "בל\"מ" או "הארכת מועד" בתוכם.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ג. תבחיני בל"מ בהיטל השבחה — חמישה תבחינים
|
||||||
|
|
||||||
|
| # | תבחין | אופי | משקל |
|
||||||
|
|---|--------|------|------|
|
||||||
|
| א | **טעות שמאית או בדין** | **תנאי סף עצמאי — ייחודי להיטל השבחה** | קריטי |
|
||||||
|
| ב | טעם סביר לאיחור | מקדים — בדומה לרישוי, אך מחמיר | גבוה |
|
||||||
|
| ג | אורך השיהוי | כמותי | גבוה |
|
||||||
|
| ד | הסתמכות הרשות (חלוקת כספים) | כמותי | גבוה |
|
||||||
|
| ה | סיכויי הערר המהותי (לכאורה) | מהותי | בינוני |
|
||||||
|
|
||||||
|
תבחין "אינטרס ציבורי" לא מופיע כתבחין עצמאי כאן — בהיטל השבחה האינטרס
|
||||||
|
הציבורי נטוע בתוך הסתמכות הרשות (תבחין ד).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ד. תבחין א — טעות שמאית או טעות בדין
|
||||||
|
|
||||||
|
### מה זו "טעות שמאית"?
|
||||||
|
לא כל מחלוקת על שווי = טעות. נדרש להוכיח אחד מאלה:
|
||||||
|
|
||||||
|
1. **טעות חישובית גלויה** — סכום שגוי, פעולה אריתמטית שגויה.
|
||||||
|
2. **שיטה שמאית פסולה** — שימוש בגישה לא מקובלת (לדוגמה: היוון לפי שיעור
|
||||||
|
שאינו ריאלי, השוואה לעסקאות שאינן מקבילות).
|
||||||
|
3. **התעלמות מנכסים דומים** — עיוורון לנתונים שהיו צריכים להילקח בחשבון.
|
||||||
|
4. **שגיאה במספרי שטח / זכויות / תכנית** — אי-תאמה לנסח / לתב"ע.
|
||||||
|
|
||||||
|
### מה זו "טעות בדין"?
|
||||||
|
שגיאה משפטית בעצם החיוב:
|
||||||
|
- **חיוב על נכס שאינו "מקרקעין" לעניין החוק** (זכויות חוזיות גרידא).
|
||||||
|
- **חיוב בגין השבחה שאינה נכנסת להגדרת "השבחה" בחוק** (לדוגמה: השבחה
|
||||||
|
שנוצרה לפני התקופה הקובעת; השבחה מכוח תכנית שאינה תכנית מתאר).
|
||||||
|
- **חיוב לפני התגבשות העילה** — דרישה לפני מימוש בהיתר או מכר.
|
||||||
|
|
||||||
|
### הוכחה דרושה
|
||||||
|
- **חוות דעת שמאית חתומה** מאת שמאי מקרקעין מוסמך, עם נתוני השוואה.
|
||||||
|
- **תיעוד הליך השומה המקורי** — אילו נתונים נלקחו? אילו לא?
|
||||||
|
- **חישוב חלופי מנומק** — לא רק "אני חולק", אלא "הנה החישוב הנכון".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ה. תבחין ב — טעם סביר לאיחור
|
||||||
|
|
||||||
|
### העקרון
|
||||||
|
בדומה לבל"מ ברישוי, אך **קפדן יותר**:
|
||||||
|
- מועד 45 ימים נחשב "מועד ארוך" — קשה יותר להצדיק החמצתו.
|
||||||
|
- החייב לרוב מקבל את השומה לידיו אישית — אין סוגיית "פרסום באתר".
|
||||||
|
- ערב פניה לעו"ד / שמאי הוא צעד צפוי וסטנדרטי.
|
||||||
|
|
||||||
|
### מצבי "טעם סביר" אופייניים
|
||||||
|
| מצב | קבילות |
|
||||||
|
|------|---------|
|
||||||
|
| מחלת המבקש (מתועדת רפואית) | קבילה |
|
||||||
|
| המצאה פגומה (לא לכתובת הנכונה) | קבילה — אך נטל הוכחה כבד |
|
||||||
|
| תקופה ארוכה של בירורים מקצועיים | חלשה — לוחות זמנים אינם מוקפאים |
|
||||||
|
| המתנה לעמדת שמאי לפני הגשת ערר | חלשה — אפשר להגיש ולתקן |
|
||||||
|
| התכתבות עם הרשות בניסיון פשרה | חלשה — לא מקפיאה מועד |
|
||||||
|
|
||||||
|
### דרישת התצהיר
|
||||||
|
**חובה** תצהיר מפורט — תאריכים, אנשי קשר, מסמכי תמיכה. ללא תצהיר —
|
||||||
|
הטענה ריקה משפטית.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ו. תבחין ג — אורך השיהוי
|
||||||
|
|
||||||
|
### חישוב
|
||||||
|
| תאריך | אירוע | שיהוי מצטבר |
|
||||||
|
|--------|--------|--------------|
|
||||||
|
| יום 0 | המצאת השומה | 0 |
|
||||||
|
| יום 45 | תום המועד הסטטוטורי | תום המועד |
|
||||||
|
| יום X | הגשת הבל"מ | X-45 ימים מעבר למועד |
|
||||||
|
|
||||||
|
### עקרון מנחה
|
||||||
|
- שיהוי של עד 30 ימים מעבר למועד (סה"כ 75 ימים מיום ההמצאה) — מקבל
|
||||||
|
התייחסות עניינית אם יש טעם סביר.
|
||||||
|
- שיהוי של מעל 90 ימים מעבר למועד — נחשב חמור; דורש הוכחה חזקה במיוחד.
|
||||||
|
- שיהוי של מעל שנה — לרוב חוסם אלא אם מדובר בטעות חישובית גלויה.
|
||||||
|
|
||||||
|
### השפעת השיהוי על הסתמכות הרשות
|
||||||
|
ככל שהזמן עובר — הסיכוי שהרשות חילקה את הכספים גבוה יותר. דרישה להחזר
|
||||||
|
שנים לאחר התשלום פוגעת בהסתמכות הרשות בצורה מובהקת.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ז. תבחין ד — הסתמכות הרשות (חלוקת הכנסות)
|
||||||
|
|
||||||
|
### ייחודיות לעומת בל"מ ברישוי
|
||||||
|
ברישוי — ההסתמכות היא של היזם הפרטי. בהיטל השבחה — ההסתמכות היא של
|
||||||
|
**הרשות הציבורית**: הכספים מועברים לקרן השבחה, מתוכננים לפרויקטים
|
||||||
|
ציבוריים, ולעיתים אף חולקו או הוצאו.
|
||||||
|
|
||||||
|
### טבלת בדיקה
|
||||||
|
| שלב | מצב הכספים | השפעה על הבל"מ |
|
||||||
|
|------|------------|-----------------|
|
||||||
|
| לפני תשלום | החייב לא שילם | קלה — אין הסתמכות הרשות |
|
||||||
|
| לאחר תשלום, לפני חלוקה | בקופת הוועדה / קרן | בינונית |
|
||||||
|
| לאחר חלוקה לרשויות | חולק לעירייה, יזם, וכו' | משמעותית |
|
||||||
|
| לאחר ביצוע פרויקטים | כספים הוצאו | מוחשית, קשה להפיך |
|
||||||
|
|
||||||
|
### עיקרון
|
||||||
|
**ככל שהכספים "התרחקו" מהקופה — דרישות הוכחת הטעות מחמירות.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ח. תבחין ה — סיכויי הערר המהותי (לכאורה)
|
||||||
|
|
||||||
|
### הבהרה מתודית
|
||||||
|
בשלב בל"מ — בוחנים סיכויי הערר רק כדי לקבוע האם יש סיבה לפתוח את הדלת.
|
||||||
|
הקריטריון: **האם יש "טענה לכאורה" המבוססת על תיעוד מקצועי?**
|
||||||
|
|
||||||
|
### סוגי טענות אופייניים
|
||||||
|
- חישוב שגוי של "המצב הקודם" / "המצב החדש"
|
||||||
|
- שיטת שיערוך פסולה (השוואה / הפרשי הון / היוון)
|
||||||
|
- התעלמות מ"זכויות מותנות" שטרם התגבשו
|
||||||
|
- חיוב כפול (הון / הכנסה / שבח)
|
||||||
|
- אי-התאמה למיקום, שימוש, או שטח
|
||||||
|
|
||||||
|
### מה לא נספר כ"סיכויי הליך"
|
||||||
|
- "אני לא מסכים לסכום" — בלי חוו"ד נגדית מבוססת.
|
||||||
|
- טענות כלליות על "המצב הכלכלי" של המבקש.
|
||||||
|
- טענות על "תקדים" שלא הוכרע בערכאה גבוהה יותר.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ט. טבלת התאמה לעובדות (placeholder לכל תיק)
|
||||||
|
|
||||||
|
| תבחין | עובדה במקרה הנוכחי | כיוון |
|
||||||
|
|--------|---------------------|-------|
|
||||||
|
| א. טעות שמאית/בדין | [סוג הטעות הנטענת + תיעוד] | [חוסם / מאפשר] |
|
||||||
|
| ב. טעם סביר | [מועד המצאה, פעולות, תצהיר] | [תומך / מחליש] |
|
||||||
|
| ג. אורך השיהוי | [X ימים מעבר ל-45] | [קל / בינוני / חמור] |
|
||||||
|
| ד. הסתמכות הרשות | [מצב הכספים: בקופה / חולק / הוצא] | [קל / משמעותי / מוחשי] |
|
||||||
|
| ה. סיכויי הליך | [חוו"ד שמאית? חישוב חלופי?] | [לכאורה / ספקולטיבי] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## י. סעיף מסקנה — מבנה אופייני
|
||||||
|
|
||||||
|
המבנה האופייני בבל"מ-היטל-השבחה הוא **קר ומקצועי** — מינימום רגש,
|
||||||
|
מקסימום שמאות:
|
||||||
|
|
||||||
|
1. **קביעת מצב השומה.** "השומה הומצאה ביום X. הבל"מ הוגשה ביום Y."
|
||||||
|
2. **תבחין א (טעות שמאית).** "המבקש טוען לטעות בX. בחינת המסמכים מעלה..."
|
||||||
|
3. **אם טעות לא הוכחה — דחייה.** "בהיעדר טעות שמאית או בדין, אין יסוד
|
||||||
|
לסטות ממועד הקבוע בחוק."
|
||||||
|
4. **אם טעות הוכחה — מעבר לתבחינים ב-ה.**
|
||||||
|
5. **מאזן.** "לאור איזון התבחינים..."
|
||||||
|
6. **הכרעה.** דחייה / קבלה / החזרה לשמאי הוועדה לבחינה.
|
||||||
|
|
||||||
|
### לשון אופיינית לדחייה
|
||||||
|
> "הבל"מ הוגשה X ימים לאחר תום המועד הסטטוטורי. המבקש לא הצביע על טעות
|
||||||
|
> שמאית או בדין; הטענות הן בגדר מחלוקת על שיקול דעת מקצועי, שאינה מצדיקה
|
||||||
|
> פתיחת שומה שקיבלה תוקף. לאור אלה, ובהינתן שהכספים שולמו וחולקו, הבל"מ
|
||||||
|
> נדחית."
|
||||||
|
|
||||||
|
### לשון אופיינית לקבלה (חריגה)
|
||||||
|
> "המבקש הצביע על טעות חישובית במספר זכויות התכנון שנלקחו בחשבון. הטעות
|
||||||
|
> מהותית ומשפיעה על השומה. בנסיבות אלה, ועל אף השיהוי, יש מקום לפתוח את
|
||||||
|
> השומה לדיון בערר עצמו."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## יא. הפניות חוצות
|
||||||
|
|
||||||
|
- ראה גם: `docs/methodology/extension-request-building_permit.md` (סעיף 152, 30 ימים)
|
||||||
|
- ראה גם: `docs/methodology/extension-request-compensation.md` (סעיף 198(ד), 30 ימים)
|
||||||
|
- ראה גם: `docs/block-schema.md` — מבנה 12 הבלוקים
|
||||||
|
- ראה גם: `skills/decision/SKILL.md` — מדריך סגנון של דפנה
|
||||||
252
docs/methodology/extension-request-building_permit.md
Normal file
252
docs/methodology/extension-request-building_permit.md
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
# מתודולוגיה — בל"מ ברישוי ובנייה (1xxx)
|
||||||
|
|
||||||
|
**appeal_subtype:** `extension_request_building_permit`
|
||||||
|
**מסלול:** סעיף 152(א) לחוק התכנון והבנייה, התשכ"ה-1965
|
||||||
|
**מועד סטטוטורי:** 30 ימים מיום המצאת ההחלטה (סעיף 152(ב))
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## א. מבוא — מהותו של בל"מ ברישוי
|
||||||
|
|
||||||
|
בל"מ ("בקשה להארכת מועד") הוא הליך מקדמי שהמבקש להגיש ערר על החלטת ועדה מקומית
|
||||||
|
לאחר חלוף 30 הימים נדרש לעבור בו לפני שיוכל לפתוח בערר עצמו. הוועדה נדרשת
|
||||||
|
לאזן בין שני אינטרסים נוגדים:
|
||||||
|
|
||||||
|
- **זכות הגישה לערכאות** — שכל בעל זכות עמידה יוכל להעמיד את החלטת הוועדה
|
||||||
|
המקומית במבחן שיפוטי, במיוחד כאשר ההחלטה נטענת כפסולה.
|
||||||
|
- **סופיות החלטות מנהליות + הסתמכות** — היזם זכאי לפעול לפי ההיתר שניתן, להשקיע
|
||||||
|
כספים, להתחיל בעבודות, ולא לחיות בחשש מתמיד שמא ההיתר ייתקף שנים לאחר אישורו.
|
||||||
|
|
||||||
|
לעומת בל"מ בהיטל השבחה (סעיף 14 לתוספת ג', 45 ימים) ובל"מ בפיצויים (סעיף 198(ד),
|
||||||
|
30 ימים אך עם סף קפדני יותר), בל"מ ברישוי משלב טון אנושי יחסית — ההסתמכות מוחשית
|
||||||
|
(חפירה, פינוי שוכרים) והאינטרסים הציבוריים (מיגון, חיזוק) ממשיים.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ב. מסגרת נורמטיבית — שלוש שכבות
|
||||||
|
|
||||||
|
### שכבה א — עליון: בר"מ 2340/02 הוועדה המקומית רמת השרון נ' אגא וכט, פ"ד נז(3) 385 (2003)
|
||||||
|
|
||||||
|
הכיר בסמכותה של ועדת הערר להאריך את המועד, בנסיבות חריגות, וקבע את הבחינה
|
||||||
|
הדו-שלבית:
|
||||||
|
1. **תנאי סף:** טעם סביר לאיחור.
|
||||||
|
2. **שיקול כולל:** השוואה בין נזקי המבקש לבין הסתמכות הצד שכנגד; היקף השיהוי;
|
||||||
|
סיכויי ההליך; אינטרס ציבורי.
|
||||||
|
|
||||||
|
### שכבה ב — עליון: עע"מ 317/10 שפר נ' סקאל יניב (נבו 23.8.2012)
|
||||||
|
|
||||||
|
הלכה מחייבת: מניין 30 הימים מתחיל **מיום הידיעה בפועל**, לא מיום הפרסום הפורמלי.
|
||||||
|
המשמעות: גם איחור-לכאורה של חודשים יכול להיות לגיטימי אם המבקש לא ידע על ההחלטה
|
||||||
|
בזמן אמת.
|
||||||
|
|
||||||
|
> "מתנגד להיתר שניתן, אשר שטח התנגדותו בפני הועדה המקומית וזו נדחתה, או שידע
|
||||||
|
> על מתן ההיתר, צריך יהיה להגיש את הערר תוך 30 יום מיום שנודע לו על מתן ההיתר."
|
||||||
|
|
||||||
|
### שכבה ג — ועדת ערר ירושלים (דפנה תמיר)
|
||||||
|
|
||||||
|
**ערר 1009/25 מפלגת נעם נ' הוועדה המרחבית הראל (נבו 27.3.2025):**
|
||||||
|
> "דיון בערר המבקש לבטל היתר שכבר יצא מחייב עמידה בלוח הזמנים שהדין מחייב,
|
||||||
|
> כל חריגה מכך מחייבת בקשה להארכת מועד ועמידה בכל התנאים לכך (זכות עמידה,
|
||||||
|
> שיהוי, הסתמכות, פגיעה וכיו'). ודוק, מחייבת בקשה להארכת מועד סדורה ומנומקת
|
||||||
|
> ולא בדרך אגב ולא בחסות תקנות הרישוי."
|
||||||
|
|
||||||
|
**ערר 1112/22 ירושלים שקופה נ' ועדה מקומית ירושלים (נבו 11.5.2023):**
|
||||||
|
> "מרחק של פחות מ-100 מ' אינו מקנה זכות התנגדות לתכנית; קל וחומר שמרחק של
|
||||||
|
> למעלה מ-400 מ' אינו מקנה זכות התנגדות לבקשה להיתר, שכן זכות ההתנגדות לבקשה
|
||||||
|
> להיתר (סעיף 149) צרה מזכות ההתנגדות לתכנית (סעיף 100)"
|
||||||
|
|
||||||
|
**בל"מ 1028/20 חלוואני (ועדת ערר ירושלים):**
|
||||||
|
> "המועד להגשת ערר הינו 30 ימים מיום שהומצאה החלטת הועדה המקומית וכי המבקשת
|
||||||
|
> הייתה ערה להליכי הבקשה להיתר"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ג. שישה תבחינים — סדר הבחינה
|
||||||
|
|
||||||
|
על פי הפסיקה המצטברת, להכרעה בבל"מ-רישוי יש לבחון שישה תבחינים. הסדר חשוב:
|
||||||
|
תבחין ו (זכות עמידה) הוא תנאי סף עצמאי — אם אין זכות עמידה אין צורך לבחון
|
||||||
|
יתר התבחינים.
|
||||||
|
|
||||||
|
| # | תבחין | אופי | מקור |
|
||||||
|
|---|--------|------|------|
|
||||||
|
| ו | **זכות עמידה** | **תנאי סף עצמאי** | עע"מ 1461/20 אנטרים; ערר 1112/22 |
|
||||||
|
| א | טעם סביר לאיחור | מקדים — נחוץ לפתיחת הדלת | עע"מ 317/10 שפר; בל"מ 1028/20 |
|
||||||
|
| ב | אורך השיהוי | כמותי — חומרת ההפרה | ערר 1096/24 אנשין |
|
||||||
|
| ג | הסתמכות + שינוי מצב לרעה | כמותי — נזק | בר"מ 2340/02 |
|
||||||
|
| ד | סיכויי ההליך | מהותי — "לכאורה" | בר"מ 2340/02 |
|
||||||
|
| ה | אינטרס ציבורי / חזקת תקינות | ערכי | הלכת חזקת תקינות |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ד. תבחין ו — זכות עמידה (תנאי סף)
|
||||||
|
|
||||||
|
### מקור הזכות
|
||||||
|
זכות הערר לפי סעיף 152 מוקנית רק למי שהוא **בעל זכות במקרקעין נשוא הבקשה
|
||||||
|
להיתר**, לא לכל בעל עניין (עע"מ 1461/20 אנטרים).
|
||||||
|
|
||||||
|
### תבחין מרחק
|
||||||
|
על פי ערר 1112/22, מרחק של מעל 100 מ' (קל וחומר מעל 400 מ') אינו מקנה זכות
|
||||||
|
התנגדות לבקשת היתר, גם בהיעדר נצפות.
|
||||||
|
|
||||||
|
### טבלת בדיקה
|
||||||
|
| פרמטר | להוכיח |
|
||||||
|
|--------|---------|
|
||||||
|
| בעל זכות בנכס נשוא הבקשה? | חוזה רכישה / נסח / שכירות מאומתת |
|
||||||
|
| בעל זכות בנכס גובל? | מפת מדידה / נסח |
|
||||||
|
| מרחק קו אווירי | מודד / Google Maps עם תיעוד |
|
||||||
|
| קיומה של נצפות | תצלום פנורמי / חוו"ד מודד |
|
||||||
|
| מעמד נציג דיירים / פינוי-בינוי | חוזה פנימי — לא יוצר זכות סטטוטורית |
|
||||||
|
|
||||||
|
**אזהרה:** טיעון של "מתנגד מטעם הציבור" או "אינטרס ציבורי כללי" — אינו מקנה
|
||||||
|
זכות עמידה. הזכות נצרכת להיות מעוגנת בזכות במקרקעין.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ה. תבחין א — טעם סביר לאיחור
|
||||||
|
|
||||||
|
### העיקרון
|
||||||
|
המבקש נדרש להוכיח שלא ידע על ההחלטה בזמן אמת **ושאי-הידיעה היא סבירה** — לא רק
|
||||||
|
שלא ידע, אלא שלא היה ניתן לצפות שיֵדע. הכלל הוא **דרך הסטטוס-קוו**: מי שהתעניין
|
||||||
|
בנכס שכן, שהיה מודע לשלטי בנייה, או שהיה לו עניין סדור בנכס — מוחזק כיודע.
|
||||||
|
|
||||||
|
### דרישות הוכחה
|
||||||
|
1. **תצהיר עובדתי** של המבקש — תאריכים מפורטים, מי אמר לו, מתי בדיוק.
|
||||||
|
2. **הוכחת ברירת המחדל של הוועדה** — היכן הפרסום היה צריך להתבצע? האם בוצע?
|
||||||
|
3. **שלושת התנאים המצטברים** (לפי הלכת שפר, כפי שיושמו בפסיקה לאחר מכן):
|
||||||
|
- זכות טיעון בהליך הרישוי וזכאות לקבל פרסום.
|
||||||
|
- פגם בהליך הפרסום בפועל.
|
||||||
|
- הפגם פגע בזכות הטיעון.
|
||||||
|
|
||||||
|
### מלכודות נפוצות
|
||||||
|
- **התכתבות עם "הדרג המקצועי" אינה מקפיאה לוחות זמנים** (בל"מ 1028/22 חמד).
|
||||||
|
- **היעדר תצהיר → גרסת אי-הידיעה חלשה ראייתית.**
|
||||||
|
- **ידיעה קודמת על ההליכים** (התנגדות שהוגשה, נוכחות בדיון, פניות בעבר) שוללת
|
||||||
|
כל תירוץ של אי-ידיעה.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ו. תבחין ב — אורך השיהוי
|
||||||
|
|
||||||
|
### שני רכיבים
|
||||||
|
1. **שיהוי מצטבר** — הזמן שחלף מהחלטת הוועדה המקומית עד הגשת הבל"מ.
|
||||||
|
2. **שיהוי סובייקטיבי** — הזמן שחלף מיום הידיעה הנטענת עד הגשת הבל"מ.
|
||||||
|
|
||||||
|
### ציר זמן לדוגמה
|
||||||
|
| תאריך | אירוע | שיהוי מצטבר |
|
||||||
|
|--------|--------|--------------|
|
||||||
|
| יום 0 | פרסום הבקשה | 0 |
|
||||||
|
| יום 30 | החלטת ועדת משנה | — |
|
||||||
|
| יום 120 | אישרור במליאה | — |
|
||||||
|
| יום X | ידיעה נטענת | חודשים-שנה |
|
||||||
|
| יום X+30 | הגשת הבל"מ | +30 ימים סובייקטיבי |
|
||||||
|
|
||||||
|
### עקרון מנחה
|
||||||
|
ערר 1096/24 אנשין (דפנה תמיר, 30.12.2024):
|
||||||
|
> "בהינתן שהערר מוגש במקום בו לא הייתה לעורר זכות קנויה וברורה להגשתו, היה
|
||||||
|
> עליו שלא להתעכב ובוודאי שלא לחכות ליום האחרון להגשת הערר"
|
||||||
|
|
||||||
|
**הכלל:** ככל שזכות העמידה רופפת יותר — דרישות הזריזות מחמירות.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ז. תבחין ג — הסתמכות הצד שכנגד
|
||||||
|
|
||||||
|
### עיקרון בר"מ 2340/02 אגא וכט
|
||||||
|
> "האם שינה הצד האחר את מצבו לרעה, האם ניתן להשיב את המצב לקדמותו"
|
||||||
|
|
||||||
|
### טבלת השקעות לבדיקה
|
||||||
|
| השקעה | תיעוד נדרש |
|
||||||
|
|--------|-----------|
|
||||||
|
| שכר טרחת מתכננים / עו"ד / יועצים | חשבוניות / קבלות / חוזה |
|
||||||
|
| תכנון מפורט (חניון, ממ"דים) | תכניות חתומות |
|
||||||
|
| היתר חפירה / חפירה בפועל | היתר + תצלומים |
|
||||||
|
| הסכמי מימון | חוזה עם בנק / משקיע |
|
||||||
|
| פינוי שוכרים / חתימות דיירים | חוזי פינוי / הסכמות |
|
||||||
|
| התקדמות פיזית (יסודות, שלד) | תצלומים מתועדים |
|
||||||
|
|
||||||
|
### "האם ניתן להשיב למצב הקדמות?"
|
||||||
|
ככל ששלב הביצוע מתקדם יותר — היכולת להפוך פוחתת. לאחר היתר חפירה, פינוי שוכרים,
|
||||||
|
ושלב הכנת יסודות — המצב לרוב בלתי-הפיך פיזית, ולפחות בלתי-הפיך כלכלית.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ח. תבחין ד — סיכויי ההליך (לכאורה)
|
||||||
|
|
||||||
|
### הבהרה מתודית
|
||||||
|
בשלב בל"מ, **בוחנים סיכויי הערר המהותי רק כדי לקבוע האם יש סיבה מספקת לפתוח
|
||||||
|
את הדלת** — לא לפסוק לגוף הערר. אם המחלוקת המהותית היא קשה ומורכבת אבל ברורה
|
||||||
|
שיש בה ממש — תבחין ד תומך בקבלת הבל"מ. אם המחלוקת תיאורטית, ספקולטיבית, או
|
||||||
|
ברורה לזכות המשיבים — תבחין ד תומך בדחייה.
|
||||||
|
|
||||||
|
### סוגים אופייניים של סוגיות מהותיות בבל"מ-רישוי
|
||||||
|
- תחולת תמ"א 38 (תקנים, מבנה קטן, איזורי סיכון רעש)
|
||||||
|
- תוקף תכנית (פקיעה, הוראות מעבר)
|
||||||
|
- חישוב סל זכויות (תיקון 3א, "קומה טיפוסית קיימת")
|
||||||
|
- מעמד תכנית חדשה (102-XXXXXX) — מופקדת? מאושרת? נסיוני?
|
||||||
|
- תנאי היתר (עמידה בתקנות, קווי בניין, חניות)
|
||||||
|
|
||||||
|
### דרך הבחינה
|
||||||
|
לכל סוגיה: (1) האם ההסתמכות על תכנית / תקן בוצעה; (2) האם יש פסיקה מנחה;
|
||||||
|
(3) האם יש מחלוקת מקצועית-עובדתית שתצריך חוות דעת.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ט. תבחין ה — אינטרס ציבורי / חזקת תקינות
|
||||||
|
|
||||||
|
### חזקת תקינות המעשה המנהלי
|
||||||
|
עיקרון יסוד בדין המנהלי: כל פעולת הוועדה נחזית כתקינה, עד שהמוכיח אחרת. נטל
|
||||||
|
ההוכחה על המבקש.
|
||||||
|
|
||||||
|
### שיקולים אופייניים בבל"מ-רישוי
|
||||||
|
| שיקול | כיוון אופייני |
|
||||||
|
|--------|---------------|
|
||||||
|
| חיזוק מבני מפני רעידות אדמה | תומך ביזם |
|
||||||
|
| ממ"דים / מיגון מפני ירי | תומך ביזם |
|
||||||
|
| הרחבת זכויות דרך / זכויות מעבר | תועלת ציבורית |
|
||||||
|
| חניות תת-קרקעיות (פינוי חניה מרחוב) | תועלת ציבורית |
|
||||||
|
| תקינות הליך (פרסום, התנגדויות, דיון) | חזקת תקינות |
|
||||||
|
| מתנגד סדרתי / בעל אינטרס נסתר | מחליש טענות המבקש |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## י. טבלת התאמה לעובדות (placeholder לכל תיק)
|
||||||
|
|
||||||
|
| תבחין | עובדה במקרה הנוכחי | כיוון |
|
||||||
|
|--------|---------------------|-------|
|
||||||
|
| ו. זכות עמידה | [לתאר מרחק, נצפות, זכויות בקרקע] | [חוסם / מאפשר / שאלה] |
|
||||||
|
| א. טעם סביר | [פרסום, ידיעה, תצהיר] | [נוטה לקבלה / לדחייה] |
|
||||||
|
| ב. אורך השיהוי | [שנים / חודשים / ימים] | [קל / בינוני / חמור] |
|
||||||
|
| ג. הסתמכות | [השקעות מצוטטות בש"ח] | [קלה / משמעותית / מוחשית] |
|
||||||
|
| ד. סיכויי הליך | [שאלות פתוחות vs. ברורות] | [לכאורה / ספקולטיבי] |
|
||||||
|
| ה. אינטרס ציבורי | [שיקולים ציבוריים בולטים] | [תומך / ניטרלי / נגד] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## יא. סעיף מסקנה — מבנה אופייני
|
||||||
|
|
||||||
|
המבנה האופייני של סעיף ההכרעה בבל"מ-רישוי הוא:
|
||||||
|
|
||||||
|
1. **פתיחה — איזון התבחינים בקצרה.** "בחנו את ששת התבחינים... ומצאנו..."
|
||||||
|
2. **תבחין ו (סף).** אם זכות העמידה רופפת/חסרה — זהו לרוב המכריע.
|
||||||
|
3. **תבחינים א-ה.** ניתוח כל אחד בקצרה, עם הפניה לפסיקה.
|
||||||
|
4. **מסקנה כוללת.** "לאור כל האמור — הבקשה להארכת מועד נדחית / מתקבלת".
|
||||||
|
5. **הוצאות.** אם רלוונטי — לפי סעיף 1.
|
||||||
|
|
||||||
|
### לשון אופיינית לדחייה (דפנה תמיר)
|
||||||
|
> "מששה התבחינים שנבחנו — חמישה מצביעים על מסקנה אחת, וגם התבחין השישי אינו
|
||||||
|
> תומך בקבלת הבקשה. נסיבות התיק אינן מצדיקות חריגה מהמועד הסטטוטורי."
|
||||||
|
|
||||||
|
### לשון אופיינית לקבלה
|
||||||
|
> "על אף השיהוי, נסיבות אי-הידיעה מתועדות; ההסתמכות בעיקרה תכנונית ולא ביצועית;
|
||||||
|
> ומחלוקת מהותית ממשית עומדת על הפרק. בנסיבות אלה, יש לפתוח את הדלת לערר על
|
||||||
|
> מנת שהסוגיות יתבררו."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## יב. הפניות חוצות
|
||||||
|
|
||||||
|
- ראה גם: `docs/methodology/extension-request-betterment_levy.md` (סעיף 14, 45 ימים)
|
||||||
|
- ראה גם: `docs/methodology/extension-request-compensation.md` (סעיף 198(ד), 30 ימים)
|
||||||
|
- ראה גם: `docs/block-schema.md` — מבנה 12 הבלוקים
|
||||||
|
- ראה גם: `skills/decision/SKILL.md` — מדריך סגנון של דפנה
|
||||||
|
- דוגמאות מעובדות: `data/cases/1017-03-26/`, `data/cases/1018-03-26/`, `data/cases/1019-03-26/`
|
||||||
215
docs/methodology/extension-request-compensation.md
Normal file
215
docs/methodology/extension-request-compensation.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# מתודולוגיה — בל"מ בפיצויים (ס' 197) (9xxx)
|
||||||
|
|
||||||
|
**appeal_subtype:** `extension_request_compensation`
|
||||||
|
**מסלול:** סעיף 198(ד) לחוק התכנון והבנייה, התשכ"ה-1965
|
||||||
|
**מועד סטטוטורי:** 30 ימים מיום החלטת הוועדה המקומית בתביעת הפיצויים
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## א. מבוא — הייחוד של בל"מ בפיצויים
|
||||||
|
|
||||||
|
בל"מ בפיצויים שונה מהותית הן מבל"מ ברישוי והן מבל"מ בהיטל השבחה:
|
||||||
|
|
||||||
|
| ממד | בל"מ ברישוי | בל"מ היטל השבחה | בל"מ פיצויים |
|
||||||
|
|------|--------------|------------------|----------------|
|
||||||
|
| מועד | 30 ימים | 45 ימים | **30 ימים** |
|
||||||
|
| סעיף | 152 | 14 לתוספת ג' | **198(ד)** |
|
||||||
|
| מהות הסעד | ביטול היתר | תיקון שומה | **פיצויי פגיעה בזכויות קניין** |
|
||||||
|
| נטל הוכחה | מקדים | טעות שמאית | **סף קפדני — פגיעה ממונית מוחשית** |
|
||||||
|
| טון אופייני | מעורב | קר/שמאי | **קר, משפטי, חמור** |
|
||||||
|
| הסתמכות | יזם / רשות | רשות (חלוקה) | **רשות + ציבור (תקציבי פיצויים)** |
|
||||||
|
|
||||||
|
### למה הסף הקפדן ביותר?
|
||||||
|
פיצויים לפי סעיף 197 הם **כספים ציבוריים** שמיועדים לפיצוי על פגיעה
|
||||||
|
ממונית מוחשית בקרקעות. הם נושאים שלוש מאפיינים שדורשים אכיפת מועדים
|
||||||
|
מחמירה:
|
||||||
|
|
||||||
|
1. **תקציבים סגורים** — הוועדה המקומית עוזבת תקציב לפיצויי 197; שיהוי
|
||||||
|
מחבל בתכנון פיננסי ובחלוקת התקציב.
|
||||||
|
2. **השפעה על תכנון עתידי** — דחייה ארוכת-טווח בבירור הזכות לפיצוי משבשת
|
||||||
|
את היכולת לתכנן הליכי הפקעה/תכנון נוספים.
|
||||||
|
3. **זכויות קניין** — שני הצדדים (תובע ורשות) נושאים אינטרסים קנייניים
|
||||||
|
ברורים. אכיפת מועדים = הגנה על שני הצדדים.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ב. מסגרת נורמטיבית
|
||||||
|
|
||||||
|
### שכבה א — חקיקה ראשית
|
||||||
|
|
||||||
|
**סעיף 197(א) לחוק התכנון והבנייה:**
|
||||||
|
> "נפגעו על ידי תכנית, שלא בדרך הפקעה, מקרקעין הנמצאים בתחום התכנית או
|
||||||
|
> גובלים עמה, מי שביום תחילתה של התכנית היה בעל המקרקעין או בעל זכות בהם
|
||||||
|
> זכאי לפיצויים מהוועדה המקומית..."
|
||||||
|
|
||||||
|
**סעיף 198(ד) — מועד הערר:**
|
||||||
|
ערר על החלטת הוועדה המקומית בתביעת פיצויים מוגש לוועדת הערר תוך 30 ימים
|
||||||
|
מיום שהומצאה ההחלטה לתובע.
|
||||||
|
|
||||||
|
### שכבה ב — עליון
|
||||||
|
|
||||||
|
**ע"א 210/88 החברה להפצת פרי הארץ נ' הוועדה המקומית כוכב יאיר (פ"ד מו(4) 627):**
|
||||||
|
ביסוס דרישת ההוכחה לפגיעה ממונית מוחשית — לא די בטענה כללית של "ירידת ערך".
|
||||||
|
נדרשת: (א) הוכחת מצב לפני התכנית; (ב) הוכחת מצב אחרי; (ג) הצבעה על קשר סיבתי
|
||||||
|
ישיר; (ד) חוות דעת שמאית כמותית.
|
||||||
|
|
||||||
|
**עע"מ 1968/00 חברת גוש 6195 נ' הוועדה המקומית הרצליה:**
|
||||||
|
חיזוק עקרון הסופיות בפיצויי 197 — שינוי מועדים בהליך פיצויים פוגע באינטרס
|
||||||
|
הציבורי הספציפי של פריסת תקציבים.
|
||||||
|
|
||||||
|
### שכבה ג — ועדות ערר
|
||||||
|
|
||||||
|
(להוסיף תקדימי דפנה תמיר בעררי 9xxx — לחפש בקורפוס "בל\"מ פיצויים" או
|
||||||
|
"הארכת מועד 197".)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ג. ארבעה תבחיני בל"מ בפיצויים
|
||||||
|
|
||||||
|
| # | תבחין | אופי | סף |
|
||||||
|
|---|--------|------|-----|
|
||||||
|
| א | **פגיעה ממונית מוחשית** | תנאי סף עצמאי | קריטי |
|
||||||
|
| ב | טעם סביר לאיחור | מקדים — קפדן | גבוה |
|
||||||
|
| ג | אורך השיהוי | כמותי — קצר במיוחד | גבוה |
|
||||||
|
| ד | הסתמכות הרשות (תקציב) | כמותי | גבוה |
|
||||||
|
|
||||||
|
לעומת בל"מ ברישוי ובהיטל השבחה — אין כאן תבחין נפרד של "סיכויי הליך";
|
||||||
|
תבחין הפגיעה (א) משלב את שני הממדים (סיכויי הליך + עצם הזכות לפיצוי).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ד. תבחין א — פגיעה ממונית מוחשית (סף הקפדני)
|
||||||
|
|
||||||
|
### הדרישה
|
||||||
|
לא די בטענה לפגיעה. נדרש להוכיח, לפחות לכאורה:
|
||||||
|
|
||||||
|
1. **בעלות / זכות במקרקעין נשוא התביעה** — נסח טאבו, חוזה מאומת, או רישום אחר.
|
||||||
|
2. **תכנית מאושרת שנכנסה לתוקף** — לא טיוטה, לא תב"ע מופקדת — תכנית בתוקף.
|
||||||
|
3. **קשר סיבתי בין התכנית לפגיעה הנטענת** — לא "ירידת ערך כללית" של אזור.
|
||||||
|
4. **חוו"ד שמאית כמותית** — מציגה את ערך הקרקע לפני ואחרי, עם נתוני השוואה.
|
||||||
|
|
||||||
|
### הוצאות מן הכלל
|
||||||
|
לא נחשבים "פגיעה ממונית" לעניין סעיף 197:
|
||||||
|
- **פגיעה תיאורטית עתידית** — תכנית שטרם נכנסה לתוקף, אופציות שלא מומשו.
|
||||||
|
- **פגיעה אסתטית/סובייקטיבית** — נוף, שכנים, אווירה.
|
||||||
|
- **פגיעה זמנית בלבד** — שיבושים בשלב בנייה שאינם משפיעים על ערך ארוך-טווח.
|
||||||
|
- **פגיעה במקרקעין מחוץ לתכנית ולא גובלים** — דרישה שטחית של "תחום התכנית
|
||||||
|
או גובלים עמה" — מצומצמת.
|
||||||
|
|
||||||
|
### דרישת ההוכחה לכאורה בשלב הבל"מ
|
||||||
|
בשלב בל"מ אין צורך להוכיח את הפגיעה במלואה; די ב**הצגת לכאורה משכנעת**
|
||||||
|
המבוססת על מסמכים מקצועיים. הצגה זו מאפשרת לבחון: האם יש בכלל מה לדון
|
||||||
|
לאחר חלוף המועד?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ה. תבחין ב — טעם סביר לאיחור
|
||||||
|
|
||||||
|
### העקרון
|
||||||
|
בפיצויים — דרישת הזריזות מחמירה מאוד. סיבות:
|
||||||
|
|
||||||
|
1. **התובע פעל מולן** — בניגוד לבל"מ ברישוי, התובע ידע על התכנית ופעל
|
||||||
|
בה (הגיש תביעה לוועדה המקומית). אי-ידיעה על ההחלטה היא חריג.
|
||||||
|
2. **המצאה אישית** — ההחלטה מומצאת אישית; פחות מקום לטענות "פרסום באתר".
|
||||||
|
3. **התובע מיוצג** — לרוב התובע פיצויים מיוצג עו"ד; "אי-ידיעה" של עו"ד
|
||||||
|
על מועד היא חולשה ראייתית מובהקת.
|
||||||
|
|
||||||
|
### מצבי "טעם סביר" אופייניים
|
||||||
|
| מצב | קבילות |
|
||||||
|
|------|---------|
|
||||||
|
| המצאה פגומה (לא לכתובת עורך הדין) | קבילה — בכפוף לתיעוד |
|
||||||
|
| מחלת התובע (מתועדת) | קבילה |
|
||||||
|
| תקופה ארוכה של "ניסיון להידברות" עם הוועדה | חלשה — לוחות זמנים לא מוקפאים |
|
||||||
|
| המתנה להחלטה שיפוטית במקרה דומה | חלשה — אפשר להגיש "במקרה ש..." |
|
||||||
|
| תקלה במשרד עורך הדין | חלשה — אחריות נשואת ייצוג |
|
||||||
|
|
||||||
|
### דרישות הוכחה
|
||||||
|
- תצהיר מפורט של התובע **וגם** של עורך דינו.
|
||||||
|
- מסמכי תמיכה (כרטיסי רישום בית חולים, אישורים רפואיים, וכו').
|
||||||
|
- תיעוד התכתבות פנימית במשרד עורך הדין (אם רלוונטי).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ו. תבחין ג — אורך השיהוי
|
||||||
|
|
||||||
|
### עקרונות
|
||||||
|
- **30 ימים בלבד** = מועד קצר במיוחד.
|
||||||
|
- כל יום מעבר מקבל ניקוד שלילי.
|
||||||
|
- שיהוי של מעל 14 ימים מעבר למועד (סה"כ 44 ימים) — נחשב מובהק.
|
||||||
|
- שיהוי של מעל 60 ימים מעבר (סה"כ 90 ימים) — דורש הצדקה חזקה במיוחד.
|
||||||
|
- שיהוי של מעל 180 ימים — חוסם אלא בנסיבות חריגות (טעות בדין, גילוי מאוחר
|
||||||
|
של עובדה מהותית).
|
||||||
|
|
||||||
|
### חישוב
|
||||||
|
| תאריך | אירוע | שיהוי מצטבר |
|
||||||
|
|--------|--------|--------------|
|
||||||
|
| יום 0 | המצאת החלטה | 0 |
|
||||||
|
| יום 30 | תום מועד סטטוטורי | 0 |
|
||||||
|
| יום X | הגשת הבל"מ | X-30 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ז. תבחין ד — הסתמכות הרשות (תקציב פיצויים)
|
||||||
|
|
||||||
|
### ייחוד בפיצויים
|
||||||
|
הוועדה המקומית מקצה תקציב לפיצויי 197 לפי החלטותיה. שיהוי בערר:
|
||||||
|
|
||||||
|
1. **פוגע בפריסה תקציבית** — תקציב עזב מהקצאתו, עבר ליעדים אחרים.
|
||||||
|
2. **מסבך הליכים שלא הוכרעו עדיין** — בעלי מקרקעין אחרים פעלו על סמך
|
||||||
|
התקציב הקיים.
|
||||||
|
3. **משפיע על מכרזים / חוזי תכנון** — שינוי בגובה הפיצויים משפיע על
|
||||||
|
החלטות פיתוח עתידיות.
|
||||||
|
|
||||||
|
### טבלת בדיקה
|
||||||
|
| שלב | מצב התקציב | השפעה |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| לפני סוף שנת כספים | תקציב פעיל, ניתן לשנות הקצאה | קלה |
|
||||||
|
| לאחר סגירת שנת כספים | תקציב חלוק | בינונית |
|
||||||
|
| לאחר העברה ליעדים אחרים | פיצוי דורש מקור חדש | משמעותית |
|
||||||
|
| לאחר ביצוע פרויקטים | בלתי הפיך כלכלית | מוחשית |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ח. טבלת התאמה לעובדות (placeholder לכל תיק)
|
||||||
|
|
||||||
|
| תבחין | עובדה במקרה הנוכחי | כיוון |
|
||||||
|
|--------|---------------------|-------|
|
||||||
|
| א. פגיעה ממונית | [חוו"ד שמאית? קשר סיבתי? תכנית בתוקף?] | [חוסם / מאפשר] |
|
||||||
|
| ב. טעם סביר | [המצאה, ייצוג, תצהיר] | [תומך / מחליש] |
|
||||||
|
| ג. אורך השיהוי | [X ימים מעבר ל-30] | [קל / מובהק / חמור] |
|
||||||
|
| ד. הסתמכות הרשות | [מצב התקציב] | [קל / משמעותי / מוחשי] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ט. סעיף מסקנה — מבנה אופייני
|
||||||
|
|
||||||
|
המבנה האופייני הוא **קפדן, מבוסס מסמכים, ללא רגש**:
|
||||||
|
|
||||||
|
1. **קביעת עובדות.** "ההחלטה הומצאה ביום X. הבל"מ הוגשה ביום Y. השיהוי
|
||||||
|
הוא Z ימים מעבר למועד הסטטוטורי."
|
||||||
|
2. **תבחין א (פגיעה).** "המבקש הציג חוו"ד / לא הציג חוו"ד. הקרקע
|
||||||
|
נמצאת בתחום התכנית / גובלת בה / מחוץ לה."
|
||||||
|
3. **אם לא הוצגה פגיעה לכאורה — דחייה מיידית.** "בהיעדר הצגה לכאורה של
|
||||||
|
פגיעה ממונית, אין יסוד לסטות ממועד הקבוע בחוק."
|
||||||
|
4. **אם הוצגה פגיעה — מעבר לתבחינים ב-ד.**
|
||||||
|
5. **מאזן והכרעה.** דחייה / קבלה / החזרה לוועדה המקומית.
|
||||||
|
|
||||||
|
### לשון אופיינית לדחייה
|
||||||
|
> "המבקש לא הציג ראיה לכאורית לפגיעה ממונית מוחשית בקרקע שבבעלותו. הקרקע
|
||||||
|
> נמצאת מחוץ לתחום התכנית ואינה גובלת עמה. בנסיבות אלה, ובהינתן שהשיהוי
|
||||||
|
> הוא של X ימים מעבר למועד הסטטוטורי הקצר של 30 הימים, אין מקום לסטייה
|
||||||
|
> מהמועד. הבל"מ נדחית."
|
||||||
|
|
||||||
|
### לשון אופיינית לקבלה (חריגה ביותר)
|
||||||
|
> "המבקש הציג חוו"ד שמאית מקצועית המראה ירידת ערך של כ-X% בקרקע הגובלת
|
||||||
|
> בתחום התכנית. ההצגה לכאורה משכנעת. בנסיבות החריגות של [פירוט], ועל אף
|
||||||
|
> הסף הקפדני שמטיל סעיף 198(ד), יש לפתוח את הדלת לדיון מהותי."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## י. הפניות חוצות
|
||||||
|
|
||||||
|
- ראה גם: `docs/methodology/extension-request-building_permit.md` (סעיף 152, 30 ימים)
|
||||||
|
- ראה גם: `docs/methodology/extension-request-betterment_levy.md` (סעיף 14, 45 ימים)
|
||||||
|
- ראה גם: `docs/block-schema.md` — מבנה 12 הבלוקים
|
||||||
|
- ראה גם: `skills/decision/SKILL.md` — מדריך סגנון של דפנה
|
||||||
203
docs/operations-runbook.md
Normal file
203
docs/operations-runbook.md
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
# Operations Runbook — עוזר משפטי
|
||||||
|
|
||||||
|
> תוכן תפעולי-עומק שהוצא מ-[`CLAUDE.md`](../CLAUDE.md) כדי לרזות את ההקשר הנטען בכל סשן (TaskMaster #107.1).
|
||||||
|
> ה-CLAUDE.md מחזיק את **הכללים הקריטיים בקצרה**; כאן נמצאים הפרטים המלאים, הפקודות, וטבלאות-הייחוס.
|
||||||
|
> כשעובדים על Deploy, Paperplip-ops, או adapters — לקרוא את הסעיף הרלוונטי כאן.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## שרת Nautilus (158.178.131.193)
|
||||||
|
|
||||||
|
| שירות | תפקיד | כתובת |
|
||||||
|
|-------|--------|-------|
|
||||||
|
| Coolify | ניהול containers | `http://158.178.131.193:8000` |
|
||||||
|
| PostgreSQL + pgvector | בסיס נתונים ראשי | `legal-ai-postgres` (`localhost:5433`, user `legal_ai`) |
|
||||||
|
| Redis | תור משימות | `legal-ai-redis` |
|
||||||
|
| Gitea | מאגר קוד | `gitea.nautilus.marcusgroup.org/ezer-mishpati` |
|
||||||
|
| ezer-mishpati-web | ממשק העלאת מסמכים (Docker/Coolify) | `legal-ai.nautilus.marcusgroup.org` |
|
||||||
|
| Paperclip | סוכן AI — מריץ Claude Code agents (pm2, מקומי) | `localhost:3100` |
|
||||||
|
| legal-chat-service | גשר claude CLI לטאב הצ'אט ב-/training (pm2, loopback) | `127.0.0.1:8770` |
|
||||||
|
| Infisical | ניהול סודות | `secret.dev.marcus-law.co.il` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ארכיטקטורת Deploy — חובה לקרוא
|
||||||
|
|
||||||
|
שלושה מודלי-הרצה דרים יחד. ערבוב ביניהם הוא הטעות הנפוצה ביותר.
|
||||||
|
|
||||||
|
### עוזר משפטי (Legal-AI) — Docker container דרך Coolify
|
||||||
|
- UUID: `gyjo0mtw2c42ej3xxvbz8zio` (build_pack: `dockerimage`, **לא** `dockerfile`)
|
||||||
|
- שינוי קוד ב-`web/` או `web-ui/` **לא נכנס לתוקף** עד ש:
|
||||||
|
1. עושים `git commit` + `git push origin main`
|
||||||
|
2. Gitea Actions בונה image → דוחף ל-registry → מפעיל redeploy ב-Coolify (`mcp__coolify__deploy`)
|
||||||
|
3. ממתינים ~2-4 דקות לבנייה
|
||||||
|
- **אסור** לנסות להריץ uvicorn / `next dev` מקומית — אין סביבת Python על המכונה
|
||||||
|
- ה-container מריץ Next.js (`:3000`, חשוף) + FastAPI (`:8000`, פנימי)
|
||||||
|
- בדיקה: `curl https://legal-ai.nautilus.marcusgroup.org/api/health`
|
||||||
|
- runbook מלא של ה-pipeline: `~/CI-CD-MIGRATION-GUIDE.md`
|
||||||
|
|
||||||
|
### Paperclip — מקומית דרך pm2
|
||||||
|
- פורט: `localhost:3100`, DB: `localhost:54329` (Postgres embedded)
|
||||||
|
- שינויי קוד נכנסים לתוקף אחרי `pm2 restart paperclip`
|
||||||
|
- **אין צורך ב-Docker או Coolify** (מיגרציה ל-Coolify נוסתה 2026-04-04 והוחזרה 2026-04-08)
|
||||||
|
- תרגום/RTL: `~/.paperclip/hebrew/` → `bash ~/.paperclip/hebrew/apply-hebrew.sh && pm2 restart paperclip`
|
||||||
|
|
||||||
|
### legal-chat-service — מקומית דרך pm2 (מאפריל 2026)
|
||||||
|
- פורט: `127.0.0.1:8770` (loopback בלבד)
|
||||||
|
- שירות aiohttp קצר שעוטף את `claude` CLI ב-streaming + session continuation, ומשרת את הטאב "שיחה" בדף `/training`. הקונטיינר משדל אליו proxy דרך `host.docker.internal:8770`.
|
||||||
|
- קוד: [`mcp-server/src/legal_mcp/chat_service/`](../mcp-server/src/legal_mcp/chat_service/)
|
||||||
|
- התקנה: `pm2 start /home/chaim/legal-ai/scripts/legal-chat-service.config.cjs && pm2 save`
|
||||||
|
- בריאות: `curl http://127.0.0.1:8770/health` → `{"ok":true,...}`
|
||||||
|
- שינויי קוד: `pm2 restart legal-chat-service`
|
||||||
|
- **אפס עלות API** — claude CLI משתמש ב-claude.ai subscription של chaim. הנחת היסוד של `claude_session.py` (claude CLI מקומי בלבד) נשמרת.
|
||||||
|
- Coolify dependency: ה-Service Definition של legal-ai חייב להכיל `extra_hosts: host.docker.internal:host-gateway` (אחרת ה-proxy יקבל ConnectError).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## מבנה תיקיות
|
||||||
|
|
||||||
|
```
|
||||||
|
/home/chaim/legal-ai/
|
||||||
|
├── CLAUDE.md ← אינדקס דק (כללים קריטיים + מצביעים)
|
||||||
|
├── docs/operations-runbook.md ← הקובץ הזה (עומק תפעולי)
|
||||||
|
├── Dockerfile ← Docker build
|
||||||
|
├── docs/ ← תיעוד + לקחים
|
||||||
|
│ ├── architecture.md ארכיטקטורה
|
||||||
|
│ ├── block-schema.md 12 בלוקים (המסמך החשוב ביותר)
|
||||||
|
│ ├── migration-plan.md תוכנית מעבר vault → DB
|
||||||
|
│ ├── legal-decision-lessons.md לקחים מ-3 החלטות
|
||||||
|
│ └── memory.md הקשר כללי — skills, פרויקטים
|
||||||
|
├── skills/ ← כלי עבודה ומדריכים
|
||||||
|
│ ├── decision/ מדריך סגנון + references + 12 בלוקים
|
||||||
|
│ ├── assistant/ קטלוג מסמכים
|
||||||
|
│ ├── docx/ עיצוב DOCX
|
||||||
|
│ ├── dafna-decision-template/ export DOCX לפי תבנית Word של דפנה
|
||||||
|
│ └── new-company-setup/ blueprint הוספת חברה חדשה
|
||||||
|
├── .claude/
|
||||||
|
│ └── agents/ ← הוראות סוכנים + HEARTBEAT.md (symlinks ב-Paperclip)
|
||||||
|
│ ├── HEARTBEAT.md checklist הפעלה משותף לכל הסוכנים
|
||||||
|
│ ├── legal-ceo.md תזמורן + בקרת זרימה
|
||||||
|
│ ├── legal-writer.md כתיבת בלוקים בסגנון דפנה
|
||||||
|
│ ├── legal-analyst.md ניתוח משפטי + חילוץ טענות
|
||||||
|
│ ├── legal-researcher.md חיפוש תקדימים
|
||||||
|
│ ├── legal-qa.md 7 שערי איכות
|
||||||
|
│ ├── legal-proofreader.md תיקון OCR
|
||||||
|
│ ├── legal-exporter.md ייצוא DOCX סופי
|
||||||
|
│ └── hermes-curator.md סוכן Hermes לניתוח סגנון post-export
|
||||||
|
├── data/
|
||||||
|
│ ├── training/ ← 4 החלטות לאימון (DOCX)
|
||||||
|
│ ├── exports/ ← טיוטות DOCX מיוצאות
|
||||||
|
│ └── cases/{case-number}/ ← תיקי עררים (מבנה שטוח, סטטוס ב-DB)
|
||||||
|
├── web/ ← FastAPI backend (Python): 75+ API endpoints
|
||||||
|
│ ├── app.py ← API ראשי
|
||||||
|
│ ├── paperclip_api.py ← אינטגרציית Paperclip: `pc_request()` + `emit_case_status_webhook()`
|
||||||
|
│ ├── paperclip_client.py ← legacy client (ישן — השתמש ב-paperclip_api.py)
|
||||||
|
│ └── gitea_client.py ← אינטגרציית Gitea
|
||||||
|
├── web-ui/ ← Next.js frontend (TypeScript/React): ממשק המשתמש
|
||||||
|
│ └── next.config.ts ← proxy: /api/* → FastAPI :8000
|
||||||
|
├── mcp-server/ ← MCP server + services + tools
|
||||||
|
├── adapters/ ← Paperclip external adapters
|
||||||
|
│ └── deepseek-paperclip-adapter/ ← `deepseek_local` (Hermes-pinned ל-DeepSeek profile)
|
||||||
|
└── scripts/ ← סקריפטים וכלי עזר (ראה scripts/SCRIPTS.md)
|
||||||
|
└── .archive/ ← סקריפטים שהושלמו (לא להריץ)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Paperclip — כללי אינטגרציה (פירוט מלא)
|
||||||
|
|
||||||
|
> הכללים הקריטיים בתמצית נמצאים ב-[`CLAUDE.md`](../CLAUDE.md). כאן הפירוט המלא, הדוגמאות, וה-"למה".
|
||||||
|
|
||||||
|
### Wakeup API — תמיד דרך API, לעולם לא דרך DB
|
||||||
|
- **הנתיב הנכון**: `POST /api/agents/{agent-id}/wakeup` (לא `/wake`!)
|
||||||
|
- **⚠️ אסור**: `INSERT INTO agent_wakeup_requests` ישירות — זה יוצר רק רשומה בלי `heartbeat_run`, והסוכן **לא יתעורר לעולם**
|
||||||
|
- **⚠️ חובה לשלוח `payload` עם `issueId`** — בלי זה הסוכן מתעורר בלי הקשר (בלי תיק, בלי issue, בלי cwd נכון)
|
||||||
|
- דוגמה נכונה:
|
||||||
|
```json
|
||||||
|
{"source": "automation", "triggerDetail": "system", "reason": "...",
|
||||||
|
"payload": {"issueId": "...", "mutation": "comment", "commentId": "..."}}
|
||||||
|
```
|
||||||
|
- **Board API Key**: שמור ב-DB (`board_api_keys`), auth: `Authorization: Bearer pbk_...`
|
||||||
|
|
||||||
|
### ניתוב comments דרך CEO
|
||||||
|
- כשמשתמש כותב תגובה על issue ב-Paperclip, הפלאגין (`plugin-legal-ai`) מעיר את ה-CEO דרך `ctx.agents.invoke()`
|
||||||
|
- ה-CEO קורא את ה-comment, מחליט על ניתוב, ויוצר issue לסוכן המתאים
|
||||||
|
- כל הסוכנים חייבים לקרוא comments אחרונים לפני שהם מתחילים לעבוד (HEARTBEAT שלבים 2b-2c)
|
||||||
|
|
||||||
|
### קריאות API — תמיד דרך helper, לעולם לא `curl` ישיר
|
||||||
|
- **bash (סוכנים):** `~/legal-ai/scripts/pc.sh <METHOD> <PATH> [BODY_JSON]` — מוסיף Authorization, X-Paperclip-Run-Id, Content-Type, base URL. ראה `HEARTBEAT.md §0`.
|
||||||
|
- **Python (FastAPI):** `from web.paperclip_api import pc_request; await pc_request("POST", "/api/...", json={...})` — שימוש ב-board API key.
|
||||||
|
- **אסור** `curl ... $PAPERCLIP_API_URL` ישיר ב-bash; **אסור** `httpx.AsyncClient` ישיר ל-Paperclip ב-Python.
|
||||||
|
- **למה:** ה-skill הרשמי דורש `X-Paperclip-Run-Id` בכל קריאה משנה issue. אצלנו ה-audit trail עבד ממילא דרך JWT claims (`runId: runIdHeader || claims.run_id`), אבל ה-helper מבטיח עקביות + תאימות ל-board API keys (long-lived) שלא נושאות JWT claims.
|
||||||
|
|
||||||
|
### Cross-company agent sync — אחרי כל שינוי הגדרות
|
||||||
|
- יש 14 סוכנים = 7 × 2 חברות (CMP=1xxx, CMPA=8xxx). Paperclip מחייב `agents.company_id NOT NULL` — אין shared agents.
|
||||||
|
- **Master = CMP (1xxx)**, **Mirror = CMPA (8xxx)**.
|
||||||
|
- אחרי כל שינוי ב-`adapter_config`, `runtime_config`, `budget_monthly_cents`, או skills של סוכן ב-master (UI, SQL, או API), **חובה להריץ:**
|
||||||
|
```bash
|
||||||
|
PAPERCLIP_BOARD_API_KEY=$(...infisical...) \
|
||||||
|
python ~/legal-ai/scripts/sync_agents_across_companies.py --verify # לבדיקה
|
||||||
|
PAPERCLIP_BOARD_API_KEY=$(...) \
|
||||||
|
python ~/legal-ai/scripts/sync_agents_across_companies.py --apply # לסנכרן
|
||||||
|
```
|
||||||
|
- הסקריפט מסנן local skills שלא קיימים ב-CMPA (מציג אזהרה), משתמש ב-API (לא DB ישיר), יוצר revisions, idempotent.
|
||||||
|
- שאלות ה-skill הרשמי של Paperclip — `paperclip` skill תחת `paperclipai/paperclip`.
|
||||||
|
|
||||||
|
### Webhook יוצא — עדכון סטטוס תיק לפלאגין
|
||||||
|
כשסטטוס תיק משתנה דרך `PUT /api/cases/{case_number}`, הבקאנד שולח webhook אסינכרוני לפלאגין:
|
||||||
|
|
||||||
|
```
|
||||||
|
PUT /api/cases/{case_number} → emit_case_status_webhook() [BackgroundTask]
|
||||||
|
→ POST /api/plugins/marcusgroup.legal-ai/webhooks/case-status
|
||||||
|
→ plugin-legal-ai/onWebhook()
|
||||||
|
→ comment בעברית על issue + CEO wakeup (כשסטטוס = qa_failed)
|
||||||
|
```
|
||||||
|
|
||||||
|
- הקוד ב-`web/paperclip_api.py` (`emit_case_status_webhook`), fire-and-forget, timeout 5s
|
||||||
|
- הפלאגין שומר idempotency key ב-state עם TTL 5 דקות למניעת spam על retry
|
||||||
|
- `GET /api/cases/stale?days=N` — תיקים שלא עודכנו N ימים; מוחרגים: `new`, `final`, `exported`
|
||||||
|
- `GET /api/chair-feedback/weekly-summary` — סיכום פידבק YU"R לשבוע האחרון
|
||||||
|
|
||||||
|
### Scheduled Jobs (plugin-legal-ai)
|
||||||
|
| Job | לוח זמנים | מה עושה |
|
||||||
|
|-----|-----------|---------|
|
||||||
|
| `stale-case-reminder` | יומי 08:00 | שולח comment אזהרה על תיקים תקועים >3 ימים |
|
||||||
|
| `weekly-feedback-analysis` | ראשון 19:00 | מעיר CEO לניתוח פידבק YU"R ועדכון `docs/legal-decision-lessons.md` |
|
||||||
|
| `sync-case-status` | כל 30 דק' | מסנכרן סטטוסי תיקים בין legal-ai ל-Paperclip |
|
||||||
|
|
||||||
|
CEO שמתעורר מ-`weekly-feedback-job` כותב לקובץ בלבד — **אין לו issueId, אל תנסה לפרסם comment או לסגור issue**.
|
||||||
|
|
||||||
|
### External adapters — `deepseek_local`
|
||||||
|
- מיקום ה-package: [`adapters/deepseek-paperclip-adapter/`](../adapters/deepseek-paperclip-adapter/) (לא ב-`node_modules`).
|
||||||
|
- רישום ב-Paperclip: רשומה ב-`~/.paperclip/adapter-plugins.json` (נטען אוטומטית ב-startup דרך `buildExternalAdapters`). אין צורך בעריכת `node_modules`.
|
||||||
|
- **מה ה-adapter עושה**: spawnל-`hermes chat` עם `HERMES_HOME=/home/chaim/.hermes/profiles/deepseek` כך שה-CLI טוען את `config.yaml` (`base_url=https://api.deepseek.com/v1`, `provider=custom`, `key_env=DEEPSEEK_API_KEY`) ואת `.env` (שמכיל את ה-key).
|
||||||
|
- **מודלים זמינים** (lookup ב-DeepSeek `/v1/models`): `deepseek-v4-pro` (default), `deepseek-v4-flash`. יופיעו כדרופ-דאון ב-UI.
|
||||||
|
- **התקנה מחדש / עדכון**: `curl -X POST -H "Authorization: Bearer pcapi_legal_install_key_2026" -H "Content-Type: application/json" -d '{"packageName":"/home/chaim/legal-ai/adapters/deepseek-paperclip-adapter","isLocalPath":true}' http://localhost:3100/api/adapters/install`. לעדכון hot — `POST /api/adapters/deepseek_local/reload`.
|
||||||
|
- **⚠ Cross-company sync**: `sync_agents_across_companies.py` **מדלג** על סוכנים עם `adapter_type` שונה בין CMP ל-CMPA. כשעוברים סוכן ל-`deepseek_local` חובה להחיל ידנית בשתי החברות לפני sync.
|
||||||
|
- **תוספת adapters עתידיים** (OpenAI ישיר, Anthropic ישיר, וכו'): אותו דפוס. ה-package הראשי חייב לייצא `createServerAdapter()` שמחזיר `{ type, label, models, agentConfigurationDoc, execute, testEnvironment, sessionCodec, listSkills, syncSkills, ... }`. ראה את [`adapters/deepseek-paperclip-adapter/dist/index.js`](../adapters/deepseek-paperclip-adapter/dist/index.js) כתבנית.
|
||||||
|
|
||||||
|
### External adapters — Hermes Curator (`curator-cmp` / `curator-cmpa`)
|
||||||
|
- פרופילי Hermes נפרדים לסוכן `hermes-curator` — מנתח החלטות סופיות ומציע עדכוני SKILL.md/lessons.md
|
||||||
|
- מיקום: `~/.hermes/profiles/curator-cmp/` + `~/.hermes/profiles/curator-cmpa/`
|
||||||
|
- מופעל אחרי export סופי; אינו מעדכן קבצים ישירות
|
||||||
|
- **תהליך אישור הצעות:** הצעות ה-curator מגיעות כ-comment ב-Paperclip → חיים בוחן ומאשר ידנית → commits ל-`SKILL.md` ו-`docs/legal-decision-lessons.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## הערות יו"ר (Chair Feedback)
|
||||||
|
|
||||||
|
מנגנון לתיעוד הערות דפנה על טיוטות:
|
||||||
|
- **DB**: טבלת `chair_feedback` (case_id, block_id, feedback_text, category, lesson_extracted)
|
||||||
|
- **API**: `GET/POST /api/feedback`, `PATCH /api/feedback/{id}/resolve`
|
||||||
|
- **MCP tools**: `record_chair_feedback`, `list_chair_feedback`
|
||||||
|
- **UI**: דף ניהול ב-`/feedback` (ב-Next.js)
|
||||||
|
- **קטגוריות**: missing_content, wrong_tone, wrong_structure, factual_error, style, other
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ניהול משימות — TaskMaster AI (פירוט)
|
||||||
|
|
||||||
|
- קובץ המשימות הקנוני: `~/legal-ai/.taskmaster/tasks/tasks.json` (יחסי ל-project root, **לא** `~/.taskmaster/tasks/tasks.json`). מכיל את כל ה-tags של legal-ai (`master`, `legal-ai`).
|
||||||
|
- פקודות עיקריות: `get_tasks`, `next_task`, `add_task`, `update_task`, `expand_task`
|
||||||
|
- לפני התחלת עבודה → `next_task`; אחרי סיום → `update_task` עם status=done; משימה מורכבת → `expand_task`
|
||||||
|
- **⚠️ מלכוד cwd ב-CLI:** הדגל `--tag` בוחר קבוצה לוגית *בתוך* הקובץ — הוא **לא** בוחר לאיזה `tasks.json` לכתוב. ה-CLI מאתר את הקובץ לפי ה-cwd. תמיד `cd ~/legal-ai` לפני `task-master add-task` או כל פקודה משנה, ואז אמת ב-MCP `get_tasks`. כשלא בטוחים — לערוך את `~/legal-ai/.taskmaster/tasks/tasks.json` ישירות.
|
||||||
@@ -155,3 +155,78 @@ CEO צריך להעביר את ה-issue ל-`in_review` (לא `in_progress`) כש
|
|||||||
### סטטוס
|
### סטטוס
|
||||||
|
|
||||||
- **תיקון בכל הסקייל** (CLAUDE.md זיכרון: `reference_paperclip_wakeup.md`)
|
- **תיקון בכל הסקייל** (CLAUDE.md זיכרון: `reference_paperclip_wakeup.md`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. מחיקת npx cache → crash-loop בהפעלה (השרת מנצח את הפאטצ')
|
||||||
|
|
||||||
|
### מה קורה
|
||||||
|
|
||||||
|
Paperclip מופעל דרך `exec npx -y paperclipai@<version> run` ב-[start-paperclip.sh](../../.paperclip/scripts/start-paperclip.sh). npx **עושה reuse** ל-cache שכבר חולץ (`~/.npm/_npx/<hash>/node_modules/@paperclipai/server/`) — הוא **לא** מחלץ מחדש בכל הפעלה. כל עוד ה-cache קיים, הפאטצ'ים שהוחלו עליו פעם אחת נשמרים על פני ריסטארטים.
|
||||||
|
|
||||||
|
הבעיה מתחילה כש-ה-cache **נמחק** (`npm cache clean`, prune, או ניקוי ידני) בזמן שהתהליך רץ. אז נוצרות שתי תקלות נפרדות:
|
||||||
|
|
||||||
|
1. **התהליך הישן ממשיך "online" אבל שבור** — המודולים של node כבר טעונים בזיכרון, אז `/api/health` עדיין מחזיר 200, אבל `GET /` קורא את `ui-dist/index.html` **מהדיסק בכל בקשה** (`readFileSync`) → `ENOENT` → **HTTP 500** (`{"error":"Internal server error"}`). גם ה-URL הציבורי `pc.nautilus...` מחזיר 500.
|
||||||
|
2. **בריסטארט נכנסים ל-crash-loop** — npx מחלץ עותק **טרי ולא-מתוקן**. השרת מריץ `assertCloudDatabaseContract()` (ראה patch §4 ב-start script) שמסרב ל-embedded PG במצב authenticated/public → **קורס מיד**, לפני שלולאת-הרקע (5/20/60ש') מספיקה להחיל את פאטץ' ה-bypass. כל ריסטארט מחלץ-וקורס מחדש ⇒ עשרות ריסטארטים, שום דבר לא מאזין על 3100.
|
||||||
|
|
||||||
|
### ראיה אמפירית — 06/06/26
|
||||||
|
|
||||||
|
```
|
||||||
|
# התהליך הישן: online 5D אבל GET / נכשל
|
||||||
|
GET / 500 — ENOENT: no such file or directory,
|
||||||
|
open '.../@paperclipai/server/ui-dist/index.html'
|
||||||
|
/api/health → 200 # שורד כי לא קורא קבצים
|
||||||
|
|
||||||
|
# אחרי restart: crash-loop
|
||||||
|
pm2 describe paperclip → status: "waiting restart", restarts: 36, nothing on :3100
|
||||||
|
ERROR log → "Paperclip server failed to start.
|
||||||
|
authenticated public deployments require DATABASE_URL ...;
|
||||||
|
refusing embedded PostgreSQL fallback"
|
||||||
|
```
|
||||||
|
|
||||||
|
הורדת החבילה איטית (~30ש', native builds) — מה שמחמיר את ה-loop: `min_uptime` של PM2 קוטע את ה-npx **באמצע ההורדה** לפני שהוא מסיים לחלץ, כך שה-cache לעולם לא מתמלא.
|
||||||
|
|
||||||
|
### ההשפעה על הצינור שלנו
|
||||||
|
|
||||||
|
Paperclip מושבת לגמרי — ה-UI לא עולה לאף משתמש, וכל סוכני Paperclip (14 הסוכנים) לא יכולים לרוץ כי הם חולקים את התהליך הזה.
|
||||||
|
|
||||||
|
### תיקון — שער סינכרוני לפני הפעלת השרת
|
||||||
|
|
||||||
|
**שורש הבעיה:** פאטץ' ה-cloud-db-bypass חייב להיות על הדיסק **לפני** שהשרת רץ; לולאת-הרקע מאוחרת מדי. ב-[start-paperclip.sh](../../.paperclip/scripts/start-paperclip.sh) נוספה `ensure_patched_before_run()` (06/06/26) שרצה סינכרונית לפני `exec`:
|
||||||
|
|
||||||
|
1. בודקת אם `@paperclipai/server/ui-dist/index.html` קיים ב-cache (ראה "מלכודות בדרך" — זה הסמן הנכון, לא `dist/index.js`).
|
||||||
|
2. אם לא — מריצה `npx -y paperclipai@<version> --help`. זה מאלץ את npx **לחלץ את כל החבילה** (כולל `ui-dist/`) כדי להריץ את ה-CLI, שמדפיס help ו**יוצא לבד ב-exit 0** — **לא** מפעיל שרת ולא תופס את 3100 (אומת). אין תהליך-רקע, אין שרת לא-מתוקן מוקדם, ואין מה להרוג.
|
||||||
|
3. מחילה את **כל** הפאטצ'ים (כולל bypass) על ה-cache המחולץ — עם guard שלא מפיל את ה-wrapper אם patch נכשל.
|
||||||
|
4. רק אז `exec npx ... run` — npx עושה reuse ל-cache המתוקן והשרת עולה נקי.
|
||||||
|
|
||||||
|
לולאת-הרקע (post-exec) נשמרה כרשת-ביטחון idempotent.
|
||||||
|
|
||||||
|
**אומת מקצה-לקצה (06/06/26):** מחיקת ה-cache בכוונה + `pm2 restart` → השער חילץ אוטומטית דרך `--help` (~64ש'), תיקן, והשרת עלה ל-200 ב-~72ש'. מונה הריסטארטים של PM2 **לא זז** (אפס crash-loop).
|
||||||
|
|
||||||
|
> **מלכודות שהתגלו בדרך (גרסה ראשונה של הפיקס נכשלה):**
|
||||||
|
> 1. **סמן חילוץ שגוי** — `dist/index.js` נכתב ~שניות **לפני** `ui-dist/`. שער שממתין ל-`dist` ומריץ מיד → ui-dist עדיין חסר → 500. הסמן הנכון הוא `ui-dist/index.html` (הקובץ האחרון, וגם זה שגרם ל-500 המקורי).
|
||||||
|
> 2. **`set -e` + patch כושל** — אם `apply-hebrew.sh` רץ בלי ui-dist הוא מחזיר שגיאה, ותחת `set -e` ה-wrapper מת → crash-loop חדש. הפתרון: `apply_all_patches || echo WARNING`.
|
||||||
|
> 3. **`pkill -f "paperclipai@..."` תופס את עצמו** — מחרוזת הדפוס מופיעה ב-command line של ה-shell שמריץ את ה-pkill, אז הוא הורג את עצמו (exit 144). זו הסיבה שגישת spawn-`run`-then-`pkill` ננטשה לטובת `--help` שיוצא לבד. אם בכל זאת צריך להרוג — לפי PID (`kill $PID; pkill -P $PID`), לא לפי `-f`.
|
||||||
|
|
||||||
|
**שחזור** — עם הפיקס פרוס, מספיק `pm2 restart paperclip` וה-`ensure_patched_before_run()` מתאושש לבד. אם צריך לעשות זאת ידנית (fix אחר, דיבוג):
|
||||||
|
```bash
|
||||||
|
pm2 stop paperclip # לעצור loop אם קיים
|
||||||
|
export PATH=/home/chaim/.nvm/versions/node/v24.14.0/bin:$PATH
|
||||||
|
npx -y paperclipai@2026.529.0 --help >/dev/null 2>&1 # חילוץ נקי שיוצא לבד (לא מפעיל שרת)
|
||||||
|
find ~/.npm/_npx -path "*@paperclipai/server/ui-dist/index.html" -type f # לאמת חילוץ מלא
|
||||||
|
# להחיל פאטצ'ים על ה-cache, ובמיוחד ה-bypass:
|
||||||
|
bash ~/.paperclip/hermes-patches/apply-cloud-db-bypass.sh
|
||||||
|
bash ~/.paperclip/hebrew/apply-hebrew.sh
|
||||||
|
bash ~/.paperclip/hermes-patches/apply-hermes-fixes.sh
|
||||||
|
bash ~/.paperclip/hermes-patches/apply-deepseek-reaper-fix.sh
|
||||||
|
grep -q HEBREW_PATCH_BYPASS_CLOUD_DB \
|
||||||
|
~/.npm/_npx/*/node_modules/@paperclipai/server/dist/index.js && echo "BYPASS OK"
|
||||||
|
pm2 start paperclip && pm2 save # reuse ל-cache המתוקן
|
||||||
|
```
|
||||||
|
> אל תשתמש ב-`pkill -f "paperclipai@..."` / `-f "@paperclipai/server"` — הדפוס תופס את ה-shell של עצמך (exit 144). אם חייבים להרוג תהליך — לפי PID.
|
||||||
|
|
||||||
|
### סטטוס
|
||||||
|
|
||||||
|
- **תוקן ב-start script** ע"י `ensure_patched_before_run()` (06/06/26) — שער סינכרוני שמחלץ+מתקן לפני exec.
|
||||||
|
- **הערה מטעה תוקנה**: ההערה הישנה בראש ה-script טענה ש-`npx run` מחלץ-מחדש בכל הפעלה (לכן הסתמכו על לולאת-הרקע בלבד) — זה לא נכון, npx עושה reuse ל-cache תקין; הסכנה היא cache **מחוק**.
|
||||||
|
- **לקח כללי**: כל patch שה-target שלו הוא assert בזמן-startup חייב להיות מוחל לפני `exec`, לא בלולאת-רקע.
|
||||||
|
|||||||
299
docs/spec/00-constitution.md
Normal file
299
docs/spec/00-constitution.md
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
# 00 — חוקת המערכת (Constitution)
|
||||||
|
|
||||||
|
זהו שער-הכניסה היחיד לספ המערכת *עוזר משפטי*. הוא מגדיר את הייעוד, עקרונות-העבודה,
|
||||||
|
תבנית ה-invariant, פרוטוקול-האימות, ה-invariants הגלובליים (G1–G11), כללי-ההנדסה,
|
||||||
|
אינדקס הספ ונספח המקורות. כל קובץ-תחום (01–07, X1–X5) כפוף לחוקה זו ומפנה אליה.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. ייעוד
|
||||||
|
|
||||||
|
> מערכת AI שמסייעת ליו"ר ועדת הערר לתכנון ובנייה (מחוז ירושלים, עו"ד דפנה תמיר) לנסח
|
||||||
|
> **החלטות מעין-שיפוטיות כתובות ומנומקות** — מסמכים משפטיים פורמליים שעומדים לביקורת
|
||||||
|
> שיפוטית — תוך שמירה על **הקול, השיקול והאחריות של היו"ר**.
|
||||||
|
|
||||||
|
- **משרת:** יו"ר הוועדה (משתמש-על) והסוכנים הפועלים בשמה.
|
||||||
|
- **מחזור-חיים:** ניהול תיקים → בסיס ידע (3 קורפוסים) → אחזור סמנטי (RAG) → סיוע-כתיבה
|
||||||
|
(12 בלוקים, סגנון דפנה) → ייצוא DOCX.
|
||||||
|
- **3 סוגי עררים:** רישוי ובנייה (1xxx, חם), היטל השבחה (8xxx, קר), פיצויים ס'197 (9xxx, קר).
|
||||||
|
- **ה"למה" העמוק:** המערכת מסייעת — היו"ר מכריעה (שערים קריטיים ידניים בכוונה); מנוע
|
||||||
|
צבירת-ידע (לומד מהחלטות סופיות ומפידבק); רב-חברתי (CMP/CMPA).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. עקרונות-עבודה
|
||||||
|
|
||||||
|
1. **אסור להניח שהקיים תקין (בהנדסה).** כל מה שמופה בקוד = "טענה לבדיקה", לא "אמת".
|
||||||
|
"תקין" מבחינה הנדסית נגזר ממקורות חיצוניים סמכותיים, לא מהמערכת שתחת חשד.
|
||||||
|
2. **פרוטוקול אימות 3-מקורות — חל על החלטות הנדסה/פיתוח בלבד:** כל invariant הנדסי/
|
||||||
|
ארכיטקטוני (תכנון ובניית האפליקציה — נתונים, מזהים, ingest, אחזור) מגובה ב-**≥3 מקורות
|
||||||
|
סמכותיים מוכרים** בעלי ידע מקצועי מוכח. כשאין 3 → מסומן `⚠ UNVERIFIED` ומועלה ליו"ר.
|
||||||
|
**התוכן המשפטי אינו כפוף לכלל זה** — הסמכות עליו היא היו"ר (דפנה) ומסמכי-הפרויקט
|
||||||
|
(block-schema, decision-methodology, legal-decision-lessons, skills/decision), לא
|
||||||
|
מקורות חיצוניים.
|
||||||
|
3. **מנגנון:** מחקר עצמאי → טיוטה לביקורת. קודם חוקרים את הסמכויות החיצוניות (להחלטות
|
||||||
|
הנדסה), ורק אז מנסחים את ה-invariant.
|
||||||
|
4. **מודל-שיתוף:** על החלטות טכניות/אדריכליות אני חוקר ומכריע מקצועית ומציג תוצאה
|
||||||
|
מוגמרת. שואל את היו"ר (חיים) רק במקום שבו *הוא* הסמכות — כוונה, עדיפויות עסקיות,
|
||||||
|
ותוכן משפטי-דומייני.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. תבנית-invariant
|
||||||
|
|
||||||
|
מבנה אחיד לכל חוק בספ (בכל הקבצים):
|
||||||
|
|
||||||
|
```
|
||||||
|
### INV-<תחום><מספר>: <כותרת קצרה>
|
||||||
|
**כלל:** <ניסוח נורמטיבי חד — מה חייב להתקיים>
|
||||||
|
**מקורות:** <≥3 סמכויות> | סטטוס: verified / ⚠ UNVERIFIED
|
||||||
|
**אכיפה:** <היכן/איך נאכף — schema / ולידציית-כתיבה / בדיקת-בריאות / שער אנושי>
|
||||||
|
**הפרה ידועה:** <דוגמה מהמערכת, אם יש — מקשר ל-audit; אחרת "—">
|
||||||
|
```
|
||||||
|
|
||||||
|
> **שדה המקורות לפי סוג invariant (שלושה מודלי-סמכות):**
|
||||||
|
> 1. **הנדסי** (תאוריה כללית — נתונים/אחזור/ארכיטקטורה) → `מקורות` = ≥3 סמכויות חיצוניות + `סטטוס`.
|
||||||
|
> 2. **תוכן-משפטי** → `מקור-סמכות` = היו"ר + מסמכי-הפרויקט (ללא סטטוס-אימות חיצוני).
|
||||||
|
> 3. **פרויקטלי-תפעולי** (עובדות על האינטגרציה/התפעול של *מערכת זו* — אין להן סמכות
|
||||||
|
> חיצונית, למשל "wakeup דרך API") → `מקור-סמכות` = ה-runbooks של הפרויקט
|
||||||
|
> (CLAUDE.md, HEARTBEAT.md, סקריפטים), **קשור** ל-invariant הנדסי גלובלי שאותו הוא מיישם.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. פרוטוקול-אימות
|
||||||
|
|
||||||
|
> חל על **invariants הנדסיים (G1–G10)** — החלטות תכנון/בניית האפליקציה. ה-invariant של
|
||||||
|
> תוכן-משפטי (G11) **אינו** כפוף לפרוטוקול זה; הסמכות עליו היא היו"ר + מסמכי-הפרויקט.
|
||||||
|
|
||||||
|
- כל invariant הנדסי נושא שדה `מקורות` + `סטטוס: verified / ⚠ UNVERIFIED`.
|
||||||
|
- **verified** = מגובה ב-**≥3 מקורות סמכותיים** מוכרים בעלי ידע מקצועי מוכח.
|
||||||
|
- **⚠ UNVERIFIED** = החלטה הנדסית שיש לה פחות מ-3 מקורות סמכותיים מאומתים. פריט כזה
|
||||||
|
**לא מוכרע לבד** — מועלה ליו"ר עם הערת-הסלמה המתעדת מה חסר והיכן יאומת.
|
||||||
|
- החלטות טכניות → מחקר עצמאי + הכרעה מקצועית + הצגת תוצאה. שאלה ליו"ר רק במקום
|
||||||
|
שבו הוא הסמכות (ראה עיקרון 4 לעיל).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Invariants גלובליים
|
||||||
|
|
||||||
|
אלה החוקים החוצים את כל המערכת — לב החוקה. הם נחלקים לשני סוגים לפי **מקור-הסמכות**:
|
||||||
|
|
||||||
|
- **G1–G10 — invariants הנדסיים** (תכנון/בניית האפליקציה): כל אחד מגובה ב-**≥3 סמכויות
|
||||||
|
טכניות מוכרות** (נספח §8). ביחד הם מייבשים את כשל-השורש החוזר: מסלולים/קורפוסים
|
||||||
|
מקבילים שמתפצלים (drift) בלי שכבה שמגדירה ואוכפת "תקין".
|
||||||
|
- **G11 — invariant תוכן-משפטי:** הסמכות עליו היא **היו"ר (דפנה) + מסמכי-הפרויקט**, לא
|
||||||
|
מקורות חיצוניים, ואינו כפוף לפרוטוקול ≥3-המקורות.
|
||||||
|
|
||||||
|
### 5א. Invariants הנדסיים (G1–G10)
|
||||||
|
|
||||||
|
### INV-G1: מזהה קנוני מנורמל בכתיבה
|
||||||
|
**כלל:** לכל ישות יש מזהה קנוני יחיד, **מנורמל בנקודת-הכתיבה** (לא תיקון-סלחני בקריאה
|
||||||
|
בלבד). `case_number` נשמר בצורה קנונית אחת; קריאה משווה מול הצורה הקנונית, לא מטליאה.
|
||||||
|
**מקורות:** SSOT (Single Source of Truth — normalization principle) · E.F. Codd, First
|
||||||
|
Normal Form (CACM 13(6), 1970) · Martin Kleppmann, *Designing Data-Intensive Applications*
|
||||||
|
(O'Reilly, 2017) | סטטוס: verified
|
||||||
|
**אכיפה:** schema (אילוץ ייחודיות על המפתח הקנוני) + ולידציית-כתיבה בנקודת-הקליטה;
|
||||||
|
מפורט ב-[X1-identifiers.md](X1-identifiers.md) ו-[02-data-model.md](02-data-model.md).
|
||||||
|
**הפרה ידועה:** `_normalize_case_number` סלחני בקריאה בלבד (קומיט "tolerant case_number
|
||||||
|
lookup"); `8126-25` לא נמצא מול האמיתי `8126-03-25` → ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-G2: מקור-אמת יחיד — אין מסלולים מקבילים מתפצלים
|
||||||
|
**כלל:** לכל סוג-נתון יש **מקור-אמת יחיד** ומסלול-קוד קנוני אחד. אסור להוסיף מסלול
|
||||||
|
מקביל ליכולת קיימת — ישויות-אחיות חולקות מסלול קנוני אחד; נתונים נגזרים (derived)
|
||||||
|
משוחזרים מהמקור, לא נכתבים במקביל.
|
||||||
|
**מקורות:** Martin Kleppmann (system of record vs. derived data, *DDIA* 2017) · Martin
|
||||||
|
Fowler (Canonical Data Model) · SSOT (Single Source of Truth) | סטטוס: verified
|
||||||
|
**אכיפה:** ביקורת-ארכיטקטורה + כלל-הנדסה "סימטריה" (§6); מפורט ב-[01-ingest.md](01-ingest.md).
|
||||||
|
**הפרה ידועה:** שני מסלולי ingest מקבילים לישויות-אחיות (`ingest_precedent` מול
|
||||||
|
`ingest_internal_decision`) שמתפצלים — לדוגמה: המסלול החיצוני מתזמן חילוץ metadata
|
||||||
|
(`request_metadata_extraction`), והמסלול הפנימי לא — ולכן ערן סופר 8046/24 נקלטה בלי
|
||||||
|
metadata → ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-G3: ingest אחיד ו-idempotent
|
||||||
|
**כלל:** קליטה היא **אחידה ו-idempotent** — upsert על מפתח דטרמיניסטי. קליטה חוזרת של
|
||||||
|
אותו פריט אינה יוצרת כפילות ואינה משנה תוצאה.
|
||||||
|
**מקורות:** Martin Kleppmann (*DDIA*, idempotence & exactly-once) · Stripe / CDC
|
||||||
|
idempotency-key pattern · ISO 8000 (Data quality) | סטטוס: verified
|
||||||
|
**אכיפה:** ולידציית-כתיבה + מפתח-upsert דטרמיניסטי בנקודת-הקליטה; מפורט ב-
|
||||||
|
[01-ingest.md](01-ingest.md).
|
||||||
|
**הפרה ידועה:** 3 החלטות "סופר" נקלטו ב-3 פורמטים שונים (`8126/24`, ציטוט-מלא
|
||||||
|
כ-case_number) — היעדר upsert דטרמיניסטי → ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-G4: חוזה-שלמות לפני "שמיש / ניתן-לחיפוש"
|
||||||
|
**כלל:** רשומה אינה נחשבת "שמישה" או "ניתנת-לחיפוש" עד ש**שדות-החובה שלה מולאו ואומתו
|
||||||
|
מול spec מפורש**. שלמות נבדקת לפני חשיפה לאחזור.
|
||||||
|
**מקורות:** ISO 8000 (completeness) · DAMA-UK *Six Primary Dimensions for Data Quality*
|
||||||
|
(2013, completeness) · ISO 15489-1:2016 (records reliability) | סטטוס: verified
|
||||||
|
**אכיפה:** חוזה-שלמות באכיפת-קוד + בדיקת-בריאות; מפורט ב-[02-data-model.md](02-data-model.md)
|
||||||
|
ו-[03-retrieval.md](03-retrieval.md).
|
||||||
|
**הפרה ידועה:** ערן סופר 8046/24 אונדקס עם `headnote`/`summary`/`tags` ריקים → ממצא
|
||||||
|
ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-G5: metadata מלא + הפרדת-קורפוס נאכפת בכל query
|
||||||
|
**כלל:** לכל פריט מואנדקס יש **metadata מלא** (כולל מזהה-מקור וסוג-קורפוס), ו**הפרדת-
|
||||||
|
הקורפוס נאכפת בכל מסלול-query** — אין דליפה בין 3 הקורפוסים.
|
||||||
|
**מקורות:** Pinecone (multitenancy / metadata filtering) · RAG attribution (Lewis et al.,
|
||||||
|
2020, NeurIPS) · ISO 8000 (Data quality) | סטטוס: verified
|
||||||
|
**אכיפה:** schema (metadata חובה) + פילטר-קורפוס נאכף בשכבת-החיפוש; מפורט ב-
|
||||||
|
[03-retrieval.md](03-retrieval.md) ו-[X5-audit-provenance.md](X5-audit-provenance.md).
|
||||||
|
**הפרה ידועה:** משימה #56 — דליפת `source_kind` ב-`halacha_filters` בין קורפוסים →
|
||||||
|
ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-G6: re-index בכל שינוי תוכן
|
||||||
|
**כלל:** כל שינוי-תוכן של פריט מואנדקס מפעיל **re-index** של ה-embedding שלו. אין
|
||||||
|
embeddings מיושנים מול התוכן הנוכחי.
|
||||||
|
**מקורות:** Pinecone (index freshness / data sync) · Weaviate (re-vectorization on update)
|
||||||
|
· RAG freshness (Lewis et al., 2020) | סטטוס: verified
|
||||||
|
**אכיפה:** טריגר re-index בנקודת-העדכון + בדיקת-בריאות (גילוי drift); מפורט ב-
|
||||||
|
[02-data-model.md](02-data-model.md) ו-[03-retrieval.md](03-retrieval.md).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-G7: מיזוג RRF — לא סכום-ציונים
|
||||||
|
**כלל:** מיזוג תוצאות בין retrievers נעשה **לפי דירוג (Reciprocal Rank Fusion)**, לא
|
||||||
|
סכום/ממוצע ציונים גולמיים — שכן ציונים בסקיילים שונים אינם בני-השוואה ישירה.
|
||||||
|
**מקורות:** Elastic (*Reciprocal Rank Fusion*) · Weaviate (*Hybrid Search Explained*) ·
|
||||||
|
OpenSearch / Azure AI Search (corroborating RRF guidance) | סטטוס: verified
|
||||||
|
**אכיפה:** קוד-המיזוג בשכבת-האחזור; מפורט ב-[03-retrieval.md](03-retrieval.md).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-G8: איכות-אחזור נמדדת — precision + recall
|
||||||
|
**כלל:** איכות-האחזור **נמדדת אמפירית (precision + recall)** באמצעות eval harness, לא
|
||||||
|
מונחת. שינוי בשכבת-האחזור מלווה במדידה.
|
||||||
|
**מקורות:** Manning, Raghavan & Schütze, *Introduction to Information Retrieval* (CUP,
|
||||||
|
2008) · RAG evaluation literature (Lewis et al., 2020 ואחריו) · Elastic (relevance
|
||||||
|
evaluation guidance) | סטטוס: verified
|
||||||
|
**אכיפה:** eval harness + בדיקת-בריאות תקופתית; מפורט ב-[03-retrieval.md](03-retrieval.md).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-G9: עקיבוּת-מקור + audit-trail ל-AI
|
||||||
|
**כלל:** כל פלט של המערכת **עקיב למקורו** (citation/provenance), וכל שימוש ב-AI מתועד
|
||||||
|
ב-**audit-trail** הניתן לביקורת.
|
||||||
|
**מקורות:** Council of Europe / CEPEJ — *European Ethical Charter on AI in judicial systems*
|
||||||
|
(2018, user-control principle) · NCSC/JTC — *Principles & Practices for AI Use in Courts* ·
|
||||||
|
ISO 15489-1:2016 (records authenticity/integrity) | סטטוס: verified
|
||||||
|
**אכיפה:** audit-trail באכיפת-קוד + עקיבוּת-מקור בכל פלט; מפורט ב-
|
||||||
|
[X5-audit-provenance.md](X5-audit-provenance.md).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-G10: המערכת מסייעת — שערים אנושיים הם invariant
|
||||||
|
**כלל:** המערכת **מסייעת ואינה מחליפה את שיקול-הדעת האנושי**. השערים האנושיים (אישור
|
||||||
|
הלכה, בחירת תוצאה, פידבק היו"ר) הם **invariant — חובה, לא רשות**.
|
||||||
|
**תיקון (החלטת-יו"ר 2026-05-31):** שער אישור-ההלכה יכול להיות מסופק ע"י **טיפול שיפוטי מצטבר**
|
||||||
|
(citator פנימי), לא רק ע"י היו"ר — הלכה ש**אומצה (followed) ע"י ≥N ערכאות/ועדות מצטטות, ללא
|
||||||
|
טיפול שלילי**, מאושרת אוטומטית. זהו **שיפוט אנושי** (של המצטטים), לא שיפוט-AI (ה-AI רק מזהה
|
||||||
|
ומסווג את הטיפול הקיים). **שער-היו"ר נשאר חובה** לזנב הלא-מצוטט ולכל טיפול שלילי
|
||||||
|
(distinguished/overruled). מפורט ב-[X11-citation-corroboration.md](X11-citation-corroboration.md)
|
||||||
|
(INV-COR1–COR6).
|
||||||
|
**מקורות:** NCSC/JTC — *Principles & Practices for AI Use in Courts* ("never replace human
|
||||||
|
judgment") · CEPEJ (2018, under user control) · Federal Judicial Center — *Judicial Writing
|
||||||
|
Manual* (2d ed.) · [לתיקון — מקורות פתוחים:] Fowler et al., *Network Analysis and the Law*
|
||||||
|
(Political Analysis 15:3, 2007) — ציטוטים-נכנסים = מדד-סמכות · Demir & Canbaz, *Validate Your
|
||||||
|
Authority: Benchmarking LLMs on Multi-Label Precedent Treatment Classification* (NLLP/ACL, 2025) ·
|
||||||
|
Hellyer (Law Library Journal 110:4, 2018, open-access) — טיפול-שיפוטי-מצטבר כמתודולוגיה מתועדת
|
||||||
|
| סטטוס: verified
|
||||||
|
**אכיפה:** שערים אנושיים בקוד-הזרימה (gate לא ניתן לעקיפה); מסלול-corroboration ב-
|
||||||
|
[X11](X11-citation-corroboration.md); מפורט ב-[05-qa-review.md](05-qa-review.md).
|
||||||
|
**הפרה ידועה:** 10/19 הלכות מאושרות, התגלה במקרה — שער ידני שקוף בלי נראות backlog →
|
||||||
|
ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### 5ב. Invariant תוכן-משפטי (G11)
|
||||||
|
|
||||||
|
### INV-G11: תוכן החלטה מנומקת
|
||||||
|
**כלל:** החלטה מנומקת מקיימת: **רקע ניטרלי** (עובדות בלבד, ללא שיפוט) · **ללא כפילות**
|
||||||
|
(בלוק דיון מפנה, לא חוזר) · **מענה לטענות הצד המפסיד** · **"מבחן-השופט"** (קריא לשופט שלא
|
||||||
|
מכיר את התיק) · **טענות מקוריות בלבד** (מכתבי הטענות).
|
||||||
|
**מקור-סמכות:** היו"ר (עו"ד דפנה תמיר) + מסמכי-הפרויקט — [block-schema.md](../block-schema.md),
|
||||||
|
[decision-methodology.md](../decision-methodology.md), [legal-decision-lessons.md](../legal-decision-lessons.md),
|
||||||
|
[skills/decision/SKILL.md](../../skills/decision/SKILL.md). **אינו כפוף לפרוטוקול ≥3-המקורות החיצוני** —
|
||||||
|
זהו תוכן משפטי-דומייני, באחריות היו"ר.
|
||||||
|
**אכיפה:** שערי QA + checklist-תוכן לפי סוג-ערר; מפורט ב-[04-analysis-writing.md](04-analysis-writing.md)
|
||||||
|
ו-[05-qa-review.md](05-qa-review.md).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. כללי-הנדסה (מונעים הישנות)
|
||||||
|
|
||||||
|
- **סימטריה:** אסור להוסיף מסלול מקביל ליכולת קיימת — מרחיבים את המסלול הקנוני
|
||||||
|
(נגזר מ-[G2](#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)).
|
||||||
|
- **נרמול לא תיקון-תסמין:** מתקנים נתון במקור (קנוני), לא מטליאים בקריאה
|
||||||
|
(נגזר מ-[G1](#inv-g1-מזהה-קנוני-מנורמל-בכתיבה)).
|
||||||
|
- **Quality-at-source:** שלמות נאכפת קרוב ככל האפשר לקליטה (Martin Fowler — Data Mesh /
|
||||||
|
quality-at-source; נגזר מ-[G4](#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש)).
|
||||||
|
- **אין בליעה שקטה:** רשומה חסרה/פגומה מסומנת ומדווחת, לא מתקבלת בשקט (תואם feedback
|
||||||
|
קיים — אסור bare `except: pass`; נגזר מ-[G4](#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. אינדקס הספ
|
||||||
|
|
||||||
|
> הערה: כל קבצי הספ (00, 01–07, X1–X12) קיימים. החוקה היא שער-הכניסה; כל קובץ-תחום כפוף לה.
|
||||||
|
|
||||||
|
| קובץ | תפקיד | אוכף invariants |
|
||||||
|
|------|--------|-----------------|
|
||||||
|
| [00-constitution.md](00-constitution.md) | חוקה — ייעוד, invariants גלובליים, כללי-הנדסה, אינדקס | G1–G11 |
|
||||||
|
| [01-ingest.md](01-ingest.md) | קליטה מאוחדת: מסמכי-תיק / פסיקה חיצונית / החלטות-ועדה — חוזה מסלול-יחיד | G2, G3 |
|
||||||
|
| [02-data-model.md](02-data-model.md) | אחסון: ישויות (cases, case_law, documents, chunks, halachot…) + חוזה-שלמות לכל ישות | G1, G4, G6 |
|
||||||
|
| [03-retrieval.md](03-retrieval.md) | 3 קורפוסים + כלי-חיפוש · hybrid/RRF · attribution · eval harness | G4, G5, G6, G7, G8, G9 |
|
||||||
|
| [04-analysis-writing.md](04-analysis-writing.md) | חילוץ טענות · 12 בלוקים · סגנון דפנה (מצטט block-schema.md) | G11 |
|
||||||
|
| [05-qa-review.md](05-qa-review.md) | שערי QA + שערים אנושיים (אישור הלכה, בחירת תוצאה, פידבק) כ-invariant | G10, G11 |
|
||||||
|
| [06-export.md](06-export.md) | ייצוא DOCX לפי תבנית דפנה | G2, G9 |
|
||||||
|
| [07-learning.md](07-learning.md) | Hermes · לקחים · לולאת פידבק היו"ר · צמיחת קורפוס (quality-at-source) | G4, G10 |
|
||||||
|
| [X1-identifiers.md](X1-identifiers.md) | מודל מזהים קנוני: נרמול case_number בכתיבה · cases מול case_law · פורמטי ציטוט | G1 |
|
||||||
|
| [X2-multi-company.md](X2-multi-company.md) | CMP/CMPA · 14 סוכנים · כללי sync | G2 |
|
||||||
|
| [X3-integration-deploy.md](X3-integration-deploy.md) | Paperclip (wakeup, ניתוב comments, webhooks) · Coolify/pm2 | G2, G9 (תפעולי) |
|
||||||
|
| [X4-agents.md](X4-agents.md) | מפת הסוכנים (דומיין + סוכני-התהליך) | G10 |
|
||||||
|
| [X5-audit-provenance.md](X5-audit-provenance.md) | audit-trail לשימוש ב-AI · עקיבוּת כל מקור מצוטט · שלמות-רשומה | G5, G9 |
|
||||||
|
| [X6-ui-api-contract.md](X6-ui-api-contract.md) | web-ui ↔ API: OpenAPI=SSoT · response models · envelope · SSE · חוזי-טופס + כללי-עיצוב | G2, G4, G9 (UI) |
|
||||||
|
| [X7-paperclip-client-params.md](X7-paperclip-client-params.md) | לקוח-Paperclip קנוני · IDs/env/keys מ-config · webhook idempotency/אירוע מגורס | G2, G9 (תפעולי) |
|
||||||
|
| [X8-field-provenance.md](X8-field-provenance.md) | מקור-מילוי כל שדה (דטרמיניסטי/Opus/ידני/נגזר) · preservation · trust · verbatim-quote | G9, G10 |
|
||||||
|
| [X9-mcp-tool-contract.md](X9-mcp-tool-contract.md) | חוזה 71 כלי-ה-MCP: envelope · שמות · idempotency · extract/get-symmetry · שלמות-הרשאות | G2, G3, G10 |
|
||||||
|
| [X10-deploy-env-secrets.md](X10-deploy-env-secrets.md) | env-catalog SSoT · מקור-config יחיד (Coolify) · ללא hardcode · secrets · drift | G2, G4, G9 |
|
||||||
|
| [X11-citation-corroboration.md](X11-citation-corroboration.md) | citator פנימי — תיקוף הלכות בטיפול-שיפוטי מצטבר · תיקון-G10 מבוקר · סף-corroboration · התאמה-להלכה | G9, G10 |
|
||||||
|
| [X12-digests-radar.md](X12-digests-radar.md) | יומונים כשכבת-גילוי (radar) — מקור-משני המצביע על הפסק המקורי · לא קורפוס-ציטוט רביעי · לא מצוטט/לא מחלץ-הלכות | G2, G4, G9 |
|
||||||
|
| [X13-court-fetch.md](X13-court-fetch.md) | אחזור-פסיקה אוטומטי מנט המשפט — 3 שכבות (עליון/מנהלי/skip) · שירות-מארח · reCAPTCHA · שער-אנושי | G2, G3, G4, G5, G9, G10 |
|
||||||
|
|
||||||
|
> **X6–X10 (מחזור-2):** מכסים את 8 משטחי-האפליקציה שמחוץ לצינור-הליבה (אינטגרציה, web-ui, מילוי-שדות,
|
||||||
|
> אחסון-ניתוחים, כלי-MCP, deploy/env). הממצאים ב-[gap-audit.md](gap-audit.md) (GAP-24..62 → FU-9..15)
|
||||||
|
> וב-[ui-audit.md](ui-audit.md). הרחבות-אחות: [02-data-model](02-data-model.md) (INV-DM4–DM6), [X4-agents](X4-agents.md) (INV-AG3).
|
||||||
|
|
||||||
|
**עקרונות:** כל קובץ עצמאי, ממוקד, agent-readable, יעד ≤~500 שורות (תפיחה = סימן
|
||||||
|
לפיצול). מסמכים קיימים (`architecture.md`, `product-specification.md`, `block-schema.md`…)
|
||||||
|
לא נמחקים ולא משוכפלים — מצוטטים כ"מקור" ומאומתים מול הסמכויות; סתירה = ממצא ל-audit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. נספח מקורות סמכותיים
|
||||||
|
|
||||||
|
(מאומתים במחקר 30.5.2026)
|
||||||
|
|
||||||
|
**ממשל-AI שיפוטי + שערים אנושיים (G9, G10)**
|
||||||
|
- NCSC / JTC — *Court Technology Standards* + *Principles & Practices for AI Use in Courts*.
|
||||||
|
https://www.ncsc.org/our-centers-projects/joint-technology-committee/court-technology-standards
|
||||||
|
- Council of Europe / CEPEJ — *European Ethical Charter on the use of AI in judicial
|
||||||
|
systems* (2018, user-control principle).
|
||||||
|
- Federal Judicial Center — *Judicial Writing Manual* (2d ed.) — לעניין שיקול-הדעת
|
||||||
|
האנושי בכתיבה השיפוטית.
|
||||||
|
https://www.fjc.gov/content/judicial-writing-manual-pocket-guide-judges-second-edition
|
||||||
|
|
||||||
|
**אחזור / RAG / IR**
|
||||||
|
- Lewis et al. (2020) — *Retrieval-Augmented Generation* (NeurIPS).
|
||||||
|
https://arxiv.org/abs/2005.11401
|
||||||
|
- Manning, Raghavan & Schütze — *Introduction to Information Retrieval* (CUP, 2008).
|
||||||
|
https://nlp.stanford.edu/IR-book/
|
||||||
|
- Elastic — *Reciprocal Rank Fusion*.
|
||||||
|
https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion
|
||||||
|
- Pinecone — *Implement multitenancy*.
|
||||||
|
https://docs.pinecone.io/guides/index-data/implement-multitenancy
|
||||||
|
- Weaviate — *Hybrid Search Explained*. https://weaviate.io/blog/hybrid-search-explained
|
||||||
|
|
||||||
|
**שלמות-נתונים / איכות / רשומות**
|
||||||
|
- DAMA-DMBOK2 + DAMA-UK — *Six Primary Dimensions for Data Quality* (2013).
|
||||||
|
- ISO 8000 — Data quality (8000-8/61/110).
|
||||||
|
- ISO 15489-1:2016 — Records management (authenticity/reliability/integrity/usability).
|
||||||
|
- Martin Kleppmann — *Designing Data-Intensive Applications* (O'Reilly, 2017).
|
||||||
|
- E.F. Codd — Relational model & normalization (CACM 13(6), 1970).
|
||||||
|
- Martin Fowler — Canonical Data Model / Data Mesh (quality-at-source).
|
||||||
|
|
||||||
|
(נספח המקורות מתייחס ל-invariants ההנדסיים G1–G10 בלבד. התוכן המשפטי — G11 — נשען על
|
||||||
|
מסמכי-הפרויקט וסמכות היו"ר, כמפורט ב-G11.)
|
||||||
150
docs/spec/01-ingest.md
Normal file
150
docs/spec/01-ingest.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# 01 — קליטה מאוחדת (Unified Ingest Contract)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) ומפרט את **חוזה הקליטה** של כל סוגי
|
||||||
|
ה-intake. הוא אוכף את [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
(מקור-אמת יחיד, אין מסלולים מקבילים) ואת [G3](00-constitution.md#inv-g3-ingest-אחיד-ו-idempotent)
|
||||||
|
(ingest אחיד ו-idempotent), ונשען על [G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש)
|
||||||
|
ו-[G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן).
|
||||||
|
|
||||||
|
כשל-השורש שהקובץ מייבש: **שני מסלולי ingest לישויות-אחיות שמתפצלים** — `ingest_precedent`
|
||||||
|
(פסיקה חיצונית) מול `ingest_internal_decision` (החלטות-ועדה). מסלולים מקבילים גוררים drift:
|
||||||
|
פריט שנקלט במסלול אחד מקבל טיפול שונה מפריט במסלול האחר, והפער מתגלה רק כשרשומה חסרה
|
||||||
|
metadata או לא נמצאת בחיפוש. החוזה כאן מגדיר **מסלול קנוני אחד** ש-3 סוגי ה-intake עוברים בו.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. שלושת סוגי ה-intake
|
||||||
|
|
||||||
|
| סוג-intake | מזהה-קנוני | קורפוס-יעד | מאפיין ייחודי |
|
||||||
|
|------------|------------|------------|----------------|
|
||||||
|
| מסמכי-תיק (case documents) | `case_number` + מזהה-מסמך | תיק ערר פעיל | משויך לתיק, מסווג לפי סוג-מסמך |
|
||||||
|
| פסיקה חיצונית (external precedent) | `citation` (קנוני) | `case_law` (external) | staging לפי `source_type`, ולידציית-enums, citation guard, multimodal |
|
||||||
|
| החלטות-ועדה (internal-committee) | `case_number` (קנוני) | `case_law` (internal_committee) | staging לפי district, `chair_name` חובה, גזירת district/proceeding_type |
|
||||||
|
|
||||||
|
שלושתם הם **ישויות-אחיות**: אותו טיפוס-עיבוד (קובץ → טקסט → chunks → embeddings → metadata
|
||||||
|
→ הלכות), נבדלים בפרמטרים בלבד — לא במסלול-קוד. זוהי משמעות "סימטריה" (חוקה §6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. המסלול הקנוני (Canonical Pipeline)
|
||||||
|
|
||||||
|
צעדי-העיבוד, **בסדר מחייב**. כל סוג-intake עובר את אותם צעדים; ההבדל הוא אילו פרמטרים
|
||||||
|
מוזרקים בקלט, לא אילו צעדים מורצים.
|
||||||
|
|
||||||
|
1. **Stage file** — העתקה דטרמיניסטית לאחסון המתמיד. נתיב-ה-staging הוא פרמטר
|
||||||
|
(`source_type` לפסיקה חיצונית, district להחלטות-ועדה), לא ענף-קוד נפרד.
|
||||||
|
2. **Extract text** — `extractor.extract_text` → `(text, page_count, page_offsets)`.
|
||||||
|
טקסט ריק = כשל מדווח (לא בליעה שקטה; חוקה §6).
|
||||||
|
3. **Strip Nevo preamble** — `extractor.strip_nevo_preamble` להסרת עטיפת-Nevo. **אחיד לכל סוג.**
|
||||||
|
4. **Chunk** — היררכי (`chunk_document_hierarchical`) אם `PARENT_DOC_RETRIEVAL_ENABLED`,
|
||||||
|
אחרת שטוח (`chunk_document`). **אותו ענף-flag בדיוק לכל סוג** — בורר הצ'אנקינג נגזר
|
||||||
|
מ-config, לא מסוג-ה-intake.
|
||||||
|
5. **Embed** — `embeddings.embed_texts(..., input_type="document")` ל-children (היררכי)
|
||||||
|
או לכל ה-chunks (שטוח).
|
||||||
|
6. **Store chunks** — `store_precedent_chunks_hierarchical` או `store_precedent_chunks`.
|
||||||
|
7. **Page-image embed (multimodal)** — אם `MULTIMODAL_ENABLED` **וגם** הקובץ PDF
|
||||||
|
**וגם** `page_count>0`: הטמעת עמודי-תמונה (`_embed_precedent_pages`). non-fatal:
|
||||||
|
מסלול-הטקסט כבר הצליח. **התנאי אחיד** — הפעלה תלויה ב-flag+סוג-קובץ, לא בסוג-ה-intake.
|
||||||
|
8. **Queue metadata extraction** — `request_metadata_extraction(case_law_id)`. נדרש לכל
|
||||||
|
סוג שתומך במטא-דאטה (ראה [INV-ING3](#inv-ing3-תור-חילוץ-מטא-דאטה--הלכות-לכל-סוג)).
|
||||||
|
9. **Queue halacha extraction** — `request_halacha_extraction(case_law_id)`.
|
||||||
|
10. **Set statuses** — `extraction_status=completed`, `halacha_status=pending`.
|
||||||
|
החילוץ ה-LLM-י (metadata + הלכות) רץ בנפרד מ-Claude Code המקומי
|
||||||
|
(`precedent_process_pending`), כי `claude` CLI אינו זמין בקונטיינר.
|
||||||
|
|
||||||
|
> **צעדים שחייבים להיות אחידים בכל סוג (תיקון האסימטריה):** 2 (extract), 3 (strip-Nevo),
|
||||||
|
> 4 (בורר-chunk לפי flag), 5–6 (embed+store), **7 (multimodal — לפי flag+PDF, לא לפי
|
||||||
|
> סוג)**, **8–9 (תיזמון שני החילוצים)**, 10 (statuses). מה ש**רשאי** להשתנות לפי סוג:
|
||||||
|
> נתיב-ה-staging (צעד 1), ולידציות-קלט ספציפיות, וגזירת-שדות (district/proceeding_type)
|
||||||
|
> — אלו פרמטרים של אותו מסלול, לא מסלול נפרד.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-ING1: מסלול-קליטה קנוני יחיד
|
||||||
|
**כלל:** כל סוגי ה-intake (מסמכי-תיק / פסיקה חיצונית / החלטות-ועדה) זורמים דרך **פונקציית-
|
||||||
|
קליטה קנונית אחת**. סוג-intake חדש מורחב דרך **פרמטרים** של אותה פונקציה — לעולם לא דרך
|
||||||
|
פונקציה מקבילה. נתון-נגזר (district, proceeding_type) מחושב בתוך המסלול, לא בענף נפרד.
|
||||||
|
**מקורות:** Martin Kleppmann, *DDIA* (O'Reilly, 2017 — system of record יחיד) · Martin
|
||||||
|
Fowler (*Canonical Data Model*) · SSOT (Single Source of Truth) | סטטוס: verified
|
||||||
|
**אכיפה:** ביקורת-ארכיטקטורה + כלל-הנדסה "סימטריה" (חוקה §6); הקליטה מתנקזת לפונקציה אחת
|
||||||
|
שמקבלת פרמטרי-סוג. אוכף את [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים).
|
||||||
|
**הפרה ידועה:** היום קיימים **שני** מסלולים — `ingest_precedent`
|
||||||
|
(`precedent_library.py:88`) ו-`ingest_internal_decision` (`internal_decisions.py:73`) —
|
||||||
|
שמשכפלים את צעדי 2–10 ומתפצלים בפרטים → ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-ING2: קליטה idempotent על המזהה הקנוני
|
||||||
|
**כלל:** הקליטה היא **idempotent על המזהה הקנוני** (`citation` לפסיקה חיצונית,
|
||||||
|
`case_number` להחלטות-ועדה ולמסמכי-תיק). קליטה חוזרת של אותו פריט = **upsert** —
|
||||||
|
אין רשומה כפולה ואין chunks כפולים; התוצאה זהה.
|
||||||
|
**מקורות:** Martin Kleppmann, *DDIA* (idempotence & exactly-once) · Stripe / CDC
|
||||||
|
idempotency-key pattern · ISO 8000 (Data quality) | סטטוס: verified
|
||||||
|
**אכיפה:** מפתח-upsert דטרמיניסטי על המזהה הקנוני בנקודת-הקליטה (`create_external_case_law`
|
||||||
|
/ `create_internal_committee_decision`) + ולידציית-כתיבה; קשור ל-
|
||||||
|
[X1-identifiers.md](X1-identifiers.md) (נרמול בכתיבה). אוכף את
|
||||||
|
[G3](00-constitution.md#inv-g3-ingest-אחיד-ו-idempotent).
|
||||||
|
**הפרה ידועה:** 3 החלטות "סופר" נקלטו ב-3 פורמטים (`8126/24`, ציטוט-מלא כ-`case_number`)
|
||||||
|
— היעדר מפתח-upsert דטרמיניסטי גרר רשומות-כפל במקום עדכון → ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-ING3: תור חילוץ מטא-דאטה + הלכות לכל סוג
|
||||||
|
**כלל:** חילוץ-מטא-דאטה **וגם** חילוץ-הלכות מתוזמנים (queue) עבור **כל** סוג-intake שתומך
|
||||||
|
בהם — תיזמון אחיד, **לא** מותנה במסלול. שני התורים נפתחים יחד בסיום העיבוד הלא-LLM-י.
|
||||||
|
**מקורות:** ISO 8000 (completeness) · DAMA-UK *Six Primary Dimensions for Data Quality*
|
||||||
|
(2013, completeness) · Martin Fowler (quality-at-source) | סטטוס: verified
|
||||||
|
**אכיפה:** קריאה ל-`request_metadata_extraction` **ו**-`request_halacha_extraction`
|
||||||
|
בנקודת-סיום-הקליטה, לכל סוג; חוזה-שלמות יסמן רשומה ללא מטא-דאטה כלא-שמישה
|
||||||
|
([G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש), מפורט ב-
|
||||||
|
[02-data-model.md](02-data-model.md)).
|
||||||
|
**הפרה ידועה:** המסלול הפנימי (`internal_decisions.py:208`) מתזמן **רק**
|
||||||
|
`request_halacha_extraction` ואינו קורא ל-`request_metadata_extraction` (בניגוד
|
||||||
|
ל-`precedent_library.py:292-293` שקורא לשניהם) → ערן סופר 8046/24 נקלטה **בלי
|
||||||
|
metadata** (headnote/summary/tags ריקים) → ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-ING4: re-index בקליטה-חוזרת (upsert ⇒ re-embed)
|
||||||
|
**כלל:** קליטה-חוזרת ששינתה את תוכן-הפריט מפעילה **re-index** — chunks ו-embeddings
|
||||||
|
ישנים נמחקים ונבנים מחדש מהתוכן החדש. אין embeddings מיושנים אחרי upsert.
|
||||||
|
**מקורות:** Pinecone (index freshness / data sync) · Weaviate (re-vectorization on update)
|
||||||
|
· RAG freshness (Lewis et al., 2020, NeurIPS) | סטטוס: verified
|
||||||
|
**אכיפה:** טריגר re-embed בנתיב ה-upsert של הקליטה + בדיקת-בריאות לגילוי drift; מפורט
|
||||||
|
ב-[02-data-model.md](02-data-model.md) ו-[03-retrieval.md](03-retrieval.md). אוכף את
|
||||||
|
[G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. מצב קיים מול יעד — audit-findings
|
||||||
|
|
||||||
|
הסעיף מתעד את ההבדלים בין שני המסלולים הקיימים. **אלו תסמינים לאיחוד תחת המסלול הקנוני,
|
||||||
|
לא התנהגויות תקינות.** כל פריט אומת מול הקוד בפועל.
|
||||||
|
|
||||||
|
- **חילוץ מטא-דאטה חסר במסלול הפנימי.** ראה [INV-ING3](#inv-ing3-תור-חילוץ-מטא-דאטה--הלכות-לכל-סוג)
|
||||||
|
(ההפרה המתועדת שם — ערן סופר 8046/24). **יעד:** צעד 8 (תור חילוץ) אחיד לשני הסוגים.
|
||||||
|
- **ולידציית-enums א-סימטרית.** המסלול החיצוני מוודא `practice_area`/`source_type` מול
|
||||||
|
רשימות חוקיות (`precedent_library.py:131-134`); המסלול הפנימי **אינו** מוודא enums.
|
||||||
|
**יעד:** ולידציה אחידה בנקודת-הקליטה (חוזה-שלמות, [G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש)).
|
||||||
|
- **staging מפוצל.** החיצוני עושה stage לפי `source_type` (`precedent_library.py:138`);
|
||||||
|
הפנימי עושה stage לפי district (`internal_decisions.py:113-115`). **יעד:** נתיב-staging
|
||||||
|
כפרמטר של המסלול הקנוני (צעד 1), לא ענף-קוד.
|
||||||
|
- **גזירת-שדות רק במסלול הפנימי.** הפנימי גוזר district מ-court (`:104`) ו-proceeding_type
|
||||||
|
מ-appeal_subtype/case_name (`:105`), ודורש `chair_name` (`:134`). החיצוני אינו גוזר אלו.
|
||||||
|
**יעד:** גזירה כפרמטר אופציונלי של המסלול הקנוני (שדות-סוג, לא מסלול-סוג).
|
||||||
|
- **citation guard רק במסלול החיצוני.** החיצוני חוסם ציטוט שמתחיל ב-`ערר`/`בל"מ`
|
||||||
|
ומפנה למסלול הפנימי (`precedent_library.py:124-130`). היעד שומר על השער הזה כניתוב-סוג
|
||||||
|
בתוך המסלול הקנוני, לא כהפרדת-פונקציות.
|
||||||
|
- **multimodal page-image embed רק במסלול החיצוני.** החיצוני מטמיע עמודי-תמונה כש-
|
||||||
|
`MULTIMODAL_ENABLED` + PDF (`precedent_library.py:272-278`); הפנימי **אינו** מטמיע
|
||||||
|
עמודי-תמונה. **יעד:** צעד 7 אחיד — מותנה ב-flag+סוג-קובץ בלבד.
|
||||||
|
- **fallback `case_name→citation` רק במסלול החיצוני.** החיצוני נופל ל-`citation` כשם
|
||||||
|
כשחסר `case_name` (`precedent_library.py:158`); הפנימי נופל ל-`case_number`
|
||||||
|
(`internal_decisions.py:130`). **יעד:** מדיניות-fallback אחת לשם-תצוגה במסלול הקנוני.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md) — invariants גלובליים + כללי-הנדסה.
|
||||||
|
- [02-data-model.md](02-data-model.md) — סכמת-האחסון + חוזה-שלמות שאוכף את תוצרי הקליטה.
|
||||||
|
- [03-retrieval.md](03-retrieval.md) — אחזור, re-index, eval — היעד של ה-chunks הנקלטים.
|
||||||
|
- [X1-identifiers.md](X1-identifiers.md) — נרמול המזהה הקנוני בכתיבה (בסיס ל-INV-ING2).
|
||||||
|
- [X5-audit-provenance.md](X5-audit-provenance.md) — שלמות-רשומה + עקיבוּת-מקור של פריט נקלט.
|
||||||
200
docs/spec/02-data-model.md
Normal file
200
docs/spec/02-data-model.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# 02 — מודל-הנתונים (Data Model & Completeness Contract)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) ומגדיר את **מודל-הנתונים הקנוני (TARGET)**
|
||||||
|
של עוזר משפטי — הישויות, שדות-המפתח, והיכן יושב כל פריט מואנדקס. הוא אוכף את
|
||||||
|
[G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה) (מזהה קנוני יחיד),
|
||||||
|
[G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש) (חוזה-שלמות) ו-
|
||||||
|
[G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן) (re-index בשינוי-תוכן).
|
||||||
|
|
||||||
|
> **TARGET, לא תיאור-מצב.** המודל כאן הוא היעד הקנוני. כל מקום שבו ה-schema בפועל
|
||||||
|
> (`mcp-server/src/legal_mcp/services/db.py`) סוטה ממנו — מתועד כ-**audit-finding** (§4),
|
||||||
|
> תסמין לאיחוד, לא התנהגות תקינה. כל טענה על ה-schema הקיים מצוטטת `file:line`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. הישויות הקנוניות
|
||||||
|
|
||||||
|
הטבלה מונה את ישויות-הליבה. "מזהה-קנוני" = השדה היחיד המזהה רשומה ([G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה)).
|
||||||
|
|
||||||
|
| ישות | תפקיד | מזהה-קנוני | שדות-מפתח (מאומתים `db.py`) |
|
||||||
|
|------|--------|-------------|------------------------------|
|
||||||
|
| `cases` | תיק ערר חי (1xxx/8xxx/9xxx) | `case_number` + `proceeding_type` | `title`, `status`, `practice_area`, `appeal_subtype`, `proceeding_type`, `chair_name` (`db.py:74-91,182-189,747,912`) |
|
||||||
|
| `documents` | מסמך-מקור משויך לתיק | `id` (UUID); FK→`cases` | `doc_type`, `title`, `file_path`, `extracted_text`, `extraction_status`, `page_count` (`db.py:93-104`) |
|
||||||
|
| `document_chunks` | chunk של מסמך-תיק + embedding | `id`; FK→`documents`/`cases` | `chunk_index`, `content`, `section_type`, `embedding vector(1024)`, `page_number` (`db.py:106-116`) |
|
||||||
|
| `case_law` | קורפוס פסיקה — חיצוני **וגם** החלטות-ועדה | ראה [§2 + INV-DM2](#inv-dm2-מזהה-קנוני-יחיד-לכל-ישות) | `case_name`, `court`, `practice_area`, `source_kind`, `proceeding_type`, `source_type`, `headnote`, `summary`, `subject_tags`, `extraction_status`, `halacha_extraction_status` (`db.py:366-378,522-526,599-611,883,907`) |
|
||||||
|
| `precedent_chunks` | chunk של פסק-דין מואנדקס (`source_kind='external_upload'`/`internal_committee`) | `id`; FK→`case_law` | `chunk_index`, `content`, `section_type`, `page_number`, `embedding vector(1024)`, `content_tsv` (`db.py:624-634,776`) |
|
||||||
|
| `halachot` | הלכה מחולצת — כלל + ציטוט מילולי | `id`; FK→`case_law` | `rule_statement`, `supporting_quote`, `rule_type`, `practice_areas`, `subject_tags`, `confidence`, `quote_verified`, `review_status`, `embedding`, `rule_tsv` (`db.py:644-666,780`) |
|
||||||
|
| `decisions` | החלטת-תיק מנוסחת (גרסה) | `id`; `UNIQUE(case_id, version)` | `version`, `status`, `outcome`, `outcome_summary` (`db.py:299-314`) |
|
||||||
|
| `decision_blocks` | בלוק (12) של החלטה | `id`; `UNIQUE(decision_id, block_id)` | `block_id`, `block_index`, `content`, `status` (`db.py:317-334`) |
|
||||||
|
| `claims` | טענת-צד (בלוק ז) | `id`; FK→`cases` | `party_role`, `claim_text`, `source_document`, `claim_type`, `claim_handling` (`db.py:349-359,506-512`) |
|
||||||
|
| `chair_feedback` | הערת-יו"ר על טיוטה | `id`; FK→`cases` | `block_id`, `feedback_text`, `category`, `lesson_extracted`, `resolved` (`db.py:452-462`) |
|
||||||
|
| `missing_precedents` | תקדים חסר שהתבקש ולא נמצא | `id` | (`db.py:806`) — backlog ל-quality-at-source |
|
||||||
|
| `style_corpus` | קורפוס-סגנון של דפנה (אימון) | `id`; FK→`documents` | `decision_number`, `full_text`, `practice_area`, `appeal_subtype` (`db.py:118-131`) |
|
||||||
|
|
||||||
|
> שכבות-עזר נוספות (`document_image_embeddings`, `precedent_image_embeddings` — multimodal,
|
||||||
|
> `db.py:707,726`; `case_law_relations` — שרשרת-תיק, `db.py:754`; `precedent_internal_citations`
|
||||||
|
> — גרף-ציטוטים, `db.py:937`) הן נגזרות ([G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)):
|
||||||
|
> משוחזרות מהמקור, לא מקור-אמת עצמאי.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. חוזה-שלמות לכל ישות (Completeness Contract)
|
||||||
|
|
||||||
|
[G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש) דורש: **רשומה אינה "שמישה /
|
||||||
|
ניתנת-לחיפוש" עד ששדות-החובה שלה מולאו ואומתו מול spec מפורש.** כל ישות מגדירה שתי רמות —
|
||||||
|
**usable** (קיימת ומזוהה) ו-**searchable** (חשופה לאחזור). רשומה שנכשלת בחוזה **מסומנת
|
||||||
|
ומדווחת — לא מתקבלת בשקט** (חוקה §6, "אין בליעה שקטה").
|
||||||
|
|
||||||
|
### 2א. `case_law` — החוזה הקונקרטי
|
||||||
|
|
||||||
|
המזהה הקנוני אינו `case_number` לבדו: `case_law` נושאת **שני** unique partial indexes לפי
|
||||||
|
`source_kind` (`db.py:904-909`) — חיצוני: `UNIQUE(case_number)`; פנימי: `UNIQUE(case_number,
|
||||||
|
proceeding_type)`. לכן המזהה הקנוני הוא **(`case_number` מנורמל, `source_kind`,
|
||||||
|
`proceeding_type`)**.
|
||||||
|
|
||||||
|
**רמת usable** (רשומה לגיטימית):
|
||||||
|
- `case_number` קנוני מנורמל-בכתיבה ([INV-DM2](#inv-dm2-מזהה-קנוני-יחיד-לכל-ישות) — **לא** ציטוט-מלא)
|
||||||
|
- `case_name` לא-ריק (לא fallback לציטוט/למספר)
|
||||||
|
- `court` לא-ריק
|
||||||
|
- `practice_area ∈ {rishuy_uvniya, betterment_levy, compensation_197}` (אכוף ב-CHECK, `db.py:614-617`)
|
||||||
|
- `source_kind` מהמילון (`external_upload` / `cited_only` / `internal_committee` / `nevo_seed`) (`db.py:599-601`, `internal_decisions.py:4`)
|
||||||
|
- `proceeding_type ∈ {ערר, בל"מ}` כשפנימי (אכוף ב-CHECK, `db.py:897-899`)
|
||||||
|
|
||||||
|
**רמת searchable** (חשוף לאחזור — מעבר ל-usable):
|
||||||
|
- **≥1 `precedent_chunk`** עם `embedding` לא-NULL (אחרת אין מה לאחזר סמנטית)
|
||||||
|
- **metadata לא-ריק:** לפחות אחד מ-`headnote` / `summary` / `subject_tags` מלא — אלו השדות
|
||||||
|
ש-search מציג ומסנן לפיהם
|
||||||
|
- `extraction_status = completed` (מטא-דאטה הושלם, `db.py:603`)
|
||||||
|
|
||||||
|
**אכיפה מפורשת:** רשומה שעוברת usable אך נכשלת ב-searchable — **מסומנת `searchable=false`
|
||||||
|
ולא מוחזרת מ-search**, ומופיעה ב-health-check כ-backlog. היא **אינה מתקבלת בשקט** כ"זמינה".
|
||||||
|
|
||||||
|
### 2ב. חוזה תמציתי לישויות נוספות
|
||||||
|
|
||||||
|
- `documents` → usable: `file_path`+`doc_type`; searchable: `extraction_status=completed` ו-`extracted_text` לא-ריק ו-≥1 `document_chunk` עם embedding.
|
||||||
|
- `halachot` → usable: `rule_statement`+`supporting_quote`; **searchable: `review_status ∈ {approved, published}` בלבד** — `pending_review`/`rejected` מוסתרות מ-`search_precedent_library` (שער-הלכה ידני, `db.py:644-660`, [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)).
|
||||||
|
- `decision_blocks` → usable: `block_id`∈12-הבלוקים; "מוכן": `status=final` ו-`content` לא-ריק.
|
||||||
|
- `chair_feedback` → usable: `feedback_text`+`category` מהמילון; "פתוח" עד `resolved=true`.
|
||||||
|
|
||||||
|
### 2ג. ישויות-נגזרות (אחסון-ניתוחים)
|
||||||
|
|
||||||
|
מעבר לישויות-המקור, המערכת **שומרת ניתוחים נגזרים** — תוצרי-חילוץ של LLM/קוד. אלו כפופים לכללי
|
||||||
|
ה-provenance של [X8](X8-field-provenance.md) ולשערי [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant):
|
||||||
|
|
||||||
|
| ישות-נגזרת | מקור-מילוי | שער-אישור | קישור-מקור |
|
||||||
|
|------------|------------|-----------|------------|
|
||||||
|
| `claims` | OPUS (`extract_claims`) | — | `source_document` (string, לא-FK) |
|
||||||
|
| `legal_arguments` (+`legal_argument_propositions`) | OPUS (`aggregate_claims_to_arguments`) | **חסר** (בניגוד ל-halachot) | `cited_precedents TEXT[]` (לא-FK) |
|
||||||
|
| `appraiser_facts` | OPUS (`extract_appraiser_facts`) | — | `document_id` (FK); `appraiser_side` default `''` |
|
||||||
|
| `halachot` | OPUS (`halacha_extractor`) | **`review_status`** ✓ | `case_law_id` (FK); `quote_verified` |
|
||||||
|
| `decision_blocks` / `decision_paragraphs` | Opus/script (`write_block`) | `status` | `model_used` + audit-event provenance (FU-7); `citations JSONB` ללא-FK |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-DM1: searchable רק כשחוזה-השלמות מתקיים
|
||||||
|
**כלל:** רשומת `case_law` נחשבת **searchable** אך ורק כשחוזה-השלמות של [§2א](#2א-case_law--החוזה-הקונקרטי)
|
||||||
|
מתקיים במלואו (מזהה קנוני · `case_name`/`court`/`practice_area`/`source_kind` · ≥1 chunk עם
|
||||||
|
embedding · metadata לא-ריק). רשומה שנכשלת **מסומנת `searchable=false` ומדווחת ל-health-check —
|
||||||
|
לא מוחזרת מ-search ולא מתקבלת בשקט**.
|
||||||
|
**מקורות:** ISO 8000 (completeness) · DAMA-UK *Six Primary Dimensions for Data Quality* (2013,
|
||||||
|
completeness) · ISO 15489-1:2016 (records reliability/usability) | סטטוס: verified
|
||||||
|
**אכיפה:** ולידציית-כתיבה בנקודת-הקליטה ([01-ingest.md](01-ingest.md) צעד 8) + בדיקת-בריאות
|
||||||
|
תקופתית שמסמנת backlog; הסינון נאכף בשכבת-החיפוש ([03-retrieval.md](03-retrieval.md)). אוכף את
|
||||||
|
[G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש).
|
||||||
|
**הפרה ידועה:** ערן סופר 8046/24 אונדקס כ-searchable עם `headnote`/`summary`/`subject_tags`
|
||||||
|
ריקים — המסלול הפנימי לא תיזמן חילוץ-מטא-דאטה ([01-ingest INV-ING3](01-ingest.md#inv-ing3-תור-חילוץ-מטא-דאטה--הלכות-לכל-סוג),
|
||||||
|
`internal_decisions.py:208`) → ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-DM2: מזהה קנוני יחיד לכל ישות
|
||||||
|
**כלל:** לכל ישות **מזהה קנוני אחד**, מנורמל בכתיבה. **אסור** ששדה-המזהה יאחסן ציטוט-מלא —
|
||||||
|
`case_number` הוא מספר-תיק מנורמל (`8126-03-25`), **לא** מחרוזת-ציטוט (`ערר 8126/24 פלוני נ' הוועדה
|
||||||
|
(נבו...)`). הציטוט המלא חי בשדה ייעודי נפרד (`citation_formatted`, `db.py:1070`), לא במזהה.
|
||||||
|
**מקורות:** SSOT (Single Source of Truth — normalization) · E.F. Codd, First Normal Form (CACM
|
||||||
|
13(6), 1970) · Martin Kleppmann, *Designing Data-Intensive Applications* (O'Reilly, 2017) | סטטוס: verified
|
||||||
|
**אכיפה:** unique partial indexes על המזהה הקנוני (`db.py:904-909`) + נרמול-בכתיבה
|
||||||
|
([X1-identifiers.md](X1-identifiers.md)); ציטוט-מלא ב-`citation_formatted` בלבד. אוכף את
|
||||||
|
[G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה).
|
||||||
|
**הפרה ידועה:** החלטות "סופר" נקלטו עם **ציטוט-מלא כ-`case_number`** (שדה-המזהה של רשומה מכיל את
|
||||||
|
מחרוזת-הציטוט במקום מספר-תיק מנורמל) — חיפוש מול `8126-03-25` נכשל, ו-`_normalize_case_number`
|
||||||
|
(`db.py:1196-1211`) רק **מטליא בקריאה** (סלחני, לא קנוני), בניגוד ל-[G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה)
|
||||||
|
→ ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-DM3: שינוי-תוכן ⇒ re-index
|
||||||
|
**כלל:** כל שינוי בתוכן-המקור של ישות מואנדקסת (`content` של chunk, `rule_statement`/`supporting_quote`
|
||||||
|
של הלכה, `full_text`/`extracted_text` של מסמך) מפעיל **re-index** של ה-embedding **ושל
|
||||||
|
ה-tsvector** הנגזרים. אין embedding או `content_tsv`/`rule_tsv`/`meta_tsv` מיושנים מול התוכן.
|
||||||
|
**מקורות:** Pinecone (index freshness / data sync) · Weaviate (re-vectorization on update) ·
|
||||||
|
RAG freshness (Lewis et al., 2020, NeurIPS) | סטטוס: verified
|
||||||
|
**אכיפה:** טריגר re-embed בנקודת-העדכון + בדיקת-בריאות לגילוי drift; ה-tsvectors `GENERATED ALWAYS
|
||||||
|
… STORED` (`db.py:776-788,1083-1090`) מתעדכנים אוטומטית, אך ה-`embedding` **אינו** generated —
|
||||||
|
הוא תלוי-טריגר. מפורט ב-[03-retrieval.md](03-retrieval.md). אוכף את
|
||||||
|
[G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-DM4: לכל ישות-נגזרת — provenance מוצהר
|
||||||
|
**כלל:** כל ישות-נגזרת (claims, legal_arguments, appraiser_facts, decision_blocks, halachot) נושאת
|
||||||
|
**provenance** — מי/מה הפיק (מודל, גרסה, זמן) ולאילו chunks/מקורות היא קשורה. מופע של
|
||||||
|
[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai); מקביל ל-[X8 INV-FP1](X8-field-provenance.md).
|
||||||
|
**מקורות:** ISO 8000-110 (data lineage) · DAMA-DMBOK2 (lineage) · ISO 15489-1:2016 (records authenticity) | סטטוס: verified
|
||||||
|
**אכיפה:** עמודות-provenance + קישור block→source (חלקית דרך audit-event ב-FU-7/GAP-19; ל-legal_arguments טרם).
|
||||||
|
**הפרה ידועה:** `legal_arguments` ללא provenance; `embedding` ללא model/version ([gap-audit GAP-42](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-DM5: פלט-ניתוח של LLM נכנס בשער-אישור (כמו halachot)
|
||||||
|
**כלל:** ישות-נגזרת שמוּלאת ע"י LLM ומשפיעה על ההחלטה נכנסת **לא-מאושרת** עד אישור-יו"ר — אותו שער כמו
|
||||||
|
`halachot.review_status`. מופע של [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant); תואם [X8 INV-FP3](X8-field-provenance.md).
|
||||||
|
**מקור-סמכות:** דפוס `halachot.review_status` (`db.py:659`); [05-qa-review.md](05-qa-review.md). (פרויקטלי-תפעולי — משרת G10.)
|
||||||
|
**אכיפה:** שדה-סטטוס-אישור על ישויות-נגזרות מהותיות.
|
||||||
|
**הפרה ידועה:** `legal_arguments` **חסר** שער-אישור — נכתב ומשמש ללא בקרת-יו"ר ([gap-audit GAP-39](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-DM6: ולידציה — CHECK-enums, FK לציטוטים, ללא טבלאות-מקבילות
|
||||||
|
**כלל:** ערכי-enum נאכפים ב-CHECK (לא TEXT חופשי); ציטוט-מקור נשמר כ-FK (לא string/array חופשי); אין שתי
|
||||||
|
טבלאות לאותה ישות. מופע של [G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש) ו-[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים). **הנדסי.**
|
||||||
|
**מקורות:** E.F. Codd (referential integrity, CACM 1970) · ISO 8000 (validity) · Kleppmann *DDIA* | סטטוס: verified
|
||||||
|
**אכיפה:** CHECK על enums; FK על `cited_precedents`/`decision_paragraphs.citations`; איחוד `case_precedents`↔`case_law`.
|
||||||
|
**הפרה ידועה:** 20+ enums כ-TEXT חופשי; `legal_arguments.cited_precedents TEXT[]` ללא-FK (הזיות-LLM נבלעות); `case_precedents` מול `case_law` מקבילות ([gap-audit GAP-40/42/43](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-DM7: סיווג-הלכה — סמכות (נגזרת) ⊥ תפקיד-כלל (מסווג). שני צירים, לא enum אחד
|
||||||
|
**כלל:** ל-`halachot` שני צירי-סיווג **אורתוגונליים** שאסור לערבב בשדה אחד:
|
||||||
|
- **סמכות (`authority`) — נגזרת בלבד, לא מאוחסנת, לא מנוחשת ע"י LLM.** `binding` (מקור מחייב את הוועדה: עליון/מנהלי) מול `persuasive` (מקור משכנע: ועדת-ערר אחרת). נגזרת דטרמיניסטית מ-`case_law.precedent_level` (`עליון`/`מנהלי`→binding; `ועדת_ערר_מחוזית`→persuasive). מקור-אמת יחיד — מחושבת בקריאה, אין עמודה כפולה ([G1](00-constitution.md#inv-g1-נרמול-במקור-לא-תיקון-בקריאה)/[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)).
|
||||||
|
- **תפקיד-כלל (`rule_type`/rule_role) — מסווג ע"י ה-LLM.** `holding` (עיקרון מהותי הכרחי להכרעה — ratio/Wambaugh) · `interpretive` (פרשנות חוק/מונח/תכנית) · `procedural` (סדר-דין: סמכות/מועדים/נטל) · `application` (החלה תלוית-עובדות — לרוב לא-הלכה) · `obiter` (אמרת-אגב). **`binding`/`persuasive` אינם ערכי תפקיד** — הם סמכות-מקור.
|
||||||
|
**הנדסי.** מופע של [G1](00-constitution.md#inv-g1-נרמול-במקור-לא-תיקון-בקריאה) (נרמול במקור: המחלץ מסווג תפקיד, לא ממציא סמכות נגזירה) ו-[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים).
|
||||||
|
**מקורות:** OASIS LegalRuleML v1.0 (`appliesAuthority`/`Strength` כ-metadata אורתוגונלי, נפרד מלוגיקת-הכלל) · SemEval-2023 Task 6 LegalEval (rhetorical-roles לפי תפקיד, סמכות נשמרת בנפרד) · Bluebook signals (משקל-סמכות = ציר נפרד מהפרופוזיציה) | סטטוס: verified (≥3 מקורות).
|
||||||
|
**ההפרה שתוקנה:** `halacha_extractor` סיווג `rule_type` לפי bindingness-של-המקור (`_coerce_halacha(is_binding)`, ברירת-מחדל `binding`/`persuasive`, guard binding→persuasive) — כלומר חישב **סמכות** במסווה של **תפקיד**. אומת אמפירית על מדגם-הזהב: `binding` שימש 19/19 פסקים חיצוניים ו-0 ועדות; `persuasive` 13/13 ועדות ו-0 חיצוניים → סיווג-לפי-מקור, התאמה לתיוג-אנושי 58% בלבד. התיקון מעביר סמכות לציר-נגזר ומשחרר את ה-LLM לסווג תפקיד נטו.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. מצב קיים מול יעד — audit-findings
|
||||||
|
|
||||||
|
ההבדלים בין ה-schema בפועל ל-TARGET. **אלו תסמינים, לא התנהגויות תקינות.** כל פריט אומת מול `db.py`.
|
||||||
|
|
||||||
|
- **`case_law` כפולת-תפקיד ללא מזהה מודע-סוג בכתיבה.** טבלה אחת משרתת פסיקה חיצונית **וגם**
|
||||||
|
החלטות-ועדה, מובדלות ב-`source_kind` (`db.py:599`). המזהה הקנוני האמיתי הוא טריפלט
|
||||||
|
(`case_number, source_kind, proceeding_type`, `db.py:904-909`), אך השדה `case_number TEXT
|
||||||
|
UNIQUE NOT NULL` המקורי (`db.py:368`) הוסר רק ב-V15 (`db.py:902-903`) — מורשת שאפשרה את
|
||||||
|
הפרת [INV-DM2](#inv-dm2-מזהה-קנוני-יחיד-לכל-ישות). **יעד:** נרמול-בכתיבה אכוף + ציטוט-מלא רק ב-`citation_formatted`.
|
||||||
|
- **`summary` קיים על `case_law` אך לא בחוזה-הקליטה הפנימי.** העמודה קיימת (`db.py:373`) אך
|
||||||
|
המסלול הפנימי אינו ממלא אותה (כפועל-יוצא מהיעדר חילוץ-מטא-דאטה, [INV-ING3](01-ingest.md#inv-ing3-תור-חילוץ-מטא-דאטה--הלכות-לכל-סוג)).
|
||||||
|
**יעד:** searchable מותנה ב-metadata לא-ריק ([INV-DM1](#inv-dm1-searchable-רק-כשחוזה-השלמות-מתקיים)).
|
||||||
|
- **שני שדות-סטטוס-חילוץ נפרדים, ללא דגל-`searchable` מפורש.** `extraction_status` +
|
||||||
|
`halacha_extraction_status` (`db.py:603-605`) מתארים תהליך, אך אין שדה יחיד שמסמן "עבר
|
||||||
|
חוזה-שלמות → searchable". **יעד:** דגל/view נגזר ש-search מסנן לפיו, מגובה health-check.
|
||||||
|
- **`embedding` אינו `GENERATED` (בניגוד ל-tsvector).** ה-tsvectors מסונכרנים אוטומטית
|
||||||
|
(`db.py:776,780,1083`), אך ה-`embedding vector(1024)` תלוי-טריגר חיצוני — נקודת-drift אפשרית
|
||||||
|
ל-[INV-DM3](#inv-dm3-שינוי-תוכן--re-index). **יעד:** טריגר re-embed מובטח + health-check ל-drift.
|
||||||
|
- **`halachot.review_status` כשער-searchable ללא נראות-backlog.** הסינון תקין (`pending_review`
|
||||||
|
מוסתר, `db.py:659`), אך אין נראות כמה ממתינות — תואם את ההפרה הידועה ב-[G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)
|
||||||
|
(10/19 מאושרות, התגלה במקרה). **יעד:** health-check חושף backlog-הלכות.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md) — invariants גלובליים (G1, G4, G6) + כללי-הנדסה.
|
||||||
|
- [01-ingest.md](01-ingest.md) — חוזה-הקליטה שמייצר את הרשומות; חוזה-השלמות כאן אוכף את תוצריו.
|
||||||
|
- [03-retrieval.md](03-retrieval.md) — שכבת-האחזור שאוכפת את הסינון searchable + re-index.
|
||||||
|
- [X1-identifiers.md](X1-identifiers.md) — נרמול המזהה הקנוני בכתיבה (בסיס ל-INV-DM2).
|
||||||
|
- [X5-audit-provenance.md](X5-audit-provenance.md) — שלמות-רשומה + עקיבוּת-מקור.
|
||||||
|
- [X8-field-provenance.md](X8-field-provenance.md) — מקור-מילוי השדות (בסיס ל-INV-DM4/DM5).
|
||||||
|
- [X9-mcp-tool-contract.md](X9-mcp-tool-contract.md) — הכלים שמייצרים את הישויות-הנגזרות.
|
||||||
186
docs/spec/03-retrieval.md
Normal file
186
docs/spec/03-retrieval.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# 03 — אחזור (Retrieval: Corpora · Hybrid/RRF · Attribution · Eval)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) ומגדיר את **שכבת-האחזור הקנונית (TARGET)** —
|
||||||
|
שלושת הקורפוסים, כלי-החיפוש המכוונים לכל אחד, מנגנון ה-hybrid (dense + lexical) ומיזוג ה-RRF,
|
||||||
|
עקיבוּת-המקור והרמוניית-המדידה. הוא אוכף את
|
||||||
|
[G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש) (חוזה-שלמות לפני "ניתן-לחיפוש"),
|
||||||
|
[G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query) (הפרדת-קורפוס בכל query),
|
||||||
|
[G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן) (re-index),
|
||||||
|
[G7](00-constitution.md#inv-g7-מיזוג-rrf--לא-סכום-ציונים) (מיזוג RRF),
|
||||||
|
[G8](00-constitution.md#inv-g8-איכות-אחזור-נמדדת--precision--recall) (eval) ו-
|
||||||
|
[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai) (עקיבוּת-מקור).
|
||||||
|
|
||||||
|
> **TARGET, לא תיאור-מצב.** כל מקום שבו הקוד בפועל סוטה מהיעד מתועד כ-**audit-finding** (§5),
|
||||||
|
> תסמין לתיקון — לא התנהגות תקינה. כל טענה על הקוד מצוטטת `file:line`.
|
||||||
|
|
||||||
|
כשל-השורש שהקובץ מייבש: **3 קורפוסים שחולקים תשתית-אחזור אחת, אך הפרדת-הקורפוס נאכפת רק על
|
||||||
|
חלק ממסלולי-ה-query** — כך שפריט מקורפוס אחד דולף לתוצאה של חיפוש בקורפוס אחר (cross-corpus leak).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. שלושת הקורפוסים וכלי-החיפוש
|
||||||
|
|
||||||
|
| קורפוס | טבלת-אחסון | `source_kind` | כלי-MCP מכוון | אימות `file:line` |
|
||||||
|
|--------|------------|----------------|----------------|--------------------|
|
||||||
|
| מסמכי-תיק + קורפוס-סגנון דפנה | `document_chunks` | — (מובחן ב-`case_id`/`practice_area`) | `search_decisions` · `search_case_documents` · `find_similar_cases` | `search.py:15,91,145` → `hybrid_search.py:41` (`search_documents_hybrid`) → `db.search_similar` (`hybrid_search.py:56`) |
|
||||||
|
| פסיקה חיצונית סמכותית | `case_law` + `precedent_chunks`/`halachot` | `external_upload` | `search_precedent_library` | `search.py`→`precedent_library.py:235` → `search_library` → `hybrid_search.py:89,101` (`source_kind="external_upload"`) |
|
||||||
|
| החלטות ועדות-ערר (פנימי) | `case_law` + `precedent_chunks`/`halachot` | `internal_committee` | `search_internal_decisions` | `search.py:228` → `internal_decisions.py:395,411-418` (`source_kind="internal_committee"`) → `hybrid_search.py:89` |
|
||||||
|
|
||||||
|
**הבחנת-שם קריטית (לא קורפוס רביעי):** `precedent_search_library` (`server.py:160`) הוא כלי **שונה** —
|
||||||
|
מחפש בציטוטים שהיו"ר צירפה ידנית לתיקים (`case_precedents`), לא בקורפוס הפסיקה הסמכותית.
|
||||||
|
`search_precedent_library` (`server.py:280`) הוא הכלי לקורפוס החיצוני. אל תבלבל ביניהם.
|
||||||
|
|
||||||
|
הקורפוס החיצוני והפנימי **חולקים טבלה אחת** (`case_law`), מובחנים ב-`source_kind` בלבד
|
||||||
|
([02-data-model §2א](02-data-model.md#2א-case_law--החוזה-הקונקרטי)). שניהם רצים דרך **אותן** פונקציות-DB
|
||||||
|
(`search_precedent_library_semantic`/`_lexical`) — לכן הפרדת-הקורפוס היא **תנאי-סינון בתוך אותה שאילתה**,
|
||||||
|
ושם נולדת ההפרה ב-§5.
|
||||||
|
|
||||||
|
> **שכבת-גילוי — יומונים, לא קורפוס-ציטוט.** מעל 3 הקורפוסים יושבת שכבת-radar נפרדת: **יומונים**
|
||||||
|
> (סיכומי עפר-טויסטר), בטבלה פיזית נפרדת `digests` עם כלי `search_digests`. היומון הוא **מקור משני
|
||||||
|
> המצביע** על הפסק המקורי — **אינו** קורפוס-ציטוט רביעי, **אינו** עקיב-בפלט ([INV-RET5](#inv-ret5-כל-span-מוחזר-עקיב-למקורו)),
|
||||||
|
> ו**אינו** נוגע ב-`case_law`/`document_chunks`. ההפרדה כאן **פיזית** (טבלה נפרדת), לא תנאי-סינון —
|
||||||
|
> ולכן [INV-RET1](#inv-ret1-הפרדת-קורפוס-נאכפת-ב-100-ממסלולי-ה-query) מתקיים טריוויאלית. מלא ב-
|
||||||
|
> [X12-digests-radar.md](X12-digests-radar.md) (INV-DIG1–DIG3).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. עיצוב ה-hybrid retrieval
|
||||||
|
|
||||||
|
לכל קורפוס שני retrievers הטרוגניים המאוחים ב-RRF, ולא בסכום-ציונים — ראה [INV-RET3](#inv-ret3-מיזוג-retrievers-הטרוגניים-ב-rrf-בלבד):
|
||||||
|
|
||||||
|
1. **Dense (semantic)** — דמיון-קוסינוס מול `embedding vector(1024)` (voyage). פסיקה:
|
||||||
|
`search_precedent_library_semantic` (`db.py:3143`); מסמכי-תיק: `db.search_similar`.
|
||||||
|
2. **Lexical (BM25-style)** — `ts_rank_cd` מול `content_tsv`/`rule_tsv`/`meta_tsv` (Postgres FTS).
|
||||||
|
פסיקה: `search_precedent_library_lexical` (`db.py:3366`). מופעל כש-`BM25_HYBRID_ENABLED`
|
||||||
|
(`hybrid_search.py:139`).
|
||||||
|
3. **מיזוג sem+lex** — `_merge_sem_lex` (`hybrid_search.py:240-308`), נוסחת
|
||||||
|
`rrf_score = 1/(k+sem_rank) + 1/(k+lex_rank)` (`hybrid_search.py:256`).
|
||||||
|
4. **שכבת-multimodal (אופציונלית)** — כש-`MULTIMODAL_ENABLED`, עמודי-תמונה (voyage-multimodal-3)
|
||||||
|
מאוחים לטקסט ב-RRF נפרד: `_merge` (`hybrid_search.py:311-389`), `text_weight/(k+rank) +
|
||||||
|
img_weight/(k+rank)` (`hybrid_search.py:356-357`).
|
||||||
|
5. **Diversity cap (MMR-style)** — `_diversify_by_case_law` (`hybrid_search.py:196-225`): לכל היותר
|
||||||
|
`max_per_case_law` hits לכל `case_law_id`, כדי שפסק-דין יחיד לא ישתלט על הרשימה.
|
||||||
|
|
||||||
|
> **למה RRF ולא סכום משוקלל:** קוסינוס (~0.4–0.7) ו-`ts_rank_cd` (~0.001–0.5, תלוי-אורך-שאילתה)
|
||||||
|
> חיים בסקיילים שונים — סכום משוקלל היה נותן לצד אחד להשתלט במקרה. RRF מאחד **לפי דירוג**, ולכן
|
||||||
|
> עמיד להבדלי-סקייל (`hybrid_search.py:248-252,319-323`). תואם feedback קיים (RRF, לא weighted-sum).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-RET1: הפרדת-קורפוס נאכפת ב-100% ממסלולי-ה-query
|
||||||
|
**כלל:** הפרדת 3 הקורפוסים נאכפת בכל מסלול-אחזור — **גם בסינון ה-chunks וגם בסינון ההלכות**.
|
||||||
|
אין פריט מקורפוס אחד שמופיע בתוצאת חיפוש שכוון לקורפוס אחר. כל ענף-SQL (semantic/lexical,
|
||||||
|
chunks/halachot) נושא את אותו תנאי-`source_kind`.
|
||||||
|
**מקורות:** Pinecone — *Implement multitenancy* (metadata-filter isolation per tenant) · RAG
|
||||||
|
attribution (Lewis et al., 2020, NeurIPS — pinned non-leaking provenance) · ISO 8000 (Data
|
||||||
|
quality / conformance) | סטטוס: verified
|
||||||
|
**אכיפה:** תנאי-`source_kind` בכל ענף-SQL בשכבת-החיפוש; בדיקת-בריאות שמריצה שאילתת-ביקורת
|
||||||
|
(חיפוש מכוון-קורפוס שמחזיר פריט בעל `source_kind` זר = כשל). אוכף את
|
||||||
|
[G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query).
|
||||||
|
**הפרה ידועה:** משימה #56 — `halacha_filters` **אינם** כוללים `cl.source_kind` ב-
|
||||||
|
`search_precedent_library_semantic` (`db.py:3168`, ענף ה-halacha; לעומת `chunk_filters` שכן —
|
||||||
|
`db.py:3169`) **וב**-`search_precedent_library_lexical` (`db.py:3401` מול `db.py:3402`). שני
|
||||||
|
ה-`halacha_sql` עושים `JOIN case_law cl` בלי לסנן `source_kind` (`db.py:3236-3238`, `db.py:3475-3477`)
|
||||||
|
→ הלכות מהקורפוס הפנימי דולפות לתוצאות החיפוש בקורפוס החיצוני ולהפך → ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-RET2: אין החזרה/אינדוקס בלי metadata מלא + locator פתיר
|
||||||
|
**כלל:** פריט אינו מוחזר מ-search (ואינו נחשף לאחזור) אלא אם **שדות-החובה שלו מולאו**
|
||||||
|
([G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש)) **ובידו locator פתיר למקור**
|
||||||
|
(`case_law_id`/`document_id` + מזהה-עמוד/chunk). רשומה ללא metadata לא-ריק או ללא chunk עם
|
||||||
|
embedding מסומנת `searchable=false` ולא מוחזרת ([02-data-model INV-DM1](02-data-model.md#inv-dm1-searchable-רק-כשחוזה-השלמות-מתקיים)).
|
||||||
|
**מקורות:** Pinecone (metadata filtering — completeness לפני שליפה) · RAG attribution (Lewis et
|
||||||
|
al., 2020) · ISO 8000 (completeness) | סטטוס: verified
|
||||||
|
**אכיפה:** חוזה-שלמות בנקודת-הקליטה ([02-data-model §2](02-data-model.md#2-חוזה-שלמות-לכל-ישות-completeness-contract))
|
||||||
|
+ סינון בשכבת-החיפוש (`embedding IS NOT NULL`, `db.py:3239,3271`; `length(trim(content))>=50`,
|
||||||
|
`db.py:3274`) + בדיקת-בריאות שחושפת backlog. אוכף את
|
||||||
|
[G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query).
|
||||||
|
**הפרה ידועה:** ערן סופר 8046/24 — נקלטה בלי metadata (headnote/summary/tags ריקים), היעדר
|
||||||
|
תיזמון חילוץ-מטא-דאטה במסלול הפנימי ([01-ingest INV-ING3](01-ingest.md#inv-ing3-תור-חילוץ-מטא-דאטה--הלכות-לכל-סוג)),
|
||||||
|
אך ללא דגל-`searchable` מפורש שימנע את חשיפתה לאחזור → ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-RET3: מיזוג retrievers הטרוגניים ב-RRF בלבד
|
||||||
|
**כלל:** מיזוג תוצאות בין retrievers שונים (semantic↔lexical, text↔image) נעשה **אך ורק
|
||||||
|
לפי דירוג (Reciprocal Rank Fusion)** — לעולם לא סכום/ממוצע ציונים גולמיים, שכן ציונים בסקיילים
|
||||||
|
שונים אינם בני-השוואה ישירה.
|
||||||
|
**מקורות:** Elastic — *Reciprocal Rank Fusion* · Weaviate — *Hybrid Search Explained* · Manning,
|
||||||
|
Raghavan & Schütze, *Introduction to Information Retrieval* (CUP, 2008) | סטטוס: verified
|
||||||
|
**אכיפה:** מיזוג sem+lex ב-`_merge_sem_lex` (`hybrid_search.py:240-308`, נוסחה ב-`:256`) ומיזוג
|
||||||
|
text+image ב-`_merge` (`hybrid_search.py:311-389`, נוסחה ב-`:356-357`), שניהם עם
|
||||||
|
`k = MULTIMODAL_RRF_K`. אוכף את [G7](00-constitution.md#inv-g7-מיזוג-rrf--לא-סכום-ציונים).
|
||||||
|
**מצב:** **כבר ממומש** (codify, לא gap) — הקוד הקיים מיישם RRF נכון בשני המיזוגים. ה-invariant
|
||||||
|
מקבע את ההתנהגות הקיימת כחוזה. **הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-RET4: איכות-אחזור נמדדת ב-eval harness עומד (precision + recall)
|
||||||
|
**כלל:** איכות-האחזור **נמדדת אמפירית** — precision **ו**-recall — מול **סט-שאילתות מתויג קבוע**
|
||||||
|
(labeled query set) ב-eval harness עומד. כל שינוי בשכבת-האחזור (משקלי-RRF, `k`, סף-chunk, embedder)
|
||||||
|
מלווה במדידה לפני/אחרי; אין כוונון "לפי תחושה".
|
||||||
|
**מקורות:** Manning, Raghavan & Schütze, *Introduction to Information Retrieval* (CUP, 2008 — fixed
|
||||||
|
relevance judgments, precision/recall) · RAG evaluation literature (Lewis et al., 2020 ואחריו) ·
|
||||||
|
Elastic — *relevance evaluation guidance* | סטטוס: verified
|
||||||
|
**אכיפה:** eval harness עם gold-set מתויג + בדיקת-בריאות תקופתית; שער-CI על שינוי שכבת-האחזור.
|
||||||
|
אוכף את [G8](00-constitution.md#inv-g8-איכות-אחזור-נמדדת--precision--recall).
|
||||||
|
**הפרה ידועה (GAP):** אין כיום eval harness ולא gold-set — קיים רק `telemetry.log_search_bg`
|
||||||
|
(`search.py:62,118,190,271`; `precedent_library.py:280`) שמתעד שאילתות בפועל, אך **אינו מודד
|
||||||
|
precision/recall מול תיוג** (תצפית, לא הערכה). היעד: harness שמריץ סט קבוע ומחזיר metrics →
|
||||||
|
ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-RET5: כל span מוחזר עקיב למקורו
|
||||||
|
**כלל:** כל קטע מוחזר נושא **עקיבוּת-מקור מלאה** — מזהה-מסמך/פסק-דין (`case_law_id`/`document_id`/
|
||||||
|
`case_number`) **ו**-locator בתוכו (`page_number` / `chunk_id` / `supporting_quote` להלכה). פלט
|
||||||
|
ללא ייחוס פתיר אינו תקין; היו"ר חייבת לאמת כל ציטוט מול מקורו.
|
||||||
|
**מקורות:** Council of Europe / CEPEJ — *European Ethical Charter on AI in judicial systems*
|
||||||
|
(2018, traceability) · RAG attribution (Lewis et al., 2020) · ISO 15489-1:2016 (records
|
||||||
|
authenticity/integrity) | סטטוס: verified
|
||||||
|
**אכיפה:** כל פורמטר-תוצאה כולל מזהה + locator: `search.py:77-86` (case_number/page/section),
|
||||||
|
`_format_internal_row` (`search.py:322-343`: case_number/case_name/court + content/page או
|
||||||
|
rule/quote להלכה). עקיבוּת מלאה מפורטת ב-[X5-audit-provenance.md](X5-audit-provenance.md). אוכף את
|
||||||
|
[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. re-index ושינוי-תוכן (G6)
|
||||||
|
|
||||||
|
האחזור מסתמך על embeddings מסונכרנים מול התוכן. ה-tsvectors (`content_tsv`/`rule_tsv`/`meta_tsv`)
|
||||||
|
הם `GENERATED ALWAYS … STORED` (`db.py:778,782,1086`) ולכן מתעדכנים אוטומטית; אך ה-`embedding
|
||||||
|
vector(1024)` **אינו** generated — הוא תלוי-טריגר-חיצוני, נקודת-drift אפשרית
|
||||||
|
([02-data-model INV-DM3](02-data-model.md#inv-dm3-שינוי-תוכן--re-index)). שינוי-תוכן חייב להפעיל
|
||||||
|
re-embed; בדיקת-בריאות מגלה embeddings מיושנים. אוכף את
|
||||||
|
[G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. מצב קיים מול יעד — audit-findings
|
||||||
|
|
||||||
|
ההבדלים בין הקוד בפועל ל-TARGET. **אלו תסמינים, לא התנהגויות תקינות.** כל פריט אומת מול הקוד.
|
||||||
|
|
||||||
|
- **דליפת-הלכות חוצת-קורפוס (משימה #56).** `halacha_filters` נפתחים רק עם `review_status`
|
||||||
|
(`db.py:3168`, `db.py:3401`) ואינם מוסיפים `cl.source_kind`, בעוד `chunk_filters` כן
|
||||||
|
(`db.py:3169`, `db.py:3402`). שני ה-`halacha_sql` עושים `JOIN case_law` בלי סינון
|
||||||
|
(`db.py:3236-3242`, `db.py:3463-3482`). **תסמין:** חיפוש בקורפוס החיצוני
|
||||||
|
(`search_precedent_library`, `source_kind="external_upload"`) יכול להחזיר הלכה שמקורה
|
||||||
|
בהחלטת-ועדה פנימית — ולהפך עבור `search_internal_decisions` (`source_kind="internal_committee"`,
|
||||||
|
`internal_decisions.py:418`). **יעד:** `halacha_filters` יתחילו ב-`cl.source_kind = '{source_kind}'`
|
||||||
|
בדיוק כמו `chunk_filters` ([INV-RET1](#inv-ret1-הפרדת-קורפוס-נאכפת-ב-100-ממסלולי-ה-query)).
|
||||||
|
- **אין eval harness — מדידת-איכות לא קיימת.** רק `telemetry.log_search_bg` מתעד שאילתות
|
||||||
|
(`search.py:62,118,190,271`); אין gold-set מתויג ואין precision/recall. **יעד:** harness עומד
|
||||||
|
([INV-RET4](#inv-ret4-איכות-אחזור-נמדדת-ב-eval-harness-עומד-precision--recall)).
|
||||||
|
- **`search_decisions` מתעד אזהרה כשאין `practice_area` אך לא חוסם.** ללא פילטר-תחום החיפוש
|
||||||
|
עלול לערבב תחומים משפטיים (`search.py:45-49,172-176` — `logger.warning`, ממשיך). **יעד:** הפרדה
|
||||||
|
לפי תחום נאכפת, לא מומלצת בלבד — תואם את עקרון ההפרדה ב-[G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query).
|
||||||
|
- **`embedding` אינו `GENERATED` (בניגוד ל-tsvector).** נקודת-drift אפשרית בין תוכן ל-embedding
|
||||||
|
אחרי עדכון ([§4](#4-re-index-ושינוי-תוכן-g6); תואם [02-data-model](02-data-model.md#inv-dm3-שינוי-תוכן--re-index)).
|
||||||
|
**יעד:** טריגר re-embed מובטח + health-check.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md) — invariants גלובליים (G4–G9) + כללי-הנדסה.
|
||||||
|
- [01-ingest.md](01-ingest.md) — חוזה-הקליטה שמייצר את ה-chunks/embeddings שהאחזור שולף.
|
||||||
|
- [02-data-model.md](02-data-model.md) — חוזה-השלמות (searchable) + re-index שהאחזור מסנן לפיהם.
|
||||||
|
- [05-qa-review.md](05-qa-review.md) — שער-הלכה הידני (`review_status`) שמגדיר אילו הלכות searchable.
|
||||||
|
- [X5-audit-provenance.md](X5-audit-provenance.md) — עקיבוּת-מקור מלאה של כל span מוחזר (בסיס ל-INV-RET5).
|
||||||
|
- [X12-digests-radar.md](X12-digests-radar.md) — שכבת-הגילוי (יומונים) שמעל הקורפוסים — מצביעה, לא מצוטטת.
|
||||||
186
docs/spec/04-analysis-writing.md
Normal file
186
docs/spec/04-analysis-writing.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# 04 — ניתוח וכתיבה (Analysis & Writing)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) ומפרט את שלב **הסיוע-בכתיבה** —
|
||||||
|
חילוץ הטענות, ארכיטקטורת 12 הבלוקים, וסגנון דפנה. הוא אוכף את
|
||||||
|
[INV-G11](00-constitution.md#inv-g11-תוכן-החלטה-מנומקת) (תוכן החלטה מנומקת).
|
||||||
|
|
||||||
|
> **⚠ מודל-סמכות שונה מ-01–03.** זהו קובץ **תוכן-משפטי**, לא קובץ-הנדסה. לפי החוקה
|
||||||
|
> (§2 עיקרון 2, §5ב) הסמכות עליו היא **היו"ר (עו"ד דפנה תמיר) + מסמכי-הפרויקט** —
|
||||||
|
> [block-schema.md](../block-schema.md), [decision-methodology.md](../decision-methodology.md),
|
||||||
|
> [legal-decision-lessons.md](../legal-decision-lessons.md),
|
||||||
|
> [corpus-analysis.md](../corpus-analysis.md), [skills/decision/SKILL.md](../../skills/decision/SKILL.md).
|
||||||
|
> ה-invariants כאן **אינם** כפופים לפרוטוקול ≥3-המקורות החיצוני, ו**אינם** נושאים
|
||||||
|
> `סטטוס: verified / ⚠ UNVERIFIED`. במקום `מקורות: … | סטטוס` הם נושאים `מקור-סמכות:`.
|
||||||
|
> מסמכי-הפרויקט הם המקור המוסמך; קובץ זה מצטט אותם בגובה-ספ, לא משכפל את ההגדרות.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. חילוץ טענות → טיעונים מאוגדים
|
||||||
|
|
||||||
|
לפני הכתיבה, חומרי-המקור הופכים למבנה-נתונים שמזין את הבלוקים. שני שלבים:
|
||||||
|
|
||||||
|
### 1.1 חילוץ טענות גולמיות (claims)
|
||||||
|
|
||||||
|
`extract_claims(case_number, doc_title="", party_hint="")` קורא לכתבי-הטענות בתיק,
|
||||||
|
ושומר טענות גולמיות ב-DB. הוא מסנן למסמכים מסוג `appeal` / `response` / `objection`
|
||||||
|
(אלא אם צוין `doc_title` מפורש), ולכל מסמך קורא ל-`claims_extractor.extract_and_store_claims`
|
||||||
|
— ראה `mcp-server/src/legal_mcp/tools/documents.py:300-347`.
|
||||||
|
|
||||||
|
כל טענה נשמרת עם `party_role` מתוך התפקידים המוכרים: **`appellant` (עוררים)** ·
|
||||||
|
**`respondent` (משיבים)** · **`committee` (ועדה מקומית)** · **`permit_applicant`
|
||||||
|
(מבקשי היתר)** · **`appraiser` (שמאי)**. `get_claims(case_number, party_role="")`
|
||||||
|
שולף ומציג אותן בעברית, עם סינון אופציונלי לפי תפקיד
|
||||||
|
(`documents.py:350-385`; מיפוי-העברית ב-`:370-376`).
|
||||||
|
|
||||||
|
### 1.2 כינוס לטיעונים משפטיים מובחנים (legal arguments)
|
||||||
|
|
||||||
|
`aggregate_claims_to_arguments(case_number, force=False)` מכנס את הפרופוזיציות
|
||||||
|
הגולמיות לטיעונים משפטיים מובחנים (de-duplication) דרך
|
||||||
|
`argument_aggregator.aggregate_claims_to_arguments`; `force=True` מוחק טיעונים קיימים
|
||||||
|
ומחשב מחדש — ראה `mcp-server/src/legal_mcp/tools/legal_arguments.py:11-33`.
|
||||||
|
`get_legal_arguments(case_number, party="")` שולף את הטיעונים המאוגדים, מקובצים לפי
|
||||||
|
צד (`appellant`/`respondent`/`committee`/`permit_applicant`/`unknown`); אם אין —
|
||||||
|
הוא מחזיר הנחיה להריץ קודם את הכינוס (`legal_arguments.py:36-83`).
|
||||||
|
|
||||||
|
> **מדוע זה חשוב לתוכן:** הטיעונים המאוגדים הם הקלט ל-[INV-WR3](#inv-wr3-מענה-לכל-טענה-של-הצד-המפסיד)
|
||||||
|
> (מענה לכל טענה עיקרית) ול-[INV-WR4](#inv-wr4-בלוק-ז--טענות-מקוריות-בלבד) (הפרדת טענות
|
||||||
|
> מקוריות מהשלמות). הסינון לפי `party_role` מאפשר לזהות את הצד המפסיד ולוודא שכל טיעון
|
||||||
|
> שלו מקבל מענה בבלוק י.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. ארכיטקטורת 12 הבלוקים (סיכום)
|
||||||
|
|
||||||
|
המבנה הפורמלי המלא — content model, constraints, משקלות, ופרמטרי-עיבוד לכל בלוק —
|
||||||
|
מוגדר ב-[block-schema.md](../block-schema.md) (המקור המוסמך). כאן רק מפת-גובה:
|
||||||
|
|
||||||
|
| בלוק | תפקיד | CREAC | תוכן מהותי? |
|
||||||
|
|------|--------|-------|-------------|
|
||||||
|
| א–ד | כותרת מוסדית · הרכב · צדדים · "החלטה" | — | לא (template-fill) |
|
||||||
|
| ה | פתיחה ("לפנינו…") | C ראשוני | קל |
|
||||||
|
| **ו** | רקע עובדתי ("פתח דבר") | — | **כן — עובדות בלבד** |
|
||||||
|
| **ז** | טענות הצדדים | — | **כן — טענות מקוריות בלבד** |
|
||||||
|
| ח | הליכים בפני הוועדה | — | כן (תיעוד, ללא הערכה) |
|
||||||
|
| ט | תכניות חלות (אופציונלי) | R | כן (כשיש מורכבות תכנונית) |
|
||||||
|
| **י** | דיון והכרעה | full-CREAC | **כן — ה-ratio decidendi** |
|
||||||
|
| יא | סיכום / סוף דבר | C אחרון | קל |
|
||||||
|
| יב | חתימות | — | לא |
|
||||||
|
|
||||||
|
יסודות תיאורטיים (CREAC · FJC Judicial Writing Manual · DITA · Akoma Ntoso),
|
||||||
|
תלויות-בין-בלוקים, וכללי-ולידציה — ב-[block-schema.md](../block-schema.md) §§1, 5, 6.
|
||||||
|
מתודולוגיית-המשקלות (Communicative / Reader-attention / Judicial-review / Empirical)
|
||||||
|
— שם §4. **טיוטת-ביניים** (Pre-Ruling Draft) בוחרת תת-קבוצת בלוקים (ו, ט, ז, ח) —
|
||||||
|
block-schema.md §7; שלב-החילוץ השמאי שלה (`extract_appraiser_facts`) מזין את בלוק ט.
|
||||||
|
|
||||||
|
> **התמקדות לפי feedback היו"ר:** הסיוע מתמקד בבלוקים המהותיים (ו–יב); בלוקים א–ד
|
||||||
|
> ממולאים מ-template ואינם דורשים ניתוח. ראה `MEMORY.md` → "התעלם מכותרות".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. סגנון דפנה (סיכום)
|
||||||
|
|
||||||
|
מדריך-הסגנון המלא הוא [skills/decision/SKILL.md](../../skills/decision/SKILL.md);
|
||||||
|
המתודולוגיה האנליטית ("איך לחשוב לפני איך לכתוב") היא
|
||||||
|
[decision-methodology.md](../decision-methodology.md). נקודות-מפתח:
|
||||||
|
|
||||||
|
- **טון לפי סוג-ערר** — רישוי (1xxx) חם יחסית; היטל-השבחה (8xxx) ופיצויים ס'197 (9xxx)
|
||||||
|
קרים ויבשים (SKILL.md §1; methodology §א.2).
|
||||||
|
- **מבנה הדיון (בלוק י)** — נפתח במסקנה (CREAC: C→R→E→A→C), סילוגיזם לכל סוגיה,
|
||||||
|
steel-manning של הצד המפסיד, ציטוט-פסיקה ב"סנדוויץ'" (methodology §§ד, ו, ז).
|
||||||
|
- **מסלול-דיון לפי תוצאה** — דחייה (עיגולים קונצנטריים) · קבלה (נימוק-נימוק) · קבלה
|
||||||
|
חלקית (מיפוי-מתחים) · היטל-השבחה (פתיחה ישירה) — SKILL.md §7.3; block-schema.md בלוק י.
|
||||||
|
- **3 מקורות-פסיקה נפרדים** — אסור לבלבל ביניהם (SKILL.md §7.5; ראה גם
|
||||||
|
[03-retrieval.md](03-retrieval.md) לשכבת-האחזור שמזינה אותם).
|
||||||
|
- **לקחים מצטברים** — [legal-decision-lessons.md](../legal-decision-lessons.md) +
|
||||||
|
ביטויי-מעבר; מתעדכנים מפידבק-היו"ר ומ-Hermes (ראה forward-ref [07-learning.md](07-learning.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Invariants של התחום — תוכן החלטה מנומקת
|
||||||
|
|
||||||
|
חמשת ה-invariants הבאים הם **פאֶטים של [INV-G11](00-constitution.md#inv-g11-תוכן-החלטה-מנומקת)**.
|
||||||
|
כולם נושאים `מקור-סמכות` (היו"ר + מסמכי-הפרויקט), **ללא** שדה-מקורות-חיצוני ו**ללא**
|
||||||
|
סטטוס-אימות — כמתחייב מהבחנת שתי-הסמכויות בחוקה (§5).
|
||||||
|
|
||||||
|
### INV-WR1: רקע ניטרלי (בלוק ו) — עובדות בלבד
|
||||||
|
**כלל:** בלוק ו מציג **עובדות בלבד** ואינו טוען. אסורות מילות-ערך/שיפוט ("חריג",
|
||||||
|
"בעייתי", "למרבה הפליאה") ואסורים ציטוטים ישירים מצדדים (אלה שייכים לבלוק ז). החלטות
|
||||||
|
קודמות מובאות כעובדה יבשה ("ביום X נדחתה תכנית Y"), ללא נימוקים. ניטרליות אינה הסתרה:
|
||||||
|
עובדה מהותית התומכת בצד המפסיד **חייבת** להופיע.
|
||||||
|
**מקור-סמכות:** היו"ר (עו"ד דפנה תמיר) + [block-schema.md](../block-schema.md) (בלוק ו,
|
||||||
|
§5.2 "רקע ניטרלי") + [decision-methodology.md](../decision-methodology.md) §ח.2.
|
||||||
|
**אכיפה:** ולידציית-תוכן בבלוק ו (סעיף עם ציטוט-צד או מילת-שיפוט → לא שייך כאן) + שערי
|
||||||
|
QA; מפורט ב-[05-qa-review.md](05-qa-review.md).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-WR2: ללא כפילות (בלוק י מפנה, לא חוזר)
|
||||||
|
**כלל:** בלוק י (דיון) **מפנה** לעובדות ולטענות שכבר הוצגו בבלוקים הקודמים ("כאמור
|
||||||
|
בסעיף X לעיל", "כפי שפורט") — ואינו חוזר עליהן. חריג יחיד: חזרה מכוונת עם שכבת-ניתוח
|
||||||
|
חדשה ("נשוב על כך כי…"). אין עובדות חדשות בדיון שלא הופיעו ברקע.
|
||||||
|
**מקור-סמכות:** היו"ר + [block-schema.md](../block-schema.md) (בלוק י, §5.2 "ללא
|
||||||
|
כפילות") + [skills/decision/SKILL.md](../../skills/decision/SKILL.md) §9.1.
|
||||||
|
**אכיפה:** ולידציית-מבנה (עובדה בדיון ללא עוגן ברקע = flag) + שערי QA;
|
||||||
|
מפורט ב-[05-qa-review.md](05-qa-review.md).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-WR3: מענה לכל טענה של הצד המפסיד
|
||||||
|
**כלל:** כל **טענה עיקרית** שהוצגה בבלוק ז — ובמיוחד של הצד המפסיד — מקבלת **מענה
|
||||||
|
מנומק** בבלוק י (ישיר, "למעלה מן הצורך", או מקובץ עם דומותיה). מותר לא להכריע בטענה
|
||||||
|
נחוצה-פחות ("נוכח מסקנתנו לעיל, אין צורך…"), אך אסור להתעלם מטענה מרכזית — הצד המפסיד
|
||||||
|
חייב לראות שהוועדה שקלה את יסודות עמדתו (steel-manning).
|
||||||
|
**מקור-סמכות:** היו"ר + [decision-methodology.md](../decision-methodology.md) §§ג.2, ו.2 +
|
||||||
|
[block-schema.md](../block-schema.md) (בלוק י MUST: "מענה לכל טענה" §5.4) +
|
||||||
|
[skills/decision/SKILL.md](../../skills/decision/SKILL.md) §6.2.
|
||||||
|
**אכיפה:** מיפוי טענות-בלוק-ז → מענה-בלוק-י (נשען על §1.2, הטיעונים המאוגדים) + שערי QA;
|
||||||
|
מפורט ב-[05-qa-review.md](05-qa-review.md).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-WR4: בלוק ז — טענות מקוריות בלבד
|
||||||
|
**כלל:** בלוק ז מכיל **אך ורק** טענות מכתבי-הטענות המקוריים (כתב-ערר, כתב-תשובה).
|
||||||
|
תוכן מהשלמות-טיעון, החלטות-ביניים, ותגובות-מאוחרות → **בלוק ח** (הליכים), לא בלוק ז.
|
||||||
|
הצגת-הטענות היא בנאמנות וללא הערכה ("טענה זו חלשה") — ההערכה שייכת לבלוק י.
|
||||||
|
**מקור-סמכות:** היו"ר + [block-schema.md](../block-schema.md) (בלוק ז Sources +
|
||||||
|
§5.2 "טענות מקוריות בלבד") + [skills/decision/SKILL.md](../../skills/decision/SKILL.md) §4.
|
||||||
|
**אכיפה:** סיווג-מקור של טענה בעת החילוץ (`extract_claims` מסנן `appeal`/`response`/
|
||||||
|
`objection`; מסמכי פוסט-דיון מתויגים `is_post_hearing` ומופנים לבלוק ח — block-schema.md §7)
|
||||||
|
+ שערי QA; מפורט ב-[05-qa-review.md](05-qa-review.md).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-WR5: "מבחן-השופט" — החלטה עצמאית וקריאה
|
||||||
|
**כלל:** ההחלטה חייבת להיות **עצמאית וקריאה לשופט שלא מכיר את התיק** — תשתית עובדתית
|
||||||
|
מלאה (בלוק ו), תיעוד procedural-fairness (בלוק ח), והנמקה שעומדת בבדיקת סבירות
|
||||||
|
ומידתיות (בלוק י). הקורא לא נדרש לחומרי-המקור כדי להבין את ההחלטה ואת הצדקתה.
|
||||||
|
**מקור-סמכות:** היו"ר + [block-schema.md](../block-schema.md) §4.3 ("מבחן השופט" /
|
||||||
|
Judicial-Review weight) + [decision-methodology.md](../decision-methodology.md) §יב
|
||||||
|
(רשימת-ביקורת) + [corpus-analysis.md](../corpus-analysis.md).
|
||||||
|
**אכיפה:** שער QA סופי ("מבחן-השופט") על ההחלטה כיחידה שלמה;
|
||||||
|
מפורט ב-[05-qa-review.md](05-qa-review.md).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. צ'קליסט-תוכן לפי סוג-ערר
|
||||||
|
|
||||||
|
בלוק י מקבל **צ'קליסט-תוכן** המוזרק אוטומטית ל-prompt לפי סוג-הערר, מתוך
|
||||||
|
`CONTENT_CHECKLISTS` ב-`mcp-server/src/legal_mcp/services/lessons.py:355`. הבורר
|
||||||
|
(`lessons.py:532-555`) ממפה לסוג: `tama38` (תמ"א 38) · `betterment_levy` (היטל-השבחה) ·
|
||||||
|
`licensing_property` · `licensing_threshold` (שאלת-סף) · `licensing_substantive`
|
||||||
|
(ברירת-מחדל לרישוי). הצ'קליסט מבטיח שהדיון מכסה את הנושאים התכנוניים/המשפטיים שדפנה
|
||||||
|
מכסה בפועל בקורפוס — ראה [corpus-analysis.md](../corpus-analysis.md) §§3, 6 לדפוסי-התוכן
|
||||||
|
ולפער שנסגר (§5.3). זהו מנגנון-תוכן באחריות היו"ר, לא חוק-הנדסה.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md#inv-g11-תוכן-החלטה-מנומקת) — INV-G11 + הבחנת
|
||||||
|
שתי-הסמכויות (תוכן-משפטי מול הנדסה).
|
||||||
|
- [03-retrieval.md](03-retrieval.md) — שכבת-האחזור (3 קורפוסי-פסיקה) שמזינה ציטוטים לבלוק י.
|
||||||
|
- [05-qa-review.md](05-qa-review.md) — שערי-QA שאוכפים את INV-WR1–WR5 + שערים אנושיים.
|
||||||
|
- [06-export.md](06-export.md) — ייצוא DOCX לפי תבנית-דפנה (אחרי הכתיבה).
|
||||||
|
- [07-learning.md](07-learning.md) — לולאת פידבק-היו"ר + Hermes שמעדכנת lessons/SKILL.
|
||||||
|
- מסמכי-המקור המוסמכים: [block-schema.md](../block-schema.md) ·
|
||||||
|
[decision-methodology.md](../decision-methodology.md) ·
|
||||||
|
[legal-decision-lessons.md](../legal-decision-lessons.md) ·
|
||||||
|
[corpus-analysis.md](../corpus-analysis.md) ·
|
||||||
|
[skills/decision/SKILL.md](../../skills/decision/SKILL.md).
|
||||||
198
docs/spec/05-qa-review.md
Normal file
198
docs/spec/05-qa-review.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# 05 — בקרת איכות ושערים אנושיים (QA & Human Review)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) ומפרט את שלב **הביקורת** לפני
|
||||||
|
ייצוא: (1) **שערי-QA אוטומטיים** (`validate_decision` — 6 בדיקות) ו-(2) **שערים אנושיים**
|
||||||
|
(אישור הלכה, בחירת תוצאה, פידבק היו"ר). הוא אוכף את
|
||||||
|
[INV-G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)
|
||||||
|
(שערים אנושיים) ואת [INV-G11](00-constitution.md#inv-g11-תוכן-החלטה-מנומקת) (תוכן מנומק).
|
||||||
|
|
||||||
|
> **⚠ קובץ מעורב — שני מודלי-סמכות.** לפי החוקה (§3, §5):
|
||||||
|
> - **שערי-הממשל** (שערים אנושיים, שער-הייצוא) הם **invariants הנדסיים** במודל
|
||||||
|
> הממשל-שיפוטי → נושאים `מקורות:` (NCSC/JTC · CEPEJ 2018 · FJC) + `סטטוס: verified`.
|
||||||
|
> - **מכניקת בדיקות-התוכן** (מה הבדיקה האוטומטית בוחנת בפועל — רקע ניטרלי, ללא כפילות,
|
||||||
|
> כיסוי-טענות) היא **תוכן-משפטי** → נושאת `מקור-סמכות:` (היו"ר + מסמכי-הפרויקט +
|
||||||
|
> [04-analysis-writing.md](04-analysis-writing.md)), **ללא** מקורות חיצוניים וללא סטטוס.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. שערי-QA אוטומטיים — `validate_decision`
|
||||||
|
|
||||||
|
`validate_decision(case_number)` (wrapper ב-`tools/drafting.py:363`, נחשף ב-`server.py:551`)
|
||||||
|
טוען את בלוקי-ההחלטה והטענות מה-DB ומריץ **6 בדיקות**, אז כותב את התוצאות לטבלת
|
||||||
|
`qa_results` ומחזיר `passed` / `critical_failures` / `export_blocked`. הליבה:
|
||||||
|
`services/qa_validator.py:292` (`validate_decision`). כל בדיקה מחזירה
|
||||||
|
`{name, passed, errors, severity}`; `severity ∈ {critical, warning}`.
|
||||||
|
|
||||||
|
> **חישוב החסימה:** `critical_failures = Σ(not passed ∧ severity=="critical")`
|
||||||
|
> (`qa_validator.py:338`), ו-`export_blocked = critical_failures > 0`
|
||||||
|
> (`qa_validator.py:362`). בדיקת `warning` שנכשלת מורידה `passed=False` אך **אינה** חוסמת
|
||||||
|
> ייצוא. ראה [§3 / INV-QA3](#inv-qa3-החלטה-לא-מיוצאת-עם-כשל-קריטי-governance--g10).
|
||||||
|
|
||||||
|
### 1.1 ששת השערים
|
||||||
|
|
||||||
|
| # | בדיקה | מה בוחנת | severity | פונקציה (file:line) |
|
||||||
|
|---|-------|----------|----------|---------------------|
|
||||||
|
| 1 | `neutral_background` | רקע (בלוק ו) ללא מילות-שיפוט (`VALUE_WORDS`) וללא ציטוט-צד (`QUOTE_INDICATORS`) | **warning** | `check_neutral_background` — `qa_validator.py:66` |
|
||||||
|
| 2 | `claims_coverage` | כל טענה מבלוק ז נענתה בבלוק י (בדיקה סמנטית דרך Claude) | **critical** | `check_claims_coverage` — `qa_validator.py:107` |
|
||||||
|
| 3 | `weight_compliance` | משקל-מילים של כל בלוק בטווח לפי סוג-ערר (`WEIGHT_RANGES`) | **warning** | `check_weight_compliance` — `qa_validator.py:177` |
|
||||||
|
| 4 | `structural_integrity` | בלוקי-חובה קיימים (ה, ז, י, יא) + בלוק י הוא הכבד ביותר | **critical** | `check_structural_integrity` — `qa_validator.py:206` |
|
||||||
|
| 5 | `no_duplication` | אין משפט מבלוק ו (>30 תווים) שחוזר מילה-במילה בבלוק י | **warning** | `check_no_duplication` — `qa_validator.py:235` |
|
||||||
|
| 6 | `sequential_numbering` | מספור-סעיפים רציף בכל הבלוקים, מתחיל ב-1, ללא פערים | **warning** | `check_sequential_numbering` — `qa_validator.py:261` |
|
||||||
|
|
||||||
|
### 1.2 דקויות חשובות (אל תניח — מהקוד)
|
||||||
|
|
||||||
|
- **רק 2 שערים קריטיים** חוסמים ייצוא: `claims_coverage` ו-`structural_integrity`. שאר
|
||||||
|
הארבעה הם `warning` בנתיב הרגיל — `qa_validator.py:86, 202, 257, 286`.
|
||||||
|
- **דקות `neutral_background` — שני נתיבי-החזרה:** הנתיב הרגיל מסומן `warning` (`:86`); נתיב
|
||||||
|
ה-fallback של בלוק-ו ריק/חסר מסומן `critical` (`:70`) **אך מחזיר `passed=True`**, ולכן
|
||||||
|
אינו נספר ב-`critical_failures` ואינו חוסם ייצוא. תפקודית — השער אינו חוסם.
|
||||||
|
- **`claims_coverage` סובלני ל-20%:** עובר אם `len(missing) ≤ total*0.2`
|
||||||
|
(`qa_validator.py:170`). מסנן לטענות `appellant`/`respondent` שאינן מבלוק-ז
|
||||||
|
(`qa_validator.py:120-129`), כי טענות `committee`/`permit_applicant` הן עמדות-הגנה ולא
|
||||||
|
דורשות מענה. כשל-פענוח של Claude → fallback `passed=True` כדי לא לחסום ייצוא על תקלת-LLM
|
||||||
|
(`qa_validator.py:148-152`).
|
||||||
|
- **`neutral_background` ריק = עובר:** בלוק ו ריק/חסר מחזיר `passed=True`
|
||||||
|
(`qa_validator.py:69`). הבדיקה היא lexical (רשימת-מילים + regex), לא סמנטית.
|
||||||
|
- **`no_duplication` תופס רק חזרה מילה-במילה** (substring) — לא פרפרזה.
|
||||||
|
- כל ריצה **מנקה** את `qa_results` הקודמות של התיק ואז כותבת מחדש (`qa_validator.py:344-357`).
|
||||||
|
|
||||||
|
### 1.3 שערי-התוכן מתפעלים את WR1–WR3
|
||||||
|
|
||||||
|
שלוש מ-6 הבדיקות הן ההפעלה האוטומטית (חלקית) של ה-invariants של התוכן ב-
|
||||||
|
[04-analysis-writing.md](04-analysis-writing.md):
|
||||||
|
|
||||||
|
| שער QA | invariant-תוכן | פער (אוטומטי מול הגדרה) |
|
||||||
|
|--------|----------------|--------------------------|
|
||||||
|
| `neutral_background` | [INV-WR1](04-analysis-writing.md#inv-wr1-רקע-ניטרלי-בלוק-ו--עובדות-בלבד) | lexical בלבד — לא תופס שיפוט עקיף; warning, לא critical |
|
||||||
|
| `no_duplication` | [INV-WR2](04-analysis-writing.md#inv-wr2-ללא-כפילות-בלוק-י-מפנה-לא-חוזר) | מילה-במילה בלבד — לא תופס כפילות מנוסחת-מחדש |
|
||||||
|
| `claims_coverage` | [INV-WR3](04-analysis-writing.md#inv-wr3-מענה-לכל-טענה-של-הצד-המפסיד) | סמנטי (Claude), סובלני ל-20% חוסר |
|
||||||
|
|
||||||
|
ראה [INV-QA4](#inv-qa4-שערי-התוכן-האוטומטיים-אוכפים-את-wr1wr3-content--g11). WR4 (טענות
|
||||||
|
מקוריות) ו-WR5 ("מבחן-השופט") **אינם** מכוסים על-ידי `validate_decision` — WR4 נאכף
|
||||||
|
בנקודת-החילוץ (`extract_claims`), WR5 הוא שער-איכות אנושי/agent. הסוכן `legal-qa`
|
||||||
|
(ראה [X4-agents.md](X4-agents.md)) מוסיף שערים ידניים מעבר ל-6 הקוד-יים (קול-דפנה,
|
||||||
|
שאילתות-קורפוס, צירוף-פסיקה) — `.claude/agents/legal-qa.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. שערים אנושיים — היו"ר מכריעה
|
||||||
|
|
||||||
|
המערכת מסייעת; ההכרעה היא של היו"ר. שלושה שערים אנושיים מובנים בקוד-הזרימה ואינם ניתנים
|
||||||
|
לעקיפה אוטומטית (זהו [INV-G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)).
|
||||||
|
|
||||||
|
### 2.1 אישור הלכה (halacha approval)
|
||||||
|
|
||||||
|
הלכות מחולצות אוטומטית מפסיקה (`halacha_extractor.py`), אך **נכנסות כ-`pending_review`
|
||||||
|
ובלתי-נראות לחיפוש** עד אישור היו"ר:
|
||||||
|
|
||||||
|
- **כתיבה:** `db.add_halacha` קובע `review_status = "approved" if auto_approve else
|
||||||
|
"pending_review"` (`db.py:3003`), כאשר `auto_approve` נגזר מסף-ביטחון
|
||||||
|
`HALACHA_AUTO_APPROVE_THRESHOLD` (ברירת-מחדל `0.80`, `config.py:111`). הלכות מתחת לסף
|
||||||
|
נשארות `pending_review`.
|
||||||
|
- **שער-האישור:** `halacha_review(halacha_id, status, reviewer="דפנה", …)`
|
||||||
|
(`tools/precedent_library.py:291`, נחשף ב-`server.py:298`) — היו"ר מאשרת/דוחה/עורכת.
|
||||||
|
`status ∈ {pending_review, approved, rejected, published}` (`precedent_library.py:311`).
|
||||||
|
- **תור-ההמתנה:** `halachot_pending(limit=100)` (`precedent_library.py:335`) מחזיר את
|
||||||
|
`review_status='pending_review'`.
|
||||||
|
- **חשיפה רק לאחר אישור:** החיפוש מסנן `h.review_status IN ('approved','published')`
|
||||||
|
(`db.py:3168` ו-`db.py:3401`) — הלכה שלא אושרה **לעולם** לא עולה בתוצאות.
|
||||||
|
|
||||||
|
### 2.2 בחירת תוצאה (outcome selection)
|
||||||
|
|
||||||
|
`set_outcome(case_number, outcome, reasoning="")` (`tools/workflow.py:145`,
|
||||||
|
`server.py:646`) — היו"ר קובעת `outcome ∈ {rejected, accepted, partial}`
|
||||||
|
(`workflow.py:163`). זוהי **הכרעה משפטית**: היא קודמת לכתיבת-הטיוטה וקובעת את מסלול-הדיון
|
||||||
|
(ראה [04-analysis-writing.md](04-analysis-writing.md) §3). אין נתיב שבו המערכת בוחרת תוצאה
|
||||||
|
לבד — אם לא סופק נימוק, המערכת מציעה כיווני-נימוק (`brainstorm`), אך הבחירה נשארת אנושית.
|
||||||
|
|
||||||
|
### 2.3 פידבק היו"ר (chair feedback)
|
||||||
|
|
||||||
|
- `record_chair_feedback(case_number, feedback_text, block_id, category, …)`
|
||||||
|
(`tools/workflow.py:348`, `server.py:896`) — מתעד הערת-דפנה; `category` מתוך
|
||||||
|
`{missing_content, wrong_tone, wrong_structure, factual_error, style, other}`
|
||||||
|
(`workflow.py:367`).
|
||||||
|
- `list_chair_feedback(case_number, category, unresolved_only=True)`
|
||||||
|
(`tools/workflow.py:393`, `server.py:910`) — שליפה לסקירה.
|
||||||
|
|
||||||
|
הפידבק מזין את לולאת-הלמידה ([07-learning.md](07-learning.md)) ואת
|
||||||
|
[legal-decision-lessons.md](../legal-decision-lessons.md). זהו שיפוט-אנושי על איכות —
|
||||||
|
לעולם לא מוסק או מוחל אוטומטית.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-QA1: אישור הלכה הוא שער אנושי (governance →G10)
|
||||||
|
**כלל:** אישור הלכה הוא **הכרעה ידנית של היו"ר**. הלכות שחולצו אוטומטית הן
|
||||||
|
`pending_review` עד שהיו"ר מאשרת; **רק הלכות מאושרות** (`approved`/`published`) עולות
|
||||||
|
בחיפוש. תור-ההמתנה חייב להיות **נראה** (`halachot_pending`) כדי שאישור-חסר לא יישאר סמוי.
|
||||||
|
**מקורות:** NCSC/JTC — *Principles & Practices for AI Use in Courts* (human-in-the-loop) ·
|
||||||
|
Council of Europe / CEPEJ (2018, under user control) · Federal Judicial Center —
|
||||||
|
*Judicial Writing Manual* (2d ed.) | סטטוס: verified
|
||||||
|
**אכיפה:** ברירת-מחדל `pending_review` בכתיבה (`db.py:3003`) + סינון
|
||||||
|
`review_status IN ('approved','published')` בכל query (`db.py:3168`, `db.py:3401`) + שער-אישור
|
||||||
|
`halacha_review` (`precedent_library.py:291`).
|
||||||
|
**הפרה ידועה:** 10/19 הלכות מאושרות — שער-ידני שקוף בלי נראות-backlog; ההפרש התגלה במקרה →
|
||||||
|
ממצא ל-[audit](../audit-report.md) (ראה גם [INV-G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)).
|
||||||
|
|
||||||
|
### INV-QA2: בחירת-תוצאה ופידבק הם שערים אנושיים (governance →G10)
|
||||||
|
**כלל:** **בחירת התוצאה** (`set_outcome`) ו**פידבק-היו"ר** (`record_chair_feedback`) הם
|
||||||
|
שערים אנושיים — **לעולם לא אוטומטיים**. המערכת מסייעת (מציעה כיווני-נימוק, מתעדת הערות),
|
||||||
|
אך ההכרעה והשיפוט-על-האיכות הם של היו"ר.
|
||||||
|
**מקורות:** NCSC/JTC — *Principles & Practices for AI Use in Courts* ("never replace human
|
||||||
|
judgment") · Council of Europe / CEPEJ (2018, under user control) · Federal Judicial
|
||||||
|
Center — *Judicial Writing Manual* (2d ed.) | סטטוס: verified
|
||||||
|
**אכיפה:** `set_outcome` דורש `outcome` מפורש מהיו"ר (`workflow.py:145-165`);
|
||||||
|
`record_chair_feedback`/`list_chair_feedback` מתעדים בלבד (`workflow.py:348, 393`) — אין
|
||||||
|
מסלול-קוד שמסיק תוצאה או פידבק לבד.
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-QA3: החלטה לא מיוצאת עם כשל קריטי (governance →G10)
|
||||||
|
**כלל:** החלטה **אינה ניתנת לייצוא** כל עוד שער-QA **קריטי** נכשל
|
||||||
|
(`claims_coverage` או `structural_integrity`). `export_blocked` חייב להיבדק לפני ייצוא;
|
||||||
|
ייצוא בכשל-קריטי הוא הפרה. שערי-`warning` שנכשלים מתועדים אך אינם חוסמים.
|
||||||
|
**מקורות:** NCSC/JTC — *Principles & Practices for AI Use in Courts* (controlled, auditable
|
||||||
|
AI output) · Council of Europe / CEPEJ (2018, under user control) · Federal Judicial
|
||||||
|
Center — *Judicial Writing Manual* (2d ed.) | סטטוס: verified
|
||||||
|
**אכיפה:** `export_blocked = critical_failures > 0` (`qa_validator.py:362`); נאכף בשער-הזרימה
|
||||||
|
של הסוכן `legal-exporter` ("לעולם אל תייצא בלי `validate_decision` קודם", "בדוק שאין
|
||||||
|
כשלים קריטיים" — `.claude/agents/legal-exporter.md:71, 149`). קושר ל-[06-export.md](06-export.md).
|
||||||
|
**הפרה ידועה:** `export_docx` (`drafting.py:384`) **אינו** מריץ `validate_decision` בעצמו —
|
||||||
|
החסימה היא ברמת-הזרימה/agent, לא hard-block בקוד-הייצוא. פער זה → ראה [§4](#4-current-vs-target--ממצאי-audit) (audit).
|
||||||
|
|
||||||
|
### INV-QA4: שערי-התוכן האוטומטיים אוכפים את WR1–WR3 (content →G11)
|
||||||
|
**כלל:** שערי-התוכן האוטומטיים מתפעלים את invariants-התוכן: `neutral_background`↔
|
||||||
|
[WR1](04-analysis-writing.md#inv-wr1-רקע-ניטרלי-בלוק-ו--עובדות-בלבד) (רקע ניטרלי) ·
|
||||||
|
`no_duplication`↔[WR2](04-analysis-writing.md#inv-wr2-ללא-כפילות-בלוק-י-מפנה-לא-חוזר)
|
||||||
|
(ללא כפילות) · `claims_coverage`↔[WR3](04-analysis-writing.md#inv-wr3-מענה-לכל-טענה-של-הצד-המפסיד)
|
||||||
|
(מענה-לטענות). האכיפה האוטומטית היא **רצפה, לא תקרה** — WR4/WR5 וההבטים העדינים (שיפוט-עקיף,
|
||||||
|
כפילות מנוסחת-מחדש) נשארים בשיקול-הדעת האנושי (INV-QA1–QA3).
|
||||||
|
**מקור-סמכות:** היו"ר (עו"ד דפנה תמיר) + [04-analysis-writing.md](04-analysis-writing.md)
|
||||||
|
(INV-WR1–WR3) + `mcp-server/src/legal_mcp/services/qa_validator.py` (הבדיקות בפועל).
|
||||||
|
**אכיפה:** `check_neutral_background` (`qa_validator.py:66`), `check_no_duplication`
|
||||||
|
(`qa_validator.py:235`), `check_claims_coverage` (`qa_validator.py:107`).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Current vs Target — ממצאי-audit
|
||||||
|
|
||||||
|
- **Halacha backlog בלתי-נראה (INV-QA1):** 10/19 הלכות מאושרות; 9 נשארו `pending_review`
|
||||||
|
ולא עלו בחיפוש. השער עבד כשורה — אך חוסר-נראות של ה-backlog הסתיר את הפער עד שהתגלה
|
||||||
|
במקרה. **Target:** מדד-נראות (count `pending_review`) כחלק מבדיקת-בריאות, לא רק
|
||||||
|
`halachot_pending` בדרישה. ראה [audit](../audit-report.md).
|
||||||
|
- **שער-ייצוא אכוף-זרימה ולא אכוף-קוד (INV-QA3):** `export_docx` לא קורא ל-`validate_decision`;
|
||||||
|
החסימה תלויה במשמעת הסוכן `legal-exporter`. **Target:** hard-block בתוך `export_docx`
|
||||||
|
(בדיקת `qa_results`/`export_blocked` לפני כתיבת DOCX) כדי שלא יהיה ניתן לעקיפה.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant) —
|
||||||
|
INV-G10 (שערים אנושיים) + INV-G11 + הבחנת שתי-הסמכויות.
|
||||||
|
- [04-analysis-writing.md](04-analysis-writing.md) — INV-WR1–WR5 שהשערים האוטומטיים מתפעלים.
|
||||||
|
- [06-export.md](06-export.md) — ייצוא DOCX (השלב אחרי המעבר בשער הקריטי).
|
||||||
|
- [07-learning.md](07-learning.md) — לולאת פידבק-היו"ר + Hermes שמעדכנת lessons/SKILL.
|
||||||
|
- [X4-agents.md](X4-agents.md) — הסוכן `legal-qa` (שערים ידניים נוספים) ו-`legal-exporter`.
|
||||||
|
- [X5-audit-provenance.md](X5-audit-provenance.md) — audit-trail לפלטי-AI ועקיבוּת-מקור.
|
||||||
168
docs/spec/06-export.md
Normal file
168
docs/spec/06-export.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# 06 — ייצוא DOCX (Export Contract)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) ומגדיר את **חוזה-הייצוא** של עוזר
|
||||||
|
משפטי: הרינדור של החלטה ל-DOCX מעוצב (גופן David, RTL, סגנונות-טמפלט). העיקרון המכונן —
|
||||||
|
**ה-DB הוא מקור-האמת היחיד, וה-DOCX הוא נתון נגזר (derived) הניתן לשחזור**. הקובץ אוכף את
|
||||||
|
[INV-G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) (מקור-אמת
|
||||||
|
יחיד / נתון-נגזר משוחזר) ואת [INV-G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai)
|
||||||
|
(עקיבוּת-מקור), והוא השלב שאחרי שער-הייצוא הקריטי של
|
||||||
|
[05-qa-review.md / INV-QA3](05-qa-review.md#inv-qa3-החלטה-לא-מיוצאת-עם-כשל-קריטי-governance--g10).
|
||||||
|
|
||||||
|
> **כללי-סגנון — סמכות אחת.** מכניקת העיצוב (line classification, dash policy, placeholder,
|
||||||
|
> מיפוי-סגנונות, RTL-runs) מתועדת במלואה בסקיל
|
||||||
|
> [`dafna-decision-template/SKILL.md`](../../skills/dafna-decision-template/SKILL.md) — **הוא
|
||||||
|
> המקור הסמכותי**. הקובץ הזה **מסכם ומפנה**, לא משכפל. כללי-הסגנון עצמם הם תוכן-משפטי-דומייני
|
||||||
|
> (סמכות היו"ר + הסקיל), בעוד שחוזה-ה-derived-data (INV-EX1) ועקיבוּת-המקור (INV-EX2) הם
|
||||||
|
> invariants הנדסיים הנושאים `מקורות` + `סטטוס`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. חוזה-הייצוא — DB הוא המקור, DOCX הוא הנגזר
|
||||||
|
|
||||||
|
החלטה מאוחסנת כ-**בלוקים מובְנים ב-DB** — `decision_blocks` (12 בלוקים, מפתח קנוני
|
||||||
|
`UNIQUE(decision_id, block_id)`) תחת `decisions` (`UNIQUE(case_id, version)`); ראה
|
||||||
|
[02-data-model.md §1](02-data-model.md). ה-DOCX **נגזר** מהבלוקים האלה ואינו מקור-אמת עצמאי:
|
||||||
|
מחיקתו אינה מאבדת תוכן, וייצוא חוזר מאותם בלוקים מפיק מסמך שקול.
|
||||||
|
|
||||||
|
**מסלול-הייצוא הקנוני (הסופי):**
|
||||||
|
|
||||||
|
1. `export_docx(case_number)` (`tools/drafting.py:384`, נחשף `server.py:557`) שולף את התיק,
|
||||||
|
ואז קורא ל-`docx_exporter.export_decision(case_id, …, mode="final")`
|
||||||
|
(`services/docx_exporter.py:306`).
|
||||||
|
2. `export_decision` שולף את הבלוקים **ישירות מ-`decision_blocks`**
|
||||||
|
(`SELECT block_id, block_index, title, content, word_count … ORDER BY block_index`,
|
||||||
|
`docx_exporter.py:336-342`) — אין מקור-תוכן אחר.
|
||||||
|
3. טוען את טמפלט-דפנה (`skills/docx/decision_template.docx`, `docx_exporter.py:27-29,364`),
|
||||||
|
מנקה את גוף-המסמך (`_clear_body`), וכותב כל בלוק עם **bookmark עוטף** (אנקור ל-revisions
|
||||||
|
עתידיים, `_wrap_block_with_bookmarks`, `docx_exporter.py:367-382`).
|
||||||
|
4. שומר לקובץ מגורסן `data/cases/{case_number}/exports/טיוטה-v{N}.docx` (גרסה אוטומטית עולה,
|
||||||
|
`docx_exporter.py:384-400`).
|
||||||
|
|
||||||
|
> **שני מסלולי-ייצוא לפי מקור-התוכן (לא מסלולים-מקבילים מתפצלים):**
|
||||||
|
> - `docx_exporter.py` — **ההחלטה הסופית** מ-12 הבלוקים ב-`decision_blocks` (`mode="final"`),
|
||||||
|
> וגם **טיוטת-ביניים** (`mode="interim"` — תת-קבוצת בלוקים בסדר חדש: רקע→תכניות→טענות→הליכים,
|
||||||
|
> `export_interim_draft`, `drafting.py:511`). שני המצבים שולפים מאותה טבלה — וריאציית-תצוגה
|
||||||
|
> של אותו מקור-אמת, לא מסלול שני.
|
||||||
|
> - `analysis_docx_exporter.py` (`build_analysis_docx`, `:401`) — מייצא את מסמך **הניתוח
|
||||||
|
> המשפטי** (`analysis-and-research.md`) שכתב `legal-analyst`, לא את בלוקי-ההחלטה. זהו תוצר-עזר
|
||||||
|
> שונה (שלב ניתוח, לא החלטה) — והוא המסלול שהסקיל מתעד בעיקר. שניהם חולקים את **אותו טמפלט
|
||||||
|
> ואותם כללי-סגנון**, כנדרש מ-[INV-G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
> (סימטריה — לא שתי שכבות-סגנון מתפצלות).
|
||||||
|
|
||||||
|
## 2. כללי-הסגנון — סיכום (הסמכות: הסקיל)
|
||||||
|
|
||||||
|
ה-service מחיל את סגנונות-הטמפלט בלבד (`paragraph.style = "Heading 2"`) — בלי font/size/indent
|
||||||
|
ידני; העיצוב (David, RTL, גדלים) מגיע מ-`styles.xml`. הפירוט המלא + ה-XML של כל סגנון:
|
||||||
|
[`SKILL.md`](../../skills/dafna-decision-template/SKILL.md) + `references/`.
|
||||||
|
|
||||||
|
- **סיווג-שורות (`_classify_line`):** כל שורה מסווגת לאחת מ-6 קטגוריות — `label_heading`,
|
||||||
|
`inline_label`, `numbered`, `bullet`, `heb_letter`, `plain` — שקובעות את הסגנון המוחל
|
||||||
|
(Heading 2 / Normal / List Paragraph). ראה
|
||||||
|
[`references/line-classification.md`](../../skills/dafna-decision-template/references/line-classification.md).
|
||||||
|
- **מדיניות-מקפים (`_no_dash`):** דפנה ביקשה "בלי מקפים בכלל" — `—` (U+2014) ו-`–` (U+2013)
|
||||||
|
מוסרים מכל טקסט נכתב; מקף רגיל (`-`) נשמר.
|
||||||
|
- **שדות-placeholder:** `chair_position` עם סימן-ריק (`[ימולא ע"י יו"ר הוועדה]` וכד') מוחלף
|
||||||
|
ב-`[טרם מולאה עמדת ועדת הוועדה]` ב-italic — סימן ויזואלי שנותר להשלים (תואם
|
||||||
|
[INV-G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant) — היו"ר
|
||||||
|
משלימה, לא המערכת).
|
||||||
|
- **RTL-runs:** כל run מסומן `<w:rtl/>` (`_mark_run_rtl`) — אחרת Word נופל ל-Times New Roman
|
||||||
|
במקום David. ראה [`references/rtl-runs.md`](../../skills/dafna-decision-template/references/rtl-runs.md).
|
||||||
|
- **מספור:** מספור אוטומטי רק ב-`List Paragraph` (decimal); שורות `(א)(ב)` מקבלות
|
||||||
|
`List Paragraph` עם `_strip_numpr()` (המספור העברי בטקסט).
|
||||||
|
|
||||||
|
## 3. רישום הגרסה — `active_draft_path` + git
|
||||||
|
|
||||||
|
לאחר כתיבת ה-DOCX, `export_docx` (`drafting.py:404-408`):
|
||||||
|
|
||||||
|
1. **`set_active_draft_path(case_id, path)`** (`db.py:1177`) — רושם את ה-DOCX שיוצא כ-
|
||||||
|
active-draft הנוכחי (`cases.active_draft_path`, `db.py:189`). שדה זה הוא **האנקור לעריכות
|
||||||
|
עוקבות** (`revise_draft`/`apply_user_edit`/`list_bookmarks`), לא מקור-אמת-תוכן מתחרה ל-DB.
|
||||||
|
2. **`git_sync.commit_and_push(case_dir, "ייצוא DOCX: …")`** (`drafting.py:408`) — מקבע את
|
||||||
|
הקובץ ב-git של תיקיית-התיק (audit-trail של פלט,
|
||||||
|
[INV-G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai); ראה
|
||||||
|
[X5-audit-provenance.md](X5-audit-provenance.md)).
|
||||||
|
|
||||||
|
אותו דפוס (`set_active_draft_path` + commit) חוזר ב-`export_interim_draft` (`drafting.py:533,536`),
|
||||||
|
`revise_draft` (`drafting.py:692,695`) ו-`apply_user_edit` (`drafting.py:579,582`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-EX1: ייצוא דטרמיניסטי ומשוחזר מהבלוקים — DOCX הוא נתון-נגזר (→G2)
|
||||||
|
**כלל:** הייצוא **דטרמיניסטי וניתן-לשחזור** מבלוקי-ההחלטה המאוחסנים ב-`decision_blocks`:
|
||||||
|
אותם בלוקים + אותו טמפלט מפיקים מסמך שקול. ה-DOCX הוא **נתון-נגזר (derived)** — **לעולם לא
|
||||||
|
מקור-אמת עצמאי**. אסור מסלול-תוכן שני שכותב DOCX ממקור שאינו ה-DB; וריאציות (final/interim)
|
||||||
|
הן תצוגות של אותו מקור.
|
||||||
|
**מקורות:** Martin Kleppmann — *Designing Data-Intensive Applications* (O'Reilly, 2017,
|
||||||
|
system-of-record מול derived data, ושחזור derived מהמקור) · Martin Fowler (Canonical Data
|
||||||
|
Model / Single Source of Truth) · SSOT (Single Source of Truth principle) | סטטוס: verified
|
||||||
|
**אכיפה:** `export_decision` שולף אך-ורק מ-`decision_blocks` (`docx_exporter.py:336-342`);
|
||||||
|
פלט מגורסן + idempotent מבחינת-תוכן; אוכף את
|
||||||
|
[INV-G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) וכלל-ההנדסה
|
||||||
|
"סימטריה" (חוקה §6).
|
||||||
|
**הפרה ידועה:** אחרי `revise_draft`/`apply_user_edit`, ה-DOCX המסומן `active_draft_path` הופך
|
||||||
|
ל"מקור-האמת" לעריכות-Track-Changes העוקבות (`db.py:185-188`), ו**בלוקי-ה-DB אינם מתעדכנים
|
||||||
|
חזרה** — הנתון-הנגזר זוחל למקור-אמת בפועל בלי סנכרון לאחור. **יעד:** או re-sync מהבלוקים, או
|
||||||
|
חוזה מפורש ש-`active_draft_path` הוא רק אנקור-revision ולא מקור-תוכן → ראה [§5](#5-current-vs-target).
|
||||||
|
|
||||||
|
### INV-EX2: עקיבוּת-מקור נשמרת בהחלטה המיוצאת (→G9)
|
||||||
|
**כלל:** ההחלטה המיוצאת **שומרת על עקיבוּת-מקור** היכן שנדרש — סמכויות-משפטיות מצוטטות
|
||||||
|
ניתנות-לאיתור (citation resolvable), והפלט מקובע ב-audit-trail (commit git). הפניות-פסיקה
|
||||||
|
בבלוקים אינן מאבדות את מקורן בעת הרינדור.
|
||||||
|
**מקורות:** Council of Europe / CEPEJ — *European Ethical Charter on AI in judicial systems*
|
||||||
|
(2018, traceability/transparency) · ISO 15489-1:2016 (records authenticity/integrity) ·
|
||||||
|
Lewis et al. (2020, NeurIPS — RAG attribution) | סטטוס: verified
|
||||||
|
**אכיפה:** `export_docx` מקבע כל פלט ב-git (`git_sync.commit_and_push`, `drafting.py:408`) +
|
||||||
|
רושם `active_draft_path` (`db.py:1177`); עקיבוּת-המקור של הציטוטים עצמם נאכפת במעלה-הזרם
|
||||||
|
(חילוץ-טענות/הלכות + provenance, [04-analysis-writing.md](04-analysis-writing.md),
|
||||||
|
[X5-audit-provenance.md](X5-audit-provenance.md)). אוכף את
|
||||||
|
[INV-G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-EX3: אין ייצוא בכשל-QA קריטי (restate של INV-QA3 →G10)
|
||||||
|
**כלל:** הייצוא **חסום** כל עוד שער-QA קריטי נכשל (`claims_coverage` / `structural_integrity`);
|
||||||
|
`export_blocked` חייב להיבדק לפני ייצוא. זהו אותו invariant של
|
||||||
|
[INV-QA3](05-qa-review.md#inv-qa3-החלטה-לא-מיוצאת-עם-כשל-קריטי-governance--g10), בצד-הייצוא.
|
||||||
|
**מקורות:** NCSC/JTC — *Principles & Practices for AI Use in Courts* (controlled, auditable
|
||||||
|
output) · Council of Europe / CEPEJ (2018, under user control) · Federal Judicial Center —
|
||||||
|
*Judicial Writing Manual* (2d ed.) | סטטוס: verified
|
||||||
|
**אכיפה:** `export_blocked = critical_failures > 0` (`qa_validator.py:362`); **נאכף ברמת-
|
||||||
|
הזרימה/agent בלבד** — הסוכן `legal-exporter` מחויב להריץ `validate_decision` ולבדוק
|
||||||
|
כשלים-קריטיים לפני ייצוא (`.claude/agents/legal-exporter.md:71,149`).
|
||||||
|
**הפרה ידועה:** `export_docx` (`drafting.py:384`) **אינו** קורא ל-`validate_decision` בעצמו —
|
||||||
|
הוא ניגש ישירות ל-`docx_exporter.export_decision` בלי לבדוק `export_blocked`. החסימה תלויה
|
||||||
|
במשמעת-הסוכן ואינה hard-block בקוד-הייצוא → ראה [§5](#5-current-vs-target) (תואם
|
||||||
|
[05-qa-review §4](05-qa-review.md#4-current-vs-target--ממצאי-audit)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Current vs Target
|
||||||
|
|
||||||
|
- **שער-ייצוא אכוף-זרימה ולא אכוף-קוד (INV-EX3 / INV-QA3).** אומת בקוד: `export_docx`
|
||||||
|
(`drafting.py:384-419`) קורא ישירות ל-`docx_exporter.export_decision` (`:403`) ללא קריאה
|
||||||
|
ל-`qa_validator.validate_decision` ובלי בדיקת `export_blocked`. החסימה מתקיימת רק כי הסוכן
|
||||||
|
`legal-exporter` מחויב להריץ QA קודם (`legal-exporter.md:71,149`) — אדם/סוכן שיקרא
|
||||||
|
ל-`export_docx` ישירות **יעקוף** את השער. **יעד:** hard-block בתוך `export_docx` — שליפת
|
||||||
|
`qa_results`/`export_blocked` ודחייה לפני כתיבת ה-DOCX, כך שאי-אפשר לעקוף.
|
||||||
|
- **`active_draft_path` כ-derived-שזוחל-למקור (INV-EX1).** ה-DOCX נגזר מהבלוקים בייצוא הראשון,
|
||||||
|
אך אחרי עריכה (`revise_draft`/`apply_user_edit`) ה-DOCX הופך ל"מקור-האמת" לעריכות הבאות
|
||||||
|
(`db.py:185-188`) בלי לעדכן את `decision_blocks` חזרה — סטייה אפשרית בין הבלוקים למסמך-החי.
|
||||||
|
**יעד:** חוזה מפורש — או re-sync מהבלוקים, או הגדרת `active_draft_path` כאנקור-revision בלבד
|
||||||
|
(לא מקור-תוכן), עם בדיקת-בריאות לגילוי drift בין הבלוקים ל-DOCX הפעיל.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md) — [INV-G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
(derived-data / מקור-יחיד) · [INV-G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai)
|
||||||
|
(עקיבוּת) · [INV-G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant) (שערים).
|
||||||
|
- [02-data-model.md](02-data-model.md) — `decisions`/`decision_blocks` (המקור שממנו מייצאים).
|
||||||
|
- [04-analysis-writing.md](04-analysis-writing.md) — כתיבת הבלוקים שמהם נגזר ה-DOCX.
|
||||||
|
- [05-qa-review.md](05-qa-review.md#inv-qa3-החלטה-לא-מיוצאת-עם-כשל-קריטי-governance--g10) —
|
||||||
|
INV-QA3 (שער-הייצוא הקריטי שקודם לשלב זה).
|
||||||
|
- [07-learning.md](07-learning.md) — `ingest_final_version` + Hermes על ההחלטה הסופית.
|
||||||
|
- [X5-audit-provenance.md](X5-audit-provenance.md) — audit-trail (commit git) ועקיבוּת-מקור.
|
||||||
|
- [`skills/dafna-decision-template/SKILL.md`](../../skills/dafna-decision-template/SKILL.md) —
|
||||||
|
**המקור הסמכותי** לכללי-הסגנון (line classification · dash policy · placeholder · RTL-runs).
|
||||||
236
docs/spec/07-learning.md
Normal file
236
docs/spec/07-learning.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# 07 — לולאת הלמידה (Learning Loop)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) ומפרט כיצד המערכת **לומדת לאורך
|
||||||
|
זמן** — מהחלטות סופיות (Hermes), מפידבק-היו"ר, ומצמיחת-הקורפוס — באופן שמזין חזרה את
|
||||||
|
הכתיבה ([04-analysis-writing.md](04-analysis-writing.md)) ואת שערי-האיכות
|
||||||
|
([05-qa-review.md](05-qa-review.md)). הוא אוכף את
|
||||||
|
[INV-G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)
|
||||||
|
(שערים אנושיים — אישור היו"ר על כל עדכון-ידע) ואת
|
||||||
|
[INV-G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש) /
|
||||||
|
כלל-ההנדסה **quality-at-source** (האחריות לאיכות יושבת במקור, לא בטלאי במורד הזרם).
|
||||||
|
|
||||||
|
> **⚠ קובץ מעורב — שני מודלי-סמכות** (לפי החוקה §3, §5):
|
||||||
|
> - **שער-הממשל** (Hermes מציע — היו"ר מאשרת ידנית; אין auto-commit ל-SKILL/lessons)
|
||||||
|
> הוא **invariant הנדסי** במודל הממשל-שיפוטי → נושא `מקורות:` (NCSC/JTC · CEPEJ 2018 ·
|
||||||
|
> FJC) + `סטטוס: verified`.
|
||||||
|
> - **כלל-ההנדסה quality-at-source** (היכן יושבת האחריות לאיכות-הידע) → invariant הנדסי
|
||||||
|
> במודל הנדסת-הנתונים → נושא `מקורות:` (Fowler — Data Mesh / quality-at-source ·
|
||||||
|
> DAMA-UK · ISO 8000) + `סטטוס: verified`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. תת-מערכת רכישת-הסגנון (Style Acquisition) — יעד-העל וההפרדה מהכתיבה
|
||||||
|
|
||||||
|
**יעד-העל של legal-ai:** שהסוכנים יכתבו וינתחו עררים **בדיוק כמו עו"ד דפנה תמיר** — להפנים את הקול והשיטה, לא רק לייצר טיוטה תקנית. ל-end זה מחייב **הפרדה מובהקת בין שתי תת-מערכות**:
|
||||||
|
|
||||||
|
| | **Writing Subsystem** | **Style-Acquisition Subsystem** |
|
||||||
|
|---|---|---|
|
||||||
|
| שאלה | "איך אכתוב את התיק כמו דפנה?" | "מה למדנו מהפער בין מה שכתבנו למה שדפנה חתמה?" |
|
||||||
|
| טריגר | issue כתיבה | `mark-final` |
|
||||||
|
| פלט | 12 בלוקים | עדכוני-קול מאושרים + מדד-מרחק |
|
||||||
|
| סוכנים | writer/analyst/qa/ceo | hermes-curator (מורחב) |
|
||||||
|
| יחס ל-artifacts-הקול | **צרכן read-only** | **היחיד שכותב** (דרך שער INV-G10) |
|
||||||
|
|
||||||
|
### 0.1 הגישה: Authorial Style Profiling, לא fine-tuning
|
||||||
|
היעד הוא **Text Style Transfer** מבוסס **פרופיל-סגנון מופשט** — להכליל את סגנון/שיטת דפנה ולהתאים לתיק הספציפי. fine-tuning של משקולות **לא רלוונטי**: המודל (Opus) סגור, והקורפוס (~48 החלטות, יו"ר חדשה) קטן מדי — מצב שבו הספרות מראה שפרופיל-מופשט + דוגמאות מנצח (≈+15% מעל RAG-בלבד). **מדיניות-העתקה לפי סוג-תוכן:** קבוע/נוסחאי (פתיחים דוקטרינליים, תבניות-סיום) → מותר להעתיק; ניתוח/טענות ספציפיים → להכליל ולהתאים; מהות (הלכה/עובדה מתיק אחר) → אסור (INV-LRN5).
|
||||||
|
|
||||||
|
### 0.2 שלושת ערוצי-ההזנה לכותב
|
||||||
|
1. **A — פרופיל-מופשט (ראשי):** voice-fingerprint + author-features כמותיים, מוזרק לכתיבה.
|
||||||
|
2. **B — דוגמאות + תבניות (תומך):** פסקאות-בלוק אמיתיות + Copy-Paste Templates + contrastive.
|
||||||
|
3. **C — deep-read (נקודתי):** voice-XXXX.md — worked example לתיק-מופת.
|
||||||
|
|
||||||
|
### 0.3 הצינור החוזר per-final (7 שלבים)
|
||||||
|
`mark-final` → [1] INTAKE (snapshot של הטיוטה) → [2] PAIRING (בלוק↔בלוק) → [3] ALIGNMENT (diff פר-בלוק) → [4] DISTILLATION (מפריד סגנון↔מהות) → [5] CURATION (Hermes + שער-יו"ר) → [6] FEEDBACK (ניתוב לערוץ A/B/C) → [7] MEASUREMENT (מדד-מרחק-סגנון).
|
||||||
|
|
||||||
|
### 0.4 ניהול ב-UI
|
||||||
|
`/methodology` = **עורך-הפרופיל** (declarative: יחסי-זהב, כללי-דיון, צ׳קליסטים, ביטויי-מעבר, אנטי-דפוסים, voice-invariants). `/training` = **שולחן-הלמידה** (קורפוס, פורטרט-סגנון, השוואת draft↔final, curator, מדד-מרחק, פנקס-התאמה).
|
||||||
|
|
||||||
|
### 0.5 Invariants חדשים
|
||||||
|
**INV-LRN4 (ניגוד-אמת → G10/G9):** למידת-קול מבוססת **pairing draft↔final ברמת-בלוק**, לא קריאת-final בלבד. כל החלטה אינה "סגורה" עד שהושוותה מול הסופי; כל סופי מנותח מול הטיוטה. נשמר פנקס-התאמה (`draft_final_pairs`) עם מצב-חיים `draft_done → final_received → analyzed → lessons_folded`.
|
||||||
|
*מקורות:* imitation-learning-from-expert-edits · contrastive personalization (arxiv 2504.08745) · author-profiling. *סטטוס: verified.*
|
||||||
|
|
||||||
|
**INV-LRN5 (טוהר-הקול → G4/G11):** שכבת-ידע-הקול (voice-fingerprint, style_patterns, exemplars) **לא תכיל הלכות/עובדות ספציפיות** — רק סגנון ושיטה. מהות מנותבת ל-precedent_library/halacha. ה-distillation מפריד במקור.
|
||||||
|
*מקורות:* quality-at-source (Data Mesh) · separation-of-concerns. *סטטוס: verified.*
|
||||||
|
|
||||||
|
### 0.6 מסלול-העלאת-סופי נקי + פאנלים אוטומטיים (מדורג)
|
||||||
|
היו"ר מעלה את **ההחלטה החתומה שלה** דרך מסלול ייעודי — `POST /api/cases/{case}/final/upload` (כפתור "העלאת החלטה סופית של היו"ר" בלשונית-הטיוטות). **נבדל** מ-`exports/upload` (גרסה-מתוקנת-שלנו+retrofit) ומ-`mark-final` (סימון export-שלנו), ולכן אינו מסלול-מקביל (G2) אלא יכולת חסרה.
|
||||||
|
הקליטה (סינכרונית ב-endpoint) מבצעת את **לולאת-צמיחת-הקורפוס** (§1.3) במלואה:
|
||||||
|
1. **קורפוס-הסגנון** (voice) תחת ה-`case_number` **המלא** (בל"מ≠ערר — מונע התנגשות-מספר) + פתיחת `draft_final_pairs` (`final_received`, INV-LRN4).
|
||||||
|
2. **ספריית-הפסיקה** — ההחלטה נכנסת ל-`case_law` כ-`internal_committee` **תמיד** (כדי שתהיה ברת-ציטוט בהחלטות עתידיות). `chair_name` נקבע **דטרמיניסטית** (תיק → ברירת-מחדל-ועדה, לעולם לא ריק — אילוץ `case_law_internal_chair_check`); לא נשען על חילוץ-LLM. מטה-דאטה נוסף (תאריך/צדדים) מועשר אסינכרונית ע"י מחלץ-Gemini.
|
||||||
|
3. **בדיקת-ציטוטים** — `extract_internal_citations` מקשר את הפסיקה שההחלטה מצטטת לספרייה; כל ציטוט שאינו בספרייה **מסומן אוטומטית** כ-`missing_precedent` (open) להעלאה ע"י היו"ר.
|
||||||
|
4. הציטוטים-המקושרים מזינים את **לולאת-ה-corroboration** (X11): ציטוט-נכנס מההחלטה שלנו מחזק את ההלכות של התקדים המצוטט (`corroboration_rebuild`).
|
||||||
|
ואז שני שלבים אוטומטיים נפרדים (`run-learning` / `run-halacha`) המעירים worker מקומי (claude/DeepSeek/Gemini מקומיים בלבד):
|
||||||
|
- **למידה:** `ingest_final_version` (Opus distillation) → **פאנל-סגנון דו-סוכני** (DeepSeek+Gemini, "למידה כפולה") שמצביע על כל לקח-style_method; הסכמה 2/2 → `decision_lesson` (`source=panel:deepseek+gemini`); פיצול → ליו"ר.
|
||||||
|
- **הלכות:** `extract_internal_citations` → `precedent_extract_halachot` → `corroboration_rebuild` → **פאנל-הלכות תלת-סוכני** (`halacha_panel_approve.py --apply`).
|
||||||
|
שני הפאנלים **הפיכים** (גיבוי-CSV ל-`data/audit/`) ומסלימים מחלוקות. ההטמעה הסופית ל-`SKILL.md`/`legal-decision-lessons.md` נשארת **אישור-יו"ר ידני** (INV-LRN1/G10) — הפאנל יוצר *הצעות* בלבד.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. שלוש לולאות-המשנה
|
||||||
|
|
||||||
|
הלמידה אינה אירוע יחיד אלא **שלוש לולאות** המתנקזות לאותם מסמכי-ידע מוסמכים
|
||||||
|
([legal-decision-lessons.md](../legal-decision-lessons.md),
|
||||||
|
[skills/decision/SKILL.md](../../skills/decision/SKILL.md)) ולקורפוסים:
|
||||||
|
|
||||||
|
### 1.1 לולאת-Hermes (post-export → הצעה → אישור)
|
||||||
|
|
||||||
|
הסוכן [hermes-curator](../../.claude/agents/hermes-curator.md) (adapter `deepseek_local`,
|
||||||
|
פרופילים `curator-cmp` / `curator-cmpa`) נקרא **אחרי שדפנה מסמנת קובץ כסופי** ב-UI
|
||||||
|
(`POST /api/cases/{case_number}/exports/{filename}/mark-final` → `pc_wake_curator_for_final()`
|
||||||
|
ב-`web/paperclip_client.py` → sub-issue + wakeup; **חיבור ישיר מה-UI, לא דרך CEO** —
|
||||||
|
`hermes-curator.md:27-35`). הוא:
|
||||||
|
|
||||||
|
- **קורא בלבד** את הטקסט הסופי (`case_get_final_text`), `get_style_guide`, ואת
|
||||||
|
`SKILL.md` / `legal-decision-lessons.md` / `corpus-analysis.md` המקומיים
|
||||||
|
(`hermes-curator.md:60-70`).
|
||||||
|
- מזהה **3–5 דפוסים/פערים** חדשים, כל ממצא מתויג `[סגנון]` / `[מבנה]` /
|
||||||
|
`[לקסיקון משפטי]` / `[טבלאי]` (`hermes-curator.md:99-108`).
|
||||||
|
- **מציע** — comment ב-Paperclip + רישום כל ממצא כ-`decision_lesson` דרך
|
||||||
|
`POST /api/training/corpus/{corpus_id}/lessons` (`source:"curator"`) שמופיע ב-UI
|
||||||
|
תחת הטאב "מה למדנו" (`hermes-curator.md:73-96`).
|
||||||
|
- **אינו מעדכן** קבצים בעצמו (skills/, lessons.py, DB) — רק מציע (`hermes-curator.md:125-130`).
|
||||||
|
|
||||||
|
### 1.2 לולאת-פידבק-היו"ר (capture → ניתוח שבועי → לקחים)
|
||||||
|
|
||||||
|
- **לכידה מובנית:** `record_chair_feedback` שומר הערת-דפנה בטבלת `chair_feedback`
|
||||||
|
(`category ∈ {missing_content, wrong_tone, wrong_structure, factual_error, style,
|
||||||
|
other}`) — `tools/workflow.py:348`, ראה [05-qa-review.md](05-qa-review.md) §2.3.
|
||||||
|
- **ניתוח שבועי:** ה-scheduled job `weekly-feedback-analysis` (ראשון 19:00,
|
||||||
|
`plugin-legal-ai/src/manifest.ts:175-179`) מושך `GET /api/chair-feedback/weekly-summary`,
|
||||||
|
ואם יש פריטים — **מעיר את ה-CEO** לעדכן את `legal-decision-lessons.md` עם הלקחים
|
||||||
|
החדשים (`worker.ts:784-837`; הוראת ה-prompt: "הוסף רק לקחים חדשים… קבץ לפי נושא"
|
||||||
|
— `worker.ts:830`).
|
||||||
|
- אין פריטים → הג'וב מדלג בשקט (`worker.ts:805`). ל-CEO שמתעורר מ-`weekly-feedback-job`
|
||||||
|
**אין `issueId`** — הוא כותב לקובץ בלבד, לא מפרסם comment ולא סוגר issue
|
||||||
|
(כלל מ-[CLAUDE.md](../../CLAUDE.md) "Scheduled Jobs").
|
||||||
|
|
||||||
|
### 1.3 לולאת-צמיחת-הקורפוס (החלטה סופית → קורפוס → אחזור)
|
||||||
|
|
||||||
|
החלטה סופית נקלטת לקורפוס-הסגנון (`ingest_final_version` — ראה [06-export.md](06-export.md)
|
||||||
|
§ Hermes), ופסיקה/החלטות-ועדה חדשות נקלטות דרך המסלול הקנוני של
|
||||||
|
[01-ingest.md](01-ingest.md). כך הקורפוס שמזין את האחזור ([03-retrieval.md](03-retrieval.md))
|
||||||
|
**גדל מהפלט עצמו** — והדיון הבא נשען על תקדים עשיר יותר. צמיחה זו כפופה לאותו חוזה-שלמות
|
||||||
|
([G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש)) כמו כל קליטה.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. הלולאה במלואה (הציור)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
┌─────────────▼─────────────┐ ┌────────────────────────┐ │
|
||||||
|
│ כתיבה (04) │ ───▶ │ QA + שערים אנושיים (05)│ │
|
||||||
|
│ 12 בלוקים · סגנון דפנה │ │ validate_decision + │ │
|
||||||
|
│ ← lessons.py CONTENT_ │ │ פידבק-היו"ר │ │
|
||||||
|
│ CHECKLISTS · SKILL.md │ └───────────┬────────────┘ │
|
||||||
|
└───────────────────────────┘ │ ייצוא (06) │
|
||||||
|
▲ ▼ │
|
||||||
|
│ ┌──────────────────────┐ │
|
||||||
|
┌────────┴──────────────┐ │ סימון "סופי" (UI) │ │
|
||||||
|
│ legal-decision- │ │ mark-final │ │
|
||||||
|
│ lessons.md + SKILL.md │ └───────┬──────────────┘ │
|
||||||
|
│ (מסמכי-ידע מוסמכים) │ │ │
|
||||||
|
└────────▲──────────────┘ ┌──────────┴───────────┐ │
|
||||||
|
│ ▼ ▼ │
|
||||||
|
│ ✋ אישור-יו"ר ידני ┌───────────────┐ ┌────────────────┐│
|
||||||
|
└──────────────────────│ Hermes curator │ │ ingest_final → ││
|
||||||
|
(commit ידני בלבד) │ → הצעות(comment)│ │ קורפוס-סגנון → ┘│
|
||||||
|
└───────────────┘ │ אחזור (03) │
|
||||||
|
┌───────────────────────────┐ └────────────────┘
|
||||||
|
│ פידבק-היו"ר (05) ──┐ │
|
||||||
|
│ chair_feedback │ │
|
||||||
|
└────────────────────┼───────┘
|
||||||
|
▼
|
||||||
|
weekly-feedback-analysis (job)
|
||||||
|
│ מעיר CEO
|
||||||
|
▼
|
||||||
|
עדכון legal-decision-lessons.md ──┐
|
||||||
|
└──▶ (חזרה ל-04 / lessons.py)
|
||||||
|
```
|
||||||
|
|
||||||
|
הקשר לכתיבה: הלקחים והצ'קליסטים שב-`CONTENT_CHECKLISTS`
|
||||||
|
(`mcp-server/src/legal_mcp/services/lessons.py:355`, בורר `get_content_checklist`
|
||||||
|
`:509-555`) ו-`get_lessons_for_outcome` (`lessons.py:309`) מוזרקים ל-prompt-הכתיבה לפי
|
||||||
|
סוג-ערר ותוצאה — ראה [04-analysis-writing.md](04-analysis-writing.md) §5. כל סגירה של
|
||||||
|
לולאה (Hermes או פידבק) שמשנה את `legal-decision-lessons.md` / `SKILL.md` משפיעה ישירות
|
||||||
|
על הכתיבה הבאה.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-LRN1: עדכון-ידע דורש אישור-יו"ר ידני — אין auto-commit (governance →G10)
|
||||||
|
**כלל:** מנגנוני-הלמידה (Hermes, ניתוח-פידבק שבועי) **מציעים בלבד**. כל שינוי ב-
|
||||||
|
[SKILL.md](../../skills/decision/SKILL.md) או ב-[legal-decision-lessons.md](../legal-decision-lessons.md)
|
||||||
|
מחייב **בחינה ואישור ידניים של היו"ר/חיים** ואז commit ידני — **לעולם לא auto-committed**.
|
||||||
|
Hermes כותב comment + `decision_lesson`, לא קבצים; ה-CEO השבועי כותב לקובץ אך הצעותיו
|
||||||
|
מאומתות ידנית לפני קיבוע. זהו פֶּאֶט של [INV-G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)
|
||||||
|
על שכבת-הידע: גם הלמידה כפופה לשיקול-הדעת האנושי.
|
||||||
|
**מקורות:** NCSC/JTC — *Principles & Practices for AI Use in Courts* (human-in-the-loop;
|
||||||
|
never replace human judgment) · Council of Europe / CEPEJ (2018, under user control) ·
|
||||||
|
Federal Judicial Center — *Judicial Writing Manual* (2d ed.) | סטטוס: verified
|
||||||
|
**אכיפה:** הסוכן read-only על תוכן ו-write רק על comments (`hermes-curator.md:1-3, 125-130`);
|
||||||
|
תהליך-האישור — הצעת-curator כ-comment ב-Paperclip → חיים בוחן ומאשר ידנית → commit ל-
|
||||||
|
`SKILL.md` ו-`docs/legal-decision-lessons.md` (מ-[CLAUDE.md](../../CLAUDE.md) "Hermes Curator");
|
||||||
|
ה-CEO השבועי מתעורר בלי `issueId` וכותב לקובץ בלבד ([CLAUDE.md](../../CLAUDE.md) "Scheduled Jobs").
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-LRN2: האחריות לאיכות יושבת במקור — quality-at-source (engineering →G4)
|
||||||
|
**כלל:** האחריות לאיכות-הידע (לקחים, הלכות, metadata של פריטים מואנדקסים) נאכפת **קרוב
|
||||||
|
ככל האפשר לנקודת-היצירה/הקליטה** — בעת ניסוח-ההחלטה, בעת לכידת-הפידבק, ובעת קליטת-פריט —
|
||||||
|
**לא** מתוקנת בדיעבד במורד-הזרם (re-OCR, טלאי-קריאה, ניחוש בזמן-חיפוש). פריט-ידע חסר-שלמות
|
||||||
|
מסומן ומדווח בנקודת-הכניסה, לא מתקבל בשקט.
|
||||||
|
**מקורות:** Martin Fowler — *Data Mesh* (quality-at-source: domain owns data quality at
|
||||||
|
the point of creation) · DAMA-UK *Six Primary Dimensions for Data Quality* (2013,
|
||||||
|
completeness) · ISO 8000 (Data quality) | סטטוס: verified
|
||||||
|
**אכיפה:** חוזה-שלמות בקליטה ([01-ingest.md](01-ingest.md) §2, [02-data-model.md](02-data-model.md))
|
||||||
|
+ "אין בליעה שקטה" (חוקה §6); לכידת-פידבק מובנית בנקודת-ההערה (`record_chair_feedback`,
|
||||||
|
`tools/workflow.py:348`); לקחים נשמרים מבני ולא ad-hoc (`lessons.py`,
|
||||||
|
[legal-decision-lessons.md](../legal-decision-lessons.md)).
|
||||||
|
**הפרה ידועה:** ראה [INV-G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש)
|
||||||
|
(ערן סופר 8046/24 אונדקס עם `headnote`/`summary`/`tags` ריקים — שלמות לא נאכפה במקור) →
|
||||||
|
ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-LRN3: ידע נלכד באופן מובנה — לא ad-hoc (engineering →G9)
|
||||||
|
**כלל:** פידבק ולקחים נלכדים ב**מבנה דטרמיניסטי ועקיב** — `chair_feedback` (עם `category`
|
||||||
|
ו-`block_id`), `decision_lessons` (עם `category`/`source`), ו-`CONTENT_CHECKLISTS` בקוד —
|
||||||
|
כך שהלמידה **עמידה וניתנת-לביקורת**, לא פזורה בהערות חופשיות. מקור-הלקח (`source:"curator"`
|
||||||
|
מול פידבק-יו"ר) משומר לעקיבוּת.
|
||||||
|
**מקורות:** ISO 15489-1:2016 (records reliability/authenticity) · DAMA-UK *Six Primary
|
||||||
|
Dimensions for Data Quality* (2013) · ISO 8000 (Data quality) | סטטוס: verified
|
||||||
|
**אכיפה:** טבלת `chair_feedback` + `record_chair_feedback`/`list_chair_feedback`
|
||||||
|
(`tools/workflow.py:348, 393`); `decision_lessons` עם `source`+`category`
|
||||||
|
(`hermes-curator.md:79-96`); `CONTENT_CHECKLISTS`/`get_lessons_for_outcome`
|
||||||
|
(`lessons.py:355, 309`). עקיבוּת-מקור קושרת ל-[X5-audit-provenance.md](X5-audit-provenance.md).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. הג'ובים המתוזמנים (תמיכת-תשתית ללולאה)
|
||||||
|
|
||||||
|
| Job (`manifest.ts`) | לוח-זמנים | תפקיד בלולאה |
|
||||||
|
|---------------------|-----------|---------------|
|
||||||
|
| `weekly-feedback-analysis` | ראשון 19:00 (`:175-179`) | מסכם פידבק-יו"ר → מעיר CEO לעדכון `legal-decision-lessons.md` (`worker.ts:784-837`) |
|
||||||
|
| `stale-case-reminder` | יומי 08:00 (`:169-172`) | תזכורת על תיקים תקועים 30+ ימים (`worker.ts:710-780`) — היגיינת-תהליך, לא ידע |
|
||||||
|
| `sync-case-status` | כל 15 דק' (`:162-166`) | מסנכרן סטטוסי-תיקים legal-ai↔Paperclip (`worker.ts:624`) — תשתית, לא ידע |
|
||||||
|
|
||||||
|
רק `weekly-feedback-analysis` הוא חלק מלולאת-הלמידה; שני האחרים הם היגיינת-תהליך/סנכרון.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant) —
|
||||||
|
INV-G10 (שערים אנושיים) + [INV-G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש)
|
||||||
|
(quality-at-source) + כלל-ההנדסה §6.
|
||||||
|
- [04-analysis-writing.md](04-analysis-writing.md) — הכתיבה שהלקחים/הצ'קליסטים מזינים (§3, §5).
|
||||||
|
- [05-qa-review.md](05-qa-review.md) — שער פידבק-היו"ר (§2.3) שמתחיל את לולאת-הפידבק.
|
||||||
|
- [01-ingest.md](01-ingest.md) — קליטה אחידה (quality-at-source) לצמיחת-הקורפוס.
|
||||||
|
- [03-retrieval.md](03-retrieval.md) — האחזור שהקורפוס הגדל מזין.
|
||||||
|
- [06-export.md](06-export.md) — `mark-final` שמפעיל את Hermes + `ingest_final_version`.
|
||||||
|
- [X5-audit-provenance.md](X5-audit-provenance.md) — עקיבוּת-מקור של לקחים (`source`).
|
||||||
|
- הסוכן: [.claude/agents/hermes-curator.md](../../.claude/agents/hermes-curator.md).
|
||||||
|
- מסמכי-הידע המוסמכים: [legal-decision-lessons.md](../legal-decision-lessons.md) ·
|
||||||
|
[skills/decision/SKILL.md](../../skills/decision/SKILL.md) ·
|
||||||
|
[corpus-analysis.md](../corpus-analysis.md).
|
||||||
13
docs/spec/README.md
Normal file
13
docs/spec/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# ספ המערכת — עוזר משפטי (Living System Spec)
|
||||||
|
|
||||||
|
זהו מקור-האמת הקנוני ל"מהו תקין" במערכת. שער-הכניסה: [00-constitution.md](00-constitution.md).
|
||||||
|
כל invariant מגובה ב-≥3 מקורות סמכותיים; פריט לא-מאומת מסומן ⚠ UNVERIFIED ומועלה ליו"ר.
|
||||||
|
|
||||||
|
מבנה: 00 חוקה · 01–07 מחזור-חיים · X1–X16 חוצי-שלבים. ראה אינדקס מלא בחוקה.
|
||||||
|
- X1–X5: מזהים · רב-חברתי · אינטגרציה+deploy · סוכנים · audit.
|
||||||
|
- X6–X10 (מחזור-2, 8 משטחי-האפליקציה): חוזה UI↔API · לקוח-Paperclip · מילוי-שדות · חוזה כלי-MCP · deploy/env/secrets.
|
||||||
|
- X11–X14 (הרחבות-תחום): citator פנימי (תיקוף-הלכות) · יומונים כשכבת-גילוי (radar) · אחזור-פסיקה אוטומטי מנט המשפט (שירות) · אחסון-אובייקטים (MinIO/S3, הגירת `data/`).
|
||||||
|
- X15–X16 (ארכיטקטורת-יסוד): שער-הפלטפורמה (Paperclip מאחורי Port — G12, מיישם G2) · עמידות-פייפליין (LangGraph כספרייה — checkpointing/replay, מחזק G3).
|
||||||
|
|
||||||
|
מפות-ממצאים: [gap-audit.md](gap-audit.md) (GAP-01..62 → FU-1..15; מחזור-1 ✅ הושלם, מחזור-2 פתוח) · [ui-audit.md](ui-audit.md) (ביקורת 13 דפי-UI).
|
||||||
|
בסיס-עיצוב: docs/superpowers/specs/2026-05-30-system-spec-design.md
|
||||||
168
docs/spec/X1-identifiers.md
Normal file
168
docs/spec/X1-identifiers.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# X1 — מודל המזהים הקנוני (Canonical Identifier Model)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) והוא ה-deep-dive על **מזהי הישויות**
|
||||||
|
של עוזר משפטי. הוא אוכף את [G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה) (מזהה
|
||||||
|
קנוני מנורמל בכתיבה) ומעמיק את [INV-DM2](02-data-model.md#inv-dm2-מזהה-קנוני-יחיד-לכל-ישות)
|
||||||
|
מ-[02-data-model.md](02-data-model.md). שני הקבצים חייבים להישאר עקביים: 02 מגדיר *אילו*
|
||||||
|
שדות מזהים כל ישות; X1 מגדיר את *הצורה הקנונית* של המזהה ו*איך* הוא מנורמל.
|
||||||
|
|
||||||
|
> **TARGET, לא תיאור-מצב.** המודל כאן הוא היעד הקנוני. כל מקום שבו הקוד בפועל
|
||||||
|
> (`mcp-server/src/legal_mcp/services/db.py`) סוטה ממנו — מתועד כ-**audit-finding** (§4),
|
||||||
|
> תסמין, לא התנהגות תקינה. כל טענה על הקוד הקיים מצוטטת `file:line` ואינה מונחת כתקינה.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. הצורה הקנונית של `case_number`
|
||||||
|
|
||||||
|
מזהה-התיק (`case_number`) הוא **מספר-תיק מנורמל** — לא מחרוזת-ציטוט, לא תווית-תצוגה. הצורה
|
||||||
|
הקנונית מוגדרת ע"י **נרמול בנקודת-הכתיבה** (write-time canonicalization), כך שכל הרשומות
|
||||||
|
חולקות פורמט יחיד והשוואה היא תמיד שוויון-מחרוזת מול הצורה הקנונית.
|
||||||
|
|
||||||
|
**הנרמול הקנוני (TARGET — מופעל בכתיבה):**
|
||||||
|
|
||||||
|
| צעד | פעולה | דוגמה |
|
||||||
|
|------|--------|--------|
|
||||||
|
| trim | הסרת רווחים מקיפים | `" 8137/24 "` → `"8137/24"` |
|
||||||
|
| prefix-strip | הסרת קידומת-הליך לפני הספרה הראשונה ("ערר", "בל\"מ", "עע\"מ") | `"ערר 8137/24"` → `"8137/24"` |
|
||||||
|
| separator | איחוד מפריד `/` → `-` | `"8137/24"` → `"8137-24"` |
|
||||||
|
|
||||||
|
> **הצורה הקנונית = המספר הרשמי שהוקצה ע"י הוועדה, נשמר ככתבו** — לרבות מקטע-החודש **כשהוקצה**
|
||||||
|
> (למשל `8126-03-25`). מספרי-מורשת מסוימים הוקצו **ללא** חודש (למשל `8126-25`); המערכת **אסור**
|
||||||
|
> שתמציא או תוסיף (pad) מקטע-חודש שמעולם לא הוקצה. הנרמול-בכתיבה הוא **פורמט-בלבד ודטרמיניסטי**
|
||||||
|
> (trim · `/`→`-` · prefix-strip) — הוא **אינו מוסיף ואינו מסיר** מקטע-חודש. הפורמט המועדף
|
||||||
|
> מכאן-ואילך כולל את החודש.
|
||||||
|
|
||||||
|
> סוג-ההליך (`proceeding_type ∈ {ערר, בל"מ}`) הוא **חלק מהמפתח הקנוני** — לא חלק ממחרוזת
|
||||||
|
> ה-`case_number`. הקידומת "ערר"/"בל\"מ" מהכותרת נשללת מהמספר ונשמרת בעמודה ייעודית
|
||||||
|
> (`cases.proceeding_type`, `db.py:912`). כך "ערר 8137/24" ו-"בל\"מ 8137/24" הם שתי
|
||||||
|
> רשומות מובחנות בעלות אותו `case_number=8137-24` ו-`proceeding_type` שונה.
|
||||||
|
|
||||||
|
**נרמול-בכתיבה הוא המנגנון הראשי; התאמה-סלחנית-בקריאה היא נוחות משנית בלבד.** כלל-ההנדסה
|
||||||
|
"נרמול לא תיקון-תסמין" (חוקה §6) קובע: מתקנים את הנתון במקור, לא מטליאים בקריאה. אם רשומה
|
||||||
|
נשמרה בצורה לא-קנונית — היעד הוא לנרמל אותה במיגרציה/בכתיבה, **לא** לסמוך על מנוע-קריאה
|
||||||
|
שיגשר על הפער. ההתאמה-הסלחנית (§3) קיימת כדי לבלוע *קלט-משתמש* רב-צורני (כותרת Paperclip),
|
||||||
|
לא כדי לתרץ נתון-מאוחסן לא-קנוני.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. שני מרחבי-מזהים: `cases` מול `case_law`
|
||||||
|
|
||||||
|
`case_number` מופיע בשתי טבלאות נפרדות עם **שני מרחבי-מזהים שונים** ו**ללא FK חוצה-טבלאות**
|
||||||
|
ביניהן. בלבול בין השניים הוא כשל-שורש: תיק חי אינו תקדים, ולהפך.
|
||||||
|
|
||||||
|
| ממד | `cases` (תיק חי) | `case_law` (קורפוס פסיקה) |
|
||||||
|
|------|------------------|---------------------------|
|
||||||
|
| תפקיד | הערר שבטיפול כעת (1xxx/8xxx/9xxx) | תקדים — פסיקה חיצונית **וגם** החלטות-ועדה |
|
||||||
|
| מפתח קנוני | `(case_number, proceeding_type)` | `(case_number, source_kind, proceeding_type)` — ראה להלן |
|
||||||
|
| אילוץ-ייחודיות | `uq_cases_number_proc` על `(case_number, proceeding_type)` (`db.py:923-924`) | שני partial unique לפי `source_kind` (`db.py:904-909`) |
|
||||||
|
| מורשת (הוסרה) | `case_number TEXT UNIQUE NOT NULL` (`db.py:76`), הוסר V15 (`db.py:921-922`) | `case_number TEXT UNIQUE NOT NULL` (`db.py:368`), הוסר V15 (`db.py:902-903`) |
|
||||||
|
| FK חוצה | **אין** — `cases` ו-`case_law` הם מרחבים נפרדים | **אין** |
|
||||||
|
|
||||||
|
**`case_law` — מזהה מודע-source_kind.** ה-V15 החליפה את `UNIQUE(case_number)` הגלובלי בשני
|
||||||
|
partial unique indexes (`db.py:904-909`):
|
||||||
|
|
||||||
|
- **`internal_committee`** (החלטות-ועדה פנימיות): `UNIQUE(case_number, proceeding_type)`
|
||||||
|
— `uq_case_law_internal_number_proc`, `WHERE source_kind = 'internal_committee'`.
|
||||||
|
- **חיצוני** (`external_upload` / `cited_only` / `nevo_seed`): `UNIQUE(case_number)`
|
||||||
|
— `uq_case_law_external_number`, `WHERE source_kind <> 'internal_committee'`.
|
||||||
|
|
||||||
|
לכן המזהה הקנוני של `case_law` הוא הטריפלט **(`case_number` מנורמל, `source_kind`,
|
||||||
|
`proceeding_type`)** — עקבי עם [02-data-model §2א](02-data-model.md#2א-case_law--החוזה-הקונקרטי).
|
||||||
|
|
||||||
|
**אין הצמדה חוצה-טבלאות.** כשהחלטת-תיק מ-`cases` מצוטטת בהמשך כתקדים, היא נכנסת ל-`case_law`
|
||||||
|
כרשומה *חדשה* (`source_kind='internal_committee'`) — לא כ-FK ל-`cases`. שני המרחבים נשארים
|
||||||
|
עצמאיים; הגישור ביניהם הוא דרך הקליטה ([01-ingest.md](01-ingest.md)), לא דרך מפתח-זר.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. ציטוט מול מזהה — `citation_formatted` הוא תצוגה, לא מפתח
|
||||||
|
|
||||||
|
הציטוט-המלא והמזהה-הקנוני הם **שני שדות נפרדים בכוונה**:
|
||||||
|
|
||||||
|
- **מזהה קנוני** = `case_number` מנורמל (`8126-03-25`) — המפתח שמשמש לחיפוש, ל-upsert,
|
||||||
|
ולאילוצי-ייחודיות.
|
||||||
|
- **ציטוט מעוצב** = `citation_formatted` (`db.py:1070`, V19) — מחרוזת-תצוגה לפי כללי-הציטוט
|
||||||
|
האחיד, למשל: `ערר (ועדות ערר - תכנון ובנייה ת"א-יפו) 81002-01-21 **אברהם אגסי נ' הועדה
|
||||||
|
המקומית** (נבו 25.9.2025)` (`db.py:1067-1068`).
|
||||||
|
|
||||||
|
הציטוט הוא **שדה נגזר לתצוגה** — מכיל את המזהה אך גם צדדים, ערכאה, ותאריך-פרסום. הוא **לעולם
|
||||||
|
אינו המפתח**. אחסון מחרוזת-ציטוט בשדה-המזהה שובר את הנרמול ([G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה)),
|
||||||
|
מערבב תצוגה עם זהות (פוגע ב-1NF — ערך לא-אטומי בשדה-מפתח), ומונע התאמת-שוויון מול המספר
|
||||||
|
המנורמל.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-ID1: `case_number` מנורמל בכתיבה — התאמה-סלחנית משנית
|
||||||
|
**כלל:** `case_number` מנורמל לצורה קנונית יחידה **בנקודת-הכתיבה** בנרמול **פורמט-בלבד
|
||||||
|
ודטרמיניסטי** (trim · prefix-strip · `/`→`-`) — הנרמול **אינו ממציא ואינו מוסיף** מקטע-חודש
|
||||||
|
שלא הוקצה. הצורה הקנונית היא **המספר הרשמי שהוקצה** (עם חודש כשהוקצה, למשל `8126-03-25`),
|
||||||
|
והשוואה-בקריאה היא שוויון מול הצורה הקנונית. **התאמה-סלחנית-בקריאה היא
|
||||||
|
נוחות משנית בלבד** — היא בולעת קלט-משתמש רב-צורני, ואינה תחליף לנרמול-בכתיבה ([G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה),
|
||||||
|
כלל-ההנדסה "נרמול לא תיקון-תסמין", חוקה §6).
|
||||||
|
**מקורות:** SSOT (Single Source of Truth — normalization principle) · E.F. Codd, First Normal
|
||||||
|
Form (CACM 13(6), 1970) · Martin Kleppmann, *Designing Data-Intensive Applications* (O'Reilly,
|
||||||
|
2017) | סטטוס: verified
|
||||||
|
**אכיפה:** נרמול-בכתיבה בנקודת-הקליטה ([01-ingest.md](01-ingest.md)) + אילוצי-ייחודיות על
|
||||||
|
המפתח הקנוני (`uq_cases_number_proc`, `db.py:923-924`; partial unique `case_law`, `db.py:904-909`).
|
||||||
|
**הפרה ידועה:** `_normalize_case_number` (`db.py:1196-1211`) מנרמל **בקריאה בלבד** ("tolerant
|
||||||
|
lookup", `db.py:1197`), ו-`get_case_by_number` (`db.py:1214-1231`) משווה two-pass (`case_number=$1`
|
||||||
|
**OR** `replace(btrim(case_number),'/','-')=$2`, `db.py:1223-1224`) — אין מסלול-כתיבה שמקנן את
|
||||||
|
הערך המאוחסן. בנפרד מכך: כשאותו תיק נקלט גם בצורה ללא-חודש וגם עם-חודש (סחף-הזנה, למשל `8126-25`
|
||||||
|
מול `8126-03-25` המתייחסים לתיק אחד), הצורה **עם-החודש (הרשמית) היא הקנונית** והרשומה החסרה
|
||||||
|
מתואמת אליה — זו **בעיית-תיאום (reconciliation)**, לא חולשה בנרמול (הנרמול אינו אמור לפדד חודש).
|
||||||
|
תיאום רשומות-מורשת מעורבות-צורה הוא **פריט ניקיון-נתונים/מיגרציה חד-פעמי** (ראה
|
||||||
|
[gap-audit / תת-פרויקט 2](../audit-report.md)), לא אלגוריתם-padding בזמן-ריצה → ממצא
|
||||||
|
ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-ID2: אין ציטוט-מלא כמזהה — הציטוט שדה-תצוגה נגזר
|
||||||
|
**כלל:** אף ישות **אינה** משתמשת במחרוזת-ציטוט-מלאה כמזהה. שדה-המזהה מכיל מספר-תיק מנורמל
|
||||||
|
בלבד; הציטוט-המלא חי בשדה ייעודי נפרד (`citation_formatted`, `db.py:1070`) ככלי-תצוגה נגזר
|
||||||
|
([G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה), [INV-DM2](02-data-model.md#inv-dm2-מזהה-קנוני-יחיד-לכל-ישות)).
|
||||||
|
**מקורות:** SSOT (Single Source of Truth — normalization principle) · E.F. Codd, First Normal
|
||||||
|
Form (CACM 13(6), 1970) · Martin Kleppmann, *Designing Data-Intensive Applications* (O'Reilly,
|
||||||
|
2017) | סטטוס: verified
|
||||||
|
**אכיפה:** הפרדת-שדות ב-schema — מזהה ב-`case_number` (אילוצי-ייחודיות, `db.py:904-909,923-924`),
|
||||||
|
ציטוט ב-`citation_formatted` בלבד (`db.py:1070`); נרמול-בכתיבה שדוחה מחרוזת-ציטוט בשדה-המזהה.
|
||||||
|
**הפרה ידועה:** החלטות "סופר" נקלטו עם **ציטוט-מלא מאוחסן כ-`case_number`** (שדה-המזהה מכיל
|
||||||
|
את מחרוזת-הציטוט במקום מספר-תיק מנורמל) — חיפוש מול המספר המנורמל נכשל, והפער מתגלגל ל-INV-ID1
|
||||||
|
(`_normalize_case_number` רק מטליא בקריאה) → ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. מצב קיים מול יעד — audit-findings
|
||||||
|
|
||||||
|
ההבדלים בין הקוד בפועל ל-TARGET. **אלו תסמינים, לא התנהגויות תקינות.** כל פריט אומת מול `db.py`.
|
||||||
|
|
||||||
|
- **נרמול בצד-הקריאה בלבד.** `_normalize_case_number` (`db.py:1196-1211`) מתואר במפורש כ-
|
||||||
|
"tolerant lookup" (`db.py:1197`) — מסיר קידומת לפני הספרה הראשונה, trim, ו-`/`→`-` — אך
|
||||||
|
**אינו מנרמל את הערך המאוחסן**. `get_case_by_number` (`db.py:1214-1231`) בונה סביבו two-pass
|
||||||
|
(exact `OR` normalized, `db.py:1223-1224`). **תסמין:** הנרמול חי כתיקון-תסמין בקריאה ולא
|
||||||
|
כקנוניזציה-בכתיבה, בניגוד ל-[G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה) וכלל-ההנדסה
|
||||||
|
§6. **יעד:** מסלול-כתיבה שמנרמל את `case_number` (פורמט-בלבד: trim/prefix-strip/`/`→`-`,
|
||||||
|
**ללא המצאת חודש**) בנקודת-הקליטה; הקריאה הופכת להשוואת-שוויון פשוטה.
|
||||||
|
- **רשומות-מורשת מעורבות-צורה (בעיית-תיאום, לא padding).** כשאותו תיק נקלט גם כ-`8126-25`
|
||||||
|
וגם כ-`8126-03-25` (סחף-הזנה), ה-two-pass אינו מזהה אותם כתיק אחד. **יעד:** תיאום חד-פעמי
|
||||||
|
של הרשומות לצורה הרשמית עם-החודש (הקנונית) במסגרת ניקיון-נתונים/מיגרציה
|
||||||
|
([gap-audit / תת-פרויקט 2](../audit-report.md)) — **לא** אלגוריתם-padding בזמן-ריצה שממציא חודש.
|
||||||
|
- **ציטוט-מלא כ-`case_number` (מורשת).** השדה המקורי `case_number TEXT UNIQUE NOT NULL`
|
||||||
|
(`cases` `db.py:76`, `case_law` `db.py:368`) לא אכף צורה — מה שאפשר אחסון מחרוזת-ציטוט בשדה
|
||||||
|
זה (החלטות "סופר"). הוחלף ב-partial unique מודע-`source_kind` ב-V15 (`db.py:902-909`), אך
|
||||||
|
**ללא ולידציית-צורה בכתיבה**. **יעד:** ולידציית-כתיבה שדוחה ערך שאינו מספר-תיק מנורמל ומפנה
|
||||||
|
ציטוט ל-`citation_formatted`.
|
||||||
|
- **שני מרחבי-מזהים, סיכון-בלבול בקוד-קריאה.** `get_case_by_number` (`db.py:1214`) פונה
|
||||||
|
ל-`cases` בלבד; `get_case_law_by_citation` (`db.py:2503`) פונה ל-`case_law` בלבד — נכון, אך
|
||||||
|
שמות-הפונקציות אינם מבדילים את מרחב-המזהים בבירור. **יעד:** תיעוד מפורש (קובץ זה) + עקביות
|
||||||
|
שמות שמשקפת `cases` מול `case_law` כשני מרחבים נפרדים ללא FK.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md) — [G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה)
|
||||||
|
(מזהה קנוני מנורמל בכתיבה) + כלל-ההנדסה "נרמול לא תיקון-תסמין" (§6).
|
||||||
|
- [02-data-model.md](02-data-model.md) — [INV-DM2](02-data-model.md#inv-dm2-מזהה-קנוני-יחיד-לכל-ישות)
|
||||||
|
(מזהה קנוני יחיד) + החוזה הקונקרטי של `case_law`; X1 הוא ה-deep-dive על אותו מזהה.
|
||||||
|
- [01-ingest.md](01-ingest.md) — נקודת-הכתיבה שבה הנרמול-בכתיבה צריך להיאכף.
|
||||||
|
- [X5-audit-provenance.md](X5-audit-provenance.md) — עקיבוּת-מקור (הציטוט כשדה-תצוגה נגזר).
|
||||||
86
docs/spec/X10-deploy-env-secrets.md
Normal file
86
docs/spec/X10-deploy-env-secrets.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# X10 — Deploy, סביבה וסודות (Deploy, Environment & Secrets)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) והוא ה-deep-dive על **קונפיגורציה, משתני-סביבה
|
||||||
|
וסודות** — מה שהיה מכוסה כחצי-deploy בלבד ב-[X3 §2](X3-integration-deploy.md). הוא מגדיר את חוזה-ה-env
|
||||||
|
(SSoT אחד), מקור-ה-config (Coolify), טיפול-הסודות, ואי-ה-hardcode. X3 נשאר הבעלים של **זרימות**-האינטגרציה;
|
||||||
|
X10 הבעלים של **הקונפיגורציה וה-deploy**.
|
||||||
|
|
||||||
|
> **invariant פרויקטלי-תפעולי + הנדסי.** ENV1/ENV3/ENV4/ENV5 נשענים על עקרונות-הנדסה מוכרים (12-Factor,
|
||||||
|
> ניהול-סודות) — ≥3 מקורות. ENV2 (מקור-config של *מערכת זו*) הוא תפעולי, נקשר ל-[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. מצב קיים (מאומת מול הקוד)
|
||||||
|
|
||||||
|
- **מודל-deploy:** legal-ai = Coolify Docker (UUID `gyjo0mtw2c42ej3xxvbz8zio`, build_pack `dockerimage`);
|
||||||
|
ה-env **מוזרק ישירות מ-Coolify**, לא מ-Infisical ([X3 §2](X3-integration-deploy.md); זיכרון `reference_legal_ai_env_architecture`).
|
||||||
|
- **40+ משתני-env** נקראים על-פני [config.py](../../mcp-server/src/legal_mcp/config.py), [web/app.py](../../web/app.py),
|
||||||
|
[paperclip_api.py](../../web/paperclip_api.py)/[paperclip_client.py](../../web/paperclip_client.py),
|
||||||
|
[gitea_client.py](../../web/gitea_client.py), [chat_proxy.py](../../web/chat_proxy.py).
|
||||||
|
- **קטלוג-UI** ([mcp_env_catalog.py](../../web/mcp_env_catalog.py)) מכסה **13 בלבד** מתוך ה-40+ → השאר בלתי-נראים
|
||||||
|
לדף-ההגדרות ולגילוי-drift.
|
||||||
|
- **Infisical:** קוד-ה-SDK ב-[config.py](../../mcp-server/src/legal_mcp/config.py) קורא `INFISICAL_TOKEN`, אך
|
||||||
|
בקונטיינר הוא **לעולם לא מוגדר** → קוד מת; ה-priority בפועל = Coolify-env בלבד.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-ENV1: env-catalog יחיד = SSoT לכל משתני-הסביבה
|
||||||
|
**כלל:** קיים **קטלוג-env יחיד** המתאר את **כל** המשתנים (שם, ברירת-מחדל, סוד?, מי-קורא, מה-שולט). אין משתנה
|
||||||
|
שנקרא-בקוד אך לא-בקטלוג, ואין משתנה-בקטלוג שלא-נקרא. מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
ו-[G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש) (שלמות-הקטלוג). **הנדסי.**
|
||||||
|
**מקורות:** *The Twelve-Factor App — III. Config* (https://12factor.net/config) · OWASP — *Configuration / Secrets Management Cheat Sheet*
|
||||||
|
(https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html) · Kleppmann *DDIA* (config as data) | סטטוס: verified
|
||||||
|
**אכיפה:** קטלוג מקיף + בדיקה ש-getenv call-sites ⊆ קטלוג. **כיום:** 13/40+ בלבד ([gap-audit GAP-60](gap-audit.md)).
|
||||||
|
**הפרה ידועה:** `PAPERCLIP_BOARD_API_KEY`/`GITEA_*`/`CHAT_SERVICE_URL`/`LEGAL_CHAT_SHARED_SECRET` לא בקטלוג; `GITEA_ACCESS_TOKEN` מול `GITEA_TOKEN` (שני שמות) ([gap-audit GAP-58](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-ENV2: מקור-config יחיד ומתועד (Coolify) — בלי קוד-מת
|
||||||
|
**כלל:** למערכת **מקור-config אחד מתועד** (Coolify-env לקונטיינר), והקוד אינו מניח מקור-שני שאינו פעיל.
|
||||||
|
אין "Infisical priority" מדומה כשאין `INFISICAL_TOKEN`. מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
(מקור-אמת יחיד) וכלל "אין בליעה שקטה" ([§6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)). **פרויקטלי-תפעולי.**
|
||||||
|
**מקור-סמכות:** זיכרון `reference_legal_ai_env_architecture`; `feedback_infisical_coolify_drift`; [X3 §2](X3-integration-deploy.md).
|
||||||
|
**אכיפה:** לתעד Coolify כ-SSoT; להסיר/לבודד את קוד-ה-Infisical או להפעילו אמיתית.
|
||||||
|
**הפרה ידועה:** קוד-Infisical ב-[config.py](../../mcp-server/src/legal_mcp/config.py) מת בקונטיינר; ה-priority המתועד לא תואם מציאות ([gap-audit GAP-55](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-ENV3: ללא hardcode — IDs/URLs/נתיבים מ-config
|
||||||
|
**כלל:** מזהים (company/agent), כתובות (Paperclip/Coolify/Gitea/chat/frontend), פורטים ונתיבים **נגזרים מ-config**,
|
||||||
|
לא קבועים בקוד. אין `/home/chaim` קשיח ואין UUID קשיח. מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
(SSoT) — תואם [X7 INV-INT5](X7-paperclip-client-params.md). **הנדסי.**
|
||||||
|
**מקורות:** *Twelve-Factor App — III. Config* · *Twelve-Factor — X. Dev/prod parity* (https://12factor.net/dev-prod-parity) ·
|
||||||
|
Google *SRE / configuration as data* (https://sre.google/workbook/configuration-design/) | סטטוס: verified
|
||||||
|
**אכיפה:** grep-gate נגד literals (UUID/URL/path) בקוד-חדש. **כיום אין.**
|
||||||
|
**הפרה ידועה:** UUIDs קשיחים ([paperclip_client.py:36-62](../../web/paperclip_client.py), [app.py:3976](../../web/app.py)); URLs קשיחים (`pc.nautilus...`, `coolify...`, `legal-ai-next...`); `LEGAL_AI_WORKSPACE_CWD="/home/chaim/legal-ai"`; chat-URL `10.0.1.1` מול תיעוד `host.docker.internal` ([gap-audit GAP-56/59/61](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-ENV4: אין secrets בקוד/בברירות-מחדל — fail-loud
|
||||||
|
**כלל:** שום סוד (creds/key/token) אינו בקוד או בברירת-מחדל; היעדר-סוד **נכשל בקול** (לא נופל לברירת-מחדל
|
||||||
|
שקטה עם creds). אין סוד מודלף ל-log או ל-git. מופע של [G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai)
|
||||||
|
(integrity) וכלל "אין בליעה שקטה" ([§6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)). **הנדסי.** תואם זיכרון `feedback_secrets_first`.
|
||||||
|
**מקורות:** OWASP — *Secrets Management Cheat Sheet* (https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html) ·
|
||||||
|
*Twelve-Factor — III. Config* (no secrets in code) · CWE-798 — *Use of Hard-coded Credentials* (https://cwe.mitre.org/data/definitions/798.html) | סטטוס: verified
|
||||||
|
**אכיפה:** ברירות-מחדל ריקות + כישלון-מפורש; secret-scan ב-CI.
|
||||||
|
**הפרה ידועה:** `PAPERCLIP_DB_URL` ברירת-מחדל `postgresql://paperclip:paperclip@...` (creds plaintext) ב-3 מקומות ([paperclip_client.py:21](../../web/paperclip_client.py), [app.py:3789,3964](../../web/app.py)) ([gap-audit GAP-57](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-ENV5: drift-detection מכסה את כל המשתנים הקריטיים
|
||||||
|
**כלל:** מנגנון גילוי-ה-drift (Coolify↔container) מכסה את **כל** המשתנים הקריטיים, לא תת-קבוצה. מופע של
|
||||||
|
[G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן) ברוח-שלו (freshness של config) ו-[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים). **הנדסי.**
|
||||||
|
**מקורות:** *Twelve-Factor — III. Config* · Google *SRE — config drift* · HashiCorp — *config drift / desired state* (https://developer.hashicorp.com/well-architected-framework) | סטטוס: verified
|
||||||
|
**אכיפה:** הרחבת ה-catalog ל-drift-detection מלא בדף-ההגדרות.
|
||||||
|
**הפרה ידועה:** רק 13/40+ במנגנון; 8+ סודות קריטיים בלתי-מנוטרים ([gap-audit GAP-60](gap-audit.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Deploy — עמידוּת (מ-X3 §2, מורחב)
|
||||||
|
- **מחזור:** commit→push→Gitea Actions→Coolify redeploy (~2-4 דק'); endpoint חדש דורש גם `npm run api:types` ([X3 §2](X3-integration-deploy.md), [INV-INT2](X3-integration-deploy.md)).
|
||||||
|
- **חולשות-עמידוּת שנמצאו:** [start.sh](../../start.sh) **אינו נכשל** אם uvicorn לא עולה (ה-UI עולה עם בקאנד שבור);
|
||||||
|
ה-curl ל-Coolify ב-[.gitea/workflows/deploy.yaml](../../.gitea/workflows/deploy.yaml) הוא fire-and-forget (אין אימות-הצלחה) ([gap-audit GAP-62](gap-audit.md)).
|
||||||
|
- **host.docker.internal:** ה-chat-service נדרש דרך gateway; תיעוד מול קוד לא-תואמים (10.0.1.1) — ENV3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. הפניות-אחיות
|
||||||
|
- [X3-integration-deploy.md](X3-integration-deploy.md) — זרימות-אינטגרציה + INV-INT2 (מחזור-deploy).
|
||||||
|
- [X7-paperclip-client-params.md](X7-paperclip-client-params.md) — IDs/keys של Paperclip (INV-INT5 תואם ENV3).
|
||||||
|
- [00-constitution.md](00-constitution.md) — [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים), [G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש), [G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai), כלל "אין בליעה שקטה" ([§6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
|
||||||
|
- זיכרונות: `reference_legal_ai_env_architecture`, `feedback_infisical_coolify_drift`, `feedback_secrets_first`.
|
||||||
|
- [config.py](../../mcp-server/src/legal_mcp/config.py), [mcp_env_catalog.py](../../web/mcp_env_catalog.py), [Dockerfile](../../Dockerfile), [start.sh](../../start.sh), [.env.example](../../.env.example).
|
||||||
182
docs/spec/X11-citation-corroboration.md
Normal file
182
docs/spec/X11-citation-corroboration.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# X11 — תיקוף-הלכות בציטוטים (Citation Corroboration / Internal Citator)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md). הוא מגדיר **שכבת citator פנימית**: שימוש
|
||||||
|
ב**ציטוטים-הנכנסים** לפסיקה (איך ערכאות וועדות מאוחרות *טיפלו* בה) כדי **לתקף ולחדד את ההלכות
|
||||||
|
שחולצו ממנה**, וכך לצמצם את היקף האישור-הידני של היו"ר. הוא אוכף את
|
||||||
|
[INV-G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant) (כפי שתוקן —
|
||||||
|
ראה §6), נשען על [INV-G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai)
|
||||||
|
(עקיבוּת-מקור), ומעמיק את מודל-הציטוטים של [02-data-model.md](02-data-model.md).
|
||||||
|
|
||||||
|
> **TARGET, לא תיאור-מצב.** המנגנון כאן הוא היעד. רכיבים שטרם נבנו מסומנים מפורשות
|
||||||
|
> כ-audit-finding (§7), ולא כהתנהגות קיימת. כל טענה על הקוד מצוטטת `file:line`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. הרעיון — citator פנימי
|
||||||
|
|
||||||
|
בעולם המשפטי, הכלים שמאמתים פסיקה לפי הציטוטים-הנכנסים אליה הם **citators** (Shepard's של
|
||||||
|
LexisNexis, KeyCite של Westlaw, BCite של Bloomberg). הם עונים על שתי שאלות: *האם הפסק עדיין
|
||||||
|
"good law"?* ו-*איך ערכאות מאוחרות טיפלו בו?* — לפי **סיווג-טיפול** (treatment) של כל ציטוט-נכנס.
|
||||||
|
|
||||||
|
המערכת שלנו מחזיקה כבר את חומר-הגלם: גרף-ציטוטים פנימי (§2). מה שחסר הוא **השכבה שמחברת אותו
|
||||||
|
להלכות** — לתקף הלכה ספציפית לפי כך שערכאות/ועדות מאוחרות *אימצו* אותה בפועל. הלכה שאומצה
|
||||||
|
שוב-ושוב ע"י פאנלים אחרים אינה "ניחוש של מודל" — היא **טיפול שיפוטי אנושי מצטבר**, וזה הבסיס
|
||||||
|
שמאפשר אישור-אוטומטי בלי לפגוע בשיקול-הדעת האנושי (ראה תיקון INV-G10, §6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. חומר-הגלם הקיים — שני גרפי-ציטוט
|
||||||
|
|
||||||
|
| טבלה | קושר | הקשר נשמר | סיווג-טיפול |
|
||||||
|
|------|------|-----------|-------------|
|
||||||
|
| `case_law_citations` (`db.py:382`) | פסיקה ← **החלטת-ועדה פנימית** (`decisions`) | `context_text` | `citation_type` (support/distinguish/overrule/obiter) |
|
||||||
|
| `precedent_internal_citations` (`db.py:938`) | פסיקה ← **פסיקה אחרת** (`case_law`) | `match_context` | — (אין שדה-טיפול) |
|
||||||
|
|
||||||
|
**audit-finding (קיים):** ב-`precedent_internal_citations` **אין** שדה סיווג-טיפול, ו-ב-
|
||||||
|
`case_law_citations` שדה `citation_type` קיים אך **ברירת-המחדל `'support'`** (`db.py:387`) —
|
||||||
|
כלומר רוב הרשומות לא סווגו בפועל. סיווג-הטיפול הוא רכיב שיש לבנות (§4, INV-COR2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. תנאי-קדם — גרף-זהות נקי
|
||||||
|
|
||||||
|
ה-corroboration מצרף ציטוטים להלכות **דרך רשומת ה-`case_law`**. אם אותו תקדים מיוצג בשתי
|
||||||
|
רשומות (stub `cited_only` + רשומת-תוכן), הציטוטים יושבים על האחת וההלכות על האחרת — וה-join
|
||||||
|
נשבר. לכן **[INV-G1](00-constitution.md#inv-g1-מזהה-קנוני-מנורמל-בכתיבה)/[INV-ID1](X1-identifiers.md)
|
||||||
|
הם תנאי-קדם קשיח** ל-X11.
|
||||||
|
|
||||||
|
**הפרה ידועה (תוקנה 2026-05-31):** אהוד שפר עע"מ 317/10 הוחזק בשתי רשומות — `external_upload`
|
||||||
|
עם ציטוט-מלא כ-`case_number` (הפרת INV-ID2) + `cited_only` stub שתפס את 7 הציטוטים-הנכנסים בנפרד
|
||||||
|
מ-53 ההלכות. מוזג לרשומה קנונית אחת; סריקת-קורפוס מלאה (128 רשומות) אישרה **0** stubs עם
|
||||||
|
ציטוטים-תקועים שנותרו. ראה [#70 / FU-2c-b](../audit-report.md). הניקוי השוטף של 49 ה-`cited_only`
|
||||||
|
(הרחבת `_DOCKET_RE`, ציטוטים-משולבים) ממשיך תחת #70.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. המנגנון (TARGET)
|
||||||
|
|
||||||
|
```
|
||||||
|
לכל הלכה h של תקדים P:
|
||||||
|
1. אסוף ציטוטים-נכנסים ל-P (שני הגרפים, §2).
|
||||||
|
2. סווג טיפול לכל ציטוט (followed / distinguished / criticized / overruled / explained)
|
||||||
|
מתוך ההקשר (context_text / match_context) — Opus 4.8 @ xhigh. [INV-COR2]
|
||||||
|
3. התאם כל ציטוט להלכה הספציפית: דמיון סמנטי בין ההקשר לבין rule_statement של h,
|
||||||
|
מעל רף; הציטוט נספר ל-h רק אם הוא נוגע *לאותה הלכה*, לא לפסק כולו. [INV-COR3]
|
||||||
|
4. ספֵר corroboration של h = מספר ציטוטים חיוביים בלתי-תלויים שהותאמו אליה.
|
||||||
|
5. אישור:
|
||||||
|
אם ≥N חיוביים בלתי-תלויים ∧ 0 שליליים → אישור-אוטומטי (corroborated). [INV-COR4]
|
||||||
|
אם יש טיפול שלילי (distinguished/criticized/overruled) → אסור אוטו;
|
||||||
|
דגל ליו"ר, ואף הדחה אם overruled. [INV-COR2]
|
||||||
|
אחרת (לא-מצוטט) → נשאר בשער-היו"ר הרגיל (סף-confidence). [INV-COR5]
|
||||||
|
6. העשרה (משני): נסח-מחדש/חדד את rule_statement לפי המסגור של הפאנל המצטט.
|
||||||
|
```
|
||||||
|
|
||||||
|
**N (סף-corroboration)** ייקבע אמפירית (≥2 ברירת-מחדל; ציטוט יחיד אינו מספיק — INV-COR4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-COR1: corroboration = טיפול שיפוטי אנושי מצטבר, לא שיפוט-AI
|
||||||
|
**כלל:** אישור-הלכה מבוסס-ציטוט נשען על כך ש**ערכאות/ועדות אנושיות אימצו את ההלכה בפועל** —
|
||||||
|
לא על ציון-ביטחון של מודל. ה-AI רק **מזהה ומסווג** את הטיפול הקיים; ההכרעה הערכית שההלכה
|
||||||
|
תקפה ניתנה ע"י השופטים המצטטים. זהו הבסיס לתיקון INV-G10 (§6).
|
||||||
|
**מקורות (פתוחים):** Fowler, Johnson, Spriggs, Jeon & Wahlbeck, *Network Analysis and the Law:
|
||||||
|
Measuring the Legal Importance of Precedents at the U.S. Supreme Court* (Political Analysis 15:3,
|
||||||
|
2007) — סמכות-תקדים נמדדת מהציטוטים-הנכנסים, מאומת בניבוי ציטוט עתידי · *LePaRD: A Large-Scale
|
||||||
|
Dataset of Judicial Citations to Precedent* (arXiv 2311.09356, 2023) · Hellyer, *Evaluating
|
||||||
|
Shepard's, KeyCite, and BCite* (Law Library Journal 110:4, 2018, open-access) | סטטוס: verified
|
||||||
|
**אכיפה:** מנגנון §4 — corroboration נספר רק מטיפול שיפוטי מתועד, לא מ-confidence.
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-COR2: סיווג-טיפול חובה לפני ספירה — שלילי לעולם לא מאשר
|
||||||
|
**כלל:** כל ציטוט-נכנס מסווג ל**טיפול** (followed/explained = חיובי-נייטרלי;
|
||||||
|
distinguished/criticized/questioned/overruled = שלילי) לפני שהוא נספר. **טיפול שלילי לעולם אינו
|
||||||
|
תורם ל-corroboration ואינו מאשר אוטומטית**; overruled → הדחת ההלכה לבדיקת-יו"ר.
|
||||||
|
**מקורות (פתוחים):** Demir & Canbaz, *Validate Your Authority: Benchmarking LLMs on Multi-Label
|
||||||
|
Precedent Treatment Classification* (NLLP Workshop @ ACL, 2025) — LLM מסווג טיפול-תקדים
|
||||||
|
(Gemini 2.5 79.1% / GPT-5-mini 67.7%) · Galgani & Hoffmann, *LEXA* — knowledge bases for automatic
|
||||||
|
legal citation classification · *Towards Automatically Classifying Case Law Citation Treatment
|
||||||
|
Using Neural Networks* · UNC Law, *Describing Negative Legal Precedent in Citators* | סטטוס: verified
|
||||||
|
**אכיפה:** שלב 2+5 ב-§4; סכֵמת-טיפול ב-`precedent_internal_citations` (שדה חדש) +
|
||||||
|
`case_law_citations.citation_type` (לא להישען על ברירת-המחדל `'support'`).
|
||||||
|
**הפרה ידועה:** סיווג-טיפול לא קיים בפועל (§2) — רכיב לבנייה.
|
||||||
|
|
||||||
|
### INV-COR3: התאמה להלכה הספציפית — לא לפסק כולו
|
||||||
|
**כלל:** ציטוט נספר ל-corroboration של הלכה h **רק אם ההקשר המצטט נוגע לאותה הלכה** (דמיון
|
||||||
|
סמנטי מעל רף). פסק מצוטט לעניין A אינו מתקף הלכה B שחולצה מאותו פסק.
|
||||||
|
**מקורות (פתוחים):** Hellyer (2018, open-access) — *"a 'followed' tag might refer to a different
|
||||||
|
legal point than the one you care about"* · Zheng, Guha, Anderson, Henderson & Ho, *CaseHOLD*
|
||||||
|
(arXiv 2104.08671, 2021) — סיווג-טיפול ברמת ה-holding הבודד, לא הפסק כולו · UChicago Library /
|
||||||
|
Northwestern Pritzker — מדריכי-מחקר (treatment ≠ point-specific) | סטטוס: verified
|
||||||
|
**אכיפה:** שלב 3 ב-§4 — רף-דמיון סמנטי בין ההקשר ל-rule_statement; Opus 4.8 כשופט-התאמה.
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-COR4: סף ≥N ציטוטים בלתי-תלויים — ציטוט יחיד אינו מספיק
|
||||||
|
**כלל:** אישור-אוטומטי דורש **≥N ציטוטים חיוביים בלתי-תלויים** — כלומר מ-**מקורות-מצטטים
|
||||||
|
מובחנים** (החלטות/פסקים שונים; שני אזכורים באותה החלטה = ציטוט אחד). ברירת-מחדל N=2. מקור יחיד
|
||||||
|
אינו ראיה מספקת; citators עצמם מפספסים 23–25% מהטיפול — לכן נדרשת חזרתיות חוצת-מקורות.
|
||||||
|
**מקורות (פתוחים):** Demir & Canbaz (NLLP/ACL 2025) — דיוק סיווג-טיפול 67.7–79.1% בלבד, לכן
|
||||||
|
סיווג בודד אינו ראיה מספקת ונדרשת חזרתיות · Fowler et al. (Political Analysis 2007) — סמכות =
|
||||||
|
*צבירת* ציטוטים, לא ציטוט יחיד · Hellyer (2018) — citator coverage gaps (פספוס 23–25% מהטיפול)
|
||||||
|
· Manning, Raghavan & Schütze, *Introduction to Information Retrieval* (CUP 2008) — aggregation of
|
||||||
|
weak signals | סטטוס: verified
|
||||||
|
**אכיפה:** שלב 4-5 ב-§4; `HALACHA_CORROBORATION_MIN_CITES` (env-tunable, ברירת-מחדל 2).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-COR5: השער האנושי נשמר לזנב הלא-מצוטט ולשלילי
|
||||||
|
**כלל:** corroboration **מצמצם** את היקף האישור-הידני; הוא **אינו מבטל** את שער-היו"ר. הלכות
|
||||||
|
לא-מצוטטות, וכל הלכה עם טיפול שלילי, **נשארות בשער-היו"ר**. גם ה-citators המקצועיים קובעים
|
||||||
|
ש"human review remains essential".
|
||||||
|
**מקורות (פתוחים):** Demir & Canbaz (NLLP/ACL 2025) — *"misclassification carries significant
|
||||||
|
risk"*, ה-citators האוטומטיים *not infallible* → עיון-אנוש נחוץ · Hellyer (2018) — *"There's no
|
||||||
|
substitute for reading the actual citing case"* · NCSC/JTC, *Principles & Practices for AI Use in
|
||||||
|
Courts* (human-in-the-loop) · CEPEJ (2018, user-control) | סטטוס: verified
|
||||||
|
**אכיפה:** שלב 5 ב-§4; שער-היו"ר הקיים ([05-qa-review.md](05-qa-review.md)) נשאר על הזנב.
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-COR6: עקיבוּת — כל אישור-אוטומטי שומר את ראיית-הציטוט
|
||||||
|
**כלל:** הלכה שאושרה ב-corroboration **שומרת את הציטוטים המתקפים** (מזהי-המקור + ההקשר +
|
||||||
|
הטיפול) כ-provenance הניתן לביקורת — מי אישר, על סמך אילו פסקים, ובאיזה טיפול.
|
||||||
|
**מקורות:** [INV-G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai) · ISO 15489-1:2016
|
||||||
|
(records authenticity) · CEPEJ (2018, transparency) | סטטוס: verified (נגזר מ-G9)
|
||||||
|
**אכיפה:** `halachot.reviewer` = `corroborated (≥N judicial citations)` + טבלת-קישור
|
||||||
|
הלכה↔ציטוטים-מתקפים; מוצג ביו"ר-UI.
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. תיקון INV-G10 (מבוקר)
|
||||||
|
|
||||||
|
INV-G10 קובע ששער אישור-ההלכה הוא invariant אנושי-חובה. **התיקון** (החלטת-יו"ר 2026-05-31)
|
||||||
|
אינו מבטל את השער אלא **מרחיב את מקור-הסמכות האנושית שלו**: השער מסופק ע"י **טיפול שיפוטי
|
||||||
|
מצטבר** (ערכאות/ועדות מצטטות) עבור תת-הקבוצה ה-corroborated החיובית, בעוד **שער-היו"ר נשאר חובה**
|
||||||
|
לזנב הלא-מצוטט ולכל טיפול-שלילי. הנוסח המתוקן + המקורות נכתבים ב-
|
||||||
|
[00-constitution.md INV-G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant).
|
||||||
|
עיקרון-העל (INV-COR1) שומר על רוח G10: זהו שיפוט אנושי (של המצטטים), לא שיפוט-AI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. מצב קיים מול יעד — audit-findings
|
||||||
|
|
||||||
|
- **קישור הלכה↔ציטוט לא קיים.** אין טבלה/שאילתה שמצרפת ציטוט-נכנס להלכה ספציפית — רכיב-ליבה
|
||||||
|
לבנייה (§4 שלב 3).
|
||||||
|
- **סיווג-טיפול חסר.** `precedent_internal_citations` ללא שדה-טיפול; `case_law_citations.citation_type`
|
||||||
|
על ברירת-מחדל `'support'` (`db.py:387`) — לא מסווג בפועל (§2, INV-COR2).
|
||||||
|
- **אישור-אוטומטי כיום מבוסס-confidence בלבד.** `db.store_halachot` מאשר ב-`confidence ≥
|
||||||
|
HALACHA_AUTO_APPROVE_THRESHOLD` (`db.py:3221`, ברירת-מחדל 0.80) — לא מבוסס-ציטוט. X11 מוסיף
|
||||||
|
מסלול-אישור שני (corroboration) לצד/מעל סף-ה-confidence.
|
||||||
|
- **גרף-זהות.** תוקן לשפר + dedup content-affecting (§3); המשך ניקוי ב-#70.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md) — INV-G9 (provenance), INV-G10 (שער אנושי, מתוקן §6),
|
||||||
|
פרוטוקול ≥3-מקורות.
|
||||||
|
- [02-data-model.md](02-data-model.md) — טבלות הציטוטים (`case_law_citations`,
|
||||||
|
`precedent_internal_citations`) + ישות `halachot`.
|
||||||
|
- [05-qa-review.md](05-qa-review.md) — שער אישור-ההלכה הקיים (נשאר על הזנב, INV-COR5).
|
||||||
|
- [07-learning.md](07-learning.md) — צמיחת-קורפוס + לולאת-הלכות.
|
||||||
|
- [X1-identifiers.md](X1-identifiers.md) — תנאי-הקדם: זהות קנונית (INV-ID1/ID2).
|
||||||
|
- [#70 / FU-2c-b](../audit-report.md) — dedup של `cited_only` (תנאי-קדם, §3).
|
||||||
185
docs/spec/X12-digests-radar.md
Normal file
185
docs/spec/X12-digests-radar.md
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
# X12 — יומונים כשכבת-גילוי (Digests Radar)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md). הוא מגדיר **שכבת-גילוי (discovery/radar)**
|
||||||
|
מעל קורפוסי-הפסיקה: קליטה וחיפוש של **יומונים** — סיכומי-עמוד-אחד של משרד עפר טויסטר ("כל יום —
|
||||||
|
היומון לענייני תכנון ובנייה") על פסק-דין/החלטה בודדים. היומון הוא **מקור משני** המצביע על פסק-הדין
|
||||||
|
המקורי; הוא **אינו** נכנס לאף אחד מ-3 קורפוסי-הציטוט, **אינו** מצוטט בהחלטה, ו**אינו** מחלץ הלכות.
|
||||||
|
הוא נשען על [INV-G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
(אין מסלול מקביל), [INV-G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש)
|
||||||
|
(שלמות + אין בליעה שקטה) ו-[INV-G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai)
|
||||||
|
(עקיבוּת-מקור), ומובחן מ-3 הקורפוסים של [03-retrieval.md](03-retrieval.md).
|
||||||
|
|
||||||
|
> **TARGET, לא תיאור-מצב.** התת-מערכת כולה היא יעד — אין כיום טבלת `digests`, כלי-`digest_*`,
|
||||||
|
> ולא אינטגרציית-חוקר. כל רכיב מסומן מפורשות כ-audit-finding לבנייה (§6). כל טענה על הקוד `file:line`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. הרעיון — radar, לא קורפוס-ציטוט
|
||||||
|
|
||||||
|
חיים מקבל כמעט יומית מייל עם **יומון**: PDF של עמוד אחד שמסכם פסק-דין/החלטה בודדים בתחום
|
||||||
|
רישוי-ובנייה / היטל-השבחה / פיצויים(ס'197). היומון אינו הטקסט המשפטי המקורי — הוא **ניתוח של צד
|
||||||
|
שלישי** (עפר טויסטר), הנושא הבהרה מודפסת: *"האמור הוא מידע ראשוני בלבד ואין הוא תחליף לייעוץ
|
||||||
|
משפטי"*. במונחי-מחקר-משפטי זהו **מקור משני (secondary authority)**: כלי-איתור והכוונה, לא סמכות
|
||||||
|
שמצטטים בהחלטה.
|
||||||
|
|
||||||
|
הערך שלו עצום דווקא כ-**radar**: כל יומון הוא *headnote + תג-נושא כתובים-מראש בידי מומחה*, המצביע
|
||||||
|
על פסק-דין מקורי. כשמנסחים החלטה, `search_digests` מחזיר את היומון הרלוונטי → החוקר קורא את ניתוח
|
||||||
|
טויסטר **כרקע** → מחלץ את מראה-המקום של פסק-הדין המקורי → מביא את הפסק עצמו לקורפוס-הפסיקה הקיים
|
||||||
|
(הזמינות גבוהה) → ומצטט **משם**. היומון מצביע; הציטוט תמיד נשען על המקור.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. מה היומון מכיל
|
||||||
|
|
||||||
|
מבנה קבוע (אומת מול הקבצים ב-`data/precedents/incoming/`, יומון 5158/5159/5160/5163):
|
||||||
|
|
||||||
|
| רכיב | דוגמה | תפקיד |
|
||||||
|
|------|-------|-------|
|
||||||
|
| מספר-יומון + תאריך-גיליון | `יומון מס' 5163 7 ביוני 2026` | מפתח-upsert + `digest_date` |
|
||||||
|
| תג-מושג | `"שיקול הדעת המצומצם"` | ציר-נושא לחיפוש |
|
||||||
|
| כותרת-הלכה | `ביהמ"ש - שיקול דעת הוועדה המחוזית אינו מצומצם...` | הסיכום בשורה |
|
||||||
|
| גוף-ניתוח (1–2 עמ') | ניתוח עפר-טויסטר | רקע + מקור-embedding |
|
||||||
|
| מראה-מקום בתחתית | `עת"מ 46111-12-22 יכין-אפק... ניתן 3.6.26... שופטת: יעל טויסטר ישראלי` | **השדה הקריטי** — הגשר לפסק המקורי |
|
||||||
|
|
||||||
|
`underlying_date` (מתן הפסק) שונה מ-`digest_date` (גיליון היומון) — מקור-באגים נפוץ; חילוץ-המטא-דאטה
|
||||||
|
מבחין ביניהם מפורשות.
|
||||||
|
|
||||||
|
**`digest_kind` (סיווג-גיליון, V32):** רוב הגיליונות הם `decision` (סיכום פס"ד → `underlying_citation`),
|
||||||
|
אך חלקם `announcement` — עדכון/הודעה ללא הכרעה (חקיקה, נוהל, ברכת-שנה) שאין לו מראה-מקום. החילוץ
|
||||||
|
מסווג כל גיליון ותמיד מחלץ `concept_tag`/`headline`/`summary` (קיימים לכל סוג); `underlying_citation`
|
||||||
|
רק ל-`decision`. **שימוש קריטי:** הגדרת-"כשל" של ה-drain self-heal היא `completed` **עם
|
||||||
|
`digest_kind=''`** (מעולם לא סווג) — כך הודעה (kind=`announcement`, בלי citation) **אינה** נחשבת כשל
|
||||||
|
ואינה מנוסה-מחדש לנצח. ההיוריסטיקה הישנה ("שני השדות ריקים") טיפלה בהודעות בטעות כ-retry אינסופי.
|
||||||
|
|
||||||
|
### 2.1 מקור שני ל-radar — העלון החודשי "עו"ד על נדל"ן"
|
||||||
|
|
||||||
|
פרסום **נפרד** מהיומון היומי: עלון חודשי ממוספר (משרדי צבי שוב + רונית אלפר), **רב-נושאי** — מאמר-עומק,
|
||||||
|
עדכוני-חקיקה, וסט מצביעי-פסיקה מקובצים לפי נושא. נקלט **לאותה טבלת `digests`** (לא קורפוס מקביל — G2),
|
||||||
|
מובחן ע"י `publication='עו"ד על נדל"ן'` (מול `'כל יום'`). עלון אחד **מתפצל ל-N שורות** דרך
|
||||||
|
`bulletin_splitter` (LLM, local-only) → `bulletin_library.ingest_bulletin`:
|
||||||
|
- **מצביעי-פסיקה** → `digest_kind='decision'` — מצטרפים ל-radar ומקושרים לפסק (autolink + X13 כמו היומון).
|
||||||
|
- **מאמרים** → `digest_kind='article'` — טקסט-מלא + embedding לחיפוש-עומק; **רקע בלבד, INV-DIG1 חל** (לא מצוטט).
|
||||||
|
- **עדכוני-חקיקה — לא נקלטים** (החלטת יו"ר).
|
||||||
|
|
||||||
|
מפתח-הדדאפ לפריט-עלון הוא **`content_hash` (per-פריט)**, כי `yomon_number` ריק (ה-upsert על yomon-number
|
||||||
|
לא חל; `uq_digests_content_hash` תופס re-runs). אידמפוטנטי. סקריפט: `scripts/ingest_bulletins.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. למה זה לא קורפוס-ציטוט רביעי (הקושיה המרכזית — G2)
|
||||||
|
|
||||||
|
[03-retrieval.md §1](03-retrieval.md#1-שלושת-הקורפוסים-וכלי-החיפוש) מגדיר 3 **קורפוסי-ציטוט**:
|
||||||
|
מסמכי-תיק+סגנון-דפנה, פסיקה-חיצונית, החלטות-ועדה. השאלה: האם יומונים = רביעי, ובכך הפרת
|
||||||
|
[INV-G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)?
|
||||||
|
|
||||||
|
**לא — בתנאי המסגור הנכון.** G2 אוסר *מסלול מקביל ליכולת קיימת*. יומונים אינם עוד-מסלול-לאחזור-
|
||||||
|
פסיקה אלא **bounded context נפרד**: ישות נפרדת (`digests`, לא `case_law`), מטרה נפרדת (הצבעה ולא
|
||||||
|
ציטוט), וחוזה נפרד. ההבחנה הקנונית: 3 הקורפוסים הם **עקיבים-בפלט** (כל ציטוט בהחלטה חוזר אליהם —
|
||||||
|
[INV-RET5](03-retrieval.md#inv-ret5-כל-span-מוחזר-עקיב-למקורו)/[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai)).
|
||||||
|
היומון **לעולם אינו עקיב-אליו בפלט** (INV-DIG1) — ולכן אינו קורפוס-ציטוט רביעי, אלא שכבה
|
||||||
|
**מקדימה** לקורפוסים. הפרדת-הקורפוס מ-[INV-RET1](03-retrieval.md#inv-ret1-הפרדת-קורפוס-נאכפת-ב-100-ממסלולי-ה-query)
|
||||||
|
מתקיימת אוטומטית: `search_digests` שואל **רק** את `digests`, ואף כלי-חיפוש-פסיקה אינו נוגע בה
|
||||||
|
(הפרדה פיזית בטבלה, לא תנאי-סינון).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. המנגנון (TARGET)
|
||||||
|
|
||||||
|
```
|
||||||
|
קליטה (מסלול קצר עצמאי — INV-DIG2):
|
||||||
|
יומון PDF → extract_text → content_hash (idempotent, INV-G3)
|
||||||
|
→ חילוץ-LLM: תג-מושג / כותרת-הלכה / תקציר / מראה-מקום / שני-תאריכים / תחום / תגיות
|
||||||
|
→ INSERT digests → embedding יחיד (תג+כותרת+תקציר+ניתוח) לחיפוש סמנטי בלבד
|
||||||
|
→ try_autolink(underlying_citation → case_law) [INV-DIG3]
|
||||||
|
⚠ ללא precedent_chunks, ללא halacha-extraction, ללא precedent metadata-extractor.
|
||||||
|
|
||||||
|
חיפוש + שימוש (radar — INV-DIG1):
|
||||||
|
legal-researcher: search_digests(סוגיה)
|
||||||
|
→ קורא ניתוח טויסטר + כותרת-הלכה = רקע/orientation בלבד
|
||||||
|
→ מחלץ את מראה-המקום של הפסק המקורי
|
||||||
|
→ הפסק בקורפוס? כן → אמת+צטט כרגיל (precedent_attach) + digest_link
|
||||||
|
לא → missing_precedent_create על *הפסק המקורי*
|
||||||
|
(notes="זוהה דרך יומון מס' NNNN") [INV-DIG3]
|
||||||
|
→ היומון לעולם אינו נרשם דרך precedent_attach ואינו supporting_quote. [INV-DIG1]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-DIG1: היומון מצביע, לא מצוטט
|
||||||
|
**כלל:** רשומת-`digest` לעולם אינה משמשת כ-`supporting_quote`/provenance בפלט-החלטה; כל ציטוט
|
||||||
|
בהחלטה נגזר מקורפוס-ציטוט (`case_law`/`document_chunks`). היומון הוא מקור משני — כלי-איתור,
|
||||||
|
לא סמכות-מצוטטת. החוקר רושם אותו כ-radar (סעיף-דוח נפרד), לא דרך `precedent_attach`.
|
||||||
|
**מקור-סמכות:** היו"ר + ההבהרה המודפסת ביומון ("מידע ראשוני בלבד... אינו תחליף לייעוץ משפטי") —
|
||||||
|
invariant תוכן-משפטי/תפעולי, **קשור** ל-[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai).
|
||||||
|
**מקורות (פתוחים, להבחנת מקור-ראשוני↔משני):** Georgetown Law Library — *Secondary Sources research
|
||||||
|
guide* (*"secondary sources... are not the law"*) · Amy E. Sloan, *Basic Legal Research: Tools and
|
||||||
|
Strategies* — primary vs. persuasive/secondary authority · *The Bluebook: A Uniform System of
|
||||||
|
Citation* — סיווג סמכות-ראשונית מול משנית | סטטוס: verified
|
||||||
|
**אכיפה:** היעדר FK מ-`decision_blocks`/ציטוטים ל-`digests`; ולידציית-QA ([05-qa-review.md](05-qa-review.md))
|
||||||
|
שדוחה ציטוט שמקורו digest; הוראת-חוקר מפורשת ([X4-agents.md](X4-agents.md), `legal-researcher.md`).
|
||||||
|
**הפרה ידועה:** — (תת-מערכת חדשה)
|
||||||
|
|
||||||
|
### INV-DIG2: מסלול-קליטה נפרד-בכוונה — לא מסלול-פסיקה מקביל
|
||||||
|
**כלל:** קליטת-יומון היא **bounded context נפרד**, ואינה עוברת ב-precedent pipeline
|
||||||
|
([01-ingest.md](01-ingest.md)): אין `precedent_chunks`, אין halacha-extraction, אין
|
||||||
|
precedent-metadata-extractor. מסלול קצר עצמאי (`digest_library.ingest_digest`) הבונה
|
||||||
|
embedding-יחיד לחיפוש סמנטי בלבד. הצהרה זו היא מה ש**מונע** הפרת-G2 — היומון אינו ישות-אחות
|
||||||
|
של `case_law` ואינו מתפצל ממסלולו.
|
||||||
|
**מקורות:** Eric Evans, *Domain-Driven Design* (2003) — Bounded Context (הקשרים שונים = מודלים
|
||||||
|
מובחנים) · Martin Kleppmann, *DDIA* (2017) — system-of-record מובחן מ-derived/index data · Martin
|
||||||
|
Fowler — Bounded Context / Canonical Data Model | סטטוס: verified
|
||||||
|
**אכיפה:** טבלה פיזית נפרדת `digests`; `ingest_digest` עושה reuse לשירותים אטומיים בלבד
|
||||||
|
(`extractor.extract_text`, `embeddings.embed_texts`) ולא ל-`ingest.ingest_document`; ביקורת-
|
||||||
|
ארכיטקטורה. אוכף את [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
+ כלל-הנדסה "סימטריה" (§6). **מקור-אמת יחיד:** מצב-הקליטה נשמר אך-ורק בטבלת `digests` (סטטוס +
|
||||||
|
`content_hash` ל-idempotency); תיקיות-קבצים (`incoming/`) הן staging בלבד, **לא** state.
|
||||||
|
**הפרה ידועה (תוקנה 2026-06-07):** `ingest_digests_batch.py` העביר קבצים ל-`data/digests/processed/`
|
||||||
|
— state מבוסס-תיקיות מקביל ל-DB. הוסר; הסקריפט מסתמך על dedup ב-content_hash (G2).
|
||||||
|
|
||||||
|
### INV-DIG3: קישור-לפסק-המקורי הוא הגשר — חוסר-קישור הוא פער גלוי
|
||||||
|
**כלל:** לכל `digest` שדה `linked_case_law_id` (FK ל-`case_law`, nullable). כשהפסק המקורי בקורפוס —
|
||||||
|
היומון מקושר אליו (אוטומטית בקליטה לפי מראה-המקום, או ידנית ב-`digest_link`). כל עוד אינו בקורפוס,
|
||||||
|
הקישור ריק ו**הפער מוצף** דרך `missing_precedent_create` על הפסק המקורי — לא נבלע בשקט.
|
||||||
|
**מקורות:** E.F. Codd — referential integrity (foreign keys, CACM 13(6), 1970) · ISO 8000 —
|
||||||
|
completeness (פער-ידע מתועד) · DAMA-DMBOK2 — data linkage / lineage | סטטוס: verified
|
||||||
|
**אכיפה:** שדה-FK `digests.linked_case_law_id` + `try_autolink` בקליטה + כלי `digest_link`/
|
||||||
|
`digest_relink`; חוסר-קישור → `missing_precedent_create` (כלל-הנדסה "אין בליעה שקטה", §6). אוכף את
|
||||||
|
[G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש) +
|
||||||
|
[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים).
|
||||||
|
**הפרה ידועה:** — (תת-מערכת חדשה)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. מצב קיים מול יעד — audit-findings
|
||||||
|
|
||||||
|
התת-מערכת כולה TARGET; אין כיום מימוש. רכיבים לבנייה:
|
||||||
|
|
||||||
|
- **טבלת `digests` + פונקציות-DB** — לא קיימות. יעד: `SCHEMA_V30` ב-`db.py` (טבלה + ivfflat/GIN/FTS
|
||||||
|
אינדקסים + UNIQUE חלקי על `yomon_number`/`content_hash` ל-idempotent) + `create_digest`/`search_digests`/
|
||||||
|
`link_digest_to_case_law` (§4, INV-DIG2/DIG3).
|
||||||
|
- **שירות + חילוץ-LLM** — `services/digest_library.py` + `services/digest_metadata_extractor.py`
|
||||||
|
לא קיימים. החילוץ נשען על `claude_session` (local-only — ייבוא lazy בתוך `ingest_digest` בלבד,
|
||||||
|
לא רץ בקונטיינר; תואם [claude_session local-only]).
|
||||||
|
- **כלי-MCP `digest_*`** — לא קיימים. יעד: `tools/digests.py` + רישום ב-`server.py`, מעטפת-envelope
|
||||||
|
אחידה לפי [X9-mcp-tool-contract.md](X9-mcp-tool-contract.md) (`search_digests` מובחן בשם מ-6 כלי-
|
||||||
|
החיפוש הקיימים — INV-TOOL2).
|
||||||
|
- **אינטגרציית-חוקר** — `legal-researcher.md` ללא `search_digests`/`digest_link` ב-`tools:` וללא שלב-
|
||||||
|
radar. יעד: שלב סריקת-יומונים לפני האימות + סעיף-דוח נפרד "radar — לא ציטוט" (INV-DIG1).
|
||||||
|
- **UI** — אין דף `/digests`. יעד: דף נפרד (לא כרטיסייה ב-`/precedents`, לשמור גבול סמכותי/משני),
|
||||||
|
אחרי `npm run api:types` ([X6-ui-api-contract.md](X6-ui-api-contract.md)).
|
||||||
|
- **אוטומציית-קליטה (Gmail) + עלון-חודשי רב-נושאי** — שלב עתידי; שלב-1 ידני (drop ל-
|
||||||
|
`data/digests/incoming/` → `scripts/ingest_digests_batch.py`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md) — G2 (אין מסלול מקביל), G4 (שלמות/אין-בליעה), G9 (עקיבוּת).
|
||||||
|
- [03-retrieval.md](03-retrieval.md) — 3 קורפוסי-הציטוט שהיומון מובחן מהם (§3); הפרדת-קורפוס.
|
||||||
|
- [01-ingest.md](01-ingest.md) — צינור-הפסיקה הקנוני שהיומון **אינו** עובר בו (INV-DIG2).
|
||||||
|
- [02-data-model.md](02-data-model.md) — `case_law` (יעד-הקישור של `linked_case_law_id`).
|
||||||
|
- [05-qa-review.md](05-qa-review.md) — שער-QA שדוחה ציטוט שמקורו digest (INV-DIG1).
|
||||||
|
- [X4-agents.md](X4-agents.md) — סוכן החוקר שצורך את ה-radar.
|
||||||
|
- [X9-mcp-tool-contract.md](X9-mcp-tool-contract.md) — חוזה כלי-ה-`digest_*`.
|
||||||
180
docs/spec/X13-court-fetch.md
Normal file
180
docs/spec/X13-court-fetch.md
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
# X13 — אחזור-פסיקה אוטומטי מנט המשפט (Court Verdict Fetch)
|
||||||
|
|
||||||
|
> כפוף ל-[חוקת המערכת](00-constitution.md). תת-מערכת **שירות** (לא קורפוס) שמורידה פסקי-דין
|
||||||
|
> ציבוריים של בתי-משפט ומזרימה אותם ל**צינור-הקליטה הקנוני** של ספריית-הפסיקה. אחות-מושגית
|
||||||
|
> ל-[X12 — Digests Radar](X12-digests-radar.md) (הטריגר העיקרי) ול-[01-ingest](01-ingest.md)
|
||||||
|
> (היעד). אינה קורפוס רביעי ואינה מסלול-ingest מקביל.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. ייעוד והקשר
|
||||||
|
|
||||||
|
יומון (digest) מצביע על פסק-דין נושא (`underlying_citation`, למשל `עת"מ 46111-12-22`). כשהפסק
|
||||||
|
אינו בקורפוס, המערכת **מאחזרת אותו אוטומטית** ממקור ציבורי, מחלצת טקסט, וקולטת אותו דרך
|
||||||
|
`precedent_library_upload` → `ingest_precedent`. כך הופך פסק-דין מ"מצוטט-בלבד" ל"שמיש לחיפוש
|
||||||
|
וחילוץ-הלכות".
|
||||||
|
|
||||||
|
**הבחנת-מקור קריטית:** רק **פסקי-דין של בתי-משפט** ניתנים לאחזור ציבורי. **החלטות ועדת-ערר**
|
||||||
|
אינן זמינות ציבורית (נדרש נבו) — מסומנות כפער ולא נשלחות לאחזור.
|
||||||
|
|
||||||
|
**דרכי-מקור ציבוריות (ניתוב לפי זמינות-פורמט-נט, לא לפי ערכאה):**
|
||||||
|
- **נט המשפט** (מציג-התיקים) משרת **כל הערכאות** — מחוזי/שלום *וגם עליון* — כל עוד יש מספר
|
||||||
|
בפורמט תיק-חודש-שנה. ASP.NET WebForms (`__doPostBack`/VIEWSTATE), anti-bot של F5, מסמכים
|
||||||
|
בצופה-עמודים (turn.js). מחייב **דפדפן-אמת** (host-side) → שירות-מארח ב-pm2 (כדפוס
|
||||||
|
`legal-chat-service`). **זהו המסלול הראשי המאומת.**
|
||||||
|
- **עליון בפורמט-סדרתי** (עע"מ/בג"ץ NNNN/YY, ללא חודש — לא ניתן לחיפוש בנט) → `supremedecisions.court.gov.il`
|
||||||
|
(httpx, ללא CAPTCHA, ללא דפדפן). **פוענח ואומת (2026-06-08):** `POST Home/SearchVerdicts` עם
|
||||||
|
`document` מובנה (`{Year:"YYYY", CaseNum, OldMainNumFormat:true, SearchText:[…]}`) + כותרת
|
||||||
|
**`X-Requested-With: XMLHttpRequest`** → רשומות; `GET Home/Download?path=&fileName=&type=4` → PDF.
|
||||||
|
בוחר מסמך best-first (פסק-דין→מספר-עמודים) ומדלג על מסמכי published-report החסומים (`s`-prefix).
|
||||||
|
תיקים ישנים-מאוד שלא דיגיטצו (למשל 389/87) → `manual`.
|
||||||
|
|
||||||
|
> **אומת end-to-end (2026-06-07) על עת"מ 46111-12-22** — פס"ד 34 עמ' הורד **אוטונומית מלא,
|
||||||
|
> נטו קוד-פתוח, ללא כרטיס-חכם וללא פתרון-CAPTCHA**. ממצאי-המפתח מהכיול:
|
||||||
|
> - **החיפוש והניווט לתיק — ללא reCAPTCHA כלל.** מסלול: דף-בית → `btnExternalSearchCases`
|
||||||
|
> → מילוי `BamaCaseNumberTextBoxH`(=מס' תיק) + `BamaMonthYearTextBoxHT`(="MM-YY") →
|
||||||
|
> `CaseDetails.aspx` → לשונית "פסקי דין" → `DecisionList.aspx` → צופה `NGCSViewerPage.aspx`.
|
||||||
|
> - **reCAPTCHA קיים רק בצופה ורק על שמירה/הדפסה מפורשת** — *לא* על הצגת המסמך. הצופה
|
||||||
|
> מגיש את העמודים כ-PNG דרך PageMethod **`GetImages`** (4 עמ'/batch) **ללא CAPTCHA**.
|
||||||
|
> אחזור = לכידת `documentNumber` מהקריאה הראשונה + משיכת כל ה-batches ב-`fetch` עם הכותרת
|
||||||
|
> **`X-Requested-With: XMLHttpRequest`** (חובה — ה-WAF חוסם AJAX בלעדיה) → הרכבת PDF (Pillow).
|
||||||
|
> - דפדפן: **Camoufox דרך חבילת-הפייתון** (`camoufox.async_api`, in-process — לא שרת-Node).
|
||||||
|
> על שרת ללא-מסך נדרש **Xvfb** (אחרת Firefox קורס). פותר-ה-reCAPTCHA האודיו (Whisper) נשמר
|
||||||
|
> כ-fallback למסלול-השמירה-המפורש בלבד; מסלול-התמונות אינו זקוק לו.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. ארכיטקטורה — שלוש שכבות (tiered)
|
||||||
|
|
||||||
|
```
|
||||||
|
underlying_citation → [classifier] → {tier, האם יש פורמט-נט (תיק-חודש-שנה)}
|
||||||
|
skip(ערר/בל"מ) → missing_precedent (נבו ידני) — לא אחזור
|
||||||
|
── ניתוב לפי זמינות-פורמט-נט, לא לפי קידומת (נט המשפט משרת כל הערכאות) ──
|
||||||
|
פורמט-נט קיים (עמ"נ/עת"מ/עליון-בפורמט-נט כמו בר"מ 72182-06-25)
|
||||||
|
→ Tier 1: legal-court-fetch-service (host/pm2 + Xvfb) — אוטונומי, מאומת
|
||||||
|
→ Camoufox(python) → external-search → CaseDetails → פסקי דין
|
||||||
|
→ NGCSViewerPage → GetImages(X-Requested-With) → PNGs → PDF
|
||||||
|
עליון סדרתי-בלבד (בג"ץ/בר"מ NNNN/YY, בלי חודש)
|
||||||
|
→ Tier 0: httpx → supremedecisions (SearchVerdicts+Download) — מפוענח ומאומת
|
||||||
|
כשל אוטונומי → Tier 2: missing_precedent + התראה (VNC עתידי) — שער-אנושי
|
||||||
|
(כל ה-tiers) → precedent_library_upload(source_type=court_ruling) → ingest_precedent
|
||||||
|
→ chunks+embeddings+halachot(pending) → relink digest / close gap
|
||||||
|
```
|
||||||
|
|
||||||
|
מצב-העבודה מנוהל בטבלת-תור `court_fetch_jobs` (idempotent, נצפה, retryable). הניקוז
|
||||||
|
האוטומטי: `legal-court-fetch-drain` (pm2 cron שעתי) → `orchestrator.drain_pending`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Invariants
|
||||||
|
|
||||||
|
### INV-CF1: מסלול-קליטה יחיד — אין ingest מקביל
|
||||||
|
**כלל:** כל ה-tiers מתנקזים ל**צינור-הקליטה הקנוני היחיד** (`precedent_library_upload` →
|
||||||
|
`ingest_precedent`). המאחזר מספק קובץ+מטא בלבד; אסור לו לכתוב `case_law`/`precedent_chunks`/
|
||||||
|
`halachot` ישירות או לשכפל לוגיקת-chunking/embedding.
|
||||||
|
**מקור-סמכות:** פרויקטלי-תפעולי — מיישם את [G2](00-constitution.md#inv-g2) (מקור-אמת יחיד, אין מסלול מקביל) על תת-מערכת זו.
|
||||||
|
**אכיפה:** האורקסטרטור קורא רק ל-API/שירות-הקליטה הקיים; ביקורת-ארכיטקטורה ב-PR.
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-CF2: אין בליעה שקטה — כל אחזור נצפה
|
||||||
|
**כלל:** לכל פסק-דין שזוהה לאחזור יש רשומת-job עם סטטוס סופי מפורש
|
||||||
|
(`done`/`failed`/`manual`). כישלון-אחזור **לעולם אינו נבלע** — הוא מסומן ומועלה (Tier 2),
|
||||||
|
לא נזרק בשקט. `except: pass` אסור.
|
||||||
|
**מקור-סמכות:** פרויקטלי-תפעולי — מיישם את [G4](00-constitution.md#inv-g4) וכלל-ההנדסה "אין בליעה שקטה" (§6).
|
||||||
|
**אכיפה:** טבלת `court_fetch_jobs` (status+error+attempts) + לוג-warning בכל כישלון + Tier-2 gate.
|
||||||
|
**הפרה ידועה:** ~~הפער ב-X12 — `try_autolink` שנכשל מחזיר `None` בשקט~~ → **תוקן**: `try_autolink` שנכשל על ציטוט פס"ד-בימ"ש מזניק job ל-`court_fetch_jobs` (status=pending); `court_fetch_drain` מנקז (סדרתי) ומקשר את היומון חזרה בהצלחה.
|
||||||
|
|
||||||
|
### INV-CF3: אוטונומי-first, שער-אנושי חובה ב-fallback
|
||||||
|
**כלל:** האחזור מנסה אוטונומית; אך כש-N נסיונות נכשלים, **שער-אנושי** (VNC לפתרון-CAPTCHA
|
||||||
|
חי / סימון missing_precedent + התראה) הוא **חובה, לא רשות**. המערכת אינה "מוותרת" ואינה
|
||||||
|
"מסתירה" — היא מסלימה לאדם.
|
||||||
|
**מקור-סמכות:** פרויקטלי-תפעולי — מיישם את [G10](00-constitution.md#inv-g10) (המערכת מסייעת; שערים אנושיים = invariant).
|
||||||
|
**אכיפה:** מונה-נסיונות בטבלת-התור + מעבר אוטומטי ל-status=`manual` עם נתיב-פעולה ל-chaim.
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-CF4: אחזור-אחראי (politeness) — סדרתי, מרווח, חתימה-אמיתית
|
||||||
|
**כלל:** האחזור מאתר-ממשלתי הוא **אחראי**: סדרתי (לא מקבילי), עם cooldown בין בקשות,
|
||||||
|
כיבוד-`robots`/תנאי-שימוש, ו-rate מתון. אסור flooding/parallel-hammering שעלול לחסום IP
|
||||||
|
או להעמיס על שירות ציבורי.
|
||||||
|
**מקורות:** RFC 9309 (*Robots Exclusion Protocol*, IETF 2022) · Google Search Central —
|
||||||
|
*Crawler / crawl-rate guidance* · OWASP — *Automated Threat Handbook* (OAT-021 Denial of
|
||||||
|
Service / responsible automation) | סטטוס: verified
|
||||||
|
**אכיפה:** האורקסטרטור והשירות אוכפים serial + `INTER_FETCH_COOLDOWN_SEC`; Camoufox מספק
|
||||||
|
חתימת-דפדפן אמיתית (לא spoof-חמדני). מראה לדפוס-התור ב-[`precedent_library.py`](../../mcp-server/src/legal_mcp/services/precedent_library.py).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-CF5: אחזור idempotent
|
||||||
|
**כלל:** אחזור הוא **idempotent** — מפתח-job דטרמיניסטי לפי `case_number` מנורמל. אחזור
|
||||||
|
חוזר של אותו תיק אינו יוצר job כפול ואינו קולט פסק-דין פעמיים (upsert על המפתח הקנוני).
|
||||||
|
**מקור-סמכות:** פרויקטלי-תפעולי — מיישם את [G3](00-constitution.md#inv-g3) (ingest idempotent) ו-[G1](00-constitution.md#inv-g1) (מזהה מנורמל בכתיבה).
|
||||||
|
**אכיפה:** אילוץ-ייחודיות על `court_fetch_jobs.case_number_norm`; הקליטה עצמה idempotent דרך `ingest_precedent`.
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-CF6: שער-סיווג מקור — רק פסקי-דין של בתי-משפט
|
||||||
|
**כלל:** רק ציטוט שסווג כ**פסק-דין של בית-משפט** נשלח לאחזור. **ועדת-ערר (ערר/בל"מ) לעולם
|
||||||
|
אינה נשלחת לאחזור-ציבורי** (נדרש נבו) — היא מסומנת `missing_precedent` בלבד. הפריט הנקלט
|
||||||
|
נושא `source_type=court_ruling`, `source_kind=external_upload`, `precedent_level` לפי הערכאה.
|
||||||
|
**מקור-סמכות:** פרויקטלי-תפעולי — מיישם את [G5](00-constitution.md#inv-g5) (metadata מלא + הפרדת-קורפוס)
|
||||||
|
ותואם את הבחנת-המקור ב-[01-ingest](01-ingest.md) (`court_ruling` מול `appeals_committee`).
|
||||||
|
**אכיפה:** המסווג מחזיר `tier=skip` ל-ערר/בל"מ; הקליטה אוכפת `source_type`.
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-CF7: עקיבוּת-מקור + גבול-ToS
|
||||||
|
**כלל:** כל אחזור נושם **provenance** מלא (`source_url`, tier, זמן, מזהה-job) ב-audit-trail.
|
||||||
|
האחזור מוגבל ל**מסמכים ציבוריים** הזמינים ללא הזדהות (smart-card); אופי המערכת הוא
|
||||||
|
**הורדה-בסיוע** (עם שער-אנושי), לא בוט-סמוי לעקיפת בקרת-גישה.
|
||||||
|
**מקור-סמכות:** פרויקטלי-תפעולי — מיישם את [G9](00-constitution.md#inv-g9) (עקיבוּת + audit-trail);
|
||||||
|
גבול-ה-ToS מועלה ליו"ר (חיים) כשיקול-מדיניות (עיקרון-עבודה 4: המשתמש הוא הסמכות).
|
||||||
|
**אכיפה:** `source_url`+tier נשמרים על `case_law`/`court_fetch_jobs`; שער-אנושי שומר על אופי בסיוע.
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. מודל-נתונים — `court_fetch_jobs`
|
||||||
|
|
||||||
|
| עמודה | טיפוס | תפקיד |
|
||||||
|
|--------|-------|-------|
|
||||||
|
| `id` | UUID PK | מזהה-job |
|
||||||
|
| `case_number_norm` | TEXT UNIQUE | מפתח-idempotency קנוני (INV-CF5) |
|
||||||
|
| `citation_raw` | TEXT | הציטוט המקורי כפי שזוהה |
|
||||||
|
| `tier` | TEXT | `supreme` \| `admin` \| `skip` |
|
||||||
|
| `court` | TEXT | ערכאה שזוהתה |
|
||||||
|
| `status` | TEXT | `pending` \| `running` \| `done` \| `failed` \| `manual` |
|
||||||
|
| `attempts` | INT | מונה-נסיונות (ל-Tier 2 gate, INV-CF3) |
|
||||||
|
| `error` | TEXT | הודעת-כישלון אחרונה (INV-CF2) |
|
||||||
|
| `case_law_id` | UUID FK | הפסק שנקלט (NULL עד done) |
|
||||||
|
| `digest_id` | UUID FK | היומון-מקור (NULL לאד-הוק) |
|
||||||
|
| `source_url` | TEXT | provenance (INV-CF7) |
|
||||||
|
| `created_at` / `updated_at` | TIMESTAMPTZ | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. רכיבי-מימוש (מיפוי לקוד)
|
||||||
|
|
||||||
|
| רכיב | קובץ | מקור-תבנית / שימוש-חוזר |
|
||||||
|
|------|------|------------------------|
|
||||||
|
| מסווג | `mcp-server/.../services/court_citation.py` | regex מ-`citation_extractor.py:67-132` |
|
||||||
|
| Tier 0 | `services/court_fetch_supreme.py` | httpx; דפוס-cooldown מ-`precedent_library.py:176-186` |
|
||||||
|
| Tier 1 שירות | `mcp-server/.../court_fetch_service/server.py` | שכפול `chat_service/server.py` (aiohttp+Bearer+bind 10.0.1.1) |
|
||||||
|
| Camoufox client | `court_fetch_service/camofox_client.py` | חיקוי `~/.hermes/.../browser_camofox.py` |
|
||||||
|
| reCAPTCHA audio | `court_fetch_service/recaptcha_audio.py` | faster-whisper מקומי |
|
||||||
|
| proxy בקונטיינר | `web/court_fetch_proxy.py` | שכפול `web/chat_proxy.py` |
|
||||||
|
| pm2 | `scripts/legal-court-fetch-service.config.cjs` | שכפול `legal-chat-service.config.cjs` |
|
||||||
|
| אורקסטרטור+תור | `services/court_fetch_orchestrator.py` + `db.py` (SCHEMA_Vxx) | דפוס-תור קיים |
|
||||||
|
| כלי-MCP | `tools/court_fetch.py` (`court_verdict_fetch` / `court_fetch_status` / `court_fetch_drain`) | חוזה-envelope [X9](X9-mcp-tool-contract.md) |
|
||||||
|
| טריגר אוטומטי | `services/digest_library.py` (`try_autolink` fail → `_enqueue_court_fetch`) → drain ע"י `orchestrator.drain_pending` | X12 |
|
||||||
|
| סוד | `COURT_FETCH_SHARED_SECRET` (Infisical + Coolify) | דפוס `LEGAL_CHAT_SHARED_SECRET`, [X10](X10-deploy-env-secrets.md) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. סיכונים (R&D — לעקוב)
|
||||||
|
- reCAPTCHA נלחם פעיל בפותרי-אודיו → שיעור-כישלון אפשרי גבוה → Tier 2 הוא קו-ההגנה (INV-CF3).
|
||||||
|
- F5/anti-bot עלול לחסום IP → politeness סדרתי + Camoufox (INV-CF4).
|
||||||
|
- שבירות מול שינויי-אתר → ריכוז selectors במקום אחד + בדיקות-עשן תקופתיות.
|
||||||
|
- גבול-ToS על אתר .gov → INV-CF7 + שיקול-יו"ר.
|
||||||
|
- ~~**Tier-0 (supremedecisions) טרם מפוענח**~~ → **פוענח ומאומת (2026-06-08)** — עליון בפורמט-סדרתי
|
||||||
|
(בג"ץ/בר"מ NNNN/YY) יורד אוטומטית דרך `Home/SearchVerdicts`+`Home/Download`. מגבלה שנותרה: תיקים
|
||||||
|
ישנים-מאוד שלא דיגיטצו בפורטל (0 רשומות) → `manual`. גם `backfill_missing_precedents.py` מזין את
|
||||||
|
ה-`missing_precedents` הפתוחים (עליון+נט-format) לתור-האחזור.
|
||||||
|
- **דליפת-זיכרון מדפדפנים יתומים** (fetch שנתקע/נהרג משאיר `camoufox-bin`) → שלוש שכבות-הגנה:
|
||||||
|
(א) `async with` סוגר את הדפדפן בכל exception; (ב) `asyncio.wait_for` קשיח (`COURT_FETCH_HARD_TIMEOUT_S`, ברירת-מחדל 180ש') מבטל hang + reap; (ג) reaper של `camoufox-bin` יתומים (`ppid=1`) לפני/אחרי כל fetch + דמון `legal-reaper` (pm2) + תקרת `max_memory_restart`. סדרתיות (INV-CF4) מבטיחה שכל דפדפן `ppid=1` הוא שארית בטוחה-להריגה. **הערה:** הדליפה הגדולה בפועל בשרת היא `task-master-mcp` (כלי נפרד), שגם אותו ה-reaper מנקה.
|
||||||
146
docs/spec/X14-storage-minio.md
Normal file
146
docs/spec/X14-storage-minio.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# X14 — אחסון-אובייקטים (Object Storage: MinIO / S3)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) והוא ה-deep-dive על **אחסון קבצים בינאריים** —
|
||||||
|
מסמכי-מקור, נגזרים, וייצוא — והגירתם ממערכת-קבצים מקומית (`data/`) ל-**MinIO** (object store תואם-S3).
|
||||||
|
הוא מגדיר את חוזה-האחסון (שכבה יחידה), סכמת-הדליות-והמפתחות, מודל-האי-שינויוּת המשפטי, ותוכנית-ההגירה.
|
||||||
|
|
||||||
|
> **invariant הנדסי + תפעולי-משפטי.** INV-STG1/2/5/6 נשענים על עקרונות מוכרים (S3 API, 12-Factor, presigned-URL,
|
||||||
|
> separation blob↔metadata) — ≥3 מקורות (docs.min.io, AWS S3 spec, minio-py). INV-STG3/4/7 הם תפעוליים/משפטיים
|
||||||
|
> של *מערכת זו* (גבול-ממשל, WORM להחלטות חתומות, git=טקסט) ונקשרים ל-[G2](00-constitution.md) (מסלול-אחסון יחיד).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. מצב קיים (מאומת מול הקוד וה-infra, 2026-06-08)
|
||||||
|
|
||||||
|
### 1.1 מלאי-הדיסק (`data/`, ללא `backups/`)
|
||||||
|
| קטגוריה | נפח | תוכן | סוג |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `data/cases/{case}/` | 1.2GB | `documents/{originals,extracted,proofread,research,backup}`, `drafts/`, `exports/`, `thumbnails/{doc_uuid}/pNNN.jpg`, `.git` per-case | מקור + נגזר |
|
||||||
|
| `data/digests/{reference,incoming}/` | 251MB | יומונים (X12) | מקור |
|
||||||
|
| `data/training/{cmp,cmpa}/{raw,proofread}/` | 157MB | קורפוס-קול + `.git` | מקור |
|
||||||
|
| `data/precedent-library/{appeals_committee,court_ruling,other}/` | 105MB | פסיקה + `thumbnails/` | מקור |
|
||||||
|
| `data/internal-decisions/{region}/` | 45MB | החלטות-פנים לפי מחוז | מקור |
|
||||||
|
| `data/exports/` | 216KB | legacy (הוחלף ב-per-case) | נגזר |
|
||||||
|
| `data/{audit,eval,logs}/` | ~52MB | CSV/JSON תפעוליים — **לא מסמכים, נשארים בדיסק** | תפעולי |
|
||||||
|
|
||||||
|
ספירה (ללא backups): ~9,449 קבצים — 2,473 JPG (thumbnails נגזרים), 883 PDF, 250 TXT (extracted), 155 DOCX, 54 DOC.
|
||||||
|
|
||||||
|
### 1.2 הקונטיינר (Coolify)
|
||||||
|
legal-ai (`gyjo0mtw2c42ej3xxvbz8zio`) רץ עם **bind-mounts**: host `data/`→`/data`, host `data/cases/`→`/cases`.
|
||||||
|
האחסון היום = תיקייה על המארח, חשופה ישירות.
|
||||||
|
|
||||||
|
### 1.3 MinIO — **כבר פרוס ובריא** ✅ (שירות Coolify `minio`, `bx2ykvw94xbutsex41hz4vv8`, 2026-06-08)
|
||||||
|
- **API:** `https://s3.nautilus.marcusgroup.org` (9000) · **Console:** `https://minio.nautilus.marcusgroup.org` (9001)
|
||||||
|
- **Credentials:** `SERVICE_USER_MINIO` / `SERVICE_PASSWORD_MINIO` (סודות מנוהלי-Coolify)
|
||||||
|
- **אחסון:** named-volume `minio-data`→`/data` — **Single-Node Single-Drive**; versioning/object-lock **לא** מופעלים עדיין
|
||||||
|
- **רשת:** רשת-Docker משלו (`bx2ykvw...`, external), **לא** משותפת ל-legal-ai → דרושה קישוריות (§4 שלב 0)
|
||||||
|
|
||||||
|
### 1.4 הקוד — **אין שכבת-אחסון מרכזית** (כשל-השורש שהתחום מייבש)
|
||||||
|
ה-I/O מפוזר על ~8 שירותים, נתיבים נבנים inline:
|
||||||
|
- העלאה: `tools/documents.py:54` (originals), `:152` (training)
|
||||||
|
- חילוץ + thumbnails: `services/processor.py:43,153`
|
||||||
|
- staging פסיקה/יומונים/החלטות: `services/ingest.py:69`
|
||||||
|
- ייצוא DOCX: `services/docx_exporter.py:462`
|
||||||
|
- הגשה (FileResponse): `web/app.py` — 6 endpoints
|
||||||
|
- git per-case: `services/git_sync.py` (`git add .` + push ל-Gitea, sweep כל 30ש׳)
|
||||||
|
|
||||||
|
### 1.5 עמודות-DB המאחסנות נתיבים (schema inline ב-`db.py`, ללא migrations)
|
||||||
|
`documents.file_path` · `cases.active_draft_path` · `case_law.source_document_path` · `digests.source_document_path`
|
||||||
|
· `document_image_pages.image_thumbnail_path` · `precedent_image_pages.image_thumbnail_path` · `draft_final_pairs.final_path`
|
||||||
|
|
||||||
|
### 1.6 Paperclip — צרכן-API בלבד
|
||||||
|
הפלאגין ניגש דרך `listDocuments`/`getDocumentText` ל-API (`plugin-legal-ai/src/legal-api.ts:89`). אינו נוגע בדיסק →
|
||||||
|
**הגירה שקופה אליו** כל עוד ה-API יציב.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-STG1: שכבת-אחסון יחידה — כל I/O דרך `storage.py`
|
||||||
|
**כלל:** קיים מודול-אחסון **יחיד** (`services/storage.py`) שכל קריאה/כתיבה של קובץ בינארי עוברת דרכו
|
||||||
|
(`put/get/presign_get/presign_put/delete/list`). אסור `open()`/`shutil.copy()`/`Path.write_bytes()` ישיר על
|
||||||
|
נתיב-אחסון מחוץ למודול. **מקיים [G2](00-constitution.md)** — מבטל את ה-I/O המפוזר (§1.4) שהוא מסלול-מקביל-מתפצל.
|
||||||
|
|
||||||
|
### INV-STG2: מפתח-אובייקט אטומי; שם עברי במטא בלבד
|
||||||
|
**כלל:** מפתח-האובייקט הוא ASCII/UUID (`cases/{case}/originals/{uuid}.pdf`). שם-הקובץ העברי המקורי נשמר ב-DB
|
||||||
|
(`*_filename`) וכ-`x-amz-meta-filename` + מוגש דרך `Content-Disposition` ב-presigned-GET. **למה:** תקציב-מפתח
|
||||||
|
1024 bytes (255/segment), עברית=2B/תו, ובעיות percent-encoding/XML — נמנעות.
|
||||||
|
|
||||||
|
### INV-STG3: דליות לפי גבול-ממשל, prefix לפי קטגוריה/תיק
|
||||||
|
**כלל:** versioning/object-lock/replication הם per-bucket → מה שדורש ממשל שונה יושב בדלי נפרד. שלוש דליות
|
||||||
|
קבועות (§3.1); תיקים/קטגוריות הם prefixes, **לא** דלי-לכל-תיק.
|
||||||
|
|
||||||
|
### INV-STG4: "סופי" = WORM (Object-Lock COMPLIANCE)
|
||||||
|
**כלל:** החלטה חתומה/סופית נכתבת לדלי `legal-immutable` עם Object-Lock **COMPLIANCE** + versioning — בלתי-ניתנת
|
||||||
|
לשינוי/מחיקה ע"י איש (כולל root) עד תום-תקופת-השמירה. טיוטות חיות בדלי רגיל ו"מקודמות" (copy) לדלי-הסגור עם החתימה.
|
||||||
|
**(הכרעת-יו"ר 2026-06-08: סופי בלבד; מסמכי-מקור — versioning ללא נעילה קשיחה.)**
|
||||||
|
|
||||||
|
### INV-STG5: pgvector נשאר מקור-האמת לטקסט/embeddings; MinIO = blob בלבד
|
||||||
|
**כלל:** טקסט-מחולץ + embeddings נשארים ב-Postgres/pgvector (מקור-אמת לאחזור). MinIO מאחסן את ה-blob המקורי
|
||||||
|
(+עותק-ארכיון אופציונלי של ה-extracted text). **אסור** ש-MinIO יהיה מקור-אמת לוקטורים. תואם
|
||||||
|
`no-reocr-retrofit` — לא מריצים OCR מחדש בהגירה.
|
||||||
|
|
||||||
|
### INV-STG6: הגשה לדפדפן דרך presigned-URL — bytes לא דרך FastAPI
|
||||||
|
**כלל:** הורדה/תצוגה/העלאה מהדפדפן עוברות ב-presigned-URL (TTL דקות) מול `s3.nautilus.marcusgroup.org`.
|
||||||
|
ה-backend מנפיק את ה-URL בלבד; ה-bytes לא עוברים דרכו. endpoints קיימים שמחזירים FileResponse → 302→presigned.
|
||||||
|
|
||||||
|
### INV-STG7: git-per-case שומר טקסט/מטא בלבד; בינאריים ב-MinIO
|
||||||
|
**כלל:** `.git` per-case ממשיך לגרסן `case.json`/`notes.md`/`documents/extracted/*.txt`/`research/*.md`. PDF/DOCX/JPG
|
||||||
|
מוחרגים מ-tracking (`.gitignore` per-case) ויושבים ב-MinIO. **(הכרעת-יו"ר 2026-06-08.)** `git_sync.py` ו-sweep
|
||||||
|
מסתמכים על אותו working-tree → ההחרגה חייבת לקדום לכל קומיט-הגירה כדי לא לשבור היסטוריה.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. ארכיטקטורת-היעד
|
||||||
|
|
||||||
|
### 3.1 דליות ומפתחות
|
||||||
|
| דלי | Versioning | Object-Lock | prefixes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `legal-documents` | ✅ | ❌ | `cases/{case}/originals/{uuid}.pdf` · `cases/{case}/proofread/{uuid}.txt` · `precedent-library/{type}/{uuid}.pdf` · `internal-decisions/{region}/{uuid}.pdf` · `digests/{uuid}.pdf` · `training/{cmp\|cmpa}/{raw\|proofread}/{uuid}.pdf` |
|
||||||
|
| `legal-immutable` | ✅ | ✅ COMPLIANCE | `decisions-final/{case}/{uuid}.docx` (החלטות חתומות בלבד) |
|
||||||
|
| `legal-derived` | ❌ | ❌ (+lifecycle) | `thumbnails/{doc_uuid}/pNNN.jpg` · `extracted/{uuid}.txt` (נגזר, ניתן-לשחזור) |
|
||||||
|
|
||||||
|
### 3.2 `services/storage.py` (לב ההגירה) — adapter כפול
|
||||||
|
```
|
||||||
|
put(category, key, data, content_type, meta) -> uri # category→bucket+prefix
|
||||||
|
get(uri) -> bytes
|
||||||
|
presign_get(key, ttl) / presign_put(key, ttl) -> url
|
||||||
|
delete(key) / list(prefix)
|
||||||
|
```
|
||||||
|
backend נבחר ב-env `STORAGE_BACKEND ∈ {filesystem, dual, s3}` (ברירת-מחדל filesystem) — מאפשר מעבר הדרגתי ללא
|
||||||
|
שינוי-התנהגות. SDK: `aioboto3` (async-native מול `endpoint_url=http://minio:9000`); `minio-py` לסקריפטי-הגירה.
|
||||||
|
|
||||||
|
### 3.3 שינויי-DB
|
||||||
|
הוספת `*_object_key` (או נרמול ל-`storage_uri` עם סכמה `s3://`/`file://`) לצד העמודות הקיימות (§1.5); backfill;
|
||||||
|
דה-קומיישן הנתיב-קובץ. תוספת inline ב-`db.py` בסגנון הקיים (אין migrations).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. תוכנית-ביצוע בשלבים (→ TaskMaster, tag legal-ai)
|
||||||
|
|
||||||
|
| שלב | תוכן | תלות |
|
||||||
|
|---|---|---|
|
||||||
|
| **0 — תשתית** | חיבור רשת-Docker (minio↔legal-ai); הזרקת credentials ל-env legal-ai (Coolify); `mc alias`; יצירת 3 דליות + הפעלת versioning + Object-Lock (immutable); הוספת `aioboto3` ל-deps | — |
|
||||||
|
| **1 — שכבת-אחסון** | `services/storage.py` + adapter כפול (default filesystem). אפס שינוי-התנהגות. PR מצהיר INV-STG1/2/3 | 0 |
|
||||||
|
| **2 — חיווט-כתיבה** | הפניית כל נקודות-הכתיבה (§1.4) דרך `storage.py`; כתיבה-כפולה (`STORAGE_BACKEND=dual`) | 1 |
|
||||||
|
| **3 — הגירת-נתונים** | `mc mirror --dry-run`→`--overwrite` של 5 הקטגוריות; backfill `*_object_key` ב-DB; אימות count+checksum | 0,2 |
|
||||||
|
| **4 — חיווט-קריאה + presigned** | endpoints→302→presigned; thumbnails דרך presigned; dual-read (S3, fallback disk); החרגת בינאריים מ-git per-case (INV-STG7) | 2,3 |
|
||||||
|
| **5 — cutover** | `STORAGE_BACKEND=s3`; `mc mirror --watch` עד החלפה; אימות מלא; כיבוי כתיבה-לדיסק | 4 |
|
||||||
|
| **6 — git + גיבוי + ניקוי** | קידום-החלטות-סופיות ל-immutable (INV-STG4); `mc mirror`/bucket-replication מתוזמן off-site; דה-קומיישן bind-mount `data/` (השארת audit/eval/logs) | 5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. סיכונים
|
||||||
|
- **I/O מפוזר** → INV-STG1 (`storage.py`) חובה לפני כל שאר השלבים, אחרת drift והפרת-G2.
|
||||||
|
- **שמות עבריים כמפתחות** → INV-STG2 (UUID-keys + מטא).
|
||||||
|
- **רשת נפרדת ל-MinIO** → לאמת קישוריות בשלב 0 לפני הכל.
|
||||||
|
- **git-per-case** מצמיד בינאריים ל-Gitea → INV-STG7, ההחרגה חייבת לקדום לכל קומיט.
|
||||||
|
- **SNSD ללא erasure-coding** → גיבוי off-site (שלב 6) הוא חובה, לא nice-to-have.
|
||||||
|
- **בידוד-worktree + ספ-first** → כל PR מצהיר invariants (G2 + INV-STG*).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. קישורים
|
||||||
|
- חוקה: [00-constitution.md](00-constitution.md) · נתונים: [02-data-model.md](02-data-model.md) · קליטה: [01-ingest.md](01-ingest.md)
|
||||||
|
- deploy/env: [X10-deploy-env-secrets.md](X10-deploy-env-secrets.md) · אינטגרציה: [X3-integration-deploy.md](X3-integration-deploy.md)
|
||||||
|
- מקורות-MinIO: docs.min.io (community), AWS S3 object-keys/bucket-naming/presigned-URL, github.com/minio/minio-py
|
||||||
149
docs/spec/X15-agent-platform-port.md
Normal file
149
docs/spec/X15-agent-platform-port.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# X15 — שער-הפלטפורמה (Agent Platform Port)
|
||||||
|
|
||||||
|
> כפוף ל-[00-constitution.md](00-constitution.md). מיישם ומחזק את **INV-G2** (מקור-אמת
|
||||||
|
> יחיד — אין מסלולים מקבילים) ברובד הקַשירה (coupling) בין שכבת-האינטליגנציה לפלטפורמת-הסוכנים.
|
||||||
|
|
||||||
|
## 0. למה המסמך הזה קיים
|
||||||
|
|
||||||
|
פלטפורמת-הסוכנים שלנו היום היא **Paperclip**. היא אינה ליבת-המערכת — היא ה**מעטפת**
|
||||||
|
(לוח-issues, סוכנים מתמידים, human-in-the-loop דרך comments, wakeup/heartbeat, תזמון,
|
||||||
|
תקציבים per-agent, adapters). ליבת-האינטליגנציה — `mcp-server/src`, ה-skills של
|
||||||
|
ההחלטה/הסגנון, ולוגיקת-ההחלטה — היא הנכס שאינו תלוי-פלטפורמה.
|
||||||
|
|
||||||
|
**כשל-השורש שהמסמך מייבש:** מגע עם Paperclip שדולף לתוך שכבת-האינטליגנציה הופך את
|
||||||
|
המעטפת מ"רכיב ניתן-להחלפה מאחורי חוזה" ל"תלות-רוחב ארוגה בכל הקוד". ככל שהדליפה גדלה,
|
||||||
|
"החלפת המעטפת" (או אפילו שדרוג גרסה — ראו ההצמדה ל-opus-4-8) הופכת מ**החלפת-רכיב**
|
||||||
|
ל**כתיבה-מחדש**. זוהי הופעה נוספת של כשל-השורש שכל הספ בא לייבש: מסלולים מקבילים
|
||||||
|
שמתפצלים (drift), הפעם בציר התלות בין שכבות.
|
||||||
|
|
||||||
|
הבסיס התאורטי: **Ports & Adapters / Hexagonal Architecture** (Alistair Cockburn),
|
||||||
|
**The Dependency Rule / Clean Architecture** (Robert C. Martin), **Anti-Corruption
|
||||||
|
Layer** (Eric Evans, DDD). כולם אומרים את אותו הדבר: התלות זורמת פנימה בלבד; הליבה
|
||||||
|
אינה יודעת על העולם החיצון; כל מגע עם מערכת-חוץ עובר דרך שכבת-תרגום אחת (port/adapter).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. השכבות והתפר
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ INTELLIGENCE (תלוי-פלטפורמה = אסור) │
|
||||||
|
│ mcp-server/src · skills/decision · skills/style · decision logic │
|
||||||
|
│ · style-acquisition │
|
||||||
|
│ ── חייב להכיל אפס סמלים ספציפיים-Paperclip ── │
|
||||||
|
└───────────────────────────────┬────────────────────────────────────┘
|
||||||
|
│ ה-PORT (שכבת-התרגום היחידה)
|
||||||
|
│ • web/agent_platform_port.py (Python)
|
||||||
|
│ • .claude/agents/HEARTBEAT.md (פרומפטים)
|
||||||
|
┌───────────────────────────────┴────────────────────────────────────┐
|
||||||
|
│ SHELL (Paperclip-specific — מותר ומוצהר) │
|
||||||
|
│ web/paperclip_client.py · web/paperclip_api.py · plugin-legal-ai │
|
||||||
|
│ · adapters/* · web-ui settings/paperclip-tab · skills/new-company │
|
||||||
|
└───────────────────────────────┬────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────┴─────┐
|
||||||
|
│ Paperclip │ ← הפלטפורמה. ניתנת-להחלפה.
|
||||||
|
└───────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**הגדרת-ה-Port:** קבוצת-הקבצים היחידה שמורשית לדבר Paperclip:
|
||||||
|
|
||||||
|
| Port surface | תפקיד | מורשה לייבא/להזכיר Paperclip |
|
||||||
|
|--------------|-------|------------------------------|
|
||||||
|
| `web/agent_platform_port.py` *(לבנייה — R2)* | תרגום אירועי-דומיין → קריאות-פלטפורמה | כן — המודול היחיד שמייבא `paperclip_client`/`paperclip_api` |
|
||||||
|
| `web/paperclip_client.py`, `web/paperclip_api.py` | מימוש-הלקוח (מאחורי ה-Port) | כן (זו המעטפת המתוכננת) |
|
||||||
|
| `.claude/agents/HEARTBEAT.md` | מקור-אמת יחיד לפרוטוקול-הריצה של הסוכנים | כן |
|
||||||
|
| `plugin-legal-ai/*`, `adapters/*` | הגשר מצד-Paperclip | כן |
|
||||||
|
| `web-ui` settings/paperclip-tab, agents-tab | UI לניהול-Paperclip עצמו | כן (מוצהר) |
|
||||||
|
| `skills/new-company-setup/SKILL.md` | blueprint-הקמה (חייב לדבר Paperclip) | כן — **חריג מוצהר** |
|
||||||
|
|
||||||
|
כל קובץ אחר — בפרט תחת `mcp-server/src`, `skills/decision`, `skills/style`,
|
||||||
|
ופרומפטי-הסוכנים פרט ל-HEARTBEAT — **אסור** שיכיל סמל ספציפי-Paperclip.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. ה-invariant
|
||||||
|
|
||||||
|
### INV-PORT1 (גלובלי: G12) — שער-הפלטפורמה
|
||||||
|
**כלל:** פלטפורמת-הסוכנים (Paperclip) נגישה אך-ורק דרך ה-Platform Port
|
||||||
|
(`web/agent_platform_port.py` + `HEARTBEAT.md` לפרומפטים). שכבת-האינטליגנציה —
|
||||||
|
`mcp-server/src`, וה-skills של ההחלטה/הסגנון — מכילה **אפס** סמלים ספציפיים-לפלטפורמה
|
||||||
|
(שמות-מוצר, wakeup/heartbeat, pc.sh/pc_request, X-Paperclip-Run-Id, enums של הפלטפורמה).
|
||||||
|
פרומפטי-הסוכנים אינם משכפלים את פרוטוקול-הריצה — הם מצביעים ל-HEARTBEAT.md בלבד. כל מגע
|
||||||
|
חדש עם הפלטפורמה עובר דרך ה-Port.
|
||||||
|
**מקורות:** Alistair Cockburn, *Hexagonal Architecture (Ports & Adapters)* · Robert C.
|
||||||
|
Martin, *Clean Architecture* (The Dependency Rule) · Eric Evans, *Domain-Driven Design*
|
||||||
|
(Anti-Corruption Layer) | סטטוס: verified
|
||||||
|
**אכיפה:** (א) ביקורת-ארכיטקטורה + רשימת-ה-Port (§1); (ב) leak-guard אוטומטי — הרחבת
|
||||||
|
[scripts/spec-guard.sh](../../scripts/spec-guard.sh) שמשווה מול baseline-הדליפה (§4) ומזהיר
|
||||||
|
על דליפה חדשה ב-Edit/Write; (ג) fitness-test ב-CI שנכשל על מונח-Paperclip קשיח חדש תחת
|
||||||
|
`mcp-server/src`; (ד) הצהרת-G12 בתבנית-ה-PR.
|
||||||
|
**הפרה ידועה:** ראו מצאי-הדליפה ב-§3 — `web/app.py` קורא ל-`pc_*` inline בלוגיקת
|
||||||
|
מחזור-חיים של תיקים; 10 פרומפטי-סוכנים משכפלים את פרוטוקול-הריצה במקום להצביע ל-HEARTBEAT.
|
||||||
|
|
||||||
|
> **סיווג:** invariant הנדסי (≥3 מקורות חיצוניים, verified). מורחב מ-G1–G10 בתור **G12**.
|
||||||
|
> רישומו ברשימת-הגלובליים ובאינדקס של [00-constitution.md](00-constitution.md) מתבצע במשימת
|
||||||
|
> R0b (תיקון-תיעוד) — עד אז המסמך הזה הוא מקור-האמת ל-G12.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. מצאי-הדליפה (baseline — נמדד 2026-06-09)
|
||||||
|
|
||||||
|
מבחן-נטישה: כמה השכבות חוצות את התפר. הספירה היא בסיס-ההשוואה ל-leak-guard.
|
||||||
|
|
||||||
|
| Layer | Paperclip hits | סיווג | מחיר-ניתוק |
|
||||||
|
|-------|----------------|-------|------------|
|
||||||
|
| `mcp-server/src` (כלים) | 5 — **הערות בלבד** | ✅ נקי (זה הנכס) | ~0 |
|
||||||
|
| `skills/` (decision/style) | 36 — רק `new-company-setup` | ✅ נקי (חריג מוצהר) | נמוך |
|
||||||
|
| `web/paperclip_client.py` | 116 | ✅ מעטפת מתוכננת | — |
|
||||||
|
| `web/paperclip_api.py` | 33 | ✅ מעטפת מתוכננת | — |
|
||||||
|
| `web/app.py` | ~33 קריאות `pc_*` + `PAPERCLIP_COMPANIES`×72 | ⚠️ דליפה מבנית (מחזור-חיים) | בינוני |
|
||||||
|
| `.claude/agents/*.md` | 288 — פרוטוקול משוכפל ב-10 פרומפטים | ⚠️⚠️ דליפה מכנית | גבוה (בנפח) |
|
||||||
|
| `web-ui` (`types.ts`×41, `cases.ts`, `sse.ts`, ...) | ~60 | ⚠️ מושגי-פלטפורמה בחוזי-פרונט | בינוני |
|
||||||
|
|
||||||
|
**הממצא המרכזי:** שכבת-האינטליגנציה (`mcp-server/src` + skills של ההחלטה/הסגנון) כבר
|
||||||
|
נקייה כמעט-לחלוטין — 5 ההיטים ב-mcp-server הם הערות בלבד (מקור `company_id`). מחיר-הגירושין
|
||||||
|
בינוני, מרוכז בשלוש שכבות-נושקות-למעטפת.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. מפת-התיקון (R-tasks)
|
||||||
|
|
||||||
|
| R | תחום | תיאור | סיכון |
|
||||||
|
|---|------|-------|-------|
|
||||||
|
| **R0** | ספ | המסמך הזה — מגדיר את ה-Port, ה-invariant, ו-baseline-הדליפה | 0 |
|
||||||
|
| **R0b** | ספ | רישום G12 ב-[00-constitution.md](00-constitution.md) (רשימת-גלובליים + אינדקס) + שורת G12 בתבנית-ה-PR + מצביע ב-CLAUDE.md | 0 |
|
||||||
|
| **R1** | פרומפטים | כל פרוטוקול-הריצה עובר ל-HEARTBEAT.md (מקור יחיד); 10 הפרומפטים מצביעים אליו בלבד. 288→~20 היטים | נמוך |
|
||||||
|
| **R2** | web | יצירת `web/agent_platform_port.py` — המודול היחיד שמייבא `paperclip_client`/`paperclip_api`. `app.py` פולט אירוע-דומיין (`case_archived`/`created`/...) שה-Port מתרגם. `PAPERCLIP_COMPANIES`→`company_map` מאחורי ה-Port | בינוני |
|
||||||
|
| **R3** | web-ui | `types.ts` → namespace `paperclip.*` נפרד; חוזי case/api כלליים נשארים נקיים. טאבי-ניהול-Paperclip נשארים (מעטפת מוצהרת) | נמוך-בינוני |
|
||||||
|
| **R4** | אכיפה | הרחבת `spec-guard.sh` ל-leak-guard מול ה-baseline + fitness-test ב-CI על `mcp-server/src` | 0 |
|
||||||
|
|
||||||
|
**עיקרון-מנחה (G2):** R1+R2 הם G2 בלבוש חדש — מאחדים פרוטוקול/מסלול משוכפל למקור אחד.
|
||||||
|
הם אינם יוצרים מסלול מקביל; הם מסירים אחד.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. מנגנון נגד דליפה-עתידית
|
||||||
|
|
||||||
|
תיקון חד-פעמי חסר-ערך אם הדליפה תחזור בפיצ'ר הבא. שלוש שכבות-אכיפה, כולן מתחברות
|
||||||
|
למנגנונים קיימים (ולא ממציאות מסלול חדש):
|
||||||
|
|
||||||
|
1. **invariant (G12)** — מוגדר כאן, נרשם בחוקה (R0b). first-class, לא הערת-שוליים.
|
||||||
|
2. **אכיפה-אוטומטית** — `spec-guard.sh` כבר מיירט כל Edit/Write בנתיב-קוד; ה-leak-guard
|
||||||
|
(R4) משווה מול baseline §3 ומזהיר על דליפה חדשה **בזמן-אמת**, לפני ה-review.
|
||||||
|
3. **חוזה-תיעוד** — תבנית-ה-PR כבר דורשת הצהרת-invariants; נוסיף שורת-G12 לצ'קליסט
|
||||||
|
("□ לא הוספתי מגע-Paperclip מחוץ ל-Platform Port"). CLAUDE.md §Paperclip + §פרוטוקול
|
||||||
|
כתיבת-קוד מצביעים לכאן.
|
||||||
|
|
||||||
|
> **כלל-זהב לכל פיתוח עתידי:** פיצ'ר חדש שנוגע בפלטפורמה — מוסיף/משנה **רק** קוד תחת
|
||||||
|
> רשימת-ה-Port (§1). אם נדרש מגע-פלטפורמה משכבת-האינטליגנציה — זו אינדיקציה לתכנון
|
||||||
|
> שגוי: הוסיפו במקום זאת אירוע-דומיין שה-Port יתרגם.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. ראו גם
|
||||||
|
- [00-constitution.md](00-constitution.md) — G2 (שאותו מיישם), G12 (לאחר R0b).
|
||||||
|
- [X7-paperclip-client-params.md](X7-paperclip-client-params.md) — פרמטרי לקוח-Paperclip (מתחת ל-Port).
|
||||||
|
- [X4-agents.md](X4-agents.md) — מפת-הסוכנים.
|
||||||
|
- [X3-integration-deploy.md](X3-integration-deploy.md) — אינטגרציה+deploy.
|
||||||
|
- [X16-pipeline-durability.md](X16-pipeline-durability.md) — עמידות-פייפליין (החלטה נפרדת, נושקת).
|
||||||
96
docs/spec/X16-pipeline-durability.md
Normal file
96
docs/spec/X16-pipeline-durability.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# X16 — עמידות-פייפליין (Durable Pipeline Execution)
|
||||||
|
|
||||||
|
> כפוף ל-[00-constitution.md](00-constitution.md). מחזק את **INV-G3** (idempotency)
|
||||||
|
> ב-checkpointing+replay לפייפליינים הדטרמיניסטיים המקומיים. נושק ל-[07-learning.md](07-learning.md)
|
||||||
|
> ו-[X11-citation-corroboration.md](X11-citation-corroboration.md).
|
||||||
|
|
||||||
|
## 0. הבעיה
|
||||||
|
|
||||||
|
שני הפייפליינים המקומיים החד-פעמיים —
|
||||||
|
[final_halacha_pipeline.py](../../scripts/final_halacha_pipeline.py) (כפתור run-halacha,
|
||||||
|
אימות-הלכות, X11) ו-[final_learning_pipeline.py](../../scripts/final_learning_pipeline.py)
|
||||||
|
(כפתור run-learning, למידת-סגנון, 07-learning) — חולקים **צורה זהה**: סקריפט מקומי,
|
||||||
|
3–4 שלבים בטור, idempotent, פאנל-LLM ארוך בסוף (CSV-gated, "can take minutes").
|
||||||
|
|
||||||
|
היום הם **ליניאריים וחסרי-זיכרון**: קריסה באמצע (ניתוק ל-DeepSeek/Gemini, restart של
|
||||||
|
קונטיינר, OOM) → הרצה-מחדש מ-שלב 0. השלבים idempotent ולכן זה **בטוח**, אבל **משלמים שוב**:
|
||||||
|
מחלצים, בונים corroboration על כל הקורפוס, ושופטים מחדש הלכות שכבר נשפטו — דקות וקריאות-LLM
|
||||||
|
לפח.
|
||||||
|
|
||||||
|
**הקשר-סיכון אמיתי:** דליפת task-master (יתומים ppid=1, ~3GB) מסכנת OOM ל-Postgres
|
||||||
|
([project_taskmaster_mcp_memory_leak]). אם OOM הורג ריצת-פאנל ארוכה — היום מתחילים מאפס.
|
||||||
|
|
||||||
|
**הבחנה מ-idempotency:** idempotency = "בטוח להריץ שוב". durable execution = "בטוח להריץ
|
||||||
|
שוב **בלי לשלם שוב**". זה שכלול, לא תחליף.
|
||||||
|
|
||||||
|
## 1. ההכרעה
|
||||||
|
|
||||||
|
להטמיע **LangGraph כספרייה בתוך הסקריפט** (לא כפלטפורמה מחליפה ל-Paperclip): מנוע-העמידות
|
||||||
|
היחיד שהוא state-of-the-art ב-checkpointing+replay+time-travel, בשימוש כ-`import` בתוך
|
||||||
|
הסקריפט המקומי. Paperclip לא מושפע — הכפתור עדיין מעיר את Hermes שמריץ את אותו ה-CLI.
|
||||||
|
|
||||||
|
> **גבול-תחום מפורש (מתחבר ל-G12/X15):** LangGraph נכנס **רק** כמנוע-פנימי של הסקריפטים
|
||||||
|
> המקומיים. אסור להשתמש בו כתחליף-פלטפורמה או כ-orchestrator של הסוכנים — זה ייצור מסלול
|
||||||
|
> מקביל ל-Paperclip (הפרת G2) ויערבב עמידות עם פלטפורמה. HITL/ניתוב-יו"ר נשאר מאחורי
|
||||||
|
> ה-Port (ראו §4 Phase 3).
|
||||||
|
|
||||||
|
**מקורות:** Temporal — *Durable Execution* · Saga / workflow-checkpointing pattern ·
|
||||||
|
Martin Kleppmann, *DDIA* (idempotence & exactly-once) · LangGraph checkpointer/replay docs.
|
||||||
|
|
||||||
|
## 2. ה-invariant
|
||||||
|
|
||||||
|
### INV-DUR1 — עמידות לפייפליינים דטרמיניסטיים
|
||||||
|
**כלל:** פייפליין דטרמיניסטי רב-שלבי משמר את התקדמותו ב-checkpoint מתמיד אחרי כל שלב
|
||||||
|
שהושלם; הרצה-חוזרת של אותה יחידת-עבודה **מדלגת** על שלבים שכבר הושלמו ומתחילה מנקודת-הכשל
|
||||||
|
המדויקת. מימוש-העמידות הוא **משותף** לכל הפייפליינים (`scripts/_pipeline_runtime.py`) —
|
||||||
|
לא מימוש-לכל-סקריפט (G2). חוזה-הכניסה (ה-CLI) נשמר ללא-שינוי.
|
||||||
|
**מקורות:** Temporal (Durable Execution) · Kleppmann *DDIA* (exactly-once) · Saga pattern
|
||||||
|
(workflow checkpointing) | סטטוס: verified
|
||||||
|
**אכיפה:** `_pipeline_runtime.py` עם LangGraph + checkpointer; thread_id דטרמיניסטי
|
||||||
|
לכל יחידת-עבודה (תיק); בדיקת kill-and-resume שמאמתת ששלבים שהושלמו אינם רצים-מחדש.
|
||||||
|
**הפרה ידועה:** היום `final_halacha_pipeline.py` / `final_learning_pipeline.py` ליניאריים
|
||||||
|
— קריסה = הרצה-מחדש מלאה (חוזרים על extract/corroboration/panel).
|
||||||
|
|
||||||
|
## 3. ארכיטקטורה
|
||||||
|
|
||||||
|
```
|
||||||
|
scripts/_pipeline_runtime.py ← מודול-עמידות משותף יחיד (G2)
|
||||||
|
• build_graph(steps) StateGraph: node לכל שלב
|
||||||
|
• SqliteSaver data/checkpoints/<pipeline>.sqlite (לא Postgres המשותף)
|
||||||
|
• run(thread_id, resume) מדלג-אוטומטית על nodes ב-checkpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
**הכרעות-תכנון:**
|
||||||
|
|
||||||
|
1. **Checkpointer = SQLite (`langgraph-checkpoint-sqlite`), לא Postgres.** קובץ תחת
|
||||||
|
`data/checkpoints/`: מקומי (תואם "local-only"), פשוט, ו**נמנע מהאזהרה** ב-CLAUDE.md נגד
|
||||||
|
migrations מ-2 worktrees על Postgres המשותף (`localhost:5433`). PostgresSaver = אופציה
|
||||||
|
עתידית אם נדרש ריכוז/observability.
|
||||||
|
2. **`thread_id = f"<pipeline>:{case_number}"`.** הרצה-חוזרת של אותו תיק מזהה checkpoint
|
||||||
|
לא-גמור וממשיכה אוטומטית; תיק שהושלם = no-op. idempotency + דילוג-checkpoint מתחברים.
|
||||||
|
3. **גרעיניות (מדורגת):**
|
||||||
|
- **גס (P0/P1):** כל שלב = node. קריסה בין-שלבים → המשך מהשלב שנפל. הפאנל node יחיד
|
||||||
|
שרץ-מחדש — אך הוא כבר CSV-backed + idempotent (מדלג פנימית על מה שנשפט).
|
||||||
|
- **עדין (P2, אופציונלי):** פירוק הפאנל ל-map מעל ההלכות/הלקחים (LangGraph `Send`),
|
||||||
|
כל פריט = יחידת-checkpoint → resume תוך-פאנל בלי לשפוט מחדש ברמת-LLM. נשען על ה-CSV
|
||||||
|
הקיים כמקור "כבר-נשפט".
|
||||||
|
4. **סמנטיקת-כשל מפורשת.** היום הכל "non-fatal, continue". עם LangGraph: nodes "מייעצים"
|
||||||
|
(extract, corroboration) — catch+record-status וממשיכים; node "קריטי" (panel) — raise
|
||||||
|
בכשל-קשה → עצירה ב-checkpoint → resume.
|
||||||
|
5. **שימור-חוזה-הכניסה.** ה-CLI (`--case`/`--limit`/`--dry-run`) זהה; run-halacha/run-learning
|
||||||
|
→ Hermes → אותו `python ...pipeline.py --case X` לא משתנה. מוסיפים `--fresh`
|
||||||
|
(ברירת-מחדל: auto-resume אם יש checkpoint לא-גמור לתיק).
|
||||||
|
|
||||||
|
## 4. גלגול מדורג
|
||||||
|
|
||||||
|
| Phase | תחום | מאמץ |
|
||||||
|
|-------|------|------|
|
||||||
|
| **P0** | deps ל-`mcp-server/pyproject` (`langgraph` + `langgraph-checkpoint-sqlite`, venv מקומי בלבד → אפס השפעת-קונטיינר). `_pipeline_runtime.py` עם SqliteSaver. עטיפת 4 שלבי-halacha כ-nodes (גס). CLI זהה. test: kill אחרי [1] → resume → assert [0],[1] לא רצו שוב | ~1 יום |
|
||||||
|
| **P1** | אותו runtime על `final_learning_pipeline` (3 שלבים) — מימוש-עמידות אחד לשניהם (G2) | חצי יום |
|
||||||
|
| **P2** | (אופציונלי) פירוק-פאנל ל-map per-item — resume תוך-פאנל | 1–2 ימים |
|
||||||
|
| **P3** | (עתידי) LangGraph `interrupt()` ל-HITL של היו"ר (split→chair, INV-G10) — **רק מאחורי ה-Port** (X15/G12) | — |
|
||||||
|
|
||||||
|
## 5. ראו גם
|
||||||
|
- [07-learning.md](07-learning.md) · [X11-citation-corroboration.md](X11-citation-corroboration.md)
|
||||||
|
- [X15-agent-platform-port.md](X15-agent-platform-port.md) — הגבול מול הפלטפורמה (G12).
|
||||||
|
- [scripts/SCRIPTS.md](../../scripts/SCRIPTS.md) — הסקריפטים המושפעים.
|
||||||
157
docs/spec/X2-multi-company.md
Normal file
157
docs/spec/X2-multi-company.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# X2 — מודל רב-החברתי וכללי ה-Sync (Multi-Company & Sync)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) והוא ה-deep-dive על **המבנה הרב-חברתי**
|
||||||
|
של עוזר משפטי — שתי החברות (CMP/CMPA), 14 הסוכנים, ואיך שינוי-הגדרות מפושט מ-Master ל-Mirror.
|
||||||
|
הוא אוכף את [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) (מקור-אמת
|
||||||
|
יחיד — אין מסלולים מקבילים מתפצלים) בהקשר של תצורת-סוכנים: שתי החברות הן שתי העתקות של אותה
|
||||||
|
מערכת, ואסור להן להתפצל (drift).
|
||||||
|
|
||||||
|
> **invariant פרויקטלי-תפעולי.** ה-invariants כאן הם **עובדות על איך המערכת *הזו* מנוהלת**
|
||||||
|
> רב-חברתית — לא תאוריה הנדסית כללית ולא תוכן משפטי. אין סמכות חיצונית ל"איך מסנכרנים
|
||||||
|
> CMP↔CMPA"; לכן הם נושאים שדה `מקור-סמכות` = הראנבוקים והקוד של הפרויקט עצמו ([CLAUDE.md](../../CLAUDE.md),
|
||||||
|
> [HEARTBEAT.md](../../.claude/agents/HEARTBEAT.md), [scripts/sync_agents_across_companies.py](../../scripts/sync_agents_across_companies.py))
|
||||||
|
> — **לא** ≥3 מקורות חיצוניים ו**ללא** סטטוס verified/UNVERIFIED. אבל כל invariant **נקשר
|
||||||
|
> לעיקרון הגלובלי שהוא משרת**: כלל אי-ה-drift הוא מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. שתי החברות: Master מול Mirror
|
||||||
|
|
||||||
|
Paperclip מחייב `agents.company_id NOT NULL` — אין סוכנים משותפים. כדי לשרת את שני סוגי
|
||||||
|
העררים, המערכת מורצת כ**שתי חברות** נפרדות, כל אחת עם מערך-סוכנים מלא משלה:
|
||||||
|
|
||||||
|
| ממד | CMP — **Master** | CMPA — **Mirror** |
|
||||||
|
|------|------------------|-------------------|
|
||||||
|
| תפקיד | מקור-האמת לתצורת-סוכנים | העתקה מסונכרנת מ-Master |
|
||||||
|
| COMPANY_ID | `42a7acd0-30c5-4cbd-ac97-7424f65df294` | `8639e837-4c9d-47fa-a76b-95788d651896` |
|
||||||
|
| סוגי תיקים | רישוי ובנייה | היטל השבחה + פיצויים ס'197 |
|
||||||
|
| טווח-מספרים | **1xxx** | **8xxx, 9xxx** |
|
||||||
|
| CEO Agent ID | `752cebdd-6748-4a04-aacd-c7ab0294ef33` | `cdbfa8bc-3d61-41a4-a2e7-677ec7d34562` |
|
||||||
|
|
||||||
|
(המקור: [HEARTBEAT.md §1](../../.claude/agents/HEARTBEAT.md), שורות 38–44; מזהי-החברות מקודדים גם
|
||||||
|
ב-[sync_agents_across_companies.py:62-63](../../scripts/sync_agents_across_companies.py).)
|
||||||
|
|
||||||
|
**14 סוכנים = 7 × 2.** כל חברה מחזיקה את אותם 7 תפקידי-סוכן (CEO, writer, analyst, researcher,
|
||||||
|
qa, proofreader, exporter — ראה [X4-agents.md](X4-agents.md)). מאחר ש-`company_id` הוא `NOT NULL`,
|
||||||
|
כל תפקיד מיוצג בשתי **רשומות-סוכן נפרדות** — אחת ל-CMP, אחת ל-CMPA. אין רשומה משותפת.
|
||||||
|
|
||||||
|
**Master = CMP, Mirror = CMPA.** התצורה נכתבת ומתוחזקת בחברת ה-Master (CMP, 1xxx), והסנכרון
|
||||||
|
הוא **חד-כיווני** CMP → CMPA ([sync...py:1-7,361-362](../../scripts/sync_agents_across_companies.py)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. ניתוב לפי חברה — סינון ב-`company_id`
|
||||||
|
|
||||||
|
הזרימה התפעולית נאכפת לפי `$PAPERCLIP_COMPANY_ID` של הסוכן הפועל ([HEARTBEAT.md §1](../../.claude/agents/HEARTBEAT.md)):
|
||||||
|
|
||||||
|
- `42a7acd0…` → הסוכן מטפל **רק** בתיקי 1xxx; `8639e837…` → **רק** בתיקי 8xxx/9xxx (שורות 43–44).
|
||||||
|
- **אסור** ליצור פרויקט/issue/תוכן לתיק מחוץ לטווח-החברה (שורה 45); issue שמכוון לתיק מחוץ
|
||||||
|
לטווח → סירוב מנומס ב-comment + העֵרת ה-CEO של החברה הנכונה (שורה 46).
|
||||||
|
- **CEO שונה לכל חברה** — בחירת ה-CEO ל-wakeup נגזרת מ-`$PAPERCLIP_COMPANY_ID`, **לעולם לא**
|
||||||
|
UUID hardcoded ([HEARTBEAT.md §4ג](../../.claude/agents/HEARTBEAT.md), שורות 143–150).
|
||||||
|
- **גבול-חברה נאכף בצד-Paperclip:** wakeup לחברה אחרת נדחה — `Agent key cannot access another
|
||||||
|
company` ([HEARTBEAT.md §4ג](../../.claude/agents/HEARTBEAT.md), שורה 157).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. כלל ה-Sync — אחרי כל שינוי-הגדרות ב-Master
|
||||||
|
|
||||||
|
> **טריגר:** כל שינוי ב-`adapter_config`, `runtime_config`, `budget_monthly_cents`, או skills
|
||||||
|
> של סוכן ב-Master (UI / SQL / API). מקור: סעיף "Cross-company agent sync" ב-[legal-ai/CLAUDE.md](../../CLAUDE.md)
|
||||||
|
> וב-[root CLAUDE.md](../../../CLAUDE.md).
|
||||||
|
|
||||||
|
הפעולה החובה — קודם בדיקה, אז החלה:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PAPERCLIP_BOARD_API_KEY=$(…infisical…) \
|
||||||
|
python ~/legal-ai/scripts/sync_agents_across_companies.py --verify # drift report
|
||||||
|
PAPERCLIP_BOARD_API_KEY=$(…) \
|
||||||
|
python ~/legal-ai/scripts/sync_agents_across_companies.py --apply # backup + apply
|
||||||
|
```
|
||||||
|
|
||||||
|
**מה הסקריפט עושה** (מאומת מול הקוד):
|
||||||
|
|
||||||
|
- **חד-כיווני CMP → CMPA**, סינכרון של שדות-תצורה מוגדרים: top-level (`budget_monthly_cents`,
|
||||||
|
`metadata`, `icon`, `title`, `role`), מפתחות `adapter_config` נבחרים (`model`, `effort`,
|
||||||
|
`timeoutSec`, `maxTurnsPerRun`, נתיבי-instructions, `cwd`…), ו-`runtime_config` כ-full-replace
|
||||||
|
([sync...py:66-75,124-160](../../scripts/sync_agents_across_companies.py)). שדות פר-חברה
|
||||||
|
(`id`, `company_id`, `adapter_type`, `agent_api_keys`, `status`, `spent_monthly_cents`,
|
||||||
|
`permissions`) **אינם** מסונכרנים ([sync...py:24-29](../../scripts/sync_agents_across_companies.py)).
|
||||||
|
- **מבוסס-API, לא DB ישיר.** ה-PATCH דרך `PATCH /api/agents/{id}` וה-skills דרך
|
||||||
|
`POST /api/agents/{id}/skills/sync` עם `Authorization: Bearer` ([sync...py:204-237](../../scripts/sync_agents_across_companies.py)).
|
||||||
|
- **מסנן skills מקומיים שלא קיימים ב-Mirror.** `desiredSkills` מושוות כ-subset; skills מקומיים
|
||||||
|
של CMP (למשל `local/eba6210d5a/legal-decision`) שלא קיימים ב-CMPA נשמטים עם אזהרה
|
||||||
|
([sync...py:138-154,194-195](../../scripts/sync_agents_across_companies.py)).
|
||||||
|
- **יוצר revisions.** סנכרון skills עובר דרך endpoint ייעודי שמייצר `skill-sync` revision
|
||||||
|
([sync...py:277-284](../../scripts/sync_agents_across_companies.py)).
|
||||||
|
- **idempotent + אל-כשל.** `--verify`/`--dry-run` כברירת-מחדל, גיבוי `pg_dump` לפני `--apply`,
|
||||||
|
pre-flight על קבצי-instructions, ו-re-verify אוטומטי אחרי ההחלה ([sync...py:9,163-173,408-465](../../scripts/sync_agents_across_companies.py)).
|
||||||
|
- **מדלג על סוכן עם `adapter_type` שונה בין החברות.** אם ל-Master ול-Mirror `adapter_type`
|
||||||
|
שונה → `SKIPPING`, ללא סנכרון ([sync...py:387-389](../../scripts/sync_agents_across_companies.py)).
|
||||||
|
זו המלכודת ב-INV-MC1 (להלן).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Invariants של התחום (פרויקטלי-תפעולי)
|
||||||
|
|
||||||
|
### INV-MC1: תצורת-סוכן ב-Master מפושטת ל-Mirror — אין drift בין החברות
|
||||||
|
**כלל:** כל שינוי ב-`adapter_config` / `runtime_config` / `budget_monthly_cents` / skills של
|
||||||
|
סוכן בחברת ה-Master (CMP) **חייב** להיות מפושט ל-Mirror (CMPA) דרך סקריפט ה-Sync המבוסס-API
|
||||||
|
(`--verify` ואז `--apply`). שתי החברות **לא מתפצלות** — הן שתי העתקות מסונכרנות של אותה תצורה
|
||||||
|
(מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) — מקור-אמת
|
||||||
|
יחיד, אין מסלולים מקבילים מתפצלים; וכלל-ההנדסה "סימטריה", [חוקה §6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
|
||||||
|
**מקור-סמכות:** סעיף "Cross-company agent sync" ב-[legal-ai/CLAUDE.md](../../CLAUDE.md) +
|
||||||
|
ב-[root CLAUDE.md](../../../CLAUDE.md) +
|
||||||
|
[scripts/sync_agents_across_companies.py](../../scripts/sync_agents_across_companies.py) +
|
||||||
|
[HEARTBEAT.md §1, §4ג](../../.claude/agents/HEARTBEAT.md). (invariant פרויקטלי-תפעולי — ללא
|
||||||
|
פרוטוקול ≥3-המקורות; משרת את העיקרון הגלובלי G2.)
|
||||||
|
**אכיפה:** סקריפט ה-Sync (idempotent, מבוסס-API, גיבוי+re-verify) — מורץ **ידנית** אחרי כל
|
||||||
|
שינוי-תצורה ב-Master. **אין אכיפה אוטומטית** (ראה §5).
|
||||||
|
**הפרה ידועה:** הסקריפט **מדלג** על סוכן ש-`adapter_type` שונה בין CMP ל-CMPA
|
||||||
|
([sync...py:387-389](../../scripts/sync_agents_across_companies.py)). כשמעבירים סוכן ל-`deepseek_local`
|
||||||
|
ב-Master, ה-Mirror נשאר על ה-adapter הישן והסנכרון מדלג עליו — **חובה להחיל את שינוי ה-`adapter_type`
|
||||||
|
ידנית בשתי החברות לפני הרצת ה-Sync** ([CLAUDE.md "External adapters — deepseek_local"](../../CLAUDE.md)),
|
||||||
|
אחרת נוצר drift שקט באותו סוכן.
|
||||||
|
|
||||||
|
### INV-MC2: אין סוכן משותף — רשומה נפרדת לכל חברה
|
||||||
|
**כלל:** סוכן **לעולם אינו רשומה משותפת** בין החברות. כל אחד מ-7 התפקידים מיוצג בשתי
|
||||||
|
רשומות-סוכן נפרדות (CMP + CMPA), שכן Paperclip מחייב `agents.company_id NOT NULL`. הסנכרון
|
||||||
|
מעתיק *ערכי-תצורה* בין שתי רשומות — לא ממזג אותן לרשומה אחת (תואם [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים):
|
||||||
|
מקור-אמת יחיד לתצורה, גם כשהיא משוכפלת על פני רשומות).
|
||||||
|
**מקור-סמכות:** סעיף "Cross-company agent sync" ב-[legal-ai/CLAUDE.md](../../CLAUDE.md) (14 agents = 7 × 2;
|
||||||
|
`agents.company_id NOT NULL`) + [sync...py:4-7,83-103](../../scripts/sync_agents_across_companies.py)
|
||||||
|
(שולף מערכי-סוכן נפרדים לכל `company_id`) + [HEARTBEAT.md §1](../../.claude/agents/HEARTBEAT.md).
|
||||||
|
(invariant פרויקטלי-תפעולי.)
|
||||||
|
**אכיפה:** אילוץ `company_id NOT NULL` בצד-Paperclip; הסקריפט מתאים סוכנים בין החברות לפי
|
||||||
|
`name` ולעולם לא יוצר רשומה משותפת ([sync...py:372,383-385](../../scripts/sync_agents_across_companies.py)
|
||||||
|
— "we never auto-create").
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. מצב קיים מול יעד — פער אכיפה
|
||||||
|
|
||||||
|
ה-Sync הוא **ידני ולא-נאכף**. הסקריפט עצמו בנוי "אל-כשל" (dry-run כברירת-מחדל, גיבוי,
|
||||||
|
re-verify), אך **שום מנגנון לא מכריח** הרצה אחרי שינוי-תצורה ב-Master:
|
||||||
|
|
||||||
|
- **drift אם שוכחים.** שינוי `adapter_config`/`runtime_config`/budget/skills ב-CMP בלי הרצת
|
||||||
|
`--apply` משאיר את CMPA מאחור — שתי החברות מתפצלות בשקט, בניגוד ל-INV-MC1. **יעד:** טריגר/
|
||||||
|
בדיקת-בריאות תקופתית שמריצה `--verify` ומדווחת drift (היום ההרצה תלויה בזיכרון המפעיל).
|
||||||
|
- **מלכודת `adapter_type`-skip.** סוכן עם `adapter_type` שונה בין החברות נשמט מהסנכרון
|
||||||
|
([sync...py:387-389](../../scripts/sync_agents_across_companies.py)) — ה-`--verify` ידווח
|
||||||
|
`SKIPPING`, אך אם המפעיל לא יחיל את שינוי ה-adapter ידנית בשתי החברות, הסוכן יישאר drifted.
|
||||||
|
**יעד:** אזהרת-SKIPPING שמתבלטת ב-report + צ'קליסט-ידני (כבר מתועד ב-[CLAUDE.md](../../CLAUDE.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md) — [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
(מקור-אמת יחיד, אין מסלולים מקבילים מתפצלים) + כלל-ההנדסה "סימטריה" ([§6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
|
||||||
|
- [X4-agents.md](X4-agents.md) — מפת 7 תפקידי-הסוכן שמשוכפלים על פני שתי החברות.
|
||||||
|
- [X3-integration-deploy.md](X3-integration-deploy.md) — Paperclip (wakeup, ניתוב comments) ו-deploy;
|
||||||
|
ה-wakeup-per-company משלים את הניתוב כאן.
|
||||||
|
- [scripts/sync_agents_across_companies.py](../../scripts/sync_agents_across_companies.py) — מימוש ה-Sync.
|
||||||
|
- [legal-ai/CLAUDE.md](../../CLAUDE.md) + [root CLAUDE.md](../../../CLAUDE.md) — סעיף
|
||||||
|
"Cross-company agent sync" + "External adapters — deepseek_local" (מלכודת ה-adapter_type).
|
||||||
|
- [.claude/agents/HEARTBEAT.md](../../.claude/agents/HEARTBEAT.md) — §1 (סינון-חברה) + §4ג (wake CEO לפי חברה).
|
||||||
220
docs/spec/X3-integration-deploy.md
Normal file
220
docs/spec/X3-integration-deploy.md
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# X3 — אינטגרציה ו-Deploy (Integration & Deploy)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) והוא ה-deep-dive על **שני ממדי-התפעול**
|
||||||
|
של עוזר משפטי: (א) **האינטגרציה עם Paperclip** — איך המערכת מעירה סוכנים, איך תגובות-משתמש
|
||||||
|
מנותבות, ואיך שינוי-סטטוס תיק מתפרסם חזרה; (ב) **מודל ה-Deploy** — שני מודלי-הרצה הדו-קיימים
|
||||||
|
על שרת Nautilus (Coolify-Docker מול pm2-מקומי) ומחזור-השינוי של legal-ai.
|
||||||
|
|
||||||
|
> **invariant פרויקטלי-תפעולי.** ה-invariants כאן הם **עובדות על איך המערכת *הזו* משתלבת
|
||||||
|
> ונפרסת** — לא תאוריה הנדסית כללית ולא תוכן משפטי. אין סמכות חיצונית ל"איך מעירים סוכן
|
||||||
|
> Paperclip" או "איך פורסים את legal-ai"; לכן הם נושאים שדה `מקור-סמכות` = הראנבוקים והקוד
|
||||||
|
> של הפרויקט עצמו ([root CLAUDE.md](../../../CLAUDE.md), [legal-ai/CLAUDE.md](../../CLAUDE.md),
|
||||||
|
> [HEARTBEAT.md](../../.claude/agents/HEARTBEAT.md), זיכרון `reference_paperclip_wakeup`,
|
||||||
|
> ו-[web/paperclip_api.py](../../web/paperclip_api.py)) — **לא** ≥3 מקורות חיצוניים ו**ללא**
|
||||||
|
> סטטוס verified/UNVERIFIED. אבל כל invariant **נקשר לעיקרון הגלובלי שהוא משרת**: כלל
|
||||||
|
> ה-wakeup-דרך-API-בלבד הוא מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
> (מסלול קנוני יחיד; ה-DB-insert המקביל אסור כי הוא מתפצל מהמסלול שיוצר `heartbeat_run`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. אינטגרציית Paperclip
|
||||||
|
|
||||||
|
עוזר משפטי משתלב עם Paperclip בשלושה כיוונים: **wakeup** (legal-ai/אוטומציה → סוכן),
|
||||||
|
**ניתוב comments** (משתמש → CEO → סוכן), ו-**webhook יוצא** (legal-ai → פלאגין).
|
||||||
|
|
||||||
|
### 1א. Wakeup — תמיד דרך API, לעולם לא דרך DB
|
||||||
|
|
||||||
|
הנתיב הקנוני היחיד להערת סוכן הוא `POST /api/agents/{agent-id}/wakeup` עם `payload` המכיל
|
||||||
|
`issueId` ([root CLAUDE.md](../../../CLAUDE.md) "Wakeup API"; [legal-ai/CLAUDE.md](../../CLAUDE.md)
|
||||||
|
"Wakeup API"; [HEARTBEAT.md §4ד, שורות 152–158](../../.claude/agents/HEARTBEAT.md)):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" \
|
||||||
|
'{"source":"automation","triggerDetail":"system","reason":"...",
|
||||||
|
"payload":{"issueId":"...","mutation":"comment","commentId":"..."}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`POST .../wakeup`, לא `/wake`** — שם-הנתיב מדויק ([legal-ai/CLAUDE.md](../../CLAUDE.md)).
|
||||||
|
- **חובה `payload.issueId`** — בלעדיו הסוכן מתעורר בלי הקשר (בלי תיק, בלי issue, בלי `cwd`
|
||||||
|
נכון) ([HEARTBEAT.md שורה 156](../../.claude/agents/HEARTBEAT.md)).
|
||||||
|
- **אסור `INSERT INTO agent_wakeup_requests` ישיר** — insert ל-DB יוצר רשומת-בקשה בלבד **בלי
|
||||||
|
`heartbeat_run`**, והסוכן **לא יתעורר לעולם** ([HEARTBEAT.md שורה 158](../../.claude/agents/HEARTBEAT.md);
|
||||||
|
זיכרון `reference_paperclip_wakeup`).
|
||||||
|
זהו בדיוק "מסלול מקביל מתפצל" שאסור לפי [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים).
|
||||||
|
- **CEO לכל חברה** — מזהה-ה-CEO ל-wakeup נגזר מ-`$PAPERCLIP_COMPANY_ID`, לעולם לא UUID
|
||||||
|
hardcoded; wakeup לחברה אחרת נדחה (`Agent key cannot access another company`)
|
||||||
|
([HEARTBEAT.md §4ג](../../.claude/agents/HEARTBEAT.md); ראה [X2-multi-company.md §2](X2-multi-company.md)).
|
||||||
|
|
||||||
|
### 1ב. ניתוב comments — דרך ה-CEO
|
||||||
|
|
||||||
|
תגובת-משתמש על issue ב-Paperclip **אינה** מנותבת ישירות לסוכן-המטרה. הזרימה
|
||||||
|
([root CLAUDE.md](../../../CLAUDE.md) "Comment routing"; [legal-ai/CLAUDE.md](../../CLAUDE.md)):
|
||||||
|
|
||||||
|
```
|
||||||
|
user comment → plugin-legal-ai → ctx.agents.invoke() מעיר CEO
|
||||||
|
→ CEO קורא comment, מחליט ניתוב, יוצר issue לסוכן המתאים
|
||||||
|
```
|
||||||
|
|
||||||
|
- ה-CEO הוא נקודת-הניתוב היחידה — סוכן-משנה לא מקבל עבודה ישירות מ-comment.
|
||||||
|
- כל סוכן **חייב** לקרוא comments אחרונים לפני שהוא מתחיל עבודה ([HEARTBEAT שלבים 2b–2c](../../.claude/agents/HEARTBEAT.md)).
|
||||||
|
|
||||||
|
### 1ג. Webhook יוצא — עדכון סטטוס תיק לפלאגין
|
||||||
|
|
||||||
|
כשסטטוס תיק משתנה דרך `PUT /api/cases/{case_number}`, הבקאנד שולח webhook אסינכרוני
|
||||||
|
לפלאגין כ-BackgroundTask, fire-and-forget:
|
||||||
|
|
||||||
|
```
|
||||||
|
PUT /api/cases/{n} → [BackgroundTask] emit_case_status_webhook()
|
||||||
|
→ POST /api/plugins/marcusgroup.legal-ai/webhooks/case-status
|
||||||
|
→ plugin-legal-ai/onWebhook() → comment בעברית + CEO wakeup (כש-qa_failed)
|
||||||
|
```
|
||||||
|
|
||||||
|
מאומת מול הקוד:
|
||||||
|
|
||||||
|
- ה-call-site: [web/app.py:2045-2061](../../web/app.py) — ה-webhook מתוזמן רק כש-`old_status
|
||||||
|
!= new_status`, ו-`company_id` נגזר מ-prefix מספר-התיק (`1`→licensing, `8/9`→betterment).
|
||||||
|
- המימוש: [web/paperclip_api.py:87-117](../../web/paperclip_api.py) — `emit_case_status_webhook`
|
||||||
|
קורא ל-`pc_request("POST", "/api/plugins/.../webhooks/case-status", ...)` עם `timeout=5.0`,
|
||||||
|
בלוק `try/except` שמתעד `logger.warning` ולעולם לא raise (לא חוסם את הקורא).
|
||||||
|
- אותו דפוס משרת אירועים נוספים: `emit_missing_precedent_webhook`
|
||||||
|
([paperclip_api.py:120-165](../../web/paperclip_api.py)) ו-`emit_export_complete_webhook`
|
||||||
|
([paperclip_api.py:168+](../../web/paperclip_api.py)).
|
||||||
|
|
||||||
|
> **חוזה ה-webhook (idempotency / at-least-once / אירוע מגורס)** מפורט ב-[X7 INV-INT7/INT8](X7-paperclip-client-params.md):
|
||||||
|
> ה-emitter הנוכחי fire-and-forget בולע שגיאות וללא event-id/dedup — יעד FU-9.
|
||||||
|
|
||||||
|
### 1ד. כל קריאת-API דרך helper — לא curl/httpx ישיר
|
||||||
|
|
||||||
|
קריאות ל-Paperclip עוברות תמיד דרך helper, לא דרך לקוח גולמי:
|
||||||
|
|
||||||
|
- **bash (סוכנים):** `~/legal-ai/scripts/pc.sh <METHOD> <PATH> [BODY]` — מוסיף אוטומטית
|
||||||
|
`Authorization: Bearer`, `X-Paperclip-Run-Id`, `Content-Type`, ו-base URL
|
||||||
|
([HEARTBEAT.md §0, שורות 15–32](../../.claude/agents/HEARTBEAT.md); [scripts/pc.sh:8-9,39-40](../../scripts/pc.sh)).
|
||||||
|
- **Python (FastAPI):** `from web.paperclip_api import pc_request` — בונה headers דרך
|
||||||
|
`_build_headers` ([paperclip_api.py:47-84](../../web/paperclip_api.py)), משתמש ב-board API key.
|
||||||
|
- **למה:** ה-skill הרשמי דורש `X-Paperclip-Run-Id` בכל קריאה משנה issue (audit trail);
|
||||||
|
ה-helper מבטיח עקביות + תאימות ל-board API keys long-lived שלא נושאות JWT claims
|
||||||
|
([legal-ai/CLAUDE.md](../../CLAUDE.md) "קריאות API — תמיד דרך helper").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. מודל ה-Deploy — שני מודלים דו-קיימים
|
||||||
|
|
||||||
|
> **קונפיגורציה, env וסודות** — ה-deep-dive המלא (catalog ה-env, מקור-config, secrets, hardcode,
|
||||||
|
> drift) ב-[X10-deploy-env-secrets.md](X10-deploy-env-secrets.md). כאן נשאר רק מודל-ההרצה.
|
||||||
|
|
||||||
|
על שרת Nautilus דרים **שני מודלי-הרצה**. ערבוב ביניהם הוא הטעות הנפוצה ביותר
|
||||||
|
([root CLAUDE.md](../../../CLAUDE.md) "Deploy architecture"; [legal-ai/CLAUDE.md](../../CLAUDE.md)
|
||||||
|
"ארכיטקטורת Deploy").
|
||||||
|
|
||||||
|
| ממד | legal-ai (web + web-ui) | Paperclip + legal-chat-service |
|
||||||
|
|------|--------------------------|--------------------------------|
|
||||||
|
| מודל | **Coolify-managed (Docker)** | **PM2-managed (Node/Python מקומי)** |
|
||||||
|
| מחזור-שינוי | commit → push → Gitea Actions build → Coolify redeploy (~2–4 דק') | עריכה → `pm2 restart` |
|
||||||
|
| Coolify UUID | `gyjo0mtw2c42ej3xxvbz8zio` | — |
|
||||||
|
| build_pack | **`dockerimage`** (לא `dockerfile`) | — |
|
||||||
|
| פורטים | Next.js `:3000` (חשוף) + FastAPI `:8000` (פנימי) | Paperclip `localhost:3100`; legal-chat-service `127.0.0.1:8770` (loopback) |
|
||||||
|
| הרצה מקומית | **אין** — אין venv של Python על ה-host; אסור `uvicorn`/`next dev` לפרוד | יש; מתחזק דרך pm2 |
|
||||||
|
|
||||||
|
### 2א. מחזור-השינוי של legal-ai (Coolify dockerimage)
|
||||||
|
|
||||||
|
שינוי קוד ב-`web/` או `web-ui/` **לא נכנס לתוקף** עד שמריצים את כל הצעדים, בסדר:
|
||||||
|
|
||||||
|
1. `git commit` + `git push origin main` ל-Gitea.
|
||||||
|
2. Gitea Actions בונה image ודוחף ל-registry (`gitea.nautilus.marcusgroup.org/...`).
|
||||||
|
3. ה-workflow מפעיל Coolify redeploy דרך API (UUID `gyjo0mtw2c42ej3xxvbz8zio`).
|
||||||
|
4. ~2–4 דקות end-to-end. בדיקה: `curl -s https://legal-ai.nautilus.marcusgroup.org/api/health`.
|
||||||
|
|
||||||
|
- **אסור** לנסות `uvicorn`/`next dev` לפרוד — הקונטיינר מספק את שני התהליכים; אין סביבת
|
||||||
|
Python על ה-host ([root CLAUDE.md](../../../CLAUDE.md); [legal-ai/CLAUDE.md](../../CLAUDE.md)).
|
||||||
|
- **endpoint חדש ≠ זמין ל-UI.** הוספת endpoint ב-`web/app.py` היא תנאי הכרחי אך לא מספיק
|
||||||
|
לצריכה מה-frontend — חובה `npm run api:types` בתוך `web-ui/` כדי לחדש את ה-OpenAPI types
|
||||||
|
([root CLAUDE.md](../../../CLAUDE.md), שורה 89; [legal-ai/CLAUDE.md](../../CLAUDE.md)).
|
||||||
|
|
||||||
|
### 2ב. legal-chat-service ו-host.docker.internal
|
||||||
|
|
||||||
|
legal-chat-service (`127.0.0.1:8770`, pm2) הוא גשר host-side שעוטף את `claude` CLI ב-streaming
|
||||||
|
לטאב הצ'אט ב-`/training`. הקונטיינר מגיע אליו דרך `host.docker.internal:8770` — ולכן ה-Service
|
||||||
|
Definition של legal-ai ב-Coolify **חייב** לכלול `extra_hosts: host.docker.internal:host-gateway`,
|
||||||
|
אחרת ה-proxy יקבל `ConnectError` ([root CLAUDE.md](../../../CLAUDE.md); [legal-ai/CLAUDE.md](../../CLAUDE.md)
|
||||||
|
"legal-chat-service"). הנחת-היסוד של "קריאות LLM רק ממקומי" נשמרת — ראה
|
||||||
|
זיכרון `feedback_claude_session_local_only`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Invariants של התחום (פרויקטלי-תפעולי)
|
||||||
|
|
||||||
|
### INV-INT1: wakeup דרך API בלבד — DB-insert אסור
|
||||||
|
**כלל:** הערת סוכן Paperclip **חייבת** לעבור דרך `POST /api/agents/{agent-id}/wakeup` עם
|
||||||
|
`payload.issueId`. **אסור** `INSERT INTO agent_wakeup_requests` ישיר — insert ל-DB אינו יוצר
|
||||||
|
`heartbeat_run`, ולכן הסוכן **לא יתעורר לעולם**. זהו המסלול הקנוני היחיד; ה-DB-insert הוא
|
||||||
|
מסלול-מקביל-מתפצל אסור (מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
— מקור-אמת/מסלול קנוני יחיד; וכלל-ההנדסה "סימטריה", [חוקה §6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
|
||||||
|
**מקור-סמכות:** "Wakeup API" ב-[root CLAUDE.md](../../../CLAUDE.md) + ב-[legal-ai/CLAUDE.md](../../CLAUDE.md) +
|
||||||
|
זיכרון `reference_paperclip_wakeup` +
|
||||||
|
[HEARTBEAT.md §4ד, שורות 152–158](../../.claude/agents/HEARTBEAT.md). (invariant פרויקטלי-תפעולי —
|
||||||
|
ללא פרוטוקול ≥3-המקורות; משרת את העיקרון הגלובלי G2.)
|
||||||
|
**אכיפה:** קריאות-wakeup דרך `pc.sh`/`pc_request` בלבד; `payload.issueId` חובה; בדיקה
|
||||||
|
ש-`heartbeat_run` נוצר. **אין אכיפה סכמתית** שתחסום insert ישיר ל-`agent_wakeup_requests` —
|
||||||
|
המניעה היא נוהל (ראה §4).
|
||||||
|
**הפרה ידועה:** insert ישיר ל-`agent_wakeup_requests` (fallback ישן) → רשומה בלי `heartbeat_run`,
|
||||||
|
הסוכן נשאר רדום (זיכרון `reference_paperclip_wakeup`).
|
||||||
|
|
||||||
|
### INV-INT2: שינוי-קוד legal-ai נכנס לתוקף רק דרך commit→push→Coolify deploy
|
||||||
|
**כלל:** שינוי קוד ב-`web/` או `web-ui/` **לא נכנס לתוקף** עד `git commit` + `git push origin main`
|
||||||
|
+ build ב-Gitea Actions + Coolify redeploy (build_pack `dockerimage`, UUID `gyjo0mtw2c42ej3xxvbz8zio`).
|
||||||
|
**אין** הרצת `uvicorn`/`next dev` מקומית לפרוד. endpoint חדש ב-`web/app.py` דורש גם
|
||||||
|
`npm run api:types` ב-`web-ui/` כדי להיחשף ל-UI.
|
||||||
|
**מקור-סמכות:** "Deploy architecture" ב-[root CLAUDE.md](../../../CLAUDE.md) (UUID, dockerimage,
|
||||||
|
no local uvicorn, api:types) + "ארכיטקטורת Deploy" ב-[legal-ai/CLAUDE.md](../../CLAUDE.md) +
|
||||||
|
זיכרון `reference_deployment`.
|
||||||
|
(invariant פרויקטלי-תפעולי — ללא פרוטוקול ≥3-המקורות.)
|
||||||
|
**אכיפה:** pipeline Gitea Actions → Coolify (אוטומטי בדחיפה ל-main); בדיקה ידנית
|
||||||
|
`curl .../api/health` אחרי deploy. **אין** מסלול-פריסה חלופי.
|
||||||
|
**הפרה ידועה:** בדיקת שינוי מול הרצה מקומית שלא קיימת — הקוד בפרוד נשאר ישן עד deploy; וכן
|
||||||
|
drift אפשרי Infisical↔Coolify env (env לא מתעדכן אוטומטית מ-Infisical, ראה
|
||||||
|
זיכרון `feedback_infisical_coolify_drift`).
|
||||||
|
|
||||||
|
### INV-INT3: כל קריאת-Paperclip דרך helper — לא curl/httpx ישיר
|
||||||
|
**כלל:** קריאות ל-Paperclip API עוברות **תמיד** דרך helper — `pc.sh` (bash/סוכנים) או
|
||||||
|
`pc_request` (Python/FastAPI) — ולעולם לא `curl`/`httpx` גולמי. ה-helper מזריק `Authorization`,
|
||||||
|
`X-Paperclip-Run-Id` (audit), ו-`Content-Type` באופן עקבי, ותומך ב-board API keys long-lived
|
||||||
|
(מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) — מסלול-גישה
|
||||||
|
קנוני יחיד ל-Paperclip; ושל [G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai) —
|
||||||
|
audit-trail עקבי).
|
||||||
|
**מקור-סמכות:** "קריאות API — תמיד דרך helper" ב-[legal-ai/CLAUDE.md](../../CLAUDE.md) +
|
||||||
|
[HEARTBEAT.md §0, שורות 15–32](../../.claude/agents/HEARTBEAT.md) +
|
||||||
|
[scripts/pc.sh:8-9,39-40](../../scripts/pc.sh) + [web/paperclip_api.py:47-84](../../web/paperclip_api.py).
|
||||||
|
(invariant פרויקטלי-תפעולי — ללא פרוטוקול ≥3-המקורות.)
|
||||||
|
**אכיפה:** נוהל + code-review; `pc.sh` ו-`pc_request` הם נקודות-הכניסה היחידות. **אין אכיפה
|
||||||
|
אוטומטית** שתחסום `httpx.AsyncClient` ישיר ל-Paperclip בקוד חדש.
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. מצב קיים מול יעד — פער אכיפה
|
||||||
|
|
||||||
|
האינטגרציה נשענת על **נוהל**, לא על מחסום-קוד:
|
||||||
|
|
||||||
|
- **wakeup (INV-INT1):** אין constraint סכמתי שחוסם insert ישיר ל-`agent_wakeup_requests`;
|
||||||
|
המניעה היא ידע-נוהל ([HEARTBEAT](../../.claude/agents/HEARTBEAT.md)). **יעד:** wrapper/בדיקת-בריאות
|
||||||
|
שמסמן בקשות-wakeup ללא `heartbeat_run` תואם.
|
||||||
|
- **helper (INV-INT3):** אין linter/בדיקה שתתפוס `httpx`/`curl` ישיר ל-Paperclip בקוד חדש.
|
||||||
|
**יעד:** כלל-lint שמכריח שימוש ב-`pc_request`/`pc.sh`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md) — [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
(מסלול קנוני יחיד) + [G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai) (audit-trail) +
|
||||||
|
כלל-ההנדסה "סימטריה" ([§6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
|
||||||
|
- [X2-multi-company.md](X2-multi-company.md) — wakeup-per-company + ניתוב לפי `company_id` משלים את §1 כאן.
|
||||||
|
- [X4-agents.md](X4-agents.md) — מפת הסוכנים שה-CEO מנתב אליהם comments.
|
||||||
|
- [root CLAUDE.md](../../../CLAUDE.md) + [legal-ai/CLAUDE.md](../../CLAUDE.md) — "Wakeup API",
|
||||||
|
"Comment routing", "Deploy architecture", "קריאות API — תמיד דרך helper".
|
||||||
|
- [.claude/agents/HEARTBEAT.md](../../.claude/agents/HEARTBEAT.md) — §0 (pc.sh), §4ג–§4ד (wake CEO + payload).
|
||||||
|
- [web/paperclip_api.py](../../web/paperclip_api.py) — `pc_request`, `emit_case_status_webhook`.
|
||||||
|
- [scripts/pc.sh](../../scripts/pc.sh) — helper ה-bash.
|
||||||
|
- [X7-paperclip-client-params.md](X7-paperclip-client-params.md) — שכבת-הלקוח + פרמטרי-החיבור (INV-INT4–INT8).
|
||||||
|
- [X10-deploy-env-secrets.md](X10-deploy-env-secrets.md) — env/secrets/deploy deep-dive (INV-ENV1–ENV5).
|
||||||
174
docs/spec/X4-agents.md
Normal file
174
docs/spec/X4-agents.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# X4 — מפת הסוכנים (Agents Map)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) והוא ה-deep-dive על **מי הם הסוכנים**
|
||||||
|
של עוזר משפטי, **מה תפקיד כל אחד**, ו**אילו קבצי-ספ כל סוכן חייב לקרוא לפני שהוא פועל**. הוא
|
||||||
|
מסייע לסוכן לדעת באיזה ספ לקרוא — ומעגן את [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)
|
||||||
|
(המערכת מסייעת; השערים האנושיים הם invariant): כל סוכן קורא את החוקה תחילה ופועל בתחום-אחריותו,
|
||||||
|
לא מחליף את שיקול-הדעת האנושי.
|
||||||
|
|
||||||
|
> **invariant פרויקטלי-תפעולי.** ה-invariants כאן הם **עובדות על איך הסוכנים של המערכת *הזו*
|
||||||
|
> מאורגנים ומופעלים** — לא תאוריה הנדסית כללית ולא תוכן משפטי. אין סמכות חיצונית ל"מי קורא מה
|
||||||
|
> לפני שהוא פועל"; לכן הם נושאים שדה `מקור-סמכות` = הראנבוקים וקבצי-הסוכן של הפרויקט עצמו
|
||||||
|
> ([HEARTBEAT.md](../../.claude/agents/HEARTBEAT.md), קבצי הסוכן תחת [.claude/agents/](../../.claude/agents/),
|
||||||
|
> ו-[החוקה](00-constitution.md)) — **לא** ≥3 מקורות חיצוניים ו**ללא** סטטוס verified/UNVERIFIED.
|
||||||
|
> אבל כל invariant **נקשר לעיקרון הגלובלי שהוא משרת**: כלל "קרא-לפני-שתפעל" + תחום-אחריות הם
|
||||||
|
> מופע של [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant) (סיוע תחת
|
||||||
|
> שערים אנושיים) ו-[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. ההפעלה המשותפת — HEARTBEAT.md
|
||||||
|
|
||||||
|
לפני כל עבודה, **כל** סוכן Paperclip עובר את ה-checklist המשותף ב-[HEARTBEAT.md](../../.claude/agents/HEARTBEAT.md):
|
||||||
|
זיהוי וסינון-חברה (§1), קריאת comments אחרונים (§1.5, 2b–2c), קריאת `heartbeat-context` עם
|
||||||
|
attachments (§1.5ב), וקריאות-API דרך `pc.sh` בלבד (§0). HEARTBEAT גובר על ה-skill הרשמי של
|
||||||
|
Paperclip בקונפליקט (project-specific מנצח default), אך אינו מחליף את החוקה — הוא מצטרף אליה:
|
||||||
|
קודם החוקה (00) + ספ-התחום, אז ה-HEARTBEAT התפעולי.
|
||||||
|
|
||||||
|
**הקשר רב-חברתי.** ל-Paperclip אילוץ `agents.company_id NOT NULL` — אין סוכן משותף. לכן כל אחד
|
||||||
|
מ-7 תפקידי הסוכן-הדומייני מיוצג בשתי רשומות (CMP / CMPA), וסוכן מטפל **רק** בתיקי-החברה שלו לפי
|
||||||
|
`$PAPERCLIP_COMPANY_ID` (1xxx ל-CMP; 8xxx/9xxx ל-CMPA). ראה [X2-multi-company.md](X2-multi-company.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. מפת הסוכנים הדומייניים (7 תפקידים × 2 חברות)
|
||||||
|
|
||||||
|
הסט המדויק (`ls .claude/agents/`): `HEARTBEAT.md`, `hermes-curator.md`, `legal-analyst.md`,
|
||||||
|
`legal-ceo.md`, `legal-exporter.md`, `legal-proofreader.md`, `legal-qa.md`, `legal-researcher.md`,
|
||||||
|
`legal-writer.md`. התפקיד נלקח מה-frontmatter של כל קובץ; עמודת "ספ לקרוא" מקשרת תפקיד לקבצי-הספ
|
||||||
|
שהוא אוכף/צורך.
|
||||||
|
|
||||||
|
| סוכן (קובץ) | תפקיד (מה-frontmatter) | ספ-תחום לקרוא לפני פעולה |
|
||||||
|
|-------------|------------------------|---------------------------|
|
||||||
|
| [legal-ceo.md](../../.claude/agents/legal-ceo.md) | מנהל תהליך כתיבת החלטות, מתזמר סוכנים, מפקח על התקדמות | **00 + כל הספ** (מתזמר → צריך תמונה מלאה); ניתוב comments → [X3 §1ב](X3-integration-deploy.md) |
|
||||||
|
| [legal-proofreader.md](../../.claude/agents/legal-proofreader.md) | מגיה — תיקון שגיאות OCR בטקסט עברי לפני ניתוח | [01-ingest.md](01-ingest.md) (קליטה/טקסט-מחולץ) |
|
||||||
|
| [legal-researcher.md](../../.claude/agents/legal-researcher.md) | חוקר תקדימים — פסיקה, מיפוי תכניות, סיכום פרוטוקולים | [03-retrieval.md](03-retrieval.md) (3 קורפוסים, hybrid/RRF, attribution); קליטת-פסיקה → [01-ingest.md](01-ingest.md) |
|
||||||
|
| [legal-analyst.md](../../.claude/agents/legal-analyst.md) | מנתח משפטי — חילוץ טענות, ניתוח אסטרטגי, שאלות מחקר | [02-data-model.md](02-data-model.md) + [03-retrieval.md](03-retrieval.md) + [04-analysis-writing.md](04-analysis-writing.md) |
|
||||||
|
| [legal-writer.md](../../.claude/agents/legal-writer.md) | כותב — כתיבת בלוקי ההחלטה בסגנון דפנה תמיר | [04-analysis-writing.md](04-analysis-writing.md) + [05-qa-review.md](05-qa-review.md) (כותב מול שערי-QA) |
|
||||||
|
| [legal-qa.md](../../.claude/agents/legal-qa.md) | בודק איכות — שלמות, ניטרליות, כיסוי טענות, משקלות לפני ייצוא | [05-qa-review.md](05-qa-review.md) (שערי QA + שערים אנושיים) |
|
||||||
|
| [legal-exporter.md](../../.claude/agents/legal-exporter.md) | מייצא — בדיקה סופית, ייצוא DOCX, שמירה מגורסת | [06-export.md](06-export.md) (ייצוא DOCX לפי תבנית דפנה) |
|
||||||
|
| [hermes-curator.md](../../.claude/agents/hermes-curator.md) | Knowledge Curator (Hermes) — מנתח החלטות סופיות post-export, מציע עדכוני skills/lessons; read-only על תוכן, write רק על comments | [07-learning.md](07-learning.md) (Hermes · לקחים · לולאת פידבק) |
|
||||||
|
|
||||||
|
**הערות על הסט:**
|
||||||
|
|
||||||
|
- **CEO = נקודת-הניתוב היחידה.** תגובת-משתמש על issue מעירה את ה-CEO; הוא מחליט ניתוב ויוצר
|
||||||
|
issue לסוכן-המשנה — סוכן-משנה לא מקבל עבודה ישירות מ-comment ([X3 §1ב](X3-integration-deploy.md)).
|
||||||
|
- **Hermes — חיבור ישיר, לא דרך CEO.** מופעל מ"סמן כסופי" ב-UI (`mark-final` → `pc_wake_curator_for_final()`),
|
||||||
|
לא מ-CEO; ופועל על מודל `deepseek_local` (לא Claude Code) — ראה [X2 INV-MC1](X2-multi-company.md#inv-mc1-תצורת-סוכן-ב-master-מפושטת-ל-mirror--אין-drift-בין-החברות)
|
||||||
|
למלכודת ה-`adapter_type`-skip בסנכרון. הצעות ה-curator עוברות **אישור-יו"ר ידני** לפני commit
|
||||||
|
ל-`SKILL.md`/`lessons.md` — מופע של [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant).
|
||||||
|
- **company_id פר-סוכן.** כל שורה בטבלה מיוצגת פעמיים (CMP + CMPA); ה-CEO לכל חברה שונה
|
||||||
|
([X2 §1](X2-multi-company.md)). הסוכן פועל רק בטווח-החברה שלו ([X2 §2](X2-multi-company.md)).
|
||||||
|
|
||||||
|
### 2א. מפת-הרשאות (tool grants) — frontmatter מול הוראות
|
||||||
|
|
||||||
|
כל קובץ-סוכן מצהיר ב-frontmatter `tools:` (כולם: `Read/Bash/Grep/Glob` + תת-קבוצת `mcp__legal-ai__*`).
|
||||||
|
מפת-ההרשאות חייבת **לתאום** את מה שהוראות-הסוכן מצריכות ([X9 INV-TOOL6](X9-mcp-tool-contract.md), INV-AG3 להלן).
|
||||||
|
|
||||||
|
**סטטוס FU-13 — נסגר (2026-06-06):** GAP-46 טופל בהכרעת-יו"ר "היבריד". התברר שהפער שמופה ב-31.5
|
||||||
|
היה רחב מדי — הכלים יוחסו לפי *תיאור-התפקיד*, לא לפי ההוראות בפועל. ההכרעה:
|
||||||
|
|
||||||
|
| סוכן | מצב בפועל | פעולה ב-FU-13 |
|
||||||
|
|------|-----------|----------------|
|
||||||
|
| legal-researcher | כבר מעניק `extract_references` + `precedent_extract_halachot`/`precedent_extract_metadata`/`precedent_process_pending` (frontmatter) | ✅ אין פער — היה מיושן |
|
||||||
|
| legal-analyst | חסר `aggregate_claims_to_arguments`; הוראותיו לא השתמשו בו | ✅ נוסף ל-frontmatter + שלב 7 ב-"שלב 1" (קיבוץ טענות→טיעונים) |
|
||||||
|
|
||||||
|
`extract_references` / `extract_internal_citations` הם **מטלת-מחקר** (חילוץ ציטוטים/רפרנסים) ושייכים
|
||||||
|
ל-`legal-researcher` (שמחזיק אותם) — **לא** ל-`legal-analyst`, שמאמת פסיקה דרך *חיפוש* (§8א בקובץ-הסוכן),
|
||||||
|
לא חילוץ. לכן הוסרו מרשימת "החסרים" של ה-analyst (INV-AG3 "לא עודף").
|
||||||
|
|
||||||
|
→ [gap-audit GAP-46](gap-audit.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. סוכני-התהליך (תת-פרויקט 5) — סעיף שמור (RESERVED)
|
||||||
|
|
||||||
|
> **סטטוס: מתוכנן, טרם נבנה.** הסעיף הזה הוא **מקום שמור מכוון** עבור סוכני-התהליך שיוגדרו
|
||||||
|
> ב**תת-פרויקט 5** — הם **אינם קיימים כיום** ואין לטעות בהם כמופעלים. הם מתועדים כאן כדי
|
||||||
|
> שהמפה תהיה שלמה ושכיוון-העבודה יהיה ברור, לא כ-TODO פתוח.
|
||||||
|
|
||||||
|
בניגוד לסוכנים הדומייניים (סעיף 2) שמטפלים בתיקי-עררים, **סוכני-התהליך** הם סוכנים שיקראו את
|
||||||
|
ספ-המערכת (קבצי 00–07, X1–X5) ו"יעשו את שיעורי-הבית" — יפעלו על *המערכת עצמה*, לא על תיק. שלושה
|
||||||
|
תפקידים מתוכננים:
|
||||||
|
|
||||||
|
| סוכן-תהליך (מתוכנן) | תפקיד מיועד |
|
||||||
|
|----------------------|-------------|
|
||||||
|
| **add-feature** | הוספת יכולת חדשה — קורא את הספ הרלוונטי, מאתר את ה-invariants שחלים, ומיישם בלי לשבור G1–G11 |
|
||||||
|
| **fix-feature** | תיקון תקלה — מאתר את ה-invariant שהופֵר (מול [audit-report.md](../audit-report.md)) ומתקן במקור, לא בתסמין |
|
||||||
|
| **spec-guardian** | שמירת עקביות הספ — מאתר drift בין הקוד לספ ובין קבצי-הספ עצמם; סתירה = ממצא ל-audit |
|
||||||
|
|
||||||
|
ההגדרה המלאה (frontmatter, tools, instructions, מיפוי תפקיד→ספ, ושערי-האישור) **תיכתב בתת-פרויקט 5**.
|
||||||
|
עד אז — אין רשומות-סוכן, אין wakeup, ואין הסתמכות עליהם בזרימה.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Invariants של התחום (פרויקטלי-תפעולי)
|
||||||
|
|
||||||
|
### INV-AG1: כל סוכן קורא את החוקה תחילה, אז את ספ-התחום הרלוונטי — לפני פעולה
|
||||||
|
**כלל:** כל סוכן (דומייני או תהליך) **חייב** לקרוא את [00-constitution.md](00-constitution.md)
|
||||||
|
תחילה, ואז את ספ-התחום הרלוונטי לתפקידו (לפי הטבלה בסעיף 2), **לפני** שהוא פועל. ה-checklist
|
||||||
|
המשותף ב-HEARTBEAT מתבצע בכל ריצה; קריאת-הספ קודמת לעבודה המהותית. סוכן אינו פועל "מהזיכרון" —
|
||||||
|
המקור הקנוני להתנהגות הוא החוקה + ספ-התחום (מופע של [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)
|
||||||
|
— המערכת מסייעת תחת שערים אנושיים, והסוכן פועל בגבולות שהחוקה מגדירה).
|
||||||
|
**מקור-סמכות:** [HEARTBEAT.md](../../.claude/agents/HEARTBEAT.md) (checklist הפעלה משותף) +
|
||||||
|
קבצי-הסוכן תחת [.claude/agents/](../../.claude/agents/) (frontmatter + instructions) +
|
||||||
|
[00-constitution.md §7](00-constitution.md#7-אינדקס-הספ) (אינדקס הספ — איזה קובץ אוכף איזה invariant).
|
||||||
|
(invariant פרויקטלי-תפעולי — ללא פרוטוקול ≥3-המקורות; משרת את העיקרון הגלובלי G10.)
|
||||||
|
**אכיפה:** נוהל — **מחוּוט** (FU-8b, 2026-05-31): סעיף "קריאת-ספ — קודם החוקה (00), אז ספ-התחום"
|
||||||
|
ב-[HEARTBEAT.md](../../.claude/agents/HEARTBEAT.md) (כולל טבלת תפקיד→ספ) + סעיף "קרא לפני פעולה (INV-AG1)"
|
||||||
|
בכל אחד מ-8 קבצי-הסוכן. אכיפה **פרוצדורלית** (נוהל לפני עבודה), לא אוטומטית: אין שער-קוד שמכריח
|
||||||
|
את הקריאה — זה גלום בטבע ה-invariant (פרויקטלי-תפעולי, מבוצע ע"י הסוכן). ראה §5.
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-AG2: סוכן דומייני פועל רק בתחום-החברה שלו
|
||||||
|
**כלל:** סוכן דומייני מטפל **רק** בתיקי-החברה שלו לפי `$PAPERCLIP_COMPANY_ID` (CMP→1xxx;
|
||||||
|
CMPA→8xxx/9xxx). אסור ליצור פרויקט/issue/תוכן לתיק מחוץ לטווח; issue מחוץ-לטווח → סירוב מנומס
|
||||||
|
ב-comment + העֵרת ה-CEO של החברה הנכונה (מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
— הפרדה נאכפת לפי `company_id`, אין מסלולים חוצי-חברה מתפצלים; ראה [X2 §2](X2-multi-company.md)).
|
||||||
|
**מקור-סמכות:** [HEARTBEAT.md §1](../../.claude/agents/HEARTBEAT.md) (סינון-חברה — כלל-ברזל) +
|
||||||
|
קבצי-הסוכן (סעיף "סינון תיקים לפי חברה") + [X2-multi-company.md §2](X2-multi-company.md).
|
||||||
|
(invariant פרויקטלי-תפעולי — ללא פרוטוקול ≥3-המקורות; משרת את העיקרון הגלובלי G2.)
|
||||||
|
**אכיפה:** סינון-חברה ב-HEARTBEAT + גבול-חברה נאכף בצד-Paperclip (`Agent key cannot access
|
||||||
|
another company`, [X2 §2](X2-multi-company.md)).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-AG3: מפת-ההרשאות תואמת את הוראות-הסוכן — לא חסר ולא עודף
|
||||||
|
**כלל:** ה-frontmatter `tools:` של כל סוכן מעניק **בדיוק** את הכלים שהוראותיו דורשות — כל כלי שההוראות
|
||||||
|
מצריכות מוענק, וכלי שמוענק-ולא-בשימוש נבחן. מופע של [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)
|
||||||
|
(שערים מוגדרים) ו-[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים); מקביל ל-[X9 INV-TOOL6](X9-mcp-tool-contract.md).
|
||||||
|
**מקור-סמכות:** frontmatter `tools:` מול ה-instructions בקבצי-[.claude/agents/](../../.claude/agents/). (פרויקטלי-תפעולי.)
|
||||||
|
**אכיפה:** בדיקת-עקביות tools↔instructions (FU-13 ✅ 2026-06-06). אכיפה אוטומטית עתידית — בתת-פרויקט 5 (spec-guardian).
|
||||||
|
**הפרה ידועה:** — (טופל ב-FU-13: legal-analyst קיבל `aggregate_claims_to_arguments`; researcher כבר היה תקין; `extract_references`/`extract_internal_citations` הם מטלת-researcher, לא analyst — ראה §2א).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. חיווט הספ לסוכנים — בוצע (FU-8b)
|
||||||
|
|
||||||
|
עד FU-8b קבצי-הסוכן וה-HEARTBEAT **לא הפנו** לספ-המערכת במפורש; הם הפנו ל-CLAUDE.md, למסמכי-`docs/`
|
||||||
|
הישנים, ול-skills. **בוצע ב-2026-05-31 (FU-8b / GAP-23):**
|
||||||
|
|
||||||
|
- **HEARTBEAT.md:** נוסף סעיף עליון "קריאת-ספ — קודם החוקה (00), אז ספ-התחום — לפני פעולה מהותית
|
||||||
|
(INV-AG1)", **לפני** §0–§8 התפעוליים, ובו טבלת תפקיד→ספ (זהה לסעיף 2 כאן). זה ממקם את קריאת-החוקה
|
||||||
|
קודם ל-checklist ההפעלה ("קודם החוקה (00) + ספ-התחום, אז ה-HEARTBEAT התפעולי").
|
||||||
|
- **8 קבצי-הסוכן:** כל אחד קיבל סעיף "קרא לפני פעולה (INV-AG1)" בראש גוף-הקובץ — קריאת
|
||||||
|
`00-constitution.md` תחילה, ואז ספ-התחום הרלוונטי לתפקידו (לפי הטבלה בסעיף 2).
|
||||||
|
- **אופי האכיפה:** פרוצדורלית (נוהל), לא שער-קוד — ראה INV-AG1 "אכיפה".
|
||||||
|
|
||||||
|
זהו תנאי-מוקדם לסוכני-התהליך (סעיף 3), שכל עבודתם היא "לקרוא את הספ ולעשות שיעורי-בית".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md) — [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)
|
||||||
|
(שערים אנושיים) + [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
(מקור-אמת/הפרדה) + [§7 אינדקס הספ](00-constitution.md#7-אינדקס-הספ).
|
||||||
|
- [X2-multi-company.md](X2-multi-company.md) — 14 סוכנים = 7 × 2, `company_id` פר-סוכן, כללי sync.
|
||||||
|
- [X3-integration-deploy.md](X3-integration-deploy.md) — wakeup, ניתוב comments דרך CEO, webhooks.
|
||||||
|
- ספ-התחום שכל סוכן צורך: [01-ingest.md](01-ingest.md), [02-data-model.md](02-data-model.md),
|
||||||
|
[03-retrieval.md](03-retrieval.md), [04-analysis-writing.md](04-analysis-writing.md),
|
||||||
|
[05-qa-review.md](05-qa-review.md), [06-export.md](06-export.md), [07-learning.md](07-learning.md).
|
||||||
|
- [.claude/agents/HEARTBEAT.md](../../.claude/agents/HEARTBEAT.md) + קבצי-הסוכן תחת
|
||||||
|
[.claude/agents/](../../.claude/agents/) — frontmatter (תפקיד) + instructions (סינון-חברה, זרימה).
|
||||||
|
- [X9-mcp-tool-contract.md](X9-mcp-tool-contract.md) — חוזה-הכלים שההרשאות (INV-AG3 / §2א) מעניקות.
|
||||||
|
- [skills/](../../skills/) — 5 skills (decision, assistant, docx, dafna-decision-template, new-company-setup); עקביות-skills↔סוכן + dedup → FU-13.
|
||||||
163
docs/spec/X5-audit-provenance.md
Normal file
163
docs/spec/X5-audit-provenance.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# X5 — Audit-Trail ועקיבוּת-מקור (Provenance)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) ומגדיר את **חוזה העקיבוּת וה-audit-trail (TARGET)**
|
||||||
|
של עוזר משפטי: (א) כל **תוצר מסיוע-AI** (בלוק-טיוטה, תוצאת-אחזור, הצעת-curator) מתעד **מה הפיק אותו**
|
||||||
|
(מקורות/נתונים/מודל); (ב) כל **סמכות מצוטטת** בהחלטה **פתירה חזרה לקורפוס**; (ג) **שלמות-הרשומה
|
||||||
|
לאורך זמן** — החלטה/רשומה שלמה ובלתי-משתנה אלא דרך **שינויים עקיבים ומיוחסים** (היסטוריית git +
|
||||||
|
Track Changes). הקובץ אוכף את
|
||||||
|
[INV-G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai) (עקיבוּת + audit-trail) ואת
|
||||||
|
[INV-G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query) (attribution באחזור).
|
||||||
|
|
||||||
|
> **TARGET, לא תיאור-מצב.** היכן שהקוד בפועל סוטה מהיעד — מתועד כ-**audit-finding** ([§5](#5-current-vs-target--ממצאי-audit)),
|
||||||
|
> תסמין לתיקון, לא התנהגות תקינה. כל טענה על הקוד מצוטטת `file:line`.
|
||||||
|
|
||||||
|
כשל-השורש שהקובץ מייבש: **קיימים רכיבי-עקיבוּת נקודתיים** (commit git לפלטים · `model_used` לכל בלוק ·
|
||||||
|
`decision_paragraphs.citations` · גרף-ציטוטים · telemetry של חיפושים), אך **אין רשומת-provenance
|
||||||
|
מאוחדת מקצה-לקצה** שמקשרת בלוק-החלטה → קטעי-הקורפוס/הגנרציות שהפיקו אותו, ו**טבלת ה-`audit_log`
|
||||||
|
אינה מתועדת בפועל** לרוב פעולות ה-AI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. שלוש שכבות העקיבוּת (TARGET)
|
||||||
|
|
||||||
|
| שכבה | מה צריך להירשם | היכן (קיים / יעד) |
|
||||||
|
|------|-----------------|---------------------|
|
||||||
|
| **A — provenance של תוצר-AI** | לכל בלוק-טיוטה/תוצאת-אחזור/הצעת-curator: מודל, סוג-גנרציה, וקטעי-המקור (chunks/precedents) שהוזנו | קיים חלקית: `decision_blocks.model_used/generation_type/temperature` (`db.py:326-328`); **חסר** קישור בלוק→קטעי-מקור |
|
||||||
|
| **B — עקיבוּת ציטוט→קורפוס** | כל סמכות מצוטטת פתירה ל-`case_law_id`/`document_id` + locator | קיים: `decision_paragraphs.citations` JSONB `[{case_law_id,text,type}]` (`db.py:343`); גרף `precedent_internal_citations` (`db.py:937-947`) |
|
||||||
|
| **C — שלמות-רשומה לאורך זמן** | החלטה/מסמך שלם ובלתי-משתנה אלא דרך שינוי עקיב ומיוחס | קיים: commit git לכל פלט (`git_sync.commit_and_push`); Track Changes ב-revisions ([06-export §3](06-export.md#3-רישום-הגרסה--active_draft_path--git)) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. רכיבי-העקיבוּת הקיימים (מאומת `file:line`)
|
||||||
|
|
||||||
|
1. **קיבוע-פלט ב-git.** כל כתיבת-DOCX/עדכון-תיק מקובעת בהיסטוריית-git של תיקיית-התיק:
|
||||||
|
`export_docx` (`drafting.py:408`), `export_interim_draft` (`drafting.py:536`),
|
||||||
|
`apply_user_edit` (`drafting.py:582`), `revise_draft` (`drafting.py:695`), עדכון-תיק
|
||||||
|
(`cases.py:387`), הוספת-מסמך (`documents.py:86`) — כולם `git_sync.commit_and_push(...)`
|
||||||
|
(`git_sync.py:75`). זו שכבת ה-audit-trail של **שלמות-הפלט** (שכבה C).
|
||||||
|
2. **provenance של מודל לכל בלוק.** `decision_blocks` נושא `model_used` / `generation_type` /
|
||||||
|
`temperature` (`db.py:326-328`), הנכתבים ב-upsert של ה-block-writer
|
||||||
|
(`block_writer.py:1017-1034`, `_build_result` `:400-407`). מתעד **איזה מודל** הפיק את הבלוק
|
||||||
|
(שכבה A — חלקי).
|
||||||
|
3. **עקיבוּת ציטוט ברמת-סעיף.** `decision_paragraphs.citations` (`db.py:343`) שומר
|
||||||
|
`[{case_law_id, text, type}]` — כל ציטוט בסעיף מצביע ל-`case_law` (שכבה B). telemetry
|
||||||
|
ממנף זאת ל-"cited == relevant" (`telemetry.py:18-23`).
|
||||||
|
4. **גרף-ציטוטים פנימי.** `precedent_internal_citations` (`db.py:937-947`) רושם קשת
|
||||||
|
החלטה→החלטה מצוטטת (resolved ל-`case_law` או stub); נחשף דרך `extract_internal_citations` /
|
||||||
|
`list_internal_citations` / `list_incoming_citations` (`citations.py:40,81,112`).
|
||||||
|
ON CONFLICT DO NOTHING → idempotent (`citations.py:54`).
|
||||||
|
5. **locator פתיר בכל תוצאת-אחזור.** כל span מוחזר נושא מזהה-מקור + locator
|
||||||
|
([03-retrieval INV-RET5](03-retrieval.md#inv-ret5-כל-span-מוחזר-עקיב-למקורו), `search.py:77-86,322-343`);
|
||||||
|
הלכות נושאות `supporting_quote` (`db.py:652`) + `page_number` (`db.py:631,711,729`).
|
||||||
|
6. **telemetry של חיפושים.** `telemetry.log_search_bg` (ב-search.py) → מפעיל את `log_search` האסינכרוני → `search_logs`
|
||||||
|
(`telemetry.py:105,161`, `search.py:62,118,190,271`) רושם query/practice_area/top_case_law_ids —
|
||||||
|
תצפית על מה נשלף, fire-and-forget (`telemetry.py:8-12,100-101`).
|
||||||
|
7. **לקחים ופידבק מיוחסים.** `decision_lessons.source` (`db.py:208`: manual/curator/chair/
|
||||||
|
style_analyzer) ו-`chair_feedback.lesson_extracted`/`applied_to` (`db.py:458-459`) מתעדים את
|
||||||
|
**מקור** הלקח ([07-learning.md](07-learning.md)).
|
||||||
|
8. **טבלת `audit_log` (פעולה כללית).** `log_action(action, case_id, document_id, details, user)` (עמודת-DB: `actor`)
|
||||||
|
(`audit.py:18-44`) → `audit_log` (`db.py:159-167`, אינדקסים `:168-170`). קיימת, אך נכתבת
|
||||||
|
כיום כמעט-ורק ב-`case_subtype_override` (`cases.py:203`) — ראה [§5](#5-current-vs-target--ממצאי-audit).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-AUD1: כל תוצר מסיוע-AI מתעד את ה-provenance שלו (→G9)
|
||||||
|
**כלל:** כל תוצר שנוצר בסיוע-AI — בלוק-טיוטה, תוצאת-אחזור, הצעת-curator — **רושם את מקורו**:
|
||||||
|
**איזה מודל** הפיק אותו, **באיזה סוג-גנרציה**, ו**אילו קטעי-מקור** (chunks/precedents/מסמכי-תיק)
|
||||||
|
הוזנו אליו. הרשומה ניתנת-לביקורת בדיעבד (מי/מתי/ממה).
|
||||||
|
**מקורות:** Council of Europe / CEPEJ — *European Ethical Charter on AI in judicial systems*
|
||||||
|
(2018, transparency/traceability + user-control) · NCSC/JTC — *Principles & Practices for AI Use
|
||||||
|
in Courts* (auditable AI output) · ISO 15489-1:2016 (records authenticity — metadata about
|
||||||
|
creation) | סטטוס: verified
|
||||||
|
**אכיפה:** `decision_blocks.model_used/generation_type/temperature` בכל upsert של בלוק
|
||||||
|
(`block_writer.py:1017-1034`); telemetry על כל חיפוש (`telemetry.py:105`); **יעד נוסף:** קישור
|
||||||
|
מפורש בלוק→קטעי-מקור (provenance edges) + כתיבת `audit_log.log_action` לכל גנרציה. אוכף את
|
||||||
|
[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai).
|
||||||
|
**הפרה ידועה (GAP):** ה-provenance קיים **חלקית** — `model_used` נרשם לכל בלוק, וה-commit ב-git
|
||||||
|
מקבע פלטים, אך **אין רשומה מאוחדת** שמקשרת בלוק-החלטה לקטעי-הקורפוס/הגנרציות שהזינו אותו, וטבלת
|
||||||
|
`audit_log` כמעט-ולא נכתבת לפעולות-AI (רק `case_subtype_override`, `cases.py:203`) → יעד
|
||||||
|
([§5](#5-current-vs-target--ממצאי-audit)).
|
||||||
|
|
||||||
|
### INV-AUD2: רשומה שמורה שלמה ובלתי-משתנה אלא דרך שינוי עקיב ומיוחס (→G9, שלמות-רשומה)
|
||||||
|
**כלל:** החלטה/רשומה שמורה היא **שלמה ובלתי-משתנה** — כל שינוי בה נעשה רק דרך **מנגנון עקיב
|
||||||
|
ומיוחס** (commit git עם הודעה + actor, או Track Changes מיוחסות), ולא דרך דריסה שקטה. ניתן
|
||||||
|
לשחזר את מצב-הרשומה בכל נקודת-זמן ולזהות מי שינה מה ומתי.
|
||||||
|
**מקורות:** ISO 15489-1:2016 (§5.2.2 — integrity: records protected against unauthorized
|
||||||
|
alteration; אמינות/שלמות-רשומה) · Council of Europe / CEPEJ (2018, traceability) · DAMA-UK —
|
||||||
|
*Six Primary Dimensions for Data Quality* (2013, consistency/integrity over time) | סטטוס: verified
|
||||||
|
**אכיפה:** קיבוע git לכל פלט (`git_sync.commit_and_push` — `drafting.py:408,536,582,695`;
|
||||||
|
`cases.py:387`; `documents.py:86`) עם הודעה תיאורית; Track Changes ב-revisions עוקבות
|
||||||
|
([06-export §3](06-export.md#3-רישום-הגרסה--active_draft_path--git)); `decision_blocks` עם מפתח
|
||||||
|
קנוני `UNIQUE(decision_id, block_id)` (`db.py:333`) ו-`updated_at`. אוכף את
|
||||||
|
[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai).
|
||||||
|
**הפרה ידועה:** עריכת-DOCX (`revise_draft`/`apply_user_edit`) הופכת את `active_draft_path` למקור-
|
||||||
|
בפועל **בלי לעדכן את בלוקי-ה-DB חזרה** — הנתון-הנגזר זוחל למקור-אמת ושלמות ה-DB מול המסמך-החי
|
||||||
|
נחלשת ([06-export INV-EX1](06-export.md#inv-ex1-ייצוא-דטרמיניסטי-ומשוחזר-מהבלוקים--docx-הוא-נתון-נגזר-g2)) → ממצא ל-[audit](../audit-report.md).
|
||||||
|
|
||||||
|
### INV-AUD3: כל סמכות מצוטטת פתירה חזרה לקורפוס (→G5)
|
||||||
|
**כלל:** כל סמכות-משפטית המצוטטת בהחלטה (פסק-דין, הלכה, מסמך-תיק) **פתירה לרשומת-מקור בקורפוס**
|
||||||
|
דרך locator יציב — `case_law_id`/`document_id` + מזהה-עמוד/chunk/quote. ציטוט שאינו פתיר אינו
|
||||||
|
תקין; הוא נחסם או מסומן לאימות-יו"ר. זהו צד-ה-attribution של [INV-RET5](03-retrieval.md#inv-ret5-כל-span-מוחזר-עקיב-למקורו).
|
||||||
|
**מקורות:** Pinecone — *Implement multitenancy* (metadata-locator לכל פריט מואנדקס) · RAG
|
||||||
|
attribution (Lewis et al., 2020, NeurIPS — pinned/non-leaking provenance) · ISO 8000 (Data
|
||||||
|
quality — completeness/identifiability) | סטטוס: verified
|
||||||
|
**אכיפה:** `decision_paragraphs.citations` `[{case_law_id,text,type}]` (`db.py:343`); גרף
|
||||||
|
`precedent_internal_citations` (`db.py:937-947`) פותר ציטוט ל-`case_law` קיים או שומר stub;
|
||||||
|
פורמטרי-האחזור מצרפים מזהה+locator (`search.py:77-86,322-343`). אוכף את
|
||||||
|
[G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query).
|
||||||
|
**הפרה ידועה (GAP):** הקישור קיים ברמת-הסעיף (`decision_paragraphs.citations`), אך **אין אכיפה**
|
||||||
|
שכל ציטוט בטקסט-הבלוק אכן מקושר לרשומת-קורפוס; ציטוט שהמודל ייצר בלי locator יכול לעבור בלי
|
||||||
|
חסימה אוטומטית — אימות נשען על שער-היו"ר ([05-qa-review](05-qa-review.md)) → יעד.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. רשומת-ה-provenance המאוחדת (TARGET)
|
||||||
|
|
||||||
|
היעד שמאחד את שלוש השכבות: לכל **בלוק-החלטה** נשמר, מעבר ל-`model_used` הקיים, **קישור לקטעי-
|
||||||
|
המקור** שהוזנו לגנרציה (chunk-ids/`case_law_id`s שהוחזרו מהאחזור והוצגו ל-writer) — כך שניתן לענות
|
||||||
|
"מאיזו פסיקה/מסמך נולד המשפט הזה?". המנגנון הקנוני המוצע: כתיבת `audit_log.log_action`
|
||||||
|
(`audit.py:18`) בכל גנרציה (`action="write_block"`, `details={model, generation_type, source_chunk_ids,
|
||||||
|
retrieved_case_law_ids}`) — הטבלה כבר תומכת ב-`details JSONB` + `actor` + `case_id`/`document_id`
|
||||||
|
(`db.py:159-167`). זה ממיר את ה-audit_log מ"כמעט-ריק" ל-audit-trail מקצה-לקצה, בלי טבלה חדשה
|
||||||
|
(תואם כלל-ההנדסה "סימטריה" — הרחבת מסלול קיים, [חוקה §6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Current vs Target — ממצאי-audit
|
||||||
|
|
||||||
|
ההבדלים בין הקוד בפועל ל-TARGET. **אלו תסמינים, לא התנהגויות תקינות.** כל פריט אומת מול הקוד.
|
||||||
|
|
||||||
|
- **`audit_log` קיימת אך כמעט-ולא נכתבת (INV-AUD1).** `log_action` (`audit.py:18-44`) ו-טבלת
|
||||||
|
`audit_log` (`db.py:159-167`) מוכנות, אך הקריאה היחידה בפועל היא `case_subtype_override`
|
||||||
|
(`cases.py:203`) — אין רישום ל-`upload`/`extract_claims`/`write_block`/`export` (למרות ש-docstring
|
||||||
|
של `log_action` מונה אותם, `audit.py:28`). **תסמין:** אין audit-trail אחיד "מי עשה מה מתי" לרוב
|
||||||
|
פעולות-ה-AI. **יעד:** קריאת `log_action` בכל פעולה משנה-מצב, כולל גנרציות.
|
||||||
|
- **אין קישור בלוק→קטעי-מקור (INV-AUD1).** `decision_blocks` מתעד `model_used`/`generation_type`
|
||||||
|
(`db.py:326-327`) אך **לא** את ה-chunks/precedents שהוזנו לגנרציה. **תסמין:** אי-אפשר לשחזר מאיזו
|
||||||
|
פסיקה/מסמך נגזר בלוק ספציפי. **יעד:** רשומת-provenance מאוחדת ([§4](#4-רשומת-ה-provenance-המאוחדת-target)).
|
||||||
|
- **ציטוט→קורפוס לא נאכף אוטומטית (INV-AUD3).** `decision_paragraphs.citations` (`db.py:343`)
|
||||||
|
תומך בקישור, אך אין בדיקה שכל ציטוט בטקסט אכן פתיר ל-`case_law`. **תסמין:** ציטוט שהמודל ייצר בלי
|
||||||
|
locator יכול לעבור. **יעד:** ולידציה שכל citation בעלת `case_law_id` פתיר, אחרת flag לאימות-יו"ר.
|
||||||
|
- **שלמות ה-DB מול ה-DOCX-החי נחלשת אחרי עריכה (INV-AUD2).** אחרי `revise_draft`/`apply_user_edit`,
|
||||||
|
`active_draft_path` הופך מקור-בפועל בלי re-sync לבלוקים (`db.py:189`;
|
||||||
|
[06-export INV-EX1](06-export.md#inv-ex1-ייצוא-דטרמיניסטי-ומשוחזר-מהבלוקים--docx-הוא-נתון-נגזר-g2)).
|
||||||
|
**יעד:** re-sync מהבלוקים או חוזה מפורש + health-check לגילוי drift.
|
||||||
|
- **telemetry בולעת שגיאות בשתיקה (תיעוד, לא הערכה).** `log_search` swallow מכוון
|
||||||
|
(`telemetry.py:100-101`) כדי שלא להפיל חיפוש — תקין כ-fire-and-forget, אך אינו audit-trail
|
||||||
|
מהימן (רשומה עלולה ללכת לאיבוד בשקט). תואם את העיקרון "אין בליעה שקטה" רק כי זו telemetry-תצפית,
|
||||||
|
לא רשומת-שלמות; ה-audit-trail המהימן הוא git ([§2.1](#2-רכיבי-העקיבוּת-הקיימים-מאומת-fileline)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. הפניות-אחיות
|
||||||
|
|
||||||
|
- [00-constitution.md](00-constitution.md) — [INV-G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai)
|
||||||
|
(עקיבוּת + audit-trail) · [INV-G5](00-constitution.md#inv-g5-metadata-מלא--הפרדת-קורפוס-נאכפת-בכל-query) (attribution).
|
||||||
|
- [03-retrieval.md](03-retrieval.md#inv-ret5-כל-span-מוחזר-עקיב-למקורו) — INV-RET5 (locator פתיר בכל span — בסיס ל-INV-AUD3).
|
||||||
|
- [06-export.md](06-export.md#inv-ex2-עקיבוּת-מקור-נשמרת-בהחלטה-המיוצאת-g9) — INV-EX2 (עקיבוּת בפלט) + commit git (INV-AUD2).
|
||||||
|
- [05-qa-review.md](05-qa-review.md) — שער-היו"ר שמאמת ציטוטים (משלים את INV-AUD3).
|
||||||
|
- [02-data-model.md](02-data-model.md) — `decision_blocks`/`decision_paragraphs`/`case_law` (הישויות שעליהן נשמרת ה-provenance).
|
||||||
|
- [07-learning.md](07-learning.md) — `decision_lessons.source` + `chair_feedback` (מקור הלקחים).
|
||||||
|
- [01-ingest.md](01-ingest.md) — קליטה שמייצרת את הקטעים שאליהם פותרים ציטוטים.
|
||||||
108
docs/spec/X6-ui-api-contract.md
Normal file
108
docs/spec/X6-ui-api-contract.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# X6 — חוזה UI↔API וכללי-עיצוב הממשק (UI↔API Contract & Design Rules)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) והוא ה-deep-dive על **הממשק (web-ui) וחוזה
|
||||||
|
ה-API בינו לבקאנד** — שלא היה מכוסה בספ עד כה. הוא מגדיר: (א) חוזה-הקשר פרונט↔בק (OpenAPI כ-SSoT,
|
||||||
|
מודלי-תשובה, envelope, SSE, טיפול-שגיאות); (ב) **כללי-עיצוב הממשק** — מקור-אמת יחיד ל-enums/תוויות,
|
||||||
|
helpers משותפים, וחוזה-טופס לכל סוג-מסמך. הממצאים בפועל מתועדים ב-[ui-audit.md](ui-audit.md).
|
||||||
|
|
||||||
|
> **שני סוגי invariant כאן.** UI1–UI5 הם **הנדסיים** (חוזה-API/קליינט כללי — ≥3 מקורות + סטטוס).
|
||||||
|
> UI6 (חוזה-טופס) הוא **פרויקטלי-תפעולי**, נגזר מ-[X8](X8-field-provenance.md), ומשרת
|
||||||
|
> [G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. ארכיטקטורה קיימת
|
||||||
|
|
||||||
|
- **web-ui** — Next.js 16 + TS + Tailwind v4 + shadcn + TanStack Query. 13 דפים (ראה [ui-audit.md](ui-audit.md)).
|
||||||
|
- **Proxy** — [next.config.ts](../../web-ui/next.config.ts): `/api/*` → `NEXT_PUBLIC_API_ORIGIN` (ברירת-מחדל `http://127.0.0.1:8000`); `/openapi.json` → schema של ה-FastAPI.
|
||||||
|
- **לקוח** — [client.ts](../../web-ui/src/lib/api/client.ts): `apiRequest<T>` + `ApiError` + `makeQueryClient`. 18 מודולי-API.
|
||||||
|
- **טיפוסים** — [types.ts](../../web-ui/src/lib/api/types.ts) (auto-gen `openapi-typescript`, 124 operations). `npm run api:types`.
|
||||||
|
- **SSE** — [sse.ts](../../web-ui/src/lib/sse.ts): `openSSE` (progress של העלאות/עיבוד).
|
||||||
|
- **בקאנד** — [web/app.py](../../web/app.py): 143 endpoints, מונוליטי, **~60% ללא Pydantic response model**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-UI1: ה-OpenAPI schema הוא ה-SSoT לחוזה — טיפוסי-לקוח נגזרים, לא ידניים-סוטים
|
||||||
|
**כלל:** חוזה ה-API מוגדר **פעם אחת** ב-OpenAPI (שמופק מהבקאנד); טיפוסי-ה-frontend **נגזרים** ממנו
|
||||||
|
(`openapi-typescript`), ואינם מתוחזקים ידנית במקביל. אין "טיפוס-מראה" מקומי שמשכפל endpoint וסוטה ממנו.
|
||||||
|
מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) (מקור-אמת יחיד).
|
||||||
|
**מקורות:** OpenAPI Specification 3.1 (single contract / source of truth; JSON-Schema 2020-12)
|
||||||
|
(https://spec.openapis.org/oas/latest.html) · Pact — *consumer-driven contract testing*
|
||||||
|
(https://docs.pact.io/) · Speakeasy — *Pact vs OpenAPI* (provider-driven SSoT)
|
||||||
|
(https://www.speakeasy.com/blog/pact-vs-openapi) | סטטוס: verified
|
||||||
|
**אכיפה:** `npm run api:types` ב-CI; איסור טיפוסי-מראה ידניים. **כיום אין** — ה-frontend מתחזק טיפוסים ידניים.
|
||||||
|
**הפרה ידועה:** [cases.ts:1-9](../../web-ui/src/lib/api/cases.ts) מתעד מפורשות שה-`/api/cases` מחזיר `unknown`
|
||||||
|
ולכן מוחזק טיפוס `CaseDetail` ידני; `PracticeArea` מוגדר ב-3 מקומות עם ערכים שונים ([ui-audit.md](ui-audit.md), [gap-audit GAP-30/31](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-UI2: לכל endpoint נצרך — response model מפורש (חוזה-שלמות API)
|
||||||
|
**כלל:** כל endpoint שה-UI צורך נושא **response model מפורש** (Pydantic), כך ש-OpenAPI מפיק טיפוס אמיתי
|
||||||
|
(לא `unknown`/`object`). זהו פאֶט של [G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש) (שלמות-חוזה לפני צריכה).
|
||||||
|
**מקורות:** OpenAPI 3.1 (schema objects) · Zalando *RESTful API Guidelines* (explicit schemas)
|
||||||
|
(https://opensource.zalando.com/restful-api-guidelines/) · FastAPI *Response Model* docs
|
||||||
|
(https://fastapi.tiangolo.com/tutorial/response-model/) | סטטוס: verified
|
||||||
|
**אכיפה:** linter/CI שמסמן endpoint נצרך ללא response_model. **כיום אין** — ~60% מהendpoints ללא מודל.
|
||||||
|
**הפרה ידועה:** רוב ה-endpoints ב-[app.py](../../web/app.py) מחזירים dict חופשי → `unknown` ב-types.ts ([gap-audit GAP-30](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-UI3: envelope-תשובה ושגיאה עקבי על-פני ה-API
|
||||||
|
**כלל:** כל ה-endpoints חולקים **מבנה-תשובה ומבנה-שגיאה אחיד** (לא string-לפעמים-JSON-לפעמים). שגיאות
|
||||||
|
לפי תבנית סטנדרטית (Problem Details). מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים).
|
||||||
|
**מקורות:** RFC 9457 — *Problem Details for HTTP APIs*
|
||||||
|
(https://www.rfc-editor.org/rfc/rfc9457) · Zalando *RESTful API Guidelines* (consistent responses) ·
|
||||||
|
Microsoft *REST API Guidelines* (error structure)
|
||||||
|
(https://github.com/microsoft/api-guidelines) | סטטוס: verified
|
||||||
|
**אכיפה:** envelope משותף ב-app.py + handler-שגיאות גלובלי. **כיום אין** — מעורב string/JSON/`{error}`/`{detail}`.
|
||||||
|
**הפרה ידועה:** [search.py](../../web/app.py) מחזיר `"לא נמצאו תוצאות."` או JSON; חלק מהכלים `{error:...}`, חלק raise ([gap-audit GAP-32](gap-audit.md), [X9 INV-TOOL1](X9-mcp-tool-contract.md)).
|
||||||
|
|
||||||
|
### INV-UI4: אין בליעת-שגיאה ב-UI
|
||||||
|
**כלל:** כל מצב-שגיאה (fetch/mutation) **מוצג או מטופל מפורשות** — error boundary ו/או טיפול ב-`error`
|
||||||
|
של `useQuery`/`useMutation`. אין כשל שקט שמשאיר את המשתמש בלי משוב. תואם כלל "אין בליעה שקטה"
|
||||||
|
([חוקה §6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
|
||||||
|
**מקורות:** React docs — *Error Boundaries*
|
||||||
|
(https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) ·
|
||||||
|
TanStack Query — *Error handling* (https://tanstack.com/query/latest/docs/framework/react/guides/query-functions#handling-and-throwing-errors) ·
|
||||||
|
Nielsen Norman Group — *Error-Message Guidelines* (https://www.nngroup.com/articles/error-message-guidelines/) | סטטוס: verified
|
||||||
|
**אכיפה:** error boundary ברמת-האפליקציה + רכיב-שגיאה משותף; code-review. **כיום חלקי** — חלק מהדפים אינם
|
||||||
|
מטפלים ב-`error`; כרטיסי-שגיאה משוכפלים ולא-עקביים.
|
||||||
|
**הפרה ידועה:** [ui-audit.md](ui-audit.md) — כרטיס-שגיאה משוכפל ×3, fallback של SSE שמסתיר כישלון כ-"completed" ([gap-audit GAP-32/33](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-UI5: חוזה-SSE/progress עם terminal states מוגדרים
|
||||||
|
**כלל:** ערוץ ה-progress (SSE) נושא **terminal states מפורשים** (completed/failed/timeout). אין הנחת-השלמה
|
||||||
|
שקטה על timeout; אי-התאמות-TTL (frontend↔backend) נמנעות. נקשר ל-freshness ([G6](00-constitution.md#inv-g6-re-index-בכל-שינוי-תוכן)).
|
||||||
|
**מקורות:** WHATWG HTML — *Server-Sent Events / EventSource* (https://html.spec.whatwg.org/multipage/server-sent-events.html) ·
|
||||||
|
MDN — *Using server-sent events* (https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) ·
|
||||||
|
TanStack Query — *Important Defaults* (staleTime/refetch) (https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults) | סטטוס: verified
|
||||||
|
**אכיפה:** סכמת-אירוע SSE עם terminal state מפורש; יישור TTL. **כיום:** fallback של 10ש' מניח completed.
|
||||||
|
**הפרה ידועה:** [documents.ts:226-232](../../web-ui/src/lib/api/documents.ts) — timeout→`{status:"completed"}`; TTL 5ש' front מול 300ש' redis ([gap-audit GAP-33](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-UI6: חוזה-טופס מוצהר לכל סוג-מסמך + שיקוף מקור-המילוי
|
||||||
|
**כלל:** לכל סוג-מסמך (מסמך-תיק / פסיקה חיצונית / החלטה פנימית) יש **חוזה-טופס מוצהר** — אילו שדות,
|
||||||
|
חובה/רשות/אוטו/pending/editable — **נגזר מ-[X8](X8-field-provenance.md)**; וה-UI **משקף את מקור-המילוי**
|
||||||
|
(מסמן מה חולץ אוטומטית/ע"י-Opus מול מה שהיו"ר הזין), כדי שהיו"ר ידע מה לאמת. מופע של
|
||||||
|
[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai) (שקיפות-מקור). **invariant פרויקטלי-תפעולי.**
|
||||||
|
**מקור-סמכות:** [X8-field-provenance.md](X8-field-provenance.md) (טבלת-ה-provenance); feedback היו"ר.
|
||||||
|
**אכיפה:** רכיב-טופס נגזר-X8 + אינדיקציית "מולא-ע"י-Opus"/"ממתין"/`searchable`. **כיום אין** — שדות-Opus
|
||||||
|
מוצגים כשדות-עריכה רגילים ללא סימון.
|
||||||
|
**הפרה ידועה:** [precedents/[id]/page.tsx](../../web-ui/src/app/precedents/%5Bid%5D/page.tsx) — `summary`/`headnote`/`key_quote` ללא חיווי-מקור; אין חיווי `searchable` ([gap-audit GAP-36](gap-audit.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. כללי-עיצוב (Design Rules) — נגזרים מה-invariants
|
||||||
|
- **SSoT ל-enums/תוויות/tones:** כל enum (CaseStatus, PracticeArea, AppealSubtype, DocType, outcome) +
|
||||||
|
תוויותיו + צבעיו מוגדרים **פעם אחת** ונצרכים מיבוא — לא משוכפלים בין דפים/רכיבים (מופע UI1/G2).
|
||||||
|
- **helpers משותפים:** פירמוט-תאריך, builder ל-FormData (העלאות), רכיב-שגיאה, query-config (intervals) —
|
||||||
|
משותפים, לא מועתקים.
|
||||||
|
- **חוזי-טופס:** ראה INV-UI6 ([X8](X8-field-provenance.md)).
|
||||||
|
|
||||||
|
הממצאים הקונקרטיים (כפילויות, הגדרות-שגויות, redundancy) ב-[ui-audit.md](ui-audit.md); התיקון — **FU-10**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. הפניות-אחיות
|
||||||
|
- [ui-audit.md](ui-audit.md) — audit דף-אחר-דף (13 דפים) בתבנית-ה-gap.
|
||||||
|
- [X8-field-provenance.md](X8-field-provenance.md) — מקור-מילוי-שדות (בסיס ל-INV-UI6).
|
||||||
|
- [X7-paperclip-client-params.md](X7-paperclip-client-params.md) — חוזה-ה-API שהפלאגין צורך.
|
||||||
|
- [X9-mcp-tool-contract.md](X9-mcp-tool-contract.md) — חוזה-envelope מקביל בכלי-ה-MCP.
|
||||||
|
- [00-constitution.md](00-constitution.md) — [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים), [G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש), [G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai), כלל "אין בליעה שקטה" ([§6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
|
||||||
|
- [web-ui/next.config.ts](../../web-ui/next.config.ts), [client.ts](../../web-ui/src/lib/api/client.ts), [types.ts](../../web-ui/src/lib/api/types.ts), [sse.ts](../../web-ui/src/lib/sse.ts).
|
||||||
155
docs/spec/X7-paperclip-client-params.md
Normal file
155
docs/spec/X7-paperclip-client-params.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# X7 — לקוח-Paperclip ופרמטרי-חיבור (Paperclip Client & Connection Parameters)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) ומשלים את [X3](X3-integration-deploy.md):
|
||||||
|
בעוד X3 מתאר את **זרימות**-האינטגרציה (wakeup, ניתוב comments, webhook), קובץ זה הוא ה-deep-dive
|
||||||
|
על **שכבת-הלקוח והפרמטרים** — *איך* legal-ai מדבר עם Paperclip בקוד (אילו לקוחות, אילו מסלולים),
|
||||||
|
ועל **כל הפרמטרים המחברים** (מזהי-חברה/סוכן, env, מפתחות, `plugin_state`, גזירת `company_id`).
|
||||||
|
|
||||||
|
> **invariant פרויקטלי-תפעולי.** ה-invariants כאן הם עובדות על איך *מערכת זו* בנויה — אין להן
|
||||||
|
> סמכות חיצונית; מקור-הסמכות = ה-runbooks והקוד ([root CLAUDE.md](../../../CLAUDE.md),
|
||||||
|
> [legal-ai/CLAUDE.md](../../CLAUDE.md), [web/paperclip_api.py](../../web/paperclip_api.py),
|
||||||
|
> [web/paperclip_client.py](../../web/paperclip_client.py)). כל invariant **נקשר** ל-G גלובלי שהוא משרת —
|
||||||
|
> כאן בעיקר [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) (מסלול קנוני יחיד)
|
||||||
|
> ו-[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai) (עקיבוּת/audit), וכלל-ההנדסה "סימטריה" ([חוקה §6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. מצב קיים — שני לקוחות מקבילים
|
||||||
|
|
||||||
|
ל-legal-ai יש **שני לקוחות Paperclip שונים** שחיים בו-זמנית, וזהו מקור-השורש לרוב הפערים כאן:
|
||||||
|
|
||||||
|
| לקוח | קובץ | אופי | מה מנהל |
|
||||||
|
|------|------|------|---------|
|
||||||
|
| "current" (API) | [web/paperclip_api.py](../../web/paperclip_api.py) | HTTP דרך `pc_request` + board API key | webhooks יוצאים, wakeup חלקי |
|
||||||
|
| "legacy" (DB-ישיר) | [web/paperclip_client.py](../../web/paperclip_client.py) | **חיבור psql ישיר** ל-DB של Paperclip + API | projects, issues, comments, wakeup, queries |
|
||||||
|
|
||||||
|
[legal-ai/CLAUDE.md](../../CLAUDE.md) מתעד ש-`paperclip_client.py` הוא "legacy — השתמש ב-paperclip_api.py",
|
||||||
|
אך בפועל ה-legacy עדיין מבצע את **רוב העבודה הכבדה** (יצירת תיקים/issues, comments, wakeup-ים),
|
||||||
|
וחלקו דרך **`INSERT`/`SELECT` ישיר** ל-DB של Paperclip — מסלול-מקביל לעוקף את ה-API.
|
||||||
|
|
||||||
|
זוהי בדיוק התבנית ש-[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) אוסר:
|
||||||
|
שני מסלולי-קוד מקבילים ליכולת אחת (גישה ל-Paperclip), שמתפצלים ועלולים לסטות.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. הפרמטרים המחברים (Connection Parameters)
|
||||||
|
|
||||||
|
### 2א. משתני-סביבה
|
||||||
|
| Var | קורא | ברירת-מחדל | סוד? |
|
||||||
|
|-----|------|-----------|------|
|
||||||
|
| `PAPERCLIP_API_URL` | [paperclip_api.py](../../web/paperclip_api.py) | `http://localhost:3100` | לא |
|
||||||
|
| `PAPERCLIP_BOARD_API_KEY` | paperclip_api.py / paperclip_client.py | `""` | **כן** (board key long-lived, לא JWT) |
|
||||||
|
| `PAPERCLIP_DB_URL` | [paperclip_client.py:21](../../web/paperclip_client.py), [app.py:3789](../../web/app.py) | `postgresql://paperclip:paperclip@127.0.0.1:54329/paperclip` | **כן — creds בתוך ברירת-המחדל** |
|
||||||
|
| `PAPERCLIP_COMPANY_ID` | [app.py:3976](../../web/app.py) | `42a7acd0-...` (CMP, hardcoded) | לא |
|
||||||
|
| `legalApiBaseUrl` | plugin (instance config) | `http://localhost:8085` | לא |
|
||||||
|
|
||||||
|
> ראה גם [X10-deploy-env-secrets.md](X10-deploy-env-secrets.md) — חוזה-ה-env המלא וטיפול-הסודות.
|
||||||
|
|
||||||
|
### 2ב. מזהים קשיחים בקוד (hardcoded) — סתירה ל-X3
|
||||||
|
[paperclip_client.py:36-62](../../web/paperclip_client.py) מכיל **מזהי-חברה וסוכן קשיחים**:
|
||||||
|
- `COMPANIES["licensing"] = "42a7acd0-..."` (CMP), `COMPANIES["betterment"] = "8639e837-..."` (CMPA)
|
||||||
|
- CEO/curator/analyst UUIDs לכל חברה (CMP CEO `752cebdd-...`, וכו').
|
||||||
|
- ה-plugin ([worker.ts](../../../plugin-legal-ai/src/worker.ts)) מכיל CEO IDs קשיחים משלו.
|
||||||
|
|
||||||
|
זו **סתירה ישירה** ל-[X3 §1א](X3-integration-deploy.md) הקובע "מזהה-ה-CEO נגזר מ-`$PAPERCLIP_COMPANY_ID`,
|
||||||
|
**לעולם לא UUID hardcoded**". הסתירה מתועדת כממצא ([gap-audit GAP-26](gap-audit.md), וכן GAP-56 ב-X10).
|
||||||
|
|
||||||
|
### 2ג. `plugin_state` keys (חוזה הקישור Paperclip↔legal-ai)
|
||||||
|
| `scope_kind` | `state_key` | ערך | משמעות |
|
||||||
|
|--------------|-------------|-----|--------|
|
||||||
|
| `issue` | `legal-case-number` | מספר-תיק | קישור issue→תיק |
|
||||||
|
| `issue` | `precedent-case-law-id` | case_law_id | קישור issue→פסיקה לחילוץ |
|
||||||
|
| `instance` | `webhook-idem-{requestId}` | timestamp | guard idempotency 5 דק' (inbound) |
|
||||||
|
|
||||||
|
### 2ד. גזירת `company_id` — שתי דרכים שונות
|
||||||
|
- **app.py**: נגזר מ-prefix מספר-התיק (`1`→licensing, `8/9`→betterment) ([X3 §1ג](X3-integration-deploy.md)).
|
||||||
|
- **paperclip_client.py**: מ-`_FALLBACK_APPEAL_TYPE_TO_COMPANY` (מיפוי tag→company) + lookup ב-DB.
|
||||||
|
|
||||||
|
שתי דרכי-גזירה לאותו ערך = drift פוטנציאלי ([gap-audit GAP-27](gap-audit.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. צד נכנס (Inbound) — הפלאגין
|
||||||
|
|
||||||
|
[plugin-legal-ai/src/worker.ts](../../../plugin-legal-ai/src/worker.ts) (לא בריפו זה) קורא ל-legal-ai דרך
|
||||||
|
`legalApiBaseUrl`. שלושה סוגי-משטח, שכולם חוזה-API שאינו מתועד היום ב-[X6](X6-ui-api-contract.md):
|
||||||
|
- **16 כלי `legal_*`** — עוטפים endpoints של `/api/cases/...`, `/api/search`, וכו'.
|
||||||
|
- **`onWebhook`** — מקבל את ה-webhook היוצא (ראה [X3 §1ג](X3-integration-deploy.md) ו-INV-INT8 להלן).
|
||||||
|
- **3 cron jobs** — `sync-case-status` (כל 15 דק'), `stale-case-reminder` (יומי), `weekly-feedback-analysis` (שבועי).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-INT4: לקוח-Paperclip קנוני יחיד — אין לקוח-מקביל ואין גישת-DB ישירה
|
||||||
|
**כלל:** כל גישה ל-Paperclip עוברת דרך **לקוח-API קנוני יחיד** (`pc_request`/`pc.sh`). **אסור** מסלול-מקביל —
|
||||||
|
לא לקוח שני, ולא `INSERT`/`SELECT`/`UPDATE` ישיר ל-DB של Paperclip. נתונים נקראים/נכתבים דרך ה-API
|
||||||
|
הרשמי בלבד; ה-DB של Paperclip הוא מקור-האמת של Paperclip, ו-legal-ai אינו מסלול-כתיבה מקביל אליו.
|
||||||
|
מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) וכלל "סימטריה" ([חוקה §6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
|
||||||
|
**מקור-סמכות:** [legal-ai/CLAUDE.md](../../CLAUDE.md) ("paperclip_client.py legacy — השתמש ב-paperclip_api.py";
|
||||||
|
"קריאות API — תמיד דרך helper"); [X3 INV-INT3](X3-integration-deploy.md). (פרויקטלי-תפעולי — משרת G2.)
|
||||||
|
**אכיפה:** איחוד שני הלקוחות ללקוח-API אחד; הסרת `PAPERCLIP_DB_URL` כמסלול-כתיבה. **כיום אין אכיפה** —
|
||||||
|
שני הלקוחות דו-קיימים (יעד FU-9).
|
||||||
|
**הפרה ידועה:** [paperclip_client.py](../../web/paperclip_client.py) — `create_project`/`post_comment`-fallback
|
||||||
|
עושים `INSERT` ישיר ל-`projects`/`issues`/`comments`/`plugin_state` ([gap-audit GAP-24, GAP-25](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-INT5: מזהי-חברה/סוכן מ-config — לא hardcoded בקוד
|
||||||
|
**כלל:** מזהי-החברה (CMP/CMPA) ומזהי-הסוכנים (CEO/curator/analyst) **נגזרים מ-config** (env/טבלת-מיפוי),
|
||||||
|
**לא** קבועים בקוד. הוספת חברה/החלפת instance אינה דורשת שינוי-קוד. מופע של
|
||||||
|
[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) (SSoT למיפוי) — מקור-אמת יחיד למיפוי.
|
||||||
|
**מקור-סמכות:** [X3 §1א](X3-integration-deploy.md) ("לעולם לא UUID hardcoded"); [X2-multi-company.md](X2-multi-company.md).
|
||||||
|
(פרויקטלי-תפעולי — משרת G2.)
|
||||||
|
**אכיפה:** טבלת-מיפוי/env יחידה; code-review. **כיום אין אכיפה** — UUIDs קשיחים.
|
||||||
|
**הפרה ידועה:** [paperclip_client.py:36-62](../../web/paperclip_client.py) + [app.py:3976](../../web/app.py) +
|
||||||
|
[plugin worker.ts](../../../plugin-legal-ai/src/worker.ts) — IDs קשיחים. **סותר את X3 §1א** ([gap-audit GAP-26](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-INT6: גזירת `company_id` קנונית יחידה
|
||||||
|
**כלל:** ל-`company_id` יש **מסלול-גזירה אחד** מתוך מספר-התיק/סוג-הערר, במקום יחיד. אסור שתי לוגיקות-גזירה
|
||||||
|
מקבילות (prefix מול fallback-map) שעלולות לסטות. מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים).
|
||||||
|
**מקור-סמכות:** [X3 §1ג](X3-integration-deploy.md); [X2-multi-company.md](X2-multi-company.md). (פרויקטלי-תפעולי.)
|
||||||
|
**אכיפה:** פונקציית-גזירה יחידה משותפת ל-app.py ול-client.py (יעד FU-9). **כיום אין.**
|
||||||
|
**הפרה ידועה:** prefix ב-[app.py](../../web/app.py) מול `_FALLBACK_APPEAL_TYPE_TO_COMPANY` ב-[paperclip_client.py](../../web/paperclip_client.py) ([gap-audit GAP-27](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-INT7: webhook יוצא — at-least-once + idempotency + ללא בליעה שקטה
|
||||||
|
**כלל:** ה-webhook היוצא (legal-ai→plugin) מספק **at-least-once** עם **מפתח-idempotency יציב** (event id),
|
||||||
|
כך שמסירה-כפולה בטוחה בצד-המקבל; וכישלון-מסירה **נרשם ומדווח** (telemetry/health), לא נבלע בשקט.
|
||||||
|
זהו invariant **הנדסי** (סמנטיקת-מסירה כללית), הקשור ל-[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai)
|
||||||
|
(עקיבוּת) ולכלל "אין בליעה שקטה" ([חוקה §6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
|
||||||
|
**מקורות:** Stripe — *Webhooks / at-least-once delivery & idempotency*
|
||||||
|
(https://docs.stripe.com/webhooks) · Hookdeck — *At-Least-Once vs Exactly-Once Webhook Delivery*
|
||||||
|
(https://hookdeck.com/webhooks/guides/webhook-delivery-guarantees) · Martin Kleppmann, *DDIA*
|
||||||
|
(O'Reilly 2017, idempotence & exactly-once semantics) | סטטוס: verified
|
||||||
|
**אכיפה:** event-id יציב + UNIQUE-dedup בצד-המקבל; ה-emitter רושם כישלון ל-telemetry (יעד). **כיום:**
|
||||||
|
inbound יש guard 5 דק' ([X3 §1ג](X3-integration-deploy.md)); **outbound אין idempotency**, וה-emitter בולע
|
||||||
|
שגיאות ב-`logger.warning` בלבד.
|
||||||
|
**הפרה ידועה:** `emit_*_webhook` ב-[paperclip_api.py](../../web/paperclip_api.py) — fire-and-forget, `try/except`
|
||||||
|
שמתעד warning ולעולם לא raise, ללא event-id/dedup ([gap-audit GAP-28](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-INT8: חוזה-אירועי-webhook מתוקען ומגורס
|
||||||
|
**כלל:** ל-webhook חוזה-אירוע **מפורש ומגורס** — `eventType` מתוך קבוצה סגורה, סכמת-payload מתועדת לכל
|
||||||
|
סוג, וגרסה. אין `eventType` חופשי ואין "ברירת-מחדל שקטה". מופע של
|
||||||
|
[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)/[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai).
|
||||||
|
**מקור-סמכות:** [X3 §1ג](X3-integration-deploy.md) (3 סוגי-האירוע: `status_change`, `missing_precedent_created`,
|
||||||
|
`export_complete`); קוד ה-emitter ([paperclip_api.py:87+](../../web/paperclip_api.py)). (פרויקטלי-תפעולי — משרת G2/G9.)
|
||||||
|
**אכיפה:** enum + סכמה משותפים emitter↔handler. **כיום:** `eventType` נופל ל-`status_change` כברירת-מחדל
|
||||||
|
אם חסר/לא-מוכר ([gap-audit GAP-29](gap-audit.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. מצב קיים מול יעד — פער אכיפה
|
||||||
|
האינטגרציה נשענת על **נוהל + שני לקוחות**, לא על מסלול-קוד קנוני אחד:
|
||||||
|
- **לקוח (INV-INT4):** יעד — לקוח-API יחיד; הסרת מסלול-ה-DB הישיר.
|
||||||
|
- **מזהים (INV-INT5/INT6):** יעד — טבלת-מיפוי/env יחידה; פונקציית-גזירה אחת.
|
||||||
|
- **webhook (INV-INT7/INT8):** יעד — event-id + dedup + enum-אירוע מגורס + רישום-כישלון.
|
||||||
|
|
||||||
|
כל אלה מקובצים ל-**FU-9** ([gap-audit.md](gap-audit.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. הפניות-אחיות
|
||||||
|
- [X3-integration-deploy.md](X3-integration-deploy.md) — זרימות (wakeup, comments, webhook) + INV-INT1/2/3.
|
||||||
|
- [X10-deploy-env-secrets.md](X10-deploy-env-secrets.md) — חוזה-env מלא, סודות, hardcoded IDs/creds.
|
||||||
|
- [X2-multi-company.md](X2-multi-company.md) — CMP/CMPA, sync, company filtering.
|
||||||
|
- [X6-ui-api-contract.md](X6-ui-api-contract.md) — חוזה ה-API שהפלאגין (inbound) צורך.
|
||||||
|
- [00-constitution.md](00-constitution.md) — [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים), [G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai), כלל "סימטריה" ([§6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
|
||||||
|
- [web/paperclip_api.py](../../web/paperclip_api.py), [web/paperclip_client.py](../../web/paperclip_client.py), [scripts/pc.sh](../../scripts/pc.sh).
|
||||||
120
docs/spec/X8-field-provenance.md
Normal file
120
docs/spec/X8-field-provenance.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# X8 — כללי-מילוי-שדות וחילוץ (Field-Population & Extraction Rules)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) והוא ה-**SSoT לכללים שכרגע סמויים בקוד**:
|
||||||
|
כשמעלים החלטה/פסק-דין/מסמך-תיק — *איזה שדה מתמלא מאיזה מקור*, ומה הכללים על-גבי זה (אי-דריסת
|
||||||
|
ערך-יו"ר, שער-אישור, ציטוט-verbatim). הכללים האלה חיים היום מפוזרים על-פני 4 שירותים; כאן הם מאוחדים.
|
||||||
|
הוא משלים את [01-ingest.md](01-ingest.md) (הפייפליין) ו-[02-data-model.md](02-data-model.md) (הסכמה),
|
||||||
|
ומזין את [X6 INV-UI6](X6-ui-api-contract.md) (שיקוף-מקור ב-UI).
|
||||||
|
|
||||||
|
> **מודלי-סמכות מעורבים.** FP1 ו-FP4 הם **הנדסיים** (lineage/integrity — ≥3 מקורות). FP2/FP3/FP5 הם
|
||||||
|
> **פרויקטלי-תפעוליים** הנקשרים ל-[G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)
|
||||||
|
> (שער אנושי) ו-[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. ארבעת מקורות-המילוי
|
||||||
|
|
||||||
|
| מקור | הגדרה | דוגמאות |
|
||||||
|
|------|-------|---------|
|
||||||
|
| **DETERMINISTIC** | parse של שם-קובץ / מטא-PDF / OCR / regex — ללא LLM | `full_text`, `extraction_status`, `source_kind`, chunks, page_number |
|
||||||
|
| **OPUS-ANALYSIS** | Claude Opus קורא את כל המסמך, ממלא **רק שדה ריק/placeholder**, אסינכרוני | `headnote`, `summary`, `key_quote`, `subject_tags`, `case_name`, `court`, `date`, `appeal_subtype`, `precedent_level`, `source_type`, `citation_formatted`, halachot |
|
||||||
|
| **CHAIR-MANUAL** | היו"ר מזין בטופס; חובה או רשות | `citation`/`case_number` (חובה), והשאר נשאר לעריכה |
|
||||||
|
| **DERIVED** | מחושב משדות אחרים | `district` מ-court, `proceeding_type` מ-appeal_subtype, `searchable` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. טבלת-provenance לפי סוג-מסמך (ה-SSoT)
|
||||||
|
|
||||||
|
> מאומת מול [precedent_metadata_extractor.py](../../mcp-server/src/legal_mcp/services/precedent_metadata_extractor.py),
|
||||||
|
> [halacha_extractor.py](../../mcp-server/src/legal_mcp/services/halacha_extractor.py),
|
||||||
|
> [ingest.py](../../mcp-server/src/legal_mcp/services/ingest.py), [db.py](../../mcp-server/src/legal_mcp/services/db.py).
|
||||||
|
|
||||||
|
### 2א. פסיקה חיצונית (`case_law`, source_kind=`external_upload`)
|
||||||
|
| שדה | מקור | הערה |
|
||||||
|
|-----|------|------|
|
||||||
|
| `case_number` (citation) | CHAIR (חובה) | מפתח idempotency |
|
||||||
|
| `full_text`, `extraction_status`, `source_kind` | DETERMINISTIC | — |
|
||||||
|
| `case_name`, `court`, `date`, `headnote`, `summary`, `key_quote`, `subject_tags`, `appeal_subtype`, `precedent_level`, `source_type`, `citation_formatted` | CHAIR או OPUS | Opus ממלא רק אם ריק |
|
||||||
|
| `is_binding` | CHAIR (default true) | קובע prompt-הלכה |
|
||||||
|
| chunks (`content`/`section_type`/`page_number`) | DETERMINISTIC | — |
|
||||||
|
| `embedding` (chunks) | Voyage (לא-LLM-reasoning) | ⚠ לא-GENERATED ([gap-audit GAP-09](gap-audit.md)) |
|
||||||
|
| כל `halachot` | OPUS | נכנס pending_review |
|
||||||
|
|
||||||
|
### 2ב. החלטה פנימית (`case_law`, source_kind=`internal_committee`)
|
||||||
|
כמו 2א, ובנוסף: `case_number` **חובה**; `chair_name`/`district`/`proceeding_type` — CHAIR או OPUS או DERIVED;
|
||||||
|
`source_type` = `appeals_committee` (DETERMINISTIC קבוע). placeholder `"(טרם חולץ)"` מסומן ל-chair_name/district
|
||||||
|
ריקים ומטופל כריק ע"י ה-extractor.
|
||||||
|
|
||||||
|
### 2ג. מסמך-תיק (`documents`)
|
||||||
|
| שדה | מקור |
|
||||||
|
|-----|------|
|
||||||
|
| `case_id`, `title` | CHAIR |
|
||||||
|
| `doc_type` | DETERMINISTIC (local_classifier) → fallback Claude אם confidence<0.8 |
|
||||||
|
| `extracted_text`, `extraction_status`, `page_count` | DETERMINISTIC |
|
||||||
|
| chunks + `embedding` | DETERMINISTIC + Voyage |
|
||||||
|
| claims / appraiser_facts | OPUS (כלי-חילוץ נפרדים — ראה [X9](X9-mcp-tool-contract.md)) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-FP1: לכל שדה מקור-מילוי מוצהר — הטבלה היא ה-SSoT
|
||||||
|
**כלל:** לכל שדה-מטא יש **מקור-מילוי מוצהר** (deterministic / opus / chair / derived), ב**מקום יחיד**
|
||||||
|
(טבלת §2). אין כללי-מילוי סמויים מפוזרים בין שירותים. מופע של
|
||||||
|
[G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai) (lineage — מאיפה כל ערך). **הנדסי.**
|
||||||
|
**מקורות:** ISO 8000-110 (data quality — provenance) · DAMA-DMBOK2 (data lineage) · OpenLineage spec
|
||||||
|
(https://openlineage.io/) | סטטוס: verified
|
||||||
|
**אכיפה:** טבלת-provenance מוצהרת (§2) + עמודת-מקור-מילוי לכל שדה-נגזר (יעד; ראה [02-data-model.md](02-data-model.md)).
|
||||||
|
**הפרה ידועה:** הכללים מפוזרים על precedent_metadata_extractor/halacha_extractor/ingest/recompute_searchable; אין SSoT ([gap-audit GAP-35](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-FP2: חילוץ-LLM אינו דורס ערך שהוזן ידנית
|
||||||
|
**כלל:** חילוץ-Opus ממלא **רק שדה ריק/placeholder** — ערך שהיו"ר הזין **לעולם אינו נדרס**. סמכות-התוכן
|
||||||
|
היא היו"ר. מופע של [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant). **פרויקטלי-תפעולי.**
|
||||||
|
**מקור-סמכות:** [precedent_metadata_extractor.py](../../mcp-server/src/legal_mcp/services/precedent_metadata_extractor.py)
|
||||||
|
(`apply_to_record` — compare-to-empty); feedback היו"ר. (משרת G10.)
|
||||||
|
**אכיפה:** לוגיקת compare-to-empty ב-extractor; convention placeholder מתועד.
|
||||||
|
**הפרה ידועה:** placeholder `"(טרם חולץ)"` כמחרוזת-קסם לא-מתועדת/שבירה ([gap-audit GAP-37](gap-audit.md)).
|
||||||
|
|
||||||
|
### INV-FP3: פלט-LLM נכנס כ-pending — רק אישור-יו"ר הופך אותו לשמיש
|
||||||
|
**כלל:** פלט-חילוץ של LLM (הלכות; ובהמשך גם טענות-משפטיות) נכנס במצב **לא-מאושר** (`pending_review`),
|
||||||
|
ואינו נחשף לחיפוש/החלטה עד **אישור-יו"ר**. מופע של [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant)
|
||||||
|
(שער אנושי) — תואם [05-qa-review.md](05-qa-review.md). **פרויקטלי-תפעולי.**
|
||||||
|
**מקור-סמכות:** [halacha_extractor.py](../../mcp-server/src/legal_mcp/services/halacha_extractor.py) (review_status); [01-ingest.md](01-ingest.md).
|
||||||
|
**אכיפה:** `review_status` חוסם חיפוש עד `approved`/`published`.
|
||||||
|
**הפרה ידועה:** `legal_arguments` **חסר** שער-אישור מקביל ([gap-audit GAP-39](gap-audit.md); [02-data-model.md](02-data-model.md)).
|
||||||
|
|
||||||
|
### INV-FP4: supporting_quote חייב להיות verbatim
|
||||||
|
**כלל:** כל ציטוט-תומך (`supporting_quote` של הלכה, `key_quote`) חייב להופיע **מילה-במילה** בטקסט-המקור;
|
||||||
|
אחרת מסומן (`quote_verified=false`). מופע של [G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai)
|
||||||
|
(integrity). **הנדסי.**
|
||||||
|
**מקורות:** ISO 15489-1:2016 (records integrity/authenticity) · RAG attribution (Lewis et al., 2020, NeurIPS) ·
|
||||||
|
NCSC/JTC — *AI in Courts* (verifiable citation) | סטטוס: verified
|
||||||
|
**אכיפה:** `proofreader.verify_quote` בעת חילוץ → `quote_verified`.
|
||||||
|
**הפרה ידועה:** — (קיים; ה-flag נכתב, אך אין חיווי ב-UI — ראה [X6 INV-UI6](X6-ui-api-contract.md)).
|
||||||
|
|
||||||
|
### INV-FP5: חילוץ אסינכרוני, מתור, צד-מארח (לא מהקונטיינר)
|
||||||
|
**כלל:** חילוץ-LLM (מטא, הלכות) רץ **אסינכרוני, מתור, מצד-המארח** — לא חוסם את ה-web ולא קורא ל-LLM
|
||||||
|
מהקונטיינר. **בחירת-מנוע לפי אופי-המשימה (לא מסלול מקביל):** חילוץ-מטא הוא משימה *תחומה* (טקסט→JSON)
|
||||||
|
ולכן רץ על **Gemini Flash** (`gemini_session`, structured JSON) — ה-claude CLI ה-agentic פגע ב-
|
||||||
|
`error_max_turns`; חילוץ-הלכות (רגיש-קול/agentic) נשאר על **`claude_session`** (CLI מקומי, מנוי דפנה).
|
||||||
|
שני המנועים מתנקזים לתור-החילוץ הקנוני היחיד ([G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)). **פרויקטלי-תפעולי.**
|
||||||
|
**מקור-סמכות:** [ingest.py](../../mcp-server/src/legal_mcp/services/ingest.py) (queue → `process_pending_extractions`); [gemini_session.py](../../mcp-server/src/legal_mcp/services/gemini_session.py) (מטא); [legal-ai/CLAUDE.md](../../CLAUDE.md) (claude_session local-only להלכות). `GEMINI_API_KEY` בצד-המארח בלבד — לא בקונטיינר (תואם `feedback_claude_session_local_only`: אין קריאות-LLM מהקונטיינר).
|
||||||
|
**אכיפה:** queue + `precedent_process_pending` + drainers מתוזמנים (`legal-metadata-drain`/CEO); קריאות-LLM רק מצד-המארח.
|
||||||
|
**הפרה ידועה:** תור-החילוץ **סמוי** (אין הבחנה pending-initial מול pending-review; אין extraction-job table) ([gap-audit GAP-45](gap-audit.md); [X9](X9-mcp-tool-contract.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. חוזה-searchable (תזכורת — מוגדר ב-02)
|
||||||
|
רשומת `case_law` היא `searchable` רק כשמתקיים חוזה-השלמות ([G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש),
|
||||||
|
[02-data-model.md](02-data-model.md), FU-2a): ≥1 chunk עם embedding · `extraction_status='completed'` ·
|
||||||
|
`case_number`/`source_kind` לא-ריקים · practice_area (לפנימי) · ≥1 שדה-מטא ({headnote/summary/subject_tags}).
|
||||||
|
ה-UI חייב **לשקף** את ה-flag הזה ([X6 INV-UI6](X6-ui-api-contract.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. הפניות-אחיות
|
||||||
|
- [01-ingest.md](01-ingest.md) — הפייפליין הקנוני (12 צעדים) שבו החילוץ יושב.
|
||||||
|
- [02-data-model.md](02-data-model.md) — סכמת השדות + חוזה-searchable + ישויות-נגזרות.
|
||||||
|
- [X6 INV-UI6](X6-ui-api-contract.md) — שיקוף מקור-המילוי ב-UI.
|
||||||
|
- [X9-mcp-tool-contract.md](X9-mcp-tool-contract.md) — כלי-החילוץ (claims/appraiser_facts/halachot/metadata).
|
||||||
|
- [00-constitution.md](00-constitution.md) — [G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai), [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant), [G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש), [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים).
|
||||||
103
docs/spec/X9-mcp-tool-contract.md
Normal file
103
docs/spec/X9-mcp-tool-contract.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# X9 — חוזה כלי-ה-MCP (Agent MCP Tool Contract)
|
||||||
|
|
||||||
|
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) והוא ה-deep-dive על **משטח כלי-ה-MCP** —
|
||||||
|
71 הכלים ש-[mcp-server](../../mcp-server/) חושף לסוכני Paperclip (CEO/analyst/researcher/writer/qa/…).
|
||||||
|
עד כה הספ תיאר *מה הסוכנים עושים* ([X4-agents.md](X4-agents.md)) אך לא **חוזה-הכלים** עצמו: envelope,
|
||||||
|
שמות, idempotency, סימטריית extract/get, ומפת-הרשאות. הקובץ מגדיר את הכללים; הממצאים → [gap-audit.md](gap-audit.md).
|
||||||
|
|
||||||
|
> **מודלי-סמכות מעורבים.** TOOL1/TOOL2/TOOL3/TOOL5 הם **הנדסיים** (עיצוב-API/כלים — ≥3 מקורות).
|
||||||
|
> TOOL4 ו-TOOL6 הם **פרויקטלי-תפעוליים**, הנקשרים ל-[G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
|
||||||
|
> ו-[G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. אינוונטר (71 כלים, [server.py](../../mcp-server/src/legal_mcp/server.py))
|
||||||
|
|
||||||
|
| דומיין | כלים (מייצג) |
|
||||||
|
|--------|--------------|
|
||||||
|
| ניהול-תיק | case_create/list/get/update/delete, case_get_final_text |
|
||||||
|
| מסמכים | document_upload, document_upload_training, document_list/get_text/update, extract_references |
|
||||||
|
| טענות+טיעונים | extract_claims, get_claims, aggregate_claims_to_arguments, get_legal_arguments |
|
||||||
|
| **חיפוש (6 — חופפים)** | search_decisions, search_case_documents, find_similar_cases, search_internal_decisions, search_precedent_library, precedent_search_library |
|
||||||
|
| **כתיבת-בלוק (6 — חופפים)** | draft_section, get_block_context, write_block, write_all_blocks, write_interim_draft, save_block_content |
|
||||||
|
| ייצוא/QA | export_docx, export_interim_draft, validate_decision, revise_draft, list_bookmarks, apply_user_edit |
|
||||||
|
| פסיקה (3 תת-מערכות) | case-attached (precedent_attach/list/remove/search_library) · library (precedent_library_*) · internal (internal_decision_*) |
|
||||||
|
| הלכות | halacha_review, halachot_pending, precedent_extract_halachot/metadata, precedent_process_pending |
|
||||||
|
| ציטוטים | extract_internal_citations, list_internal_citations, list_incoming_citations |
|
||||||
|
| missing-precedents | missing_precedent_create/list/close |
|
||||||
|
| workflow/feedback | workflow_status, get_metrics, processing_status, set_outcome, brainstorm_directions, approve_direction, ingest_final_version, record/list_chair_feedback |
|
||||||
|
| appraiser/style | extract_appraiser_facts, style_corpus_enrich, style_corpus_pending_enrichment |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Invariants של התחום
|
||||||
|
|
||||||
|
### INV-TOOL1: envelope-תשובה עקבי לכל הכלים
|
||||||
|
**כלל:** כל כלי מחזיר **מבנה אחיד** (למשל `{status, data, message}`) — לא string-לפעמים-JSON-לפעמים-`{error}`.
|
||||||
|
שגיאה מובחנת ממצב-ריק ממצב-הצלחה באופן עקבי. מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים);
|
||||||
|
מקביל ל-[X6 INV-UI3](X6-ui-api-contract.md). **הנדסי.**
|
||||||
|
**מקורות:** Anthropic — *MCP / tool result conventions* (https://modelcontextprotocol.io/) ·
|
||||||
|
JSON-RPC 2.0 (result/error envelope) (https://www.jsonrpc.org/specification) · RFC 9457 (Problem Details) | סטטוס: verified
|
||||||
|
**אכיפה:** wrapper-תשובה משותף בכל הכלים — `tools/envelope.py` (`ok`/`empty`/`err` → `{status,data,message}`, status ∈ ok/empty/error — מבחין הצלחה/ריק/שגיאה), SSoT יחיד שמחליף את 5 ה-`_ok`/`_err` המשוכפלים. עיקרון: envelope-`status` משקף אם **הקריאה לכלי** הצליחה; תוצאות-עסקיות (failed_gates/results/...) נשמרות בתוך `data`. צרכני-API ב-`web/app.py` מפרקים דרך `envelope_unwrap` (+בדיקת `status=="error"`→4xx) כדי לשמר את חוזה-ה-UI↔API (X6) ללא-שינוי. **GAP-48 ✅ הושלם (2026-06-06):** כל ~12 משפחות-הכלים הומרו ל-envelope (search · precedent_library · citations · internal_decisions · missing_precedents · training_enrichment · precedents · legal_arguments · cases · documents · workflow · drafting). מסלול הפקת-ההחלטה (`export_docx` שער-QA) מאומת ב-`test_export_qa_gate`. 182/182 טסטים עוברים.
|
||||||
|
**הפרה ידועה:** — (נסגר)
|
||||||
|
|
||||||
|
### INV-TOOL2: שמות עקביים + חיפוש לפי-קורפוס
|
||||||
|
**כלל:** שמות-הכלים עוקבים אחר convention אחיד, ושם משקף התנהגות. כלי-חיפוש מובחנים **לפי הקורפוס**
|
||||||
|
(style / internal / external / case-attached), לא ב-6 שמות חופפים; כלי-כתיבת-בלוק אינם חופפים (context מול write).
|
||||||
|
מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) ("סימטריה", [§6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)). **הנדסי.**
|
||||||
|
**מקורות:** Anthropic — *Writing effective tools / clear names* (https://www.anthropic.com/engineering/writing-tools-for-agents) ·
|
||||||
|
Google *API Design Guide* (naming) (https://cloud.google.com/apis/design/naming_convention) ·
|
||||||
|
Zalando *RESTful API Guidelines* | סטטוס: verified
|
||||||
|
**אכיפה:** איחוד/מיזוג כלי-חיפוש + כלי-בלוק; rename של שמות-מטעים. **GAP-49 (חלק קריטי) ✅ נסגר (2026-06-06):** הכלי המטעה `precedent_search_library` (חיפוש ציטוטים מצורפים-לתיק) שונה ל-**`search_case_precedents`** — מבטל את ההיפוך המסוכן מול `search_precedent_library` (הספרייה הסמכותית); הישן נשמר כ-alias deprecated לתאימות. docstrings של שני הכלים הובהרו (case-attached מול authoritative). 5 כלי-החיפוש הנותרים (search_decisions=סגנון-דפנה · search_case_documents=תיק · find_similar_cases=cross-case · search_internal_decisions=ועדות-ערר · search_precedent_library=פסיקה-סמכותית) מחפשים קורפוסים מובחנים עם שמות סבירים.
|
||||||
|
**GAP-50 ✅ נסגר (2026-06-06, הכרעת-יו"ר):** הכפילות האמיתית היחידה — `draft_section` (הקשר לפי-סעיף, ישן) — סומנה **deprecated** לטובת `get_block_context` (הקשר לפי-בלוק, תואם 12-הבלוקים). שאר כלי-הכתיבה (`write_block`/`write_all_blocks`/`save_block_content`/`write_interim_draft`) **מובחנים בכוונה** — משרתים זרימות שונות (CLI/initial-draft מול תהליך-ה-writer שבו "התיקון חי בקובץ, לא ב-DB"), ולא מוזגו במכוון.
|
||||||
|
**הפרה ידועה:** — (נסגר)
|
||||||
|
|
||||||
|
### INV-TOOL3: idempotency בכל כלי-מוטציה
|
||||||
|
**כלל:** כלי שמשנה-מצב הוא **idempotent על מפתח דטרמיניסטי** — קריאה חוזרת אינה יוצרת כפילות. מופע של
|
||||||
|
[G3](00-constitution.md#inv-g3-ingest-אחיד-ו-idempotent). **הנדסי.**
|
||||||
|
**מקורות:** Stripe — *Idempotent requests* (https://docs.stripe.com/api/idempotent_requests) ·
|
||||||
|
Kleppmann *DDIA* (idempotence) · IETF — *Idempotency-Key header* draft (https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/) | סטטוס: verified
|
||||||
|
**אכיפה:** upsert/ON CONFLICT (או בדיקת-מפתח ברמת-אפליקציה) בכלי-מוטציה. **GAP-52 ✅ נסגר (2026-06-06):** `case_create` (מפתח case_number, UNIQUE), `precedent_attach` (מפתח case_id+section_id+citation+quote), `document_upload` (מפתח case_id+SHA-256 של הקובץ — מדלג על OCR/embed כפול) — כולם מחזירים את הקיים במקום כפילות. נבחרה בדיקת-מפתח ברמת-אפליקציה (לא UNIQUE-constraint) כדי לא לשבור startup על נתונים-קיימים כפולים. קודמים: `missing_precedent_create`/`precedent_link_cases`/`extract_internal_citations`.
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-TOOL4: סימטריית extract/get + persistence
|
||||||
|
**כלל:** לכל כלי-חילוץ שכותב ל-DB יש **כלי-קריאה (get) מקביל**, והפלט **נשמר durably** (לא מוחזר-ונאבד).
|
||||||
|
מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) (מקור-אמת נגיש). **פרויקטלי-תפעולי.**
|
||||||
|
**מקור-סמכות:** דפוס `extract_claims`↔`get_claims`, `aggregate`↔`get_legal_arguments` ב-[server.py](../../mcp-server/src/legal_mcp/server.py).
|
||||||
|
**אכיפה:** לכל extract — get מקביל. **GAP-44 ✅ + GAP-45 ✅ נסגרו (2026-06-06):** נוסף `get_appraiser_facts` (קורא `list_appraiser_facts`+`detect_appraiser_conflicts`, ללא חילוץ-מחדש); נוסף `extraction_status` שחושף את עומק תור-החילוץ (metadata/halacha) + גיל הבקשה הוותיקה — read-only. **GAP-47 (חלק provenance) ✅ נסגר (2026-06-06):** `draft_section` מחזיר `document_id`+`page`+`score` לכל קטע (provenance מ-`search_similar` שהיה נזרק) → מקור-אמת נגיש ובר-ציטוט (G9). נותר ב-GAP-47: הנחיות-יו"ר ל-DB (פרוסה נפרדת).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-TOOL5: limit-caps על כל כלי-רשימה/חיפוש
|
||||||
|
**כלל:** לכל כלי שמחזיר רשימה יש **תקרת-limit נאכפת** (הגנה מפני עומס/DoS); pagination היכן שרלוונטי. **הנדסי.**
|
||||||
|
**מקורות:** OWASP API Security Top 10 — *API4:2023 Unrestricted Resource Consumption* (https://owasp.org/API-Security/editions/2023/en/0xa4-unrestricted-resource-consumption/) ·
|
||||||
|
Microsoft *REST API Guidelines* (pagination) · Stripe API (limit caps) | סטטוס: verified
|
||||||
|
**אכיפה:** clamp ל-max בכל כלי-רשימה. **GAP-53 ✅ נסגר (2026-06-06):** `_clamp_limit` (תקרה 200) על ~13 כלי list/search ב-[server.py](../../mcp-server/src/legal_mcp/server.py); `list_chair_feedback` קיבל param `limit` (server→workflow→db עם `LIMIT`).
|
||||||
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
|
### INV-TOOL6: שלמות-הרשאות — כל כלי שהוראות-הסוכן דורשות מוענק
|
||||||
|
**כלל:** מפת-ההרשאות (אילו כלים מוענקים לכל סוכן) **תואמת** את מה שהוראות-הסוכן מצריכות — לא חסר ולא עודף.
|
||||||
|
מופע של [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant) (שערים מוגדרים); מפורט ב-[X4-agents.md](X4-agents.md). **פרויקטלי-תפעולי.**
|
||||||
|
**מקור-סמכות:** frontmatter `tools:` ב-[.claude/agents/](../../.claude/agents/) מול הוראות-הסוכן.
|
||||||
|
**אכיפה:** בדיקת-עקביות tools↔instructions (יעד FU-13).
|
||||||
|
**הפרה ידועה:** legal-analyst חסר `aggregate_claims_to_arguments`/`extract_references`/`extract_internal_citations`; researcher חסר טריגרי-חילוץ ([gap-audit GAP-46](gap-audit.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. הערות-עיצוב
|
||||||
|
- **set_outcome — GAP-51 ✅ נסגר (2026-06-06):** SSoT יחיד = 3 תוצאות קנוניות `rejection/partial_acceptance/full_acceptance`
|
||||||
|
ב-`lessons.VALID_OUTCOMES`; `OUTCOME_LABELS_HE` = מפת-תוויות עברית אחת (אנגלית ב-DB, עברית ב-UI); `canonical_outcome()`
|
||||||
|
ממפה ערכי-legacy (rejected/accepted/partial). `betterment_levy` הוצא מהיותו תוצאה → `PRACTICE_AREA_OVERRIDES`
|
||||||
|
(override לפי practice_area מעל התוצאה). נתונים נורמלו (~9 שורות, גיבוי ב-`data/audit/gap51-outcome-backup-*`).
|
||||||
|
- **3 מסלולי-קליטת-פסיקה** (library / internal / training) עם ולידציה א-סימטרית — נקשר ל-[01-ingest.md](01-ingest.md) / GAP-01/05.
|
||||||
|
|
||||||
|
הממצאים המלאים + התיקון → **FU-14** ([gap-audit.md](gap-audit.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. הפניות-אחיות
|
||||||
|
- [X4-agents.md](X4-agents.md) — מפת-הסוכנים + ההרשאות (INV-TOOL6).
|
||||||
|
- [X8-field-provenance.md](X8-field-provenance.md) — כלי-החילוץ ומה שהם שומרים.
|
||||||
|
- [X6-ui-api-contract.md](X6-ui-api-contract.md) — envelope מקביל בצד-ה-API.
|
||||||
|
- [01-ingest.md](01-ingest.md), [03-retrieval.md](03-retrieval.md) — מסלולי-קליטה/חיפוש שהכלים עוטפים.
|
||||||
|
- [00-constitution.md](00-constitution.md) — [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים), [G3](00-constitution.md#inv-g3-ingest-אחיד-ו-idempotent), [G10](00-constitution.md#inv-g10-המערכת-מסייעת--שערים-אנושיים-הם-invariant).
|
||||||
|
- [mcp-server/src/legal_mcp/server.py](../../mcp-server/src/legal_mcp/server.py), [tools/](../../mcp-server/src/legal_mcp/tools/).
|
||||||
234
docs/spec/gap-audit.md
Normal file
234
docs/spec/gap-audit.md
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# Gap-Audit — פערים בין המערכת הקיימת ל-spec
|
||||||
|
|
||||||
|
מסמך זה הוא **מפת-הפערים הקנונית** בין המערכת הקיימת (קוד ב-`web/`, `mcp-server/`,
|
||||||
|
`scripts/`) לבין ה-invariants שב-[`docs/spec/`](README.md). הוא תוצר של תת-פרויקט 2
|
||||||
|
(מיפוי-פערים), ומובחן מ-[`docs/audit-report.md`](../audit-report.md) הישן: ה-audit הוא
|
||||||
|
דוח-מצב נקודתי, וזה ה-gap-map שמקשר כל ממצא ל-invariant מופר וליחידת-תיקון.
|
||||||
|
|
||||||
|
**איך הופק:** סקירה חוצת-קבצים של כל קבצי-הספ (00 + 01–07 + X1–X5) מול הקוד הקיים,
|
||||||
|
30.5.2026. כל ממצא נושא: `invariant מופר` (ה-G*/INV-* שהוא סותר), הערכת-`severity`,
|
||||||
|
`קבצים מושפעים` (file:line), ו-`תיקון מוצע`.
|
||||||
|
|
||||||
|
**הערה על severity/priority:** דירוג ה-severity להלן הוא הערכה הנדסית (לפי סיכון
|
||||||
|
לשלמות-נתונים, דליפה חוצת-קורפוס, ועקיפת שער אנושי). **קביעת ה-priority בפועל —
|
||||||
|
מה לתקן ראשון — היא של היו"ר.** ה-severity מנמק; הוא אינו מכריע.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 23 הממצאים
|
||||||
|
|
||||||
|
> **סטטוס מחזור-1 (עודכן 31.5.2026):** כל 23 הממצאים **✅ נסגרו** — FU-1..FU-8b מוזגו ל-main
|
||||||
|
> (PRs #11–#23: FU-1/2a, FU-2b #15, FU-2c #17, FU-3, FU-4, FU-5 #18, FU-6, FU-7 #13, FU-8a #16, FU-8b #23).
|
||||||
|
> 122 בדיקות עוברות. הטבלה נשמרת כתיעוד-מקור; פירוט-ה-FU והסטטוס בסעיף "יחידות-תיקון".
|
||||||
|
|
||||||
|
| ID | כותרת | invariant מופר | severity | קבצים מושפעים (file:line) | תיקון מוצע |
|
||||||
|
|----|-------|----------------|----------|---------------------------|------------|
|
||||||
|
| GAP-01 | שני מסלולי ingest מקבילים שמתפצלים | INV-ING1, G2 | High | `precedent_library.py:88`, `internal_decisions.py:73` | מסלול-קליטה קנוני יחיד; ישויות-אחיות חולקות פייפליין |
|
||||||
|
| GAP-02 | ingest פנימי מדלג על חילוץ metadata | INV-ING3, DM1, RET2 | Critical | `internal_decisions.py:208` | להוסיף `request_metadata_extraction` לכל סוג; חוסם indexing ריק |
|
||||||
|
| GAP-03 | אין upsert דטרמיניסטי על מזהה קנוני | INV-ING2, G3 | Critical | `precedent_library.py`, `internal_decisions.py` | upsert על מפתח קנוני — קליטה חוזרת = update לא duplicate |
|
||||||
|
| GAP-04 | ולידציית-enum א-סימטרית | INV-G4 | Medium | `precedent_library.py:131-134` | להחיל אותה ולידציית practice_area/source_type בשני המסלולים |
|
||||||
|
| GAP-05 | staging/derivation/citation-guard/multimodal/fallback א-סימטריים | INV-ING1, G2 | High | `01-ingest §4` (שני המסלולים) | מיזוג כל שלבי-העיבוד למסלול הקנוני האחד |
|
||||||
|
| GAP-06 | case_number מנורמל בקריאה בלבד | INV-G1, ID1 | High | `db.py:1196-1211` | נרמול בנקודת-הכתיבה; `8126-25`→canonical |
|
||||||
|
| GAP-07 | מספרי-תיק מעורבים (חודש/חסר) — reconciliation חד-פעמי | INV-ID1 | High | data (cases, case_law) | מיגרציה: canonical = הצורה הרשמית שהוקצתה [chair-confirmed] |
|
||||||
|
| GAP-08 | ציטוט-מלא נשמר כ-case_number | INV-DM2, ID2 | Medium | data (legacy pre-V15) | ניקוי: ציטוט = שדה-תצוגה נגזר, לא מזהה |
|
||||||
|
| GAP-09 | `embedding` אינו GENERATED (בניגוד ל-tsvectors) | INV-DM3, RET, G6 | High | schema (chunks/case_law) | re-index באכיפה — טריגר או GENERATED-equivalent בשינוי תוכן |
|
||||||
|
| GAP-10 | דליפת הלכה חוצת-קורפוס | INV-RET1, G5 | Critical | `db.py:3168`, `db.py:3401`, JOINs `3236-3238`/`3475-3477` | להוסיף `cl.source_kind` ל-halacha_filters |
|
||||||
|
| GAP-11 | אין eval harness / gold-set מתויג | INV-RET4, G8 | High | `telemetry.log_search_bg` (היחיד) | להקים eval harness + gold-set; precision/recall נמדד |
|
||||||
|
| GAP-12 | search_decisions מזהיר אך לא חוסם practice_area חסר | INV-RET, G5 | High | `search.py:45-49`, `search.py:172-176` | לחסום query בלי practice_area — ערבוב-תחום אסור |
|
||||||
|
| GAP-13 | אין דגל `searchable` מפורש | INV-DM1 | Medium | schema (case_law, chunks) | דגל `searchable` שמסומן רק כשחוזה-השלמות מתקיים |
|
||||||
|
| GAP-14 | backlog הלכות סמוי | INV-QA1, G10 | Medium | (אין health-check) | לחשוף `pending_review` ב-health-check / dashboard |
|
||||||
|
| GAP-15 | שער-ייצוא נאכף-זרימה ולא נאכף-קוד | INV-QA3, EX3 | Critical | `drafting.py:384` | `export_docx` קורא `validate_decision` + בודק `export_blocked` |
|
||||||
|
| GAP-16 | neutral_background קריטי-אך-עובר | INV-QA3 (`05 §1.2`) | High | `qa_validator.py:70` | בלוק-ו ריק/חסר = passed=False; חוסם ייצוא |
|
||||||
|
| GAP-17 | active_draft_path נגזר זוחל ל-source-of-truth | INV-EX1, AUD2 | High | `db.py:189` | DOCX = נגזר; re-sync בלוקים אחרי revise/apply_user_edit |
|
||||||
|
| GAP-18 | audit_log כמעט לא נכתב | INV-AUD1 | High | `cases.py:203` (היחיד) | כתיבת audit על upload/extract/write_block/export |
|
||||||
|
| GAP-19 | אין קישור block→source-chunks | INV-AUD1 | High | `decision_blocks` (model_used בלבד) | לתעד אילו chunks/precedents הזינו כל בלוק |
|
||||||
|
| GAP-20 | citation→corpus לא נאכף אוטומטית | INV-AUD3 | Medium | `decision_paragraphs.citations` | ולידציה שכל ציטוט בטקסט פתיר לקורפוס |
|
||||||
|
| GAP-21 | cross-company sync ידני ולא-נאכף | INV-MC1 | Medium | `sync_agents_across_companies.py:387-389` | אכיפת `--apply` אחרי שינוי-Master; להרעיש על דילוג adapter_type |
|
||||||
|
| GAP-22 | אינטגרציית-Paperclip על נוהל ולא מחסום-קוד | INV-INT1, INT3 | Medium | schema / lint (אין) | אילוץ-schema נגד DB-insert; linter נגד httpx/curl גולמי |
|
||||||
|
| GAP-23 | הספ עדיין לא מחובר לסוכנים | INV-AG1 | High | `.claude/agents/HEARTBEAT.md`, agent files | חובת קריאת 00-constitution + ספ-תחום לפני פעולה |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ממצאי מחזור-2 (8 משטחי-האפליקציה מחוץ לצינור-הליבה) — GAP-24..62
|
||||||
|
|
||||||
|
> הופקו בסקירת-קוד word-for-word (30–31.5.2026) של 8 המשטחים: גבול-Paperclip, web-ui,
|
||||||
|
> מילוי-שדות, אחסון-ניתוחים, כלי-MCP (71), סוכנים+skills, deploy/env. ממצאי-ה-UI ברמת-הדף
|
||||||
|
> מפורטים ב-[ui-audit.md](ui-audit.md). ה-invariants ב-[X6](X6-ui-api-contract.md)–[X10](X10-deploy-env-secrets.md).
|
||||||
|
> **כל מחזור-2 פתוח** (אומת 31.5.2026: creds plaintext קיימים, 2 לקוחות קיימים, אין get_appraiser_facts, analyst חסר 3 כלים).
|
||||||
|
|
||||||
|
| ID | כותרת | invariant מופר | severity | קבצים מושפעים (file:line) | תיקון מוצע |
|
||||||
|
|----|-------|----------------|----------|---------------------------|------------|
|
||||||
|
| GAP-24 | שני לקוחות Paperclip מקבילים (api מול client legacy) | INV-INT4, G2 | High | `web/paperclip_api.py`, `web/paperclip_client.py` | לקוח-API קנוני יחיד |
|
||||||
|
| GAP-25 | גישת-DB ישירה ל-Paperclip (INSERT projects/issues/plugin_state) עוקפת API+audit | INV-INT4, G2, G9 | High | `web/paperclip_client.py` | להעביר הכל ל-API; להסיר מסלול-DB |
|
||||||
|
| GAP-26 | company/agent IDs קשיחים — **סותר X3 §1א** | INV-INT5, G2 | High | `web/paperclip_client.py:36-62`, `web/app.py:3976`, plugin `worker.ts` | מיפוי מ-config/env |
|
||||||
|
| GAP-27 | `company_id` נגזר בשתי דרכים (prefix מול fallback-map) | INV-INT6, G2 | Medium | `web/app.py` (prefix), `web/paperclip_client.py` (`_FALLBACK_APPEAL_TYPE_TO_COMPANY`) | פונקציית-גזירה יחידה |
|
||||||
|
| GAP-28 | webhooks fire-and-forget בולעים שגיאות, ללא idempotency | INV-INT7, G9, §6 | Medium | `web/paperclip_api.py:87-205` | event-id+dedup+רישום-כישלון |
|
||||||
|
| GAP-29 | חוזה-אירוע webhook לא-מתוקען (eventType חופשי, default שקט) | INV-INT8, G2 | Medium | `web/paperclip_api.py:87+`, plugin `onWebhook` | enum-אירוע מגורס |
|
||||||
|
| GAP-30 | ~60% endpoints ללא Pydantic → `unknown` → טיפוסים ידניים סוטים | INV-UI1/UI2, G2/G4 | High | `web/app.py` (רוב), `web-ui/src/lib/api/cases.ts:1-9` | response models + `api:types` |
|
||||||
|
| GAP-31 | `PracticeArea`/enum-סטטוס משוכפלים פרונט (3 מקומות, ערכים שונים) | INV-UI1, G2 | High | `web-ui/src/lib/practice-area.ts:12`, `lib/api/precedent-library.ts:26`, `components/precedents/practice-area.ts` | SSoT יחיד (ui-audit UI-A1/B1) |
|
||||||
|
| GAP-32 | אין envelope עקבי; שגיאות נבלעות ב-UI | INV-UI3/UI4, §6 | Medium | `web/app.py` (search ועוד), דפי-UI | envelope אחיד + error-card |
|
||||||
|
| GAP-33 | fallback SSE מסתיר כישלון; cache-TTL לא-תואם (5ש'↔300ש') | INV-UI5 | Low | `web-ui/src/lib/api/documents.ts:226-232` | terminal-state מפורש |
|
||||||
|
| GAP-34 | URLs קשיחים ב-UI/בק | INV-UI3/ENV3 | Low | `web-ui/.../app-shell.tsx:70`, `web/app.py:110` | env |
|
||||||
|
| GAP-35 | מקור-מילוי-שדות לא-מוצהר — מפוזר על 4 שירותים | INV-FP1, G9 | High | `precedent_metadata_extractor.py`, `halacha_extractor.py`, `ingest.py`, `db.py` (recompute_searchable) | טבלת-provenance SSoT (X8 §2) |
|
||||||
|
| GAP-36 | אין שקיפות-UI למה מולא ע"י Opus מול ידני | INV-UI6/FP1, G9 | Medium | `web-ui/src/app/precedents/[id]/page.tsx:160-185` | חיווי מקור-מילוי |
|
||||||
|
| GAP-37 | placeholder `"(טרם חולץ)"` כמחרוזת-קסם לא-מתועדת | INV-FP2 | Low | `internal_decisions.py`, `precedent_metadata_extractor.py` | constant מתועד |
|
||||||
|
| GAP-38 | שתי עמודות-סטטוס-חילוץ ב-case_law | INV-DM1, G2 | Medium | `db.py:603-606` | סטטוס יחיד / extraction-jobs |
|
||||||
|
| GAP-39 | `legal_arguments` ללא שער-אישור (בניגוד ל-halachot) | INV-DM5, G10 | High | `db.py:845-872` | `review_status` ל-legal_arguments |
|
||||||
|
| GAP-40 | `legal_arguments.cited_precedents TEXT[]` ללא FK → הזיות-LLM נבלעות | INV-DM6, G9, §6 | Medium | `db.py:858`, `argument_aggregator.py` | FK + דיווח-כישלון-קישור |
|
||||||
|
| GAP-41 | `appraiser_facts`↔`claims` התנגשות; `appraiser_side` default '' מעורפל | INV-DM6 | Medium | `db.py:549-576` | CHECK + הבחנה document↔case |
|
||||||
|
| GAP-42 | 20+ enums כ-TEXT חופשי; אין embedding-provenance | INV-DM6/DM4, G4 | Medium | `db.py` (source_type, rule_type, status…) | CHECK-enums + עמודת-model |
|
||||||
|
| GAP-43 | `case_precedents`↔`case_law` טבלאות-פסיקה מקבילות legacy | INV-G2 | Low | `db.py` | איחוד/סימון-deprecated |
|
||||||
|
| GAP-44 | אסימטריית extract/get — אין `get_appraiser_facts` (חילוץ-חוזר יקר) | INV-TOOL4, G2 | High | `mcp-server/.../drafting.py`, `server.py:563` | להוסיף `get_appraiser_facts` |
|
||||||
|
| GAP-45 | תור-חילוץ סמוי (pending-initial מול pending-review); אין extraction-job table | INV-TOOL4/FP5, G10 | Medium | `precedent_library.py`, `ingest.py` | `*_extraction_status` tool + טבלת-jobs |
|
||||||
|
| GAP-46 | הרשאות-סוכן לא-מתועדות (analyst/researcher חסרי כלים) | INV-AG3/TOOL6 | High | `.claude/agents/legal-analyst.md`, `legal-researcher.md` | יישור tools↔instructions |
|
||||||
|
| GAP-47 | `draft_section` ללא provenance (chunk→document/page); הנחיות-יו"ר ב-md ולא DB | INV-TOOL4, G9 | Medium | `mcp-server/.../drafting.py` | provenance בפלט + DB ל-directions |
|
||||||
|
| GAP-48 | envelope-תשובה לא-עקבי (71 כלים: string/JSON/{error}) | INV-TOOL1, G2 | Medium | `mcp-server/.../server.py`, tools/ | wrapper `{status,data,message}` |
|
||||||
|
| GAP-49 | 6 כלי-חיפוש חופפים + `precedent_search_library` שם-מטעה | INV-TOOL2, G2 | Medium | `server.py` (search_*), `precedents.py:81` | ✅ **שם-מטעה תוקן** (`precedent_search_library`→`search_case_precedents`, alias deprecated); 5 הנותרים = קורפוסים מובחנים בשמות סבירים |
|
||||||
|
| GAP-50 | 6 כלי-כתיבת-בלוק חופפים (draft_section/get_block_context/write_*/save_*) | INV-TOOL2, G2 | Medium | `server.py:500-616` | ✅ **draft_section deprecated→get_block_context** (הכרעת-יו"ר); write_*/save_* מובחנים בכוונה (זרימות שונות), לא מוזגו |
|
||||||
|
| GAP-51 | `set_outcome` enum-mismatch (3≠4); אוצרות-מילים סותרות | INV-TOOL1/UI1 | Medium | `block_writer.py:442` מול `lessons.py:11`, `workflow.py:145` | SSoT יחיד ל-outcome |
|
||||||
|
| GAP-52 | רוב הכלים לא-idempotent (case_create/document_upload/precedent_attach) | INV-TOOL3, G3 | Medium | `server.py`, tools/ | upsert/ON CONFLICT |
|
||||||
|
| GAP-53 | אין limit-caps (precedent_library_list/search_*/list_chair_feedback) | INV-TOOL5 | Low | tools/ | clamp ל-max |
|
||||||
|
| GAP-54 | 3 מסלולי-קליטת-פסיקה ולידציה א-סימטרית; citation-guard לא-מתועד | INV-ING1, G2 | Medium | `precedent_library.py`, `internal_decisions.py` | ✅ **נפתר ע"י FU-1** — שני מסלולי-הפסיקה (library+internal) עוברים דרך `ingest.ingest_document` הקנוני (ולידציית-enums + citation-guard סימטריים, מתועד ב-01-ingest §4); המסלול ה-3 (training→`style_corpus`) הוא קורפוס נפרד במכוון (סגנון, לא פסיקה). מאומת ב-`test_unified_ingest.py` |
|
||||||
|
| GAP-55 | Infisical dead-code; מקור-config לא-מתועד (Coolify-only) | INV-ENV2, G2 | Medium | `mcp-server/.../config.py` | לתעד Coolify SSoT / לבודד Infisical |
|
||||||
|
| GAP-56 | UUIDs קשיחים (company/agent) — תואם GAP-26 | INV-ENV3/INT5 | High | `web/paperclip_client.py:36-62`, `web/app.py:3976` | config-driven |
|
||||||
|
| GAP-57 | creds plaintext בברירת-מחדל (`paperclip:paperclip`) | INV-ENV4, G9, §6 | High | `web/paperclip_client.py:21`, `web/app.py:3789,3964` | default ריק + fail-loud |
|
||||||
|
| GAP-58 | `GITEA_ACCESS_TOKEN`↔`GITEA_TOKEN` שני שמות; קטלוג חלקי | INV-ENV1 | Low | `web/gitea_client.py:22`, `git_sync.py:30`, `tools/cases.py:28` | שם קנוני יחיד + קטלוג |
|
||||||
|
| GAP-59 | chat-URL docs↔reality (`10.0.1.1` מול `host.docker.internal`) | INV-ENV3 | Medium | `web/chat_proxy.py:49`, `chat_service/server.py` | יישור env + תיעוד |
|
||||||
|
| GAP-60 | 13/40+ env vars ב-drift-catalog; 8+ סודות בלתי-מנוטרים | INV-ENV5/ENV1 | Medium | `web/mcp_env_catalog.py` | קטלוג מקיף |
|
||||||
|
| GAP-61 | URLs + `/home/chaim` קשיחים | INV-ENV3 | Low | `web/paperclip_client.py:31`, app.py | env/config |
|
||||||
|
| GAP-62 | start.sh לא-נכשל-על-uvicorn; deploy-curl fire-and-forget | INV-ENV2/§6 | Low | `start.sh`, `.gitea/workflows/deploy.yaml` | health-gate + אימות-deploy |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## יחידות-תיקון מוצעות (Proposed Fix-Units)
|
||||||
|
|
||||||
|
23 הממצאים מקובצים ל-8 יחידות-עבודה קוהרנטיות. הקיבוץ נגזר מהעיקרון שרבים מהממצאים
|
||||||
|
נפתרים יחד (כל פערי ה-ingest-asymmetry → יחידה אחת). זהו זרע למשימות TaskMaster
|
||||||
|
ולתת-פרויקט 3 (שכבת-שלמות).
|
||||||
|
|
||||||
|
> **✅ מחזור-1 הושלם (31.5.2026):** FU-1..FU-8b כולם מוזגו ל-main. מחזור-2 (FU-9..15, להלן)
|
||||||
|
> נגזר מ-GAP-24..62 ו**פתוח**.
|
||||||
|
|
||||||
|
### FU-1 — איחוד מסלול-הקליטה (Unify ingest path)
|
||||||
|
- **מכסה:** GAP-01, GAP-02, GAP-04, GAP-05
|
||||||
|
- **מספק invariants:** INV-ING1, INV-ING3, INV-G2, INV-G4; (תורם ל-DM1/RET2 דרך GAP-02)
|
||||||
|
- **effort:** L
|
||||||
|
- **תלויות:** — (יסוד — FU-2/FU-3 נשענים עליה)
|
||||||
|
- **סוג:** pure-code
|
||||||
|
|
||||||
|
### FU-2 — קליטה idempotent + מזהים קנוניים
|
||||||
|
- **מכסה:** GAP-03, GAP-06, GAP-07, GAP-08, GAP-13
|
||||||
|
- **מספק invariants:** INV-ING2, INV-G3, INV-G1, INV-ID1, INV-ID2, INV-DM2, INV-DM1
|
||||||
|
- **effort:** L
|
||||||
|
- **תלויות:** FU-1 (מסלול אחד לפני upsert אחיד)
|
||||||
|
- **סוג:** **data-migration** — GAP-07 reconciliation של case_number מעורב (chair-confirmed),
|
||||||
|
GAP-08 ניקוי ציטוט-כ-מזהה; + code (upsert key, write-time normalize, דגל searchable)
|
||||||
|
|
||||||
|
### FU-3 — re-index באכיפה בשינוי-תוכן
|
||||||
|
- **מכסה:** GAP-09
|
||||||
|
- **מספק invariants:** INV-DM3, INV-G6, INV-RET (freshness)
|
||||||
|
- **effort:** M
|
||||||
|
- **תלויות:** FU-1 (re-embed יושב בקליטה הקנונית)
|
||||||
|
- **סוג:** **data-migration** — re-chunk/re-embed של רשומות קיימות + טריגר/אכיפה קדימה
|
||||||
|
|
||||||
|
### FU-4 — הפרדת-קורפוס נאכפת בכל query
|
||||||
|
- **מכסה:** GAP-10, GAP-12
|
||||||
|
- **מספק invariants:** INV-RET1, INV-G5
|
||||||
|
- **effort:** M
|
||||||
|
- **תלויות:** — (עצמאי; דחוף — Critical leak)
|
||||||
|
- **סוג:** pure-code
|
||||||
|
|
||||||
|
### FU-5 — eval harness + נראות-בריאות
|
||||||
|
- **מכסה:** GAP-11, GAP-14
|
||||||
|
- **מספק invariants:** INV-RET4, INV-G8, INV-QA1, INV-G10 (נראות backlog)
|
||||||
|
- **effort:** M
|
||||||
|
- **תלויות:** FU-2 (gold-set יציב דורש מזהים קנוניים)
|
||||||
|
- **סוג:** pure-code + **chair-decision** — הגדרת gold-set מתויג דורשת אישור היו"ר
|
||||||
|
(מה "תוצאה נכונה" לכל query)
|
||||||
|
|
||||||
|
### FU-6 — שערי-QA נאכפים-קוד (Code-enforced gates)
|
||||||
|
- **מכסה:** GAP-15, GAP-16
|
||||||
|
- **מספק invariants:** INV-QA3, INV-EX3, INV-G10
|
||||||
|
- **effort:** S
|
||||||
|
- **תלויות:** — (עצמאי; חוסם עקיפת-ייצוא)
|
||||||
|
- **סוג:** pure-code
|
||||||
|
|
||||||
|
### FU-7 — Audit-trail + provenance (זרע תת-פרויקט 3)
|
||||||
|
- **מכסה:** GAP-17, GAP-18, GAP-19, GAP-20
|
||||||
|
- **מספק invariants:** INV-AUD1, INV-AUD2, INV-AUD3, INV-EX1, INV-G9
|
||||||
|
- **effort:** L
|
||||||
|
- **תלויות:** FU-1 (provenance נלכד בקליטה/כתיבה הקנונית)
|
||||||
|
- **סוג:** pure-code (schema-additive) — חלק מ-GAP-17 דורש **data-backfill** קל
|
||||||
|
לסנכרון בלוקים↔DOCX קיימים
|
||||||
|
|
||||||
|
### FU-8 — מחסומי-תהליך הופכים למחסומי-קוד
|
||||||
|
- **מכסה:** GAP-21, GAP-22, GAP-23
|
||||||
|
- **מספק invariants:** INV-MC1, INV-INT1, INV-INT3, INV-AG1
|
||||||
|
- **effort:** M
|
||||||
|
- **תלויות:** ה-spec גמור (GAP-23 דורש קבצי-ספ יציבים לחבר לסוכנים)
|
||||||
|
- **סוג:** pure-code + **chair-decision** — GAP-23 (חיבור ספ לסוכני-Paperclip) הוא
|
||||||
|
prerequisite לתת-פרויקט 5 ומשנה התנהגות-סוכן בייצור
|
||||||
|
- **סטטוס:** ✅ FU-8a (GAP-21/22, PR #16) + FU-8b (GAP-23, PR #23) מוזגו.
|
||||||
|
|
||||||
|
> **— מחזור-2 (FU-9..15): 8 משטחי-האפליקציה מחוץ לצינור-הליבה. כולם פתוחים. —**
|
||||||
|
|
||||||
|
### FU-9 — לקוח-Paperclip קנוני
|
||||||
|
- **מכסה:** GAP-24..29 · **invariants:** INV-INT4–INT8 · **effort:** L · **תלויות:** [X7](X7-paperclip-client-params.md) יציב
|
||||||
|
- **סוג:** code — איחוד 2 הלקוחות, הסרת מסלול-DB, IDs מ-config, company_id יחיד, webhook idempotency+enum
|
||||||
|
|
||||||
|
### FU-10 — חוזה UI↔API + design-system SSoT
|
||||||
|
- **מכסה:** GAP-30..34 + [ui-audit](ui-audit.md) (UI-A1..D6) · **invariants:** INV-UI1–UI6 · **effort:** L · **תלויות:** —
|
||||||
|
- **סוג:** code — Pydantic models+`api:types`, SSoT ל-enums/תוויות/tones, helpers משותפים, ניקוי redundancy
|
||||||
|
|
||||||
|
### FU-11 — מילוי-שדות מוצהר + שקיפות-UI
|
||||||
|
- **מכסה:** GAP-35..37 · **invariants:** INV-FP1–FP5, UI6 · **effort:** M · **תלויות:** —
|
||||||
|
- **סוג:** code — טבלת-provenance SSoT, formalize placeholder, חיווי "מולא-ע"י-Opus" + searchable + pending ב-UI
|
||||||
|
|
||||||
|
### FU-12 — חיזוק אחסון-הניתוחים
|
||||||
|
- **מכסה:** GAP-38..43 · **invariants:** INV-DM4–DM6 · **effort:** M · **תלויות:** FU-1
|
||||||
|
- **סוג:** code + data-migration קל — provenance, שער-אישור ל-legal_arguments, CHECK-enums, FK, איחוד case_precedents
|
||||||
|
|
||||||
|
### FU-13 — סוכנים + skills — ✅ נסגר (2026-06-06)
|
||||||
|
- **מכסה:** GAP-46 (מרחיב GAP-23) · **invariants:** INV-AG3, INV-TOOL6 · **effort:** S · **תלויות:** ה-spec יציב
|
||||||
|
- **סוג:** code/docs — שלמות-הרשאות (tools↔instructions), DRY-boilerplate, dedup-skills
|
||||||
|
- **סטטוס:** הכרעת-יו"ר "היבריד". התברר שהפער ב-31.5 היה רחב מדי (יוחס לפי תיאור-תפקיד, לא הוראות בפועל).
|
||||||
|
researcher כבר היה תקין (מיושן ב-spec). analyst קיבל `aggregate_claims_to_arguments` + שלב 7 ("שלב 1");
|
||||||
|
`extract_references`/`extract_internal_citations` נשארו אצל researcher (מטלת-מחקר, לא analyst). עודכן [X4 §2א](X4-agents.md).
|
||||||
|
|
||||||
|
### FU-14 — חוזה כלי-ה-MCP
|
||||||
|
- **מכסה:** GAP-44,45,47..54 · **invariants:** INV-TOOL1–TOOL5 · **effort:** L · **תלויות:** FU-1
|
||||||
|
- **סוג:** code — envelope אחיד, מיזוג חיפוש/בלוקים, idempotency, limit-caps, get-symmetry, set_outcome SSoT
|
||||||
|
- **סטטוס חלקי (פרוסה 1, 2026-06-06):** ✅ **GAP-44** — נוסף `get_appraiser_facts` (ה-get המקביל ל-extract, INV-TOOL4); ✅ **GAP-53** — נוסף `_clamp_limit` (תקרה 200, INV-TOOL5) על ~13 כלי list/search + הוספת limit ל-`list_chair_feedback` (שהיה ללא תקרה).
|
||||||
|
- **סטטוס חלקי (פרוסה 2, 2026-06-06):** ✅ **GAP-52** (INV-TOOL3 idempotency) — `case_create`/`precedent_attach`/`document_upload` מחזירים קיים במקום כפילות (בדיקת-מפתח ברמת-אפליקציה; document_upload לפי SHA-256 → מדלג OCR/embed כפול); ✅ **GAP-45** (INV-TOOL4 visibility) — נוסף `extraction_status` שחושף עומק תור-החילוץ (metadata/halacha) + גיל הבקשה הוותיקה.
|
||||||
|
- **סטטוס חלקי (פרוסה 3, 2026-06-06):** ✅ **GAP-51** (set_outcome SSoT, הכרעת-יו"ר "3 תוצאות + הוצאת betterment_levy") — קנוני `rejection/partial_acceptance/full_acceptance` ב-`lessons.VALID_OUTCOMES`; `OUTCOME_LABELS_HE` (עברית-ב-UI SSoT); `canonical_outcome()` ל-legacy; `betterment_levy`→`PRACTICE_AREA_OVERRIDES` (override לפי practice_area); block_writer/set_outcome/drafting/web-ui יושרו; נתונים נורמלו (9 שורות + גיבוי).
|
||||||
|
- **סטטוס חלקי (פרוסה 4, 2026-06-06):** ✅ **GAP-47 (חלק provenance, INV-TOOL4/G9)** — `draft_section` חושף בפלט `document_id`+`page`+`score` לכל קטע ב-`case_documents`/`precedents` (ה-provenance כבר נשלף ב-`search_similar` ונזרק; כעת מוחזר), כך שהכותב יכול לעקוב אל המקור ולצטטו. תוספתי, לא-שובר. **נותר ב-GAP-47:** העברת הנחיות-יו"ר מ-`analysis-and-research.md` ל-DB (`get_chair_directions`) — שינוי-מסלול גדול יותר הנוגע ל-UI של דפנה ולזרימת-האנליסט; לפרוסה נפרדת.
|
||||||
|
- **סטטוס חלקי (פרוסה 5, 2026-06-06):** 🔄 **GAP-48 (envelope אחיד, INV-TOOL1) — תחילת מיגרציה הדרגתית.** נוצר `tools/envelope.py` כ-SSoT יחיד (`ok`/`empty`/`err` → `{status,data,message}`, status מבחין הצלחה/ריק/שגיאה) המחליף 3 מוסכמות סותרות (raw payload / `{error}` / `{status,message}` אד-הוק) ו-5 עותקי `_ok`/`_err` משוכפלים. **משפחת-החיפוש הראשונה הומרה** (`search_decisions`/`search_case_documents`/`find_similar_cases`/`search_internal_decisions`); `web/app.py` מפרק דרך `envelope_unwrap` לשמירת חוזה-ה-API (X6) ללא-שינוי; טסט `test_search_domain_scope` עודכן לחוזה החדש (5/5 ✅). **החלטה הנדסית:** הדרגתי לפי-משפחה ולא big-bang — מפת-צרכנים (Explore) הראתה ש-server.py הוא pass-through, web-ui מבודד (`/api/*`), ורק 17 כלים נצרכים ישירות מ-app.py — כך הסיכון לסוכנים החיים ממוזער. נותרו ~73 כלים בפרוסות הבאות.
|
||||||
|
- **סטטוס חלקי (פרוסה 6, 2026-06-06):** 🔄 **GAP-48 — מיגרציה רוחבית.** הומרו 10 משפחות נוספות ל-envelope: `precedent_library` (14), `citations` (3), `internal_decisions` (1), `missing_precedents` (4), `training_enrichment` (2), `precedents` (4), `legal_arguments` (2), `cases` (7), `documents` (8), `workflow` (9). בוטלו 5 עותקי `_ok`/`_err` משוכפלים (alias ל-SSoT, G2). עיקרון: envelope-`status` = הצלחת-הקריאה; תוצאה-עסקית (idempotent_existing/noop/...) ב-`data`. צרכני-app.py של cases/workflow/precedents חוּוטו דרך `envelope_unwrap` + בדיקת `status=="error"`→4xx, לשמירת חוזה-ה-API. כל הטסטים עוברים (182/182; `test_corpus_constraints` עודכן לחוזה). **נותר:** משפחת `drafting` (18 כלים — מסלול הפקת-ההחלטה) בפרוסה נפרדת.
|
||||||
|
- **פרוסה 7, 2026-06-06 — ✅ GAP-48 הושלם.** משפחת `drafting` (18 כלים) הומרה ל-envelope. export_docx/revise_draft/apply_user_edit משתמשים ב-`err`-לכשל (כך שהסוכן והמשתמש רואים את הכשל ברמת-המעטפת), כש-`failed_gates` רוכב ב-`data`; 6 צרכני-app.py (get_decision_template/apply_user_edit×2/revise_draft/list_bookmarks/export_docx) חוּוטו עם בדיקת envelope-status; `test_export_qa_gate` עודכן לחוזה (182/182 עוברים). **GAP-48 סגור — כל ~12 המשפחות אחידות.**
|
||||||
|
- **פרוסה 8, 2026-06-06 — ✅ GAP-49 (החלק הקריטי).** השם המטעה `precedent_search_library` (ציטוטים מצורפים-לתיק) שונה ל-`search_case_precedents` ובכך בוטל ההיפוך המסוכן מול `search_precedent_library` (ספרייה סמכותית — מקור CREAC). הישן נשמר כ-alias deprecated (ב-server.py) → אפס שבירה לסוכנים חיים. docstrings הובהרו; עודכנו app.py (typeahead) + legal-researcher/legal-writer docs + precedent_library docstring. 5 כלי-החיפוש הנותרים מחפשים קורפוסים מובחנים בשמות סבירים — לא בוצע rename-המוני (churn גבוה, ערך נמוך). 182/182 עוברים. **⚠ אחרי merge+deploy:** סנכרון cross-company של doc-הסוכן (frontmatter `search_case_precedents`). נותר ב-FU-14: GAP-50 (מיזוג כלי-בלוק — נוגע בתהליך-הכתיבה, דורש הכרעת-יו"ר), GAP-54, GAP-47-חלק-ב.
|
||||||
|
- **פרוסה 9, 2026-06-06 — ✅ GAP-50 (הכרעת-יו"ר).** מיפוי הראה שכלי-הבלוק אינם "כפילות מיותרת": `write_block`/`write_all_blocks`/`save_block_content`/`write_interim_draft` משרתים זרימות שונות (CLI/initial-draft מול תהליך-ה-writer "התיקון בקובץ, לא ב-DB"). הכפילות האמיתית היחידה — `draft_section` (הקשר לפי-סעיף, כמעט-נטוש) חופף ל-`get_block_context` (לפי-בלוק, קנוני). הוחלט (יו"ר): **draft_section deprecated** (docstring ב-server.py+drafting.py מפנה ל-get_block_context; draft-decision.md עודכן) — בלי הסרה, בלי מיזוג כלי-הכתיבה (שמירת תהליך-הכתיבה המכוון). 182/182 עוברים. **GAP-49+50 סגורים.** נותר ב-FU-14: GAP-54 (איחוד קליטת-פסיקה), GAP-47-חלק-ב (הנחיות-יו"ר→DB).
|
||||||
|
- **פרוסה 10, 2026-06-06 — ✅ GAP-54 (נסגר כ-resolved-by-FU-1).** אימות (G2: לא לפתור מחדש): `ingest.ingest_document` הוא המסלול הקנוני; `precedent_library` ו-`internal_decisions` שניהם עוברים דרכו עם ולידציית-enums + citation-guard סימטריים (מתועד ב-01-ingest §4); training→`style_corpus` הוא קורפוס נפרד במכוון. 9/9 `test_unified_ingest` עוברים — אין קוד לכתוב. **FU-14 כמעט-מלא: נותר רק GAP-47-חלק-ב** (העברת הנחיות-יו"ר מ-`analysis-and-research.md` ל-DB) — פיצ'ר UI+זרימת-אנליסט נפרד, לא דחוף.
|
||||||
|
|
||||||
|
### FU-15 — deploy/env/secrets
|
||||||
|
- **מכסה:** GAP-55..62 · **invariants:** INV-ENV1–ENV5 · **effort:** M · **תלויות:** —
|
||||||
|
- **סוג:** code/config + **chair-decision** (rotation סודות) — env-catalog SSoT, מקור-config יחיד, de-hardcode, drift מלא, start.sh עמיד
|
||||||
|
- **סטטוס חלקי:** GAP-57 (creds plaintext, אבטחה CWE-798) **נסגר ב-web/ 2026-06-06** — 3 מופעים ב-`web/paperclip_api.py`/`paperclip_client.py`/`app.py` הומרו ל-`require_paperclip_db_url()` fail-loud. נותרו 2 מופעים בסקריפטים מקומיים (`sync_agents_across_companies.py`, `sync_missing_agent_skills.py`) + GAP-55,56,58–62 — לטיפול ב-FU-15 המלא.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## סיכום סיווג לפי סוג-עבודה
|
||||||
|
|
||||||
|
- **pure-code (ללא מיגרציה):** FU-1, FU-4, FU-6; הליבה של FU-7, FU-8.
|
||||||
|
- **דורש data-migration:** FU-2 (case_number reconciliation, ניקוי ציטוטים), FU-3
|
||||||
|
(re-chunk/re-embed), backfill קל ב-FU-7 (סנכרון בלוקים↔DOCX).
|
||||||
|
- **דורש chair-decision:** FU-5 (הגדרת gold-set), FU-8/GAP-23 (חיבור ספ לסוכנים);
|
||||||
|
GAP-07 כבר chair-confirmed (canonical = הצורה הרשמית שהוקצתה).
|
||||||
|
|
||||||
|
**רצף מומלץ (תלויות):** FU-1 → FU-2 → FU-3; FU-4 ו-FU-6 במקביל (עצמאיים, Critical);
|
||||||
|
FU-7 אחרי FU-1; FU-5 אחרי FU-2; FU-8 אחרי ייצוב-הספ. **(מחזור-1 ✅ הושלם.)**
|
||||||
|
|
||||||
|
**מחזור-2 (FU-9..15) — 8 משטחי-האפליקציה:** FU-10 (UI+design-system) ו-FU-15 (deploy/env) עצמאיים —
|
||||||
|
ניתן במקביל. FU-9 (לקוח-Paperclip) אחרי [X7](X7-paperclip-client-params.md). FU-12 (אחסון) ו-FU-14 (כלי-MCP)
|
||||||
|
אחרי FU-1. FU-11 (מילוי-שדות) עצמאי. FU-13 (סוכנים+skills) אחרי ייצוב-הספ.
|
||||||
|
**סיווג:** pure-code — FU-9/10/11/13/14; +data-migration קל — FU-12; +chair-decision — FU-15 (rotation סודות).
|
||||||
|
priority בפועל — של היו"ר.
|
||||||
72
docs/spec/ui-audit.md
Normal file
72
docs/spec/ui-audit.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# UI-Audit — ביקורת דף-אחר-דף של ה-web-ui
|
||||||
|
|
||||||
|
מסמך זה הוא **מפת-הממצאים של ה-frontend** (web-ui), מקביל ל-[gap-audit.md](gap-audit.md) אך ברמת-הדף/הרכיב.
|
||||||
|
הוא תוצר סריקה word-for-word של 13 הדפים (5 cases-flow + 5 knowledge + 3 admin) + השכבה המשותפת.
|
||||||
|
כל ממצא נושא: `invariant מופר` (מ-[X6](X6-ui-api-contract.md)/[X8](X8-field-provenance.md)) · `severity` ·
|
||||||
|
`file:line` · `תיקון`. severity = הערכה הנדסית; priority = היו"ר.
|
||||||
|
|
||||||
|
**איך הופק:** סקירת 13 הדפים + `src/lib/api/*` + `src/components/*`, מאומת מול הקוד. התיקון מקובץ ל-**FU-10**.
|
||||||
|
|
||||||
|
> **דפים שנסרקו:** dashboard, cases/new, cases/[caseNumber], cases/[caseNumber]/compose, archive ·
|
||||||
|
> precedents, precedents/[id], training, methodology, missing-precedents · settings, skills, diagnostics.
|
||||||
|
> **נווט מלא:** כל 13 הדפים נגישים מ-[app-shell.tsx](../../web-ui/src/components/app-shell.tsx) — אין דף-יתום.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. מוגדר-לא-נכון (Wrong Definitions)
|
||||||
|
|
||||||
|
| ID | כותרת | invariant | severity | file:line | תיקון |
|
||||||
|
|----|-------|-----------|----------|-----------|-------|
|
||||||
|
| UI-A1 | `PracticeArea` מוגדר ב-**3 מקומות עם ערכים שונים** — [lib/practice-area.ts:12](../../web-ui/src/lib/practice-area.ts) (`appeals_committee/national_insurance/labor_law` — שאריות מפרויקט אחר!), [lib/api/precedent-library.ts:26](../../web-ui/src/lib/api/precedent-library.ts) (`rishuy_uvniya/...`), ו-[components/precedents/practice-area.ts](../../web-ui/src/components/precedents/practice-area.ts) | UI1, G2 | **CRITICAL** | 3 קבצים | SSoT יחיד; הסרת שאריות national_insurance/labor_law |
|
||||||
|
| UI-A2 | `key_quote` חסר מטיפוס `Precedent`; גישה דרך `as {key_quote?:string}` | UI1/UI2 | **CRITICAL** | [precedents/[id]/page.tsx:178](../../web-ui/src/app/precedents/%5Bid%5D/page.tsx), [precedent-edit-sheet.tsx:94](../../web-ui/src/components/precedents/precedent-edit-sheet.tsx) | הוספת `key_quote` לטיפוס/OpenAPI |
|
||||||
|
| UI-A3 | תווית לא-עקבית לאותו ערך: "פיצויים (197)" מול "פיצויים לפי ס' 197" | UI1 | High | [precedents/[id]/page.tsx:26-35](../../web-ui/src/app/precedents/%5Bid%5D/page.tsx) מול practice-area.ts | תווית מ-SSoT יחיד |
|
||||||
|
| UI-A4 | enum נצרך לא-נגזר-מטיפוס (zod ידחה subtype חדש) | UI1 | High | [schemas/case.ts:78-86](../../web-ui/src/lib/schemas/case.ts) | zod נגזר מ-PracticeArea/AppealSubtype |
|
||||||
|
| UI-A5 | `expectedOutcomes`/`set_outcome` — אוצר-מילים לא-תואם בק (`rejected/accepted/partial` מול `rejection/.../betterment_levy`) | UI1, G2 | High | [schemas/case.ts:35-41](../../web-ui/src/lib/schemas/case.ts); בק `block_writer.py:442`/`lessons.py:11` | SSoT יחיד ל-enum-תוצאה |
|
||||||
|
|
||||||
|
## 2. כפילות (Duplication)
|
||||||
|
|
||||||
|
| ID | כותרת | invariant | severity | file:line | תיקון |
|
||||||
|
|----|-------|-----------|----------|-----------|-------|
|
||||||
|
| UI-B1 | `CaseStatus` + `STATUS_LABELS` + `STATUS_TONE` ב-3 מקומות | UI1, G2 | **CRITICAL** | [cases.ts:16-33](../../web-ui/src/lib/api/cases.ts), [status-badge.tsx:11-29,77-95](../../web-ui/src/components/cases/status-badge.tsx), [status-changer.tsx:18-24](../../web-ui/src/components/cases/status-changer.tsx) | enum+labels+tones מ-SSoT, ייבוא |
|
||||||
|
| UI-B2 | `STATUS_LABELS` של מסמכים משוכפל (ולא-שלם) | UI1 | High | [upload-sheet.tsx:39-46](../../web-ui/src/components/documents/upload-sheet.tsx), [documents-panel.tsx:39-46](../../web-ui/src/components/cases/documents-panel.tsx) | ל-`lib/doc-types.ts` |
|
||||||
|
| UI-B3 | פירמוט-תאריך משוכפל ×5 | UI/§6 | Medium | archive.tsx, case-header.tsx, documents-panel.tsx (+2) | `lib/format.ts` משותף |
|
||||||
|
| UI-B4 | תוויות practice-area/source-type משוכפלות | UI1 | High | [precedents/[id]/page.tsx:26-35](../../web-ui/src/app/precedents/%5Bid%5D/page.tsx) | ייבוא מ-practice-area.ts |
|
||||||
|
| UI-B5 | boilerplate העלאת-קבצים (FormData+fetch) ×4 | §6/G2 | Medium | documents.ts, training.ts, exports.ts, missing-precedents.ts | `uploadMultipart<T>()` ב-client.ts |
|
||||||
|
| UI-B6 | כרטיס-שגיאה משוכפל ×3 | UI4 | Medium | detail/library/missing pages | `<ErrorCard>` משותף |
|
||||||
|
|
||||||
|
## 3. מיותר / מת (Redundancy / Dead)
|
||||||
|
|
||||||
|
| ID | כותרת | invariant | severity | file:line | תיקון |
|
||||||
|
|----|-------|-----------|----------|-----------|-------|
|
||||||
|
| UI-C1 | 3 דפי-פסיקה חופפים (/precedents, /training, /missing-precedents) — גבולות מטושטשים | G2 | Medium | 3 דפים | הגדרת אחריות; שקילת איחוד |
|
||||||
|
| UI-C2 | כפתור "חלץ מטא-דאטה" שלא מרענן, מפנה ל-CLI ידני | UI5/FP5 | Medium | [precedent-edit-sheet.tsx:130](../../web-ui/src/components/precedents/precedent-edit-sheet.tsx) | auto-refresh/poll על תור-החילוץ |
|
||||||
|
| UI-C3 | `useCase` refetch כל 5ש' גם במנוחה/בעריכה | UI5 | Low | [cases.ts:150-152](../../web-ui/src/lib/api/cases.ts) | interval מותנה-סטטוס |
|
||||||
|
| UI-C4 | magic-numbers (intervals) מפוזרים ב-18 מודולים | UI5/§6 | Low | כל `lib/api/*` | `lib/api/query-config.ts` |
|
||||||
|
|
||||||
|
## 4. אי-עקביות + הפרת-כללים (Inconsistency / Rule-Violations)
|
||||||
|
|
||||||
|
| ID | כותרת | invariant | severity | file:line | תיקון |
|
||||||
|
|----|-------|-----------|----------|-----------|-------|
|
||||||
|
| UI-D1 | ~60% endpoints `unknown` → טיפוסים ידניים-סוטים | UI1/UI2 | **CRITICAL** | [cases.ts:1-9](../../web-ui/src/lib/api/cases.ts) (מתועד מפורשות) + בק | Pydantic models + `api:types` |
|
||||||
|
| UI-D2 | שדות-Opus מוצגים ללא חיווי "חולץ-אוטומטית"; היו"ר לא יודע מה לאמת | UI6/FP1 | High | [precedents/[id]/page.tsx:160-185](../../web-ui/src/app/precedents/%5Bid%5D/page.tsx) | badge "מולא-ע"י-Opus" |
|
||||||
|
| UI-D3 | אין חיווי `searchable`; הלכות `pending_review` לא מובלטות בדף-הפרט | UI6/FP3 | High | precedents/[id]/page.tsx | חיווי searchable + אזהרת-pending |
|
||||||
|
| UI-D4 | fallback SSE מסתיר כישלון כ-"completed"; TTL 5ש'↔300ש' | UI4/UI5 | Medium | [documents.ts:226-232](../../web-ui/src/lib/api/documents.ts) | terminal-state מפורש |
|
||||||
|
| UI-D5 | query-keys לא-עקביים (חלק `.all`, חלק לא; חלק exported, חלק לא) | §6 | Low | agents.ts, feedback.ts (+) | convention אחיד |
|
||||||
|
| UI-D6 | URLs קשיחים (`PAPERCLIP_BASE`, coolify, frontend) | UI3/ENV3 | Low | [app-shell.tsx:70](../../web-ui/src/components/app-shell.tsx) | env (ראה [X10](X10-deploy-env-secrets.md)) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. סיכום ל-FU-10
|
||||||
|
- **SSoT ל-enums/תוויות/tones** (UI-A1..A5, UI-B1/B2/B4) — תיקון-השורש של רוב הממצאים.
|
||||||
|
- **Pydantic models + OpenAPI=SSoT** (UI-D1) — מבטל את הטיפוסים-הידניים.
|
||||||
|
- **helpers משותפים** (UI-B3/B5/B6, UI-C4) — תאריך, upload, error-card, query-config.
|
||||||
|
- **שקיפות-מקור-מילוי** (UI-D2/D3) — נגזר מ-[X8](X8-field-provenance.md)/[X6 INV-UI6](X6-ui-api-contract.md).
|
||||||
|
- **ניקוי redundancy** (UI-C1..C3).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. הפניות-אחיות
|
||||||
|
- [X6-ui-api-contract.md](X6-ui-api-contract.md) — ה-invariants (UI1–UI6) שממצאים אלו מפרים.
|
||||||
|
- [X8-field-provenance.md](X8-field-provenance.md) — מקור-מילוי (בסיס ל-UI-D2/D3).
|
||||||
|
- [gap-audit.md](gap-audit.md) — GAP-30..34 (התקבילים ברמת-הארכיטקטורה) + FU-10.
|
||||||
|
- [00-constitution.md](00-constitution.md) — [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים), [G4](00-constitution.md#inv-g4-חוזה-שלמות-לפני-שמיש--ניתן-לחיפוש), [G9](00-constitution.md#inv-g9-עקיבוּת-מקור--audit-trail-ל-ai).
|
||||||
833
docs/superpowers/plans/2026-05-30-fu1-unified-ingest.md
Normal file
833
docs/superpowers/plans/2026-05-30-fu1-unified-ingest.md
Normal file
@@ -0,0 +1,833 @@
|
|||||||
|
# FU-1 Unified Ingest Path — 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:** Collapse the two parallel ingest functions (`ingest_precedent`, `ingest_internal_decision`) into one canonical pipeline parameterized by an `IntakeSpec`, closing GAP-01/02/04/05.
|
||||||
|
|
||||||
|
**Architecture:** New module `services/ingest.py` holds a Template-Method skeleton `ingest_document(spec, ...)`; per-type variation rides on a frozen `IntakeSpec` config object (staging resolver, validate callable, enum_fields data, derive callable, display-name fallback, injected `create_record`). The two existing public functions stay as named entry points that build a spec and delegate. The DB-create functions are NOT merged (FU-2 boundary) — only routed via `spec.create_record`.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.12, asyncpg, pytest (offline, monkeypatched I/O), local `.venv` at `mcp-server/.venv`.
|
||||||
|
|
||||||
|
**Spec:** [docs/superpowers/specs/2026-05-30-fu1-unified-ingest-design.md](../specs/2026-05-30-fu1-unified-ingest-design.md)
|
||||||
|
|
||||||
|
**Run tests with:** `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_unified_ingest.py -v`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- **Create** `mcp-server/src/legal_mcp/services/ingest.py` — canonical pipeline + `IntakeSpec` + shared helpers (`_stage_file`, `_coerce_date`, `_safe_filename`, `_embed_pages`).
|
||||||
|
- **Create** `mcp-server/tests/test_unified_ingest.py` — offline behavioral tests.
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/services/precedent_library.py` — `ingest_precedent` becomes a thin wrapper building `_EXTERNAL_SPEC`; delete inline pipeline + moved helpers; keep everything else (search, reextract, process_pending, list, delete, get).
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/services/internal_decisions.py` — `ingest_internal_decision` becomes a thin wrapper building `_INTERNAL_SPEC`; delete inline pipeline + moved helpers; keep migrate_*, enrich_*, search_internal.
|
||||||
|
|
||||||
|
**Unchanged callers (verify, don't edit):** `tools/precedent_library.py`, `tools/internal_decisions.py`, `web/` HTTP handlers — they call the two public functions whose signatures are preserved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Failing tests for the unified pipeline
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Test: `mcp-server/tests/test_unified_ingest.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""FU-1: unified ingest pipeline tests (offline, all I/O monkeypatched).
|
||||||
|
|
||||||
|
Proves both intake types flow through services.ingest.ingest_document and that
|
||||||
|
the canonical pipeline is symmetric: BOTH metadata and halacha extraction are
|
||||||
|
queued for BOTH types (GAP-02 regression), enum validation applies to both
|
||||||
|
(GAP-04), multimodal is gated by flag+PDF not by intake type (GAP-05), and the
|
||||||
|
external citation guard is preserved.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from legal_mcp import config
|
||||||
|
from legal_mcp.services import db, embeddings, chunker, extractor
|
||||||
|
from legal_mcp.services import ingest, precedent_library, internal_decisions
|
||||||
|
|
||||||
|
|
||||||
|
def _run(coro):
|
||||||
|
return asyncio.run(coro)
|
||||||
|
|
||||||
|
|
||||||
|
class _Chunk:
|
||||||
|
def __init__(self, i):
|
||||||
|
self.chunk_index = i
|
||||||
|
self.content = f"chunk-{i}"
|
||||||
|
self.section_type = "body"
|
||||||
|
self.page_number = 1
|
||||||
|
self.role = "child"
|
||||||
|
self.local_id = f"c{i}"
|
||||||
|
self.parent_local_id = None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def patched(monkeypatch, tmp_path):
|
||||||
|
"""Patch every I/O boundary. Record queue + create calls."""
|
||||||
|
calls = {"metadata": [], "halacha": [], "create": [], "chunks": [], "pages": []}
|
||||||
|
|
||||||
|
async def _extract_text(path):
|
||||||
|
return ("full decision text", 2, [0, 100])
|
||||||
|
|
||||||
|
def _strip(text):
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _chunk(text, page_offsets=None):
|
||||||
|
return [_Chunk(0), _Chunk(1)]
|
||||||
|
|
||||||
|
async def _embed(texts, input_type="document"):
|
||||||
|
return [[0.0] * 8 for _ in texts]
|
||||||
|
|
||||||
|
async def _store_chunks(cid, dicts):
|
||||||
|
calls["chunks"].append((cid, len(dicts)))
|
||||||
|
return len(dicts)
|
||||||
|
|
||||||
|
async def _create_external(**kw):
|
||||||
|
calls["create"].append(("external", kw))
|
||||||
|
return {"id": uuid4()}
|
||||||
|
|
||||||
|
async def _create_internal(**kw):
|
||||||
|
calls["create"].append(("internal", kw))
|
||||||
|
return {"id": uuid4()}
|
||||||
|
|
||||||
|
async def _req_meta(cid):
|
||||||
|
calls["metadata"].append(cid)
|
||||||
|
|
||||||
|
async def _req_hal(cid):
|
||||||
|
calls["halacha"].append(cid)
|
||||||
|
|
||||||
|
async def _set_status(cid, status):
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr(extractor, "extract_text", _extract_text)
|
||||||
|
monkeypatch.setattr(extractor, "strip_nevo_preamble", _strip)
|
||||||
|
monkeypatch.setattr(chunker, "chunk_document", _chunk)
|
||||||
|
monkeypatch.setattr(embeddings, "embed_texts", _embed)
|
||||||
|
monkeypatch.setattr(db, "store_precedent_chunks", _store_chunks)
|
||||||
|
monkeypatch.setattr(db, "create_external_case_law", _create_external)
|
||||||
|
monkeypatch.setattr(db, "create_internal_committee_decision", _create_internal)
|
||||||
|
monkeypatch.setattr(db, "request_metadata_extraction", _req_meta)
|
||||||
|
monkeypatch.setattr(db, "request_halacha_extraction", _req_hal)
|
||||||
|
monkeypatch.setattr(db, "set_case_law_extraction_status", _set_status)
|
||||||
|
monkeypatch.setattr(db, "set_case_law_halacha_status", _set_status)
|
||||||
|
# Force flat chunking + multimodal OFF unless a test flips it.
|
||||||
|
monkeypatch.setattr(config, "PARENT_DOC_RETRIEVAL_ENABLED", False)
|
||||||
|
monkeypatch.setattr(config, "MULTIMODAL_ENABLED", False)
|
||||||
|
return calls
|
||||||
|
|
||||||
|
|
||||||
|
def _make_pdf(tmp_path) -> str:
|
||||||
|
p = tmp_path / "decision.pdf"
|
||||||
|
p.write_bytes(b"%PDF-1.4 fake")
|
||||||
|
return str(p)
|
||||||
|
|
||||||
|
|
||||||
|
def test_internal_queues_BOTH_metadata_and_halacha(patched, tmp_path):
|
||||||
|
"""GAP-02 regression: the internal path must queue metadata too."""
|
||||||
|
_run(internal_decisions.ingest_internal_decision(
|
||||||
|
case_number="8046/24", text="decision text", chair_name="דפנה תמיר",
|
||||||
|
district="ירושלים", practice_area="betterment_levy",
|
||||||
|
))
|
||||||
|
assert len(patched["metadata"]) == 1, "internal path must queue metadata (GAP-02)"
|
||||||
|
assert len(patched["halacha"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_external_queues_both(patched, tmp_path):
|
||||||
|
_run(precedent_library.ingest_precedent(
|
||||||
|
file_path=_make_pdf(tmp_path), citation="עע\"מ 1234/20",
|
||||||
|
practice_area="rishuy_uvniya", source_type="court_ruling",
|
||||||
|
))
|
||||||
|
assert len(patched["metadata"]) == 1
|
||||||
|
assert len(patched["halacha"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_both_types_go_through_ingest_document(patched, tmp_path, monkeypatch):
|
||||||
|
seen = []
|
||||||
|
real = ingest.ingest_document
|
||||||
|
|
||||||
|
async def _spy(spec, **kw):
|
||||||
|
seen.append(spec.source_kind)
|
||||||
|
return await real(spec, **kw)
|
||||||
|
|
||||||
|
monkeypatch.setattr(ingest, "ingest_document", _spy)
|
||||||
|
_run(internal_decisions.ingest_internal_decision(
|
||||||
|
case_number="8046/24", text="t", chair_name="דפנה תמיר", practice_area="betterment_levy"))
|
||||||
|
_run(precedent_library.ingest_precedent(
|
||||||
|
file_path=_make_pdf(tmp_path), citation="עע\"מ 1/20", practice_area="rishuy_uvniya"))
|
||||||
|
assert seen == ["internal_committee", "external_upload"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_validation_rejects_bad_practice_area_internal(patched, tmp_path):
|
||||||
|
"""GAP-04: internal path must validate enums like the external one."""
|
||||||
|
with pytest.raises(ValueError, match="practice_area"):
|
||||||
|
_run(internal_decisions.ingest_internal_decision(
|
||||||
|
case_number="8046/24", text="t", chair_name="x", practice_area="bogus"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_validation_rejects_bad_practice_area_external(patched, tmp_path):
|
||||||
|
with pytest.raises(ValueError, match="practice_area"):
|
||||||
|
_run(precedent_library.ingest_precedent(
|
||||||
|
file_path=_make_pdf(tmp_path), citation="עע\"מ 1/20", practice_area="bogus"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_external_citation_guard_still_blocks_arar(patched, tmp_path):
|
||||||
|
with pytest.raises(ValueError, match="ערר"):
|
||||||
|
_run(precedent_library.ingest_precedent(
|
||||||
|
file_path=_make_pdf(tmp_path), citation="ערר 1234/24"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_internal_text_path_works_without_file(patched):
|
||||||
|
out = _run(internal_decisions.ingest_internal_decision(
|
||||||
|
case_number="8046/24", text="t", chair_name="x", practice_area="betterment_levy"))
|
||||||
|
assert out["status"] == "completed"
|
||||||
|
assert out["case_law_id"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_internal_requires_file_or_text(patched):
|
||||||
|
with pytest.raises(ValueError, match="file_path or text"):
|
||||||
|
_run(internal_decisions.ingest_internal_decision(
|
||||||
|
case_number="8046/24", chair_name="x", practice_area="betterment_levy"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_display_name_fallback_uses_canonical_id(patched, tmp_path):
|
||||||
|
_run(internal_decisions.ingest_internal_decision(
|
||||||
|
case_number="8046/24", text="t", chair_name="x", practice_area="betterment_levy"))
|
||||||
|
kind, kw = patched["create"][0]
|
||||||
|
assert kw["case_name"] == "8046/24", "missing case_name falls back to canonical id"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_unified_ingest.py -v`
|
||||||
|
Expected: FAIL — `ModuleNotFoundError: No module named 'legal_mcp.services.ingest'` (or ImportError).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit the red tests**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/tests/test_unified_ingest.py
|
||||||
|
git commit -m "test(ingest): failing tests for unified pipeline (FU-1)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Canonical module `ingest.py` — IntakeSpec + shared helpers
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `mcp-server/src/legal_mcp/services/ingest.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the module header, IntakeSpec, and shared helpers**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Canonical ingest pipeline (FU-1).
|
||||||
|
|
||||||
|
One pipeline for all sibling-entity intake types (external precedent,
|
||||||
|
internal committee decision). Per-type variation rides on an ``IntakeSpec``
|
||||||
|
config object — never a parallel function. See
|
||||||
|
docs/spec/01-ingest.md and docs/superpowers/specs/2026-05-30-fu1-unified-ingest-design.md.
|
||||||
|
|
||||||
|
claude_session rule preserved: this module only QUEUES extraction
|
||||||
|
(``request_*_extraction`` = pure DB writes). It never imports
|
||||||
|
halacha_extractor / precedent_metadata_extractor, so it is safe to call
|
||||||
|
from the FastAPI container where the ``claude`` CLI is unavailable.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import date
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Awaitable, Callable
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
from legal_mcp import config
|
||||||
|
from legal_mcp.services import chunker, db, embeddings, extractor
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ProgressCb = Callable[[str, int, str], Awaitable[None]]
|
||||||
|
|
||||||
|
|
||||||
|
async def _noop_progress(_status: str, _percent: int, _msg: str) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class IntakeSpec:
|
||||||
|
"""Describes everything that varies between intake types."""
|
||||||
|
source_kind: str
|
||||||
|
id_field: str
|
||||||
|
staging_root: Path
|
||||||
|
staging_subdir: Callable[[dict], str]
|
||||||
|
validate: Callable[[dict], None]
|
||||||
|
enum_fields: dict[str, frozenset[str]]
|
||||||
|
derive: Callable[[dict], dict]
|
||||||
|
display_name_fallback: str
|
||||||
|
create_record: Callable[..., Awaitable[dict]]
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_date(value) -> date | None:
|
||||||
|
if value is None or value == "":
|
||||||
|
return None
|
||||||
|
if isinstance(value, date):
|
||||||
|
return value
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
return date.fromisoformat(value[:10])
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_filename(name: str) -> str:
|
||||||
|
base = Path(name).name
|
||||||
|
return re.sub(r"[^\w.\-+א-ת ]", "_", base) or f"upload-{uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _stage_file(src_path: Path, root: Path, subdir: str) -> Path:
|
||||||
|
dest_dir = root / (subdir or "other")
|
||||||
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
dest = dest_dir / f"{uuid4().hex[:8]}_{_safe_filename(src_path.name)}"
|
||||||
|
shutil.copy2(src_path, dest)
|
||||||
|
return dest
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_enums(spec: IntakeSpec, inputs: dict) -> None:
|
||||||
|
for field_name, allowed in spec.enum_fields.items():
|
||||||
|
value = inputs.get(field_name, "") or ""
|
||||||
|
if value not in allowed:
|
||||||
|
raise ValueError(f"invalid {field_name}: {value!r}")
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the multimodal page-embed helper (moved verbatim from precedent_library.py)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def _embed_pages(case_law_id: UUID, pdf_path: Path, page_count: int) -> dict:
|
||||||
|
"""Render PDF pages → embed via voyage-multimodal → store. Non-fatal caller."""
|
||||||
|
thumb_dir = spec_thumb_dir(case_law_id)
|
||||||
|
rendered = await asyncio.to_thread(
|
||||||
|
extractor.render_pages_for_multimodal,
|
||||||
|
pdf_path, config.MULTIMODAL_DPI, config.MULTIMODAL_THUMB_DPI, thumb_dir,
|
||||||
|
)
|
||||||
|
images = [pil for pil, _ in rendered]
|
||||||
|
thumbs = [t for _, t in rendered]
|
||||||
|
img_embs = await embeddings.embed_images(images)
|
||||||
|
|
||||||
|
page_records = []
|
||||||
|
for i, (emb, thumb) in enumerate(zip(img_embs, thumbs)):
|
||||||
|
rel_thumb = None
|
||||||
|
if thumb is not None:
|
||||||
|
try:
|
||||||
|
rel_thumb = str(thumb.relative_to(config.DATA_DIR))
|
||||||
|
except ValueError:
|
||||||
|
rel_thumb = str(thumb)
|
||||||
|
page_records.append({
|
||||||
|
"page_number": i + 1, "embedding": emb, "image_thumbnail_path": rel_thumb,
|
||||||
|
})
|
||||||
|
stored = await db.store_precedent_image_embeddings(
|
||||||
|
case_law_id, page_records, model_name=config.MULTIMODAL_MODEL,
|
||||||
|
)
|
||||||
|
logger.info("Multimodal: stored %d page-image embeddings for case_law %s", stored, case_law_id)
|
||||||
|
return {"pages_embedded": stored}
|
||||||
|
|
||||||
|
|
||||||
|
def spec_thumb_dir(case_law_id: UUID) -> Path:
|
||||||
|
"""Thumbnails live under the precedent-library tree regardless of intake type."""
|
||||||
|
return Path(config.DATA_DIR) / "precedent-library" / "thumbnails" / str(case_law_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify the module imports cleanly**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -c "from legal_mcp.services import ingest; print(ingest.IntakeSpec.__name__)"`
|
||||||
|
Expected: prints `IntakeSpec`, no error.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/ingest.py
|
||||||
|
git commit -m "feat(ingest): IntakeSpec + shared helpers for canonical pipeline (FU-1)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Canonical `ingest_document`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mcp-server/src/legal_mcp/services/ingest.py` (append `ingest_document`)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Append the canonical pipeline function**
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def ingest_document(
|
||||||
|
spec: IntakeSpec,
|
||||||
|
*,
|
||||||
|
inputs: dict,
|
||||||
|
file_path: str | Path | None = None,
|
||||||
|
text: str | None = None,
|
||||||
|
document_id: UUID | None = None,
|
||||||
|
progress: ProgressCb | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Run the canonical 12-step pipeline for one intake item.
|
||||||
|
|
||||||
|
``inputs`` carries the type-specific record fields (citation/case_number,
|
||||||
|
case_name, court, practice_area, etc.). ``spec`` decides how they are
|
||||||
|
validated, staged, derived, and which DB-create runs. Returns a dict with
|
||||||
|
at least: status, case_law_id, chunks.
|
||||||
|
"""
|
||||||
|
progress = progress or _noop_progress
|
||||||
|
|
||||||
|
# Step 1: input validation (type-specific) + enums (uniform mechanism).
|
||||||
|
if not file_path and text is None:
|
||||||
|
raise ValueError("either file_path or text is required")
|
||||||
|
spec.validate(inputs)
|
||||||
|
_validate_enums(spec, inputs)
|
||||||
|
|
||||||
|
# Step 2: field derivation (identity for external).
|
||||||
|
inputs = {**inputs, **spec.derive(inputs)}
|
||||||
|
|
||||||
|
# Steps 3-5: stage (if file) + extract + strip.
|
||||||
|
page_count = 0
|
||||||
|
page_offsets = None
|
||||||
|
staged: Path | None = None
|
||||||
|
if file_path:
|
||||||
|
src = Path(file_path)
|
||||||
|
if not src.is_file():
|
||||||
|
raise FileNotFoundError(f"file not found: {src}")
|
||||||
|
await progress("staging", 5, "מעתיק את הקובץ לאחסון")
|
||||||
|
staged = _stage_file(src, spec.staging_root, spec.staging_subdir(inputs))
|
||||||
|
await progress("extracting", 15, "מחלץ טקסט מהקובץ")
|
||||||
|
try:
|
||||||
|
raw_text, page_count, page_offsets = await extractor.extract_text(str(staged))
|
||||||
|
except Exception as e:
|
||||||
|
await progress("failed", 100, f"כשל בחילוץ טקסט: {e}")
|
||||||
|
raise
|
||||||
|
raw_text = extractor.strip_nevo_preamble((raw_text or "")).strip()
|
||||||
|
else:
|
||||||
|
raw_text = (text or "").strip()
|
||||||
|
if not raw_text:
|
||||||
|
await progress("failed", 100, "לא נמצא טקסט בקובץ")
|
||||||
|
raise ValueError("no extractable text in file")
|
||||||
|
|
||||||
|
# Step 6: DB create (type-specific, routed — get case_law_id).
|
||||||
|
await progress("storing_metadata", 25, "שומר את הרשומה במסד הנתונים")
|
||||||
|
display_name = (inputs.get("case_name") or "").strip() or (
|
||||||
|
inputs.get(spec.display_name_fallback) or ""
|
||||||
|
).strip()
|
||||||
|
record = await spec.create_record(
|
||||||
|
full_text=raw_text,
|
||||||
|
case_name=display_name,
|
||||||
|
decision_date=_coerce_date(inputs.get("decision_date")),
|
||||||
|
document_id=document_id,
|
||||||
|
**{k: v for k, v in inputs.items()
|
||||||
|
if k not in {"case_name", "decision_date", "file_path", "text"}},
|
||||||
|
)
|
||||||
|
case_law_id = UUID(str(record["id"]))
|
||||||
|
|
||||||
|
try:
|
||||||
|
stored_chunks = await _chunk_embed_store(case_law_id, raw_text, page_offsets, page_count, progress)
|
||||||
|
|
||||||
|
# Step 9: multimodal — uniform: flag + PDF + page_count, NOT intake type.
|
||||||
|
if (config.MULTIMODAL_ENABLED and page_count > 0
|
||||||
|
and staged is not None and staged.suffix.lower() == ".pdf"):
|
||||||
|
try:
|
||||||
|
await progress("embedding_images", 70, f"מטמיע {page_count} עמודי תמונה (multimodal)")
|
||||||
|
await _embed_pages(case_law_id, staged, page_count)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Multimodal embedding failed (non-fatal): %s", e)
|
||||||
|
|
||||||
|
# Steps 10-12: queue BOTH extractions (GAP-02 fix) + statuses.
|
||||||
|
await db.set_case_law_extraction_status(case_law_id, "completed")
|
||||||
|
await db.set_case_law_halacha_status(case_law_id, "pending")
|
||||||
|
await db.request_metadata_extraction(case_law_id)
|
||||||
|
await db.request_halacha_extraction(case_law_id)
|
||||||
|
|
||||||
|
await progress("completed", 100,
|
||||||
|
f"נקלט: {stored_chunks} chunks. חילוץ הלכות ומטא-דאטה ממתינים בתור.")
|
||||||
|
return {
|
||||||
|
"status": "completed",
|
||||||
|
"case_law_id": str(case_law_id),
|
||||||
|
"chunks": stored_chunks,
|
||||||
|
"halachot": 0,
|
||||||
|
"halachot_pending": True,
|
||||||
|
"metadata_filled": [],
|
||||||
|
"pages": page_count,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("ingest_document failed (%s): %s", spec.source_kind, e)
|
||||||
|
await db.set_case_law_extraction_status(case_law_id, "failed")
|
||||||
|
await progress("failed", 100, f"כשל בעיבוד: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def _chunk_embed_store(case_law_id, text, page_offsets, page_count, progress) -> int:
|
||||||
|
"""Steps 7-8: chunk (hierarchical/flat by flag) → embed children → store."""
|
||||||
|
if config.PARENT_DOC_RETRIEVAL_ENABLED:
|
||||||
|
await progress("chunking", 40, f"מחלק את הטקסט ל-chunks היררכיים ({page_count} עמ')")
|
||||||
|
h_chunks = chunker.chunk_document_hierarchical(text, page_offsets=page_offsets)
|
||||||
|
if not h_chunks:
|
||||||
|
return 0
|
||||||
|
children = [c for c in h_chunks if c.role == "child"]
|
||||||
|
parents = [c for c in h_chunks if c.role == "parent"]
|
||||||
|
await progress("embedding", 55, f"מייצר embeddings ל-{len(children)} children ({len(parents)} parents)")
|
||||||
|
child_vectors = await embeddings.embed_texts([c.content for c in children], input_type="document")
|
||||||
|
chunk_dicts: list[dict] = []
|
||||||
|
for p in parents:
|
||||||
|
chunk_dicts.append({
|
||||||
|
"role": "parent", "local_id": p.local_id, "parent_local_id": None,
|
||||||
|
"chunk_index": p.chunk_index, "content": p.content,
|
||||||
|
"section_type": p.section_type, "page_number": p.page_number, "embedding": None,
|
||||||
|
})
|
||||||
|
for c, v in zip(children, child_vectors):
|
||||||
|
chunk_dicts.append({
|
||||||
|
"role": "child", "local_id": c.local_id, "parent_local_id": c.parent_local_id,
|
||||||
|
"chunk_index": c.chunk_index, "content": c.content,
|
||||||
|
"section_type": c.section_type, "page_number": c.page_number, "embedding": v,
|
||||||
|
})
|
||||||
|
counts = await db.store_precedent_chunks_hierarchical(case_law_id, chunk_dicts)
|
||||||
|
return counts["children"]
|
||||||
|
else:
|
||||||
|
await progress("chunking", 40, f"מחלק את הטקסט ל-chunks ({page_count} עמ')")
|
||||||
|
chunks = chunker.chunk_document(text, page_offsets=page_offsets)
|
||||||
|
if not chunks:
|
||||||
|
return 0
|
||||||
|
await progress("embedding", 55, f"מייצר embeddings ל-{len(chunks)} chunks")
|
||||||
|
chunk_vectors = await embeddings.embed_texts([c.content for c in chunks], input_type="document")
|
||||||
|
chunk_dicts = [
|
||||||
|
{"chunk_index": c.chunk_index, "content": c.content,
|
||||||
|
"section_type": c.section_type, "page_number": c.page_number, "embedding": v}
|
||||||
|
for c, v in zip(chunks, chunk_vectors)
|
||||||
|
]
|
||||||
|
return await db.store_precedent_chunks(case_law_id, chunk_dicts)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify import**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -c "from legal_mcp.services import ingest; print(ingest.ingest_document.__name__)"`
|
||||||
|
Expected: prints `ingest_document`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/ingest.py
|
||||||
|
git commit -m "feat(ingest): canonical ingest_document pipeline (FU-1)"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note on `create_record` kwargs:** the wrappers (Tasks 4-5) build `inputs` so the
|
||||||
|
> leftover keys after popping `case_name`/`decision_date`/`file_path`/`text` exactly match
|
||||||
|
> each DB-create's remaining parameters. Verify against the signatures:
|
||||||
|
> `create_external_case_law(case_number, full_text, court, practice_area, appeal_subtype, subject_tags, summary, headnote, source_type, precedent_level, is_binding, ...)`
|
||||||
|
> and `create_internal_committee_decision(case_number, full_text, court, chair_name, district, practice_area, appeal_subtype, subject_tags, summary, is_binding, proceeding_type, ...)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: External spec + rewrite `ingest_precedent` as wrapper
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mcp-server/src/legal_mcp/services/precedent_library.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace the top-of-file ingest section with a spec + wrapper**
|
||||||
|
|
||||||
|
Replace the body of `ingest_precedent` (lines ~88-317) and remove `_stage_file`, `_coerce_date`,
|
||||||
|
`_safe_filename`, `_embed_precedent_pages`, and the `_VALID_*` constants used only by ingest.
|
||||||
|
Keep `_VALID_PRACTICE_AREAS`/`_VALID_SOURCE_TYPES` values but move them into the spec. Add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from legal_mcp.services import ingest
|
||||||
|
|
||||||
|
PRECEDENT_LIBRARY_DIR = Path(config.DATA_DIR) / "precedent-library"
|
||||||
|
|
||||||
|
_VALID_PRACTICE_AREAS = frozenset({"", "rishuy_uvniya", "betterment_levy", "compensation_197"})
|
||||||
|
_VALID_SOURCE_TYPES = frozenset({"", "court_ruling", "appeals_committee"})
|
||||||
|
|
||||||
|
|
||||||
|
def _external_validate(inputs: dict) -> None:
|
||||||
|
citation = (inputs.get("citation") or "").strip()
|
||||||
|
if not citation:
|
||||||
|
raise ValueError("citation is required")
|
||||||
|
if citation.startswith(("ערר ", "ערר(", 'בל"מ ', 'בל"מ(', "ARAR ")):
|
||||||
|
raise ValueError(
|
||||||
|
"ציטוט שמתחיל ב-'ערר' או 'בל\"מ' הוא החלטת ועדת ערר. "
|
||||||
|
"השתמש ב-internal_decision_upload (דורש chair_name + district), "
|
||||||
|
"לא ב-precedent_library_upload."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _external_staging_subdir(inputs: dict) -> str:
|
||||||
|
st = inputs.get("source_type") or ""
|
||||||
|
return st if st in {"court_ruling", "appeals_committee"} else "other"
|
||||||
|
|
||||||
|
|
||||||
|
_EXTERNAL_SPEC = ingest.IntakeSpec(
|
||||||
|
source_kind="external_upload",
|
||||||
|
id_field="citation",
|
||||||
|
staging_root=PRECEDENT_LIBRARY_DIR,
|
||||||
|
staging_subdir=_external_staging_subdir,
|
||||||
|
validate=_external_validate,
|
||||||
|
enum_fields={"practice_area": _VALID_PRACTICE_AREAS, "source_type": _VALID_SOURCE_TYPES},
|
||||||
|
derive=lambda inputs: {},
|
||||||
|
display_name_fallback="citation",
|
||||||
|
create_record=_create_external_record,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_external_record(**kw) -> dict:
|
||||||
|
"""Adapter: maps canonical inputs (citation) to create_external_case_law(case_number)."""
|
||||||
|
return await db.create_external_case_law(
|
||||||
|
case_number=kw["citation"].strip(),
|
||||||
|
case_name=kw["case_name"],
|
||||||
|
full_text=kw["full_text"],
|
||||||
|
court=(kw.get("court") or "").strip(),
|
||||||
|
decision_date=kw.get("decision_date"),
|
||||||
|
practice_area=kw.get("practice_area", ""),
|
||||||
|
appeal_subtype=(kw.get("appeal_subtype") or "").strip(),
|
||||||
|
subject_tags=list(kw.get("subject_tags") or []),
|
||||||
|
summary=(kw.get("summary") or "").strip(),
|
||||||
|
headnote=(kw.get("headnote") or "").strip(),
|
||||||
|
source_type=kw.get("source_type", ""),
|
||||||
|
precedent_level=kw.get("precedent_level", ""),
|
||||||
|
is_binding=kw.get("is_binding", True),
|
||||||
|
document_id=kw.get("document_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def ingest_precedent(
|
||||||
|
*,
|
||||||
|
file_path: str | Path,
|
||||||
|
citation: str,
|
||||||
|
case_name: str = "",
|
||||||
|
court: str = "",
|
||||||
|
decision_date=None,
|
||||||
|
source_type: str = "",
|
||||||
|
precedent_level: str = "",
|
||||||
|
practice_area: str = "",
|
||||||
|
appeal_subtype: str = "",
|
||||||
|
subject_tags: list[str] | None = None,
|
||||||
|
is_binding: bool = True,
|
||||||
|
headnote: str = "",
|
||||||
|
summary: str = "",
|
||||||
|
document_id: UUID | None = None,
|
||||||
|
progress: ingest.ProgressCb | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Ingest one external precedent. Thin wrapper over the canonical pipeline."""
|
||||||
|
inputs = {
|
||||||
|
"citation": citation, "case_name": case_name, "court": court,
|
||||||
|
"decision_date": decision_date, "source_type": source_type,
|
||||||
|
"precedent_level": precedent_level, "practice_area": practice_area,
|
||||||
|
"appeal_subtype": appeal_subtype, "subject_tags": subject_tags,
|
||||||
|
"is_binding": is_binding, "headnote": headnote, "summary": summary,
|
||||||
|
}
|
||||||
|
return await ingest.ingest_document(
|
||||||
|
_EXTERNAL_SPEC, inputs=inputs, file_path=file_path,
|
||||||
|
document_id=document_id, progress=progress,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
> Define `_create_external_record` ABOVE `_EXTERNAL_SPEC` (Python resolves the name at
|
||||||
|
> dataclass-construction time). Reorder if needed.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run external-path tests**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_unified_ingest.py -k "external" -v`
|
||||||
|
Expected: `test_external_queues_both`, `test_enum_validation_rejects_bad_practice_area_external`,
|
||||||
|
`test_external_citation_guard_still_blocks_arar` PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/precedent_library.py
|
||||||
|
git commit -m "refactor(ingest): ingest_precedent delegates to canonical pipeline (FU-1)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Internal spec + rewrite `ingest_internal_decision` as wrapper
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mcp-server/src/legal_mcp/services/internal_decisions.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace the ingest section with a spec + wrapper**
|
||||||
|
|
||||||
|
Remove `_coerce_date`, `_safe_filename`, and the inline pipeline body of
|
||||||
|
`ingest_internal_decision` (lines ~73-220). Keep `_VALID_DISTRICTS`, `_COURT_TO_DISTRICT`,
|
||||||
|
`_district_from_court`, and all migrate_*/enrich_*/search_internal functions. Add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from legal_mcp.services import ingest
|
||||||
|
|
||||||
|
INTERNAL_DECISIONS_DIR = Path(config.DATA_DIR) / "internal-decisions"
|
||||||
|
|
||||||
|
_VALID_PRACTICE_AREAS = frozenset({"", "rishuy_uvniya", "betterment_levy", "compensation_197"})
|
||||||
|
_VALID_DISTRICTS = frozenset({"", "ירושלים", "מרכז", "תל אביב", "צפון", "דרום", "ארצי"})
|
||||||
|
|
||||||
|
|
||||||
|
def _internal_validate(inputs: dict) -> None:
|
||||||
|
if not (inputs.get("case_number") or "").strip():
|
||||||
|
raise ValueError("case_number is required")
|
||||||
|
|
||||||
|
|
||||||
|
def _internal_derive(inputs: dict) -> dict:
|
||||||
|
district = (inputs.get("district") or "").strip() or _district_from_court(inputs.get("court") or "")
|
||||||
|
proc = (inputs.get("proceeding_type") or "").strip() or derive_proceeding_type(
|
||||||
|
appeal_subtype=inputs.get("appeal_subtype") or "", subject=inputs.get("case_name") or "",
|
||||||
|
)
|
||||||
|
return {"district": district, "proceeding_type": proc}
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_internal_record(**kw) -> dict:
|
||||||
|
return await db.create_internal_committee_decision(
|
||||||
|
case_number=kw["case_number"].strip(),
|
||||||
|
case_name=kw["case_name"],
|
||||||
|
full_text=kw["full_text"],
|
||||||
|
court=(kw.get("court") or "").strip(),
|
||||||
|
decision_date=kw.get("decision_date"),
|
||||||
|
chair_name=(kw.get("chair_name") or "").strip(),
|
||||||
|
district=kw.get("district", ""),
|
||||||
|
practice_area=kw.get("practice_area", ""),
|
||||||
|
appeal_subtype=(kw.get("appeal_subtype") or "").strip(),
|
||||||
|
subject_tags=list(kw.get("subject_tags") or []),
|
||||||
|
summary=(kw.get("summary") or "").strip(),
|
||||||
|
is_binding=kw.get("is_binding", True),
|
||||||
|
document_id=kw.get("document_id"),
|
||||||
|
proceeding_type=kw.get("proceeding_type") or "ערר",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_INTERNAL_SPEC = ingest.IntakeSpec(
|
||||||
|
source_kind="internal_committee",
|
||||||
|
id_field="case_number",
|
||||||
|
staging_root=INTERNAL_DECISIONS_DIR,
|
||||||
|
staging_subdir=lambda inputs: (inputs.get("district") or "other"),
|
||||||
|
validate=_internal_validate,
|
||||||
|
enum_fields={"practice_area": _VALID_PRACTICE_AREAS, "district": _VALID_DISTRICTS},
|
||||||
|
derive=_internal_derive,
|
||||||
|
display_name_fallback="case_number",
|
||||||
|
create_record=_create_internal_record,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def ingest_internal_decision(
|
||||||
|
*,
|
||||||
|
case_number: str,
|
||||||
|
case_name: str = "",
|
||||||
|
court: str = "",
|
||||||
|
decision_date=None,
|
||||||
|
chair_name: str = "",
|
||||||
|
district: str = "",
|
||||||
|
practice_area: str = "",
|
||||||
|
appeal_subtype: str = "",
|
||||||
|
subject_tags: list[str] | None = None,
|
||||||
|
summary: str = "",
|
||||||
|
is_binding: bool = True,
|
||||||
|
file_path: str | Path | None = None,
|
||||||
|
text: str | None = None,
|
||||||
|
document_id: UUID | None = None,
|
||||||
|
queue_halachot: bool = True, # retained for signature compat; pipeline always queues
|
||||||
|
proceeding_type: str = "",
|
||||||
|
) -> dict:
|
||||||
|
"""Ingest one appeals-committee decision. Thin wrapper over the canonical pipeline."""
|
||||||
|
inputs = {
|
||||||
|
"case_number": case_number, "case_name": case_name, "court": court,
|
||||||
|
"decision_date": decision_date, "chair_name": chair_name, "district": district,
|
||||||
|
"practice_area": practice_area, "appeal_subtype": appeal_subtype,
|
||||||
|
"subject_tags": subject_tags, "summary": summary, "is_binding": is_binding,
|
||||||
|
"proceeding_type": proceeding_type,
|
||||||
|
}
|
||||||
|
out = await ingest.ingest_document(
|
||||||
|
_INTERNAL_SPEC, inputs=inputs, file_path=file_path, text=text,
|
||||||
|
document_id=document_id,
|
||||||
|
)
|
||||||
|
return {"status": out["status"], "case_law_id": out["case_law_id"],
|
||||||
|
"chunks": out["chunks"], "halachot_pending": True}
|
||||||
|
```
|
||||||
|
|
||||||
|
> `queue_halachot=False` was only used by `migrate_from_style_corpus`. The canonical pipeline
|
||||||
|
> always queues both (per INV-ING3). Confirm with the user during execution that bulk
|
||||||
|
> re-migration queueing is acceptable; the migrate path is out of FU-1 scope but calls this
|
||||||
|
> wrapper. If suppression is still required, that is a follow-up — note it, do not silently drop.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the full test file**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_unified_ingest.py -v`
|
||||||
|
Expected: ALL 9 tests PASS — including `test_internal_queues_BOTH_metadata_and_halacha` (GAP-02).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/internal_decisions.py
|
||||||
|
git commit -m "refactor(ingest): ingest_internal_decision delegates to canonical pipeline; queue metadata too (GAP-02, FU-1)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Dead-code sweep, smoke import, full suite
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Verify: `mcp-server/src/legal_mcp/services/precedent_library.py`, `internal_decisions.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Confirm no orphaned references to removed helpers**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && grep -rn "_embed_precedent_pages\|_stage_file\|_safe_filename\|_coerce_date" src/legal_mcp/services/precedent_library.py src/legal_mcp/services/internal_decisions.py`
|
||||||
|
Expected: NO matches (all moved to `ingest.py`). If any remain in code paths other than ingest, leave them; if orphaned, delete.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Smoke-import every affected module + its callers**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai/mcp-server && .venv/bin/python -c "
|
||||||
|
from legal_mcp.services import ingest, precedent_library, internal_decisions
|
||||||
|
from legal_mcp.tools import precedent_library as t1, internal_decisions as t2
|
||||||
|
import inspect
|
||||||
|
sig_p = inspect.signature(precedent_library.ingest_precedent)
|
||||||
|
sig_i = inspect.signature(internal_decisions.ingest_internal_decision)
|
||||||
|
assert 'citation' in sig_p.parameters and 'file_path' in sig_p.parameters
|
||||||
|
assert 'case_number' in sig_i.parameters and 'text' in sig_i.parameters
|
||||||
|
print('signatures preserved; imports clean')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
Expected: prints `signatures preserved; imports clean`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run the entire test suite (no regressions elsewhere)**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/ -q`
|
||||||
|
Expected: all pre-existing tests still pass + the 9 new ones.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Lint the changed files (match repo style)**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m ruff check src/legal_mcp/services/ingest.py src/legal_mcp/services/precedent_library.py src/legal_mcp/services/internal_decisions.py 2>/dev/null || echo "ruff not configured — skip"`
|
||||||
|
Expected: clean, or "skip".
|
||||||
|
|
||||||
|
- [ ] **Step 5: Update TaskMaster #59 → done**
|
||||||
|
|
||||||
|
Mark subtasks 59.1-59.4 and task 59 as done via task-master (verify via MCP get_task).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Final commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add -A mcp-server/
|
||||||
|
git commit -m "chore(ingest): dead-code sweep + smoke checks for unified pipeline (FU-1)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Notes
|
||||||
|
|
||||||
|
- **GAP-01** (single path) → Tasks 2-5. **GAP-02** (metadata queue) → Task 3 step 1 + test `test_internal_queues_BOTH_metadata_and_halacha`. **GAP-04** (enum validation) → `_validate_enums` + tests. **GAP-05** (staging/derive/multimodal/fallback/guard unified) → Task 3 + specs in Tasks 4-5.
|
||||||
|
- **Boundary preserved:** DB-create functions untouched (routed via `create_record`); no migration.
|
||||||
|
- **Open execution check:** `queue_halachot=False` suppression in `migrate_from_style_corpus` (Task 5 note) — surface to user, do not silently change bulk-migration behavior.
|
||||||
|
- **claude_session rule:** `ingest.py` imports only db/chunker/embeddings/extractor — no LLM extractors. Safe for container.
|
||||||
613
docs/superpowers/plans/2026-05-30-fu2a-idempotent-ingest.md
Normal file
613
docs/superpowers/plans/2026-05-30-fu2a-idempotent-ingest.md
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
# FU-2a: Idempotent Ingest + Write-Time Normalization + `searchable` Flag — 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:** Make ingest idempotent (`ON CONFLICT` upsert), normalize identifiers at the write boundary (type-aware), and add a materialized `searchable` flag — all forward-only, no identifier migration.
|
||||||
|
|
||||||
|
**Architecture:** Pure-code + one schema-additive migration (V21) in `db.py`. The two `create_*_case_law` functions move from app-level SELECT-then-INSERT/UPDATE to atomic `INSERT … ON CONFLICT … DO UPDATE` against the existing V15 partial unique indexes (predicate repeated). A new `_canonical_case_number` normalizes at write for identifier-keyed corpora (internal/cases), not for external (citation is its id). A new `searchable` boolean is recomputed from the completeness contract on ingest/metadata completion; the search-layer filter is gated behind a dry-run.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.12, asyncpg, PostgreSQL (pgvector) at localhost:5433, pytest offline, local `.venv` at `mcp-server/.venv`.
|
||||||
|
|
||||||
|
**Spec:** [docs/superpowers/specs/2026-05-30-fu2a-idempotent-ingest-design.md](../specs/2026-05-30-fu2a-idempotent-ingest-design.md)
|
||||||
|
|
||||||
|
**Run tests:** `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_idempotent_ingest.py -v`
|
||||||
|
**DB smoke (real Postgres):** source `~/.env`, connect to `localhost:5433` db `legal_ai` (see Task 6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/services/db.py`:
|
||||||
|
- add `_canonical_case_number(s)` (pure) near `_normalize_case_number` (~line 1196).
|
||||||
|
- add pure `_compute_searchable(row, has_embedded_chunk)` + async `recompute_searchable(...)`.
|
||||||
|
- add `SCHEMA_V21_SQL` (after V20, ~line 1094) + wire into `_run_schema_migrations` (~line 1119).
|
||||||
|
- normalize at write in `create_case`, `create_internal_committee_decision` (NOT `create_external_case_law`).
|
||||||
|
- convert `create_external_case_law` + `create_internal_committee_decision` to `ON CONFLICT … DO UPDATE`.
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/services/ingest.py`: call `db.recompute_searchable(case_law_id)` after statuses are set (uniform, both types).
|
||||||
|
- **Modify** the search layer (`services/hybrid_search.py` and/or `db.py` search functions) — gated `searchable = true` filter (Task 6, only if dry-run is clean).
|
||||||
|
- **Create** `mcp-server/tests/test_idempotent_ingest.py` — offline tests for the pure pieces + ingest wiring.
|
||||||
|
|
||||||
|
**Unchanged:** public signatures of `ingest_precedent`/`ingest_internal_decision` (FU-1) and the DB-create parameter lists. Normalization/upsert live inside the write boundary.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Failing tests (pure logic + ingest wiring)
|
||||||
|
|
||||||
|
**Files:** Create `mcp-server/tests/test_idempotent_ingest.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""FU-2a: idempotent ingest + write-time normalization + searchable flag.
|
||||||
|
|
||||||
|
Offline tests for the *pure* pieces (canonical normalization, completeness
|
||||||
|
predicate) and ingest wiring. The real ON CONFLICT upsert is verified by a
|
||||||
|
DB smoke test against localhost:5433 (see plan Task 6), since it requires a
|
||||||
|
live Postgres partial unique index.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from legal_mcp.services import db, ingest
|
||||||
|
|
||||||
|
|
||||||
|
def _run(coro):
|
||||||
|
return asyncio.run(coro)
|
||||||
|
|
||||||
|
|
||||||
|
# ── GAP-06: canonical normalization (pure, deterministic) ──────────────
|
||||||
|
@pytest.mark.parametrize("raw,expected", [
|
||||||
|
("ערר 8137/24", "8137-24"),
|
||||||
|
(" עע\"מ 1/20 ", "1-20"),
|
||||||
|
("8126-03-25", "8126-03-25"), # month segment preserved
|
||||||
|
("בל\"מ 1010-01-25", "1010-01-25"),
|
||||||
|
("8047/23", "8047-23"),
|
||||||
|
])
|
||||||
|
def test_canonical_case_number(raw, expected):
|
||||||
|
assert db._canonical_case_number(raw) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_canonical_does_not_invent_month():
|
||||||
|
# No month in input → none added (X1 §1).
|
||||||
|
assert db._canonical_case_number("8126/24") == "8126-24"
|
||||||
|
|
||||||
|
|
||||||
|
# ── GAP-13: completeness predicate (pure) ──────────────────────────────
|
||||||
|
def _complete_row():
|
||||||
|
return {
|
||||||
|
"case_number": "8047-23", "case_name": "פלוני נ' הוועדה",
|
||||||
|
"practice_area": "rishuy_uvniya", "source_kind": "internal_committee",
|
||||||
|
"extraction_status": "completed", "headnote": "תקציר",
|
||||||
|
"summary": "", "subject_tags": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_searchable_true_when_complete():
|
||||||
|
assert db._compute_searchable(_complete_row(), has_embedded_chunk=True) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_searchable_false_without_embedded_chunk():
|
||||||
|
assert db._compute_searchable(_complete_row(), has_embedded_chunk=False) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_searchable_false_without_metadata():
|
||||||
|
row = _complete_row()
|
||||||
|
row["headnote"] = ""; row["summary"] = ""; row["subject_tags"] = []
|
||||||
|
assert db._compute_searchable(row, has_embedded_chunk=True) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_searchable_false_when_extraction_incomplete():
|
||||||
|
row = _complete_row(); row["extraction_status"] = "pending"
|
||||||
|
assert db._compute_searchable(row, has_embedded_chunk=True) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_searchable_false_without_core_fields():
|
||||||
|
row = _complete_row(); row["practice_area"] = ""
|
||||||
|
assert db._compute_searchable(row, has_embedded_chunk=True) is False
|
||||||
|
|
||||||
|
|
||||||
|
# ── ingest wires in recompute_searchable (both types) ──────────────────
|
||||||
|
def test_ingest_calls_recompute_searchable(monkeypatch, tmp_path):
|
||||||
|
calls = {"recompute": [], "meta": [], "hal": []}
|
||||||
|
|
||||||
|
async def _extract_text(path): return ("text", 1, [0])
|
||||||
|
monkeypatch.setattr(ingest.extractor, "extract_text", _extract_text)
|
||||||
|
monkeypatch.setattr(ingest.extractor, "strip_nevo_preamble", lambda t: t)
|
||||||
|
monkeypatch.setattr(ingest.chunker, "chunk_document",
|
||||||
|
lambda t, page_offsets=None: [type("C", (), {
|
||||||
|
"chunk_index": 0, "content": "c", "section_type": "b",
|
||||||
|
"page_number": 1})()])
|
||||||
|
|
||||||
|
async def _embed(texts, input_type="document"): return [[0.0] * 8 for _ in texts]
|
||||||
|
monkeypatch.setattr(ingest.embeddings, "embed_texts", _embed)
|
||||||
|
|
||||||
|
async def _store(cid, dicts): return len(dicts)
|
||||||
|
monkeypatch.setattr(ingest.db, "store_precedent_chunks", _store)
|
||||||
|
|
||||||
|
async def _create_internal(**kw): return {"id": uuid4()}
|
||||||
|
monkeypatch.setattr(ingest.db, "create_internal_committee_decision", _create_internal)
|
||||||
|
|
||||||
|
async def _noop(*a, **k): return None
|
||||||
|
monkeypatch.setattr(ingest.db, "set_case_law_extraction_status", _noop)
|
||||||
|
monkeypatch.setattr(ingest.db, "set_case_law_halacha_status", _noop)
|
||||||
|
monkeypatch.setattr(ingest.db, "request_metadata_extraction",
|
||||||
|
lambda cid: calls["meta"].append(cid) or _noop())
|
||||||
|
monkeypatch.setattr(ingest.db, "request_halacha_extraction",
|
||||||
|
lambda cid: calls["hal"].append(cid) or _noop())
|
||||||
|
|
||||||
|
async def _recompute(cid): calls["recompute"].append(cid)
|
||||||
|
monkeypatch.setattr(ingest.db, "recompute_searchable", _recompute)
|
||||||
|
monkeypatch.setattr(ingest.config, "PARENT_DOC_RETRIEVAL_ENABLED", False)
|
||||||
|
monkeypatch.setattr(ingest.config, "MULTIMODAL_ENABLED", False)
|
||||||
|
|
||||||
|
from legal_mcp.services import internal_decisions
|
||||||
|
_run(internal_decisions.ingest_internal_decision(
|
||||||
|
case_number="8047/23", text="t", chair_name="x", practice_area="rishuy_uvniya"))
|
||||||
|
assert len(calls["recompute"]) == 1, "ingest must recompute searchable after success"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run to verify failure**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_idempotent_ingest.py -v`
|
||||||
|
Expected: FAIL — `AttributeError: module 'legal_mcp.services.db' has no attribute '_canonical_case_number'` (and `_compute_searchable`, `recompute_searchable`).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/tests/test_idempotent_ingest.py
|
||||||
|
git commit -m "test(ingest): failing tests for idempotent ingest + searchable (FU-2a)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: `_canonical_case_number` + write-time normalization
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/services/db.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `_canonical_case_number` next to `_normalize_case_number` (~line 1212)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _canonical_case_number(s: str) -> str:
|
||||||
|
"""Canonical write-time form per X1 §1: trim · prefix-strip · '/'→'-'.
|
||||||
|
|
||||||
|
Deterministic and format-only — does NOT add or remove a month segment.
|
||||||
|
Used at the write boundary for identifier-keyed corpora (internal
|
||||||
|
committee decisions, active cases). NOT for external precedents, whose
|
||||||
|
canonical identifier is the full citation.
|
||||||
|
"""
|
||||||
|
s = (s or "").strip()
|
||||||
|
m = re.search(r"\d", s)
|
||||||
|
if m:
|
||||||
|
s = s[m.start():]
|
||||||
|
return s.strip().replace("/", "-")
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Normalize at write in `create_case` (~line 1158)**
|
||||||
|
|
||||||
|
Change the INSERT's `case_number` binding to normalized form. Replace `case_id, case_number, title,` with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
case_id, _canonical_case_number(case_number), title,
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Normalize at write in `create_internal_committee_decision` (top of function body, ~line 2649)**
|
||||||
|
|
||||||
|
Immediately after `pool = await get_pool()`, add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
case_number = _canonical_case_number(case_number)
|
||||||
|
```
|
||||||
|
|
||||||
|
(Do NOT add this to `create_external_case_law` — external keeps its citation verbatim; that function only `.strip()`s, which the caller adapter already does.)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run normalization tests**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_idempotent_ingest.py -k "canonical" -v`
|
||||||
|
Expected: `test_canonical_case_number` (5 cases) + `test_canonical_does_not_invent_month` PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/db.py
|
||||||
|
git commit -m "feat(ingest): write-time canonical case_number normalization (GAP-06, FU-2a)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Convert both create functions to `ON CONFLICT DO UPDATE`
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/services/db.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace `create_external_case_law` body (lines 2566-2624, from `pool = await get_pool()` to `return _row_to_case_law(row)`)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
pool = await get_pool()
|
||||||
|
tags_json = json.dumps(subject_tags or [], ensure_ascii=False)
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
# Atomic upsert on the V15 partial unique index
|
||||||
|
# uq_case_law_external_number (case_number) WHERE source_kind <> 'internal_committee'.
|
||||||
|
# The predicate is repeated in ON CONFLICT (required for partial indexes).
|
||||||
|
# This also subsumes the old cited_only→external_upload promotion: a
|
||||||
|
# cited_only row with the same case_number conflicts and is promoted by
|
||||||
|
# DO UPDATE. Scoped to the external partial index, so an internal row with
|
||||||
|
# the same number is NOT touched (the old SELECT-without-source_kind could
|
||||||
|
# wrongly promote it).
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
INSERT INTO case_law (
|
||||||
|
case_number, case_name, court, date, subject_tags,
|
||||||
|
summary, key_quote, full_text, source_url,
|
||||||
|
source_kind, document_id, extraction_status,
|
||||||
|
halacha_extraction_status, practice_area, appeal_subtype,
|
||||||
|
headnote, source_type, precedent_level, is_binding
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6, $7, $8, $9,
|
||||||
|
'external_upload', $10, 'processing', 'pending',
|
||||||
|
$11, $12, $13, $14, $15, $16
|
||||||
|
)
|
||||||
|
ON CONFLICT (case_number) WHERE source_kind <> 'internal_committee'
|
||||||
|
DO UPDATE SET
|
||||||
|
case_name = EXCLUDED.case_name,
|
||||||
|
court = COALESCE(NULLIF(EXCLUDED.court, ''), case_law.court),
|
||||||
|
date = COALESCE(EXCLUDED.date, case_law.date),
|
||||||
|
practice_area = EXCLUDED.practice_area,
|
||||||
|
appeal_subtype = EXCLUDED.appeal_subtype,
|
||||||
|
subject_tags = EXCLUDED.subject_tags,
|
||||||
|
summary = COALESCE(NULLIF(EXCLUDED.summary, ''), case_law.summary),
|
||||||
|
headnote = EXCLUDED.headnote,
|
||||||
|
key_quote = COALESCE(NULLIF(EXCLUDED.key_quote, ''), case_law.key_quote),
|
||||||
|
full_text = EXCLUDED.full_text,
|
||||||
|
source_url = COALESCE(NULLIF(EXCLUDED.source_url, ''), case_law.source_url),
|
||||||
|
source_type = EXCLUDED.source_type,
|
||||||
|
precedent_level = EXCLUDED.precedent_level,
|
||||||
|
is_binding = EXCLUDED.is_binding,
|
||||||
|
document_id = COALESCE(EXCLUDED.document_id, case_law.document_id),
|
||||||
|
source_kind = 'external_upload',
|
||||||
|
extraction_status = 'processing',
|
||||||
|
halacha_extraction_status = 'pending'
|
||||||
|
RETURNING *
|
||||||
|
""",
|
||||||
|
case_number, case_name, court, decision_date, tags_json,
|
||||||
|
summary, key_quote, full_text, source_url,
|
||||||
|
document_id, practice_area, appeal_subtype, headnote,
|
||||||
|
source_type, precedent_level, is_binding,
|
||||||
|
)
|
||||||
|
return _row_to_case_law(row)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace `create_internal_committee_decision` body (lines 2649-2708)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
pool = await get_pool()
|
||||||
|
case_number = _canonical_case_number(case_number)
|
||||||
|
tags_json = json.dumps(subject_tags or [], ensure_ascii=False)
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
# Atomic upsert on V15 partial unique index
|
||||||
|
# uq_case_law_internal_number_proc (case_number, proceeding_type)
|
||||||
|
# WHERE source_kind = 'internal_committee'. Predicate repeated for the
|
||||||
|
# partial index. Replaces the old SELECT-then-INSERT/UPDATE (race-prone).
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
INSERT INTO case_law (
|
||||||
|
case_number, case_name, court, date, chair_name, district,
|
||||||
|
subject_tags, summary, full_text,
|
||||||
|
source_kind, source_type, document_id,
|
||||||
|
extraction_status, halacha_extraction_status,
|
||||||
|
practice_area, appeal_subtype, is_binding, proceeding_type
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6,
|
||||||
|
$7, $8, $9,
|
||||||
|
'internal_committee', 'appeals_committee', $10,
|
||||||
|
'processing', 'pending',
|
||||||
|
$11, $12, $13, $14
|
||||||
|
)
|
||||||
|
ON CONFLICT (case_number, proceeding_type)
|
||||||
|
WHERE source_kind = 'internal_committee'
|
||||||
|
DO UPDATE SET
|
||||||
|
case_name = EXCLUDED.case_name,
|
||||||
|
court = COALESCE(NULLIF(EXCLUDED.court, ''), case_law.court),
|
||||||
|
date = COALESCE(EXCLUDED.date, case_law.date),
|
||||||
|
chair_name = COALESCE(NULLIF(EXCLUDED.chair_name, ''), case_law.chair_name),
|
||||||
|
district = COALESCE(NULLIF(EXCLUDED.district, ''), case_law.district),
|
||||||
|
practice_area = EXCLUDED.practice_area,
|
||||||
|
appeal_subtype = EXCLUDED.appeal_subtype,
|
||||||
|
subject_tags = EXCLUDED.subject_tags,
|
||||||
|
summary = COALESCE(NULLIF(EXCLUDED.summary, ''), case_law.summary),
|
||||||
|
full_text = EXCLUDED.full_text,
|
||||||
|
source_type = 'appeals_committee',
|
||||||
|
source_kind = 'internal_committee',
|
||||||
|
is_binding = EXCLUDED.is_binding,
|
||||||
|
document_id = COALESCE(EXCLUDED.document_id, case_law.document_id),
|
||||||
|
extraction_status = 'processing',
|
||||||
|
halacha_extraction_status = 'pending'
|
||||||
|
RETURNING *
|
||||||
|
""",
|
||||||
|
case_number, case_name, court, decision_date, chair_name, district,
|
||||||
|
tags_json, summary, full_text,
|
||||||
|
document_id, practice_area, appeal_subtype, is_binding,
|
||||||
|
proceeding_type,
|
||||||
|
)
|
||||||
|
return _row_to_case_law(row)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify import + no syntax error**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -c "from legal_mcp.services import db; print('db imports')"`
|
||||||
|
Expected: prints `db imports`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/db.py
|
||||||
|
git commit -m "feat(ingest): atomic ON CONFLICT upsert in create_*_case_law (GAP-03, FU-2a)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: V21 migration — `searchable` column + recompute
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/services/db.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `SCHEMA_V21_SQL` after `SCHEMA_V20_SQL` (~line 1094)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ── V21: explicit `searchable` flag (GAP-13 / INV-DM1) ─────────────
|
||||||
|
# Materialized completeness flag — a case_law row is exposed to search only
|
||||||
|
# when it satisfies the completeness contract (02-data-model §2a). Recomputed
|
||||||
|
# on ingest/metadata completion via recompute_searchable(); not inferred at
|
||||||
|
# query time. Default false so a freshly-inserted row is excluded until proven
|
||||||
|
# complete. Health-check surfaces count(*) FILTER (WHERE NOT searchable).
|
||||||
|
SCHEMA_V21_SQL = """
|
||||||
|
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS searchable boolean NOT NULL DEFAULT false;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_case_law_searchable ON case_law (searchable);
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Wire V21 into `_run_schema_migrations` (~line 1119) and bump the log line**
|
||||||
|
|
||||||
|
After `await conn.execute(SCHEMA_V20_SQL)` add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await conn.execute(SCHEMA_V21_SQL)
|
||||||
|
```
|
||||||
|
|
||||||
|
Change the log line `"Database schema initialized (v1-v20)"` → `"Database schema initialized (v1-v21)"`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add `_compute_searchable` (pure) + `recompute_searchable` (async) near the case_law helpers (after `create_internal_committee_decision`, ~line 2709)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _compute_searchable(row: dict, has_embedded_chunk: bool) -> bool:
|
||||||
|
"""Completeness contract (INV-DM1 / 02-data-model §2a).
|
||||||
|
|
||||||
|
A row is searchable IFF: canonical id present · case_name/practice_area/
|
||||||
|
source_kind present · ≥1 chunk with a non-null embedding · extraction
|
||||||
|
completed · metadata non-empty (≥1 of headnote/summary/subject_tags).
|
||||||
|
Pure — `has_embedded_chunk` is supplied by the caller (cross-table check).
|
||||||
|
"""
|
||||||
|
if not has_embedded_chunk:
|
||||||
|
return False
|
||||||
|
if (row.get("extraction_status") or "") != "completed":
|
||||||
|
return False
|
||||||
|
if not (row.get("case_number") or "").strip():
|
||||||
|
return False
|
||||||
|
if not (row.get("case_name") or "").strip():
|
||||||
|
return False
|
||||||
|
if not (row.get("practice_area") or "").strip():
|
||||||
|
return False
|
||||||
|
if not (row.get("source_kind") or "").strip():
|
||||||
|
return False
|
||||||
|
tags = row.get("subject_tags") or []
|
||||||
|
has_meta = bool((row.get("headnote") or "").strip()) \
|
||||||
|
or bool((row.get("summary") or "").strip()) \
|
||||||
|
or (len(tags) > 0)
|
||||||
|
return has_meta
|
||||||
|
|
||||||
|
|
||||||
|
async def recompute_searchable(case_law_id: "UUID | str | None" = None) -> int:
|
||||||
|
"""Recompute and persist the `searchable` flag. Idempotent / reversible.
|
||||||
|
|
||||||
|
If case_law_id is None, recompute ALL rows (used by the V21 backfill and
|
||||||
|
the dry-run). Returns the number of rows now marked searchable=true.
|
||||||
|
"""
|
||||||
|
pool = await get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
if case_law_id is not None:
|
||||||
|
cid = case_law_id if isinstance(case_law_id, UUID) else UUID(str(case_law_id))
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"SELECT * FROM case_law WHERE id = $1", cid)
|
||||||
|
else:
|
||||||
|
rows = await conn.fetch("SELECT * FROM case_law")
|
||||||
|
n_true = 0
|
||||||
|
for r in rows:
|
||||||
|
row = dict(r)
|
||||||
|
# subject_tags is stored jsonb; _row_to_case_law parses it, but here
|
||||||
|
# we read raw — normalize to a list length check.
|
||||||
|
tags = row.get("subject_tags")
|
||||||
|
if isinstance(tags, str):
|
||||||
|
try:
|
||||||
|
tags = json.loads(tags)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
tags = []
|
||||||
|
row["subject_tags"] = tags or []
|
||||||
|
has_chunk = await conn.fetchval(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM precedent_chunks "
|
||||||
|
"WHERE case_law_id = $1 AND embedding IS NOT NULL)", row["id"])
|
||||||
|
val = _compute_searchable(row, bool(has_chunk))
|
||||||
|
await conn.execute(
|
||||||
|
"UPDATE case_law SET searchable = $2 WHERE id = $1", row["id"], val)
|
||||||
|
if val:
|
||||||
|
n_true += 1
|
||||||
|
return n_true
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the completeness-predicate tests**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_idempotent_ingest.py -k "searchable and not ingest" -v`
|
||||||
|
Expected: all `test_compute_searchable_*` PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/db.py
|
||||||
|
git commit -m "feat(data-model): V21 searchable flag + recompute_searchable (GAP-13, FU-2a)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Wire `recompute_searchable` into ingest
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/services/ingest.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Call recompute after statuses are set in `ingest_document`**
|
||||||
|
|
||||||
|
In `ingest.py`, find the block (added by FU-1) that sets statuses + queues extraction:
|
||||||
|
```python
|
||||||
|
await db.set_case_law_extraction_status(case_law_id, "completed")
|
||||||
|
await db.set_case_law_halacha_status(case_law_id, "pending")
|
||||||
|
await db.request_metadata_extraction(case_law_id)
|
||||||
|
await db.request_halacha_extraction(case_law_id)
|
||||||
|
```
|
||||||
|
Immediately AFTER `request_halacha_extraction`, add:
|
||||||
|
```python
|
||||||
|
await db.recompute_searchable(case_law_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
> Rationale: at this point chunks+embeddings are stored and extraction_status is
|
||||||
|
> completed, so the completeness predicate is meaningful. Metadata may still be
|
||||||
|
> pending (queued), so the row may compute searchable=false until metadata fills —
|
||||||
|
> the metadata extractor also calls recompute (Task 5 Step 2).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Call recompute after metadata extraction fills fields**
|
||||||
|
|
||||||
|
In `mcp-server/src/legal_mcp/services/precedent_metadata_extractor.py`, find `extract_and_apply`'s success path (where it persists the filled metadata fields). After the DB update that writes the extracted metadata, add a call:
|
||||||
|
```python
|
||||||
|
await db.recompute_searchable(case_law_id)
|
||||||
|
```
|
||||||
|
(Import `db` is already present in that module; if not, add `from legal_mcp.services import db`. Confirm by reading the file's imports first.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run the ingest-wiring test**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_idempotent_ingest.py -k "ingest_calls_recompute" -v`
|
||||||
|
Expected: `test_ingest_calls_recompute_searchable` PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/ingest.py mcp-server/src/legal_mcp/services/precedent_metadata_extractor.py
|
||||||
|
git commit -m "feat(ingest): recompute searchable on ingest + metadata completion (GAP-13, FU-2a)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: DB smoke + dry-run + GATED search filter
|
||||||
|
|
||||||
|
**Files:** Modify search layer ONLY if dry-run is clean (see Step 4).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Apply the V21 migration to the local DB and smoke-test upsert idempotency**
|
||||||
|
|
||||||
|
Run (sources env, exercises real Postgres):
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai && set -a && source ~/.env && set +a
|
||||||
|
cd mcp-server && .venv/bin/python -c "
|
||||||
|
import asyncio, uuid
|
||||||
|
from legal_mcp.services import db
|
||||||
|
async def main():
|
||||||
|
await db.get_pool() # runs migrations incl V21
|
||||||
|
# idempotent internal upsert: same (case_number, proceeding_type) twice
|
||||||
|
cn = 'ZZ9999/24'
|
||||||
|
r1 = await db.create_internal_committee_decision(case_number=cn, case_name='t', full_text='x', practice_area='rishuy_uvniya')
|
||||||
|
r2 = await db.create_internal_committee_decision(case_number=cn, case_name='t2', full_text='x2', practice_area='rishuy_uvniya')
|
||||||
|
assert r1['id'] == r2['id'], 'upsert must update, not duplicate'
|
||||||
|
# cleanup
|
||||||
|
pool = await db.get_pool()
|
||||||
|
async with pool.acquire() as c:
|
||||||
|
await c.execute(\"DELETE FROM case_law WHERE case_number = 'ZZ9999-24'\")
|
||||||
|
print('UPSERT IDEMPOTENT OK; normalized stored as ZZ9999-24')
|
||||||
|
asyncio.run(main())
|
||||||
|
"
|
||||||
|
```
|
||||||
|
Expected: `UPSERT IDEMPOTENT OK` and no duplicate. (Note: `ZZ9999/24` normalizes to `ZZ9999-24` — confirms write-time normalization too.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Backfill the `searchable` flag (recompute, reversible)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai && set -a && source ~/.env && set +a
|
||||||
|
cd mcp-server && .venv/bin/python -c "
|
||||||
|
import asyncio
|
||||||
|
from legal_mcp.services import db
|
||||||
|
async def main():
|
||||||
|
n = await db.recompute_searchable()
|
||||||
|
print('recompute_searchable: rows now searchable =', n)
|
||||||
|
asyncio.run(main())
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Dry-run report — which rows would drop from search if the filter is enabled**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai && set -a && source ~/.env && set +a
|
||||||
|
PGPASSWORD="$POSTGRES_PASSWORD" psql "host=$POSTGRES_HOST port=$POSTGRES_PORT dbname=$POSTGRES_DB user=$POSTGRES_USER" -c "
|
||||||
|
SELECT source_kind,
|
||||||
|
count(*) AS total,
|
||||||
|
count(*) FILTER (WHERE NOT searchable) AS would_drop
|
||||||
|
FROM case_law GROUP BY source_kind ORDER BY source_kind;"
|
||||||
|
```
|
||||||
|
Report the table to the controller. **Decision gate:** if `would_drop` includes legitimate, currently-findable precedents (e.g. external_upload / internal_committee rows that users rely on), DO NOT enable the search filter in Step 4 — stop and report; the filter waits for FU-2b. If `would_drop` is only genuinely-incomplete rows, proceed.
|
||||||
|
|
||||||
|
- [ ] **Step 4: (GATED) Enable `searchable = true` filter in the search layer**
|
||||||
|
|
||||||
|
ONLY if Step 3 is clean. Read `mcp-server/src/legal_mcp/services/hybrid_search.py` to find the `case_law` WHERE clauses in `search_precedent_library_hybrid` / `search_documents_hybrid`. Add `AND cl.searchable = true` (alias as used in that query) to the case_law-joined precedent search paths. Add a focused test asserting a non-searchable row is excluded (monkeypatch or DB smoke). If deferred, write a one-line note in the spec §7 that the filter is pending FU-2b and skip.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add health-check visibility**
|
||||||
|
|
||||||
|
Find the health-check endpoint/function (search `def health` / `processing_status` in `web/app.py` or `tools/`). Add a field `non_searchable_case_law = SELECT count(*) FROM case_law WHERE NOT searchable`. Keep it a single cheap COUNT.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add -A mcp-server/ web/
|
||||||
|
git commit -m "feat(retrieval): gated searchable filter + health-check visibility (GAP-13, FU-2a)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Full suite + smoke + lint + TaskMaster
|
||||||
|
|
||||||
|
- [ ] **Step 1: Full test suite**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/ -q`
|
||||||
|
Expected: all pass (the FU-1 77 + new FU-2a tests). Report the summary line.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Smoke-import**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -c "from legal_mcp.services import db, ingest, precedent_library, internal_decisions; print('clean')"`
|
||||||
|
Expected: `clean`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Lint changed files (if ruff available)**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m ruff check src/legal_mcp/services/db.py src/legal_mcp/services/ingest.py 2>/dev/null; echo "exit=$?"`
|
||||||
|
Expected: clean or "ruff not available".
|
||||||
|
|
||||||
|
- [ ] **Step 4: Mark TaskMaster #60 + subtasks done**
|
||||||
|
|
||||||
|
Controller handles this (edit `.taskmaster/tasks/tasks.json`, verify via MCP get_task). Subtasks 60.1 (GAP-03), 60.2 (GAP-06), 60.5 (GAP-13).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Notes
|
||||||
|
|
||||||
|
- **GAP-03** → Task 3 (ON CONFLICT both functions). **GAP-06** → Task 2 (`_canonical_case_number` + write-time, type-aware). **GAP-13** → Tasks 4-5 (column + recompute + wiring) and gated Task 6 (filter).
|
||||||
|
- **No identifier migration** — FU-2b (#67) owns GAP-07/08. The V21 backfill only sets a derived, reversible flag.
|
||||||
|
- **Gated search filter** (Task 6 Step 3-4): the behavior-visible change is contingent on a clean dry-run; otherwise deferred. Surface the dry-run table to the user.
|
||||||
|
- **Offline-test limitation:** ON CONFLICT needs real Postgres → verified by Task 6 Step 1 smoke; offline tests cover the pure logic (normalize, completeness) and ingest wiring.
|
||||||
|
- **Type-consistency:** `_canonical_case_number`, `_compute_searchable(row, has_embedded_chunk)`, `recompute_searchable(case_law_id=None)` — names used identically in tests (Task 1) and impl (Tasks 2,4).
|
||||||
421
docs/superpowers/plans/2026-05-30-fu3-reindex-on-change.md
Normal file
421
docs/superpowers/plans/2026-05-30-fu3-reindex-on-change.md
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
# FU-3: Re-Index on Content Change — 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:** Detect content changes via a SHA-256 `content_hash`, expose a standalone `reindex_case_law` that re-embeds from stored `full_text` (no re-OCR, no file needed), and surface embedding-drift in the health-check — enforcing INV-G6 where embeddings can't be DB-GENERATED.
|
||||||
|
|
||||||
|
**Architecture:** Two additive `case_law` columns (V23): `content_hash` (hash of current full_text, written at the create boundary) and `indexed_hash` (hash the current chunks/embeddings were built from, set by `mark_indexed` after a successful store). Stale ⇔ `content_hash IS DISTINCT FROM indexed_hash`. `reindex_case_law` reuses the canonical `_chunk_embed_store` over stored text. Backfill only computes hashes (no re-embed — existing rows keep their vectors).
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.12, asyncpg, PostgreSQL@localhost:5433, voyage embeddings API, pytest offline, `.venv` at `mcp-server/.venv`.
|
||||||
|
|
||||||
|
**Spec:** [docs/superpowers/specs/2026-05-30-fu3-reindex-on-change-design.md](../specs/2026-05-30-fu3-reindex-on-change-design.md)
|
||||||
|
|
||||||
|
**Run tests:** `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_reindex_on_change.py -v`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/services/db.py` — `_content_hash`; V23 migration; `content_hash` in `create_external_case_law`/`create_internal_committee_decision`/`create_case`; `mark_indexed`; `list_stale_case_law`; `recompute_content_hashes`.
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/services/ingest.py` — `reindex_case_law`; call `mark_indexed` after `_chunk_embed_store` in `ingest_document`.
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/services/metrics.py` — `stale_embedding_case_law` count.
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/tools/precedent_library.py` + `server.py` — MCP tool `precedent_reindex`.
|
||||||
|
- **Create** `mcp-server/tests/test_reindex_on_change.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Failing tests
|
||||||
|
|
||||||
|
**Files:** Create `mcp-server/tests/test_reindex_on_change.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""FU-3: re-index on content change (offline, monkeypatched I/O)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from legal_mcp.services import db, ingest
|
||||||
|
|
||||||
|
|
||||||
|
def _run(coro):
|
||||||
|
return asyncio.run(coro)
|
||||||
|
|
||||||
|
|
||||||
|
# ── content_hash is deterministic ──────────────────────────────────────
|
||||||
|
def test_content_hash_deterministic():
|
||||||
|
h1 = db._content_hash("פסק דין כלשהו")
|
||||||
|
h2 = db._content_hash("פסק דין כלשהו")
|
||||||
|
assert h1 == h2 and len(h1) == 64 # sha256 hex
|
||||||
|
|
||||||
|
|
||||||
|
def test_content_hash_empty_is_blank():
|
||||||
|
assert db._content_hash("") == ""
|
||||||
|
assert db._content_hash(None) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_content_hash_changes_with_text():
|
||||||
|
assert db._content_hash("alpha") != db._content_hash("beta")
|
||||||
|
|
||||||
|
|
||||||
|
# ── mark_indexed copies content_hash → indexed_hash ─────────────────────
|
||||||
|
def test_mark_indexed_executes_update(monkeypatch):
|
||||||
|
seen = {}
|
||||||
|
|
||||||
|
class _Conn:
|
||||||
|
async def execute(self, q, *a):
|
||||||
|
seen["q"] = q; seen["args"] = a
|
||||||
|
async def __aenter__(self): return self
|
||||||
|
async def __aexit__(self, *a): return False
|
||||||
|
|
||||||
|
class _Pool:
|
||||||
|
def acquire(self): return _Conn()
|
||||||
|
|
||||||
|
async def _pool(): return _Pool()
|
||||||
|
monkeypatch.setattr(db, "get_pool", _pool)
|
||||||
|
|
||||||
|
cid = uuid4()
|
||||||
|
_run(db.mark_indexed(cid))
|
||||||
|
assert "indexed_hash" in seen["q"] and "content_hash" in seen["q"]
|
||||||
|
assert seen["args"][0] == cid
|
||||||
|
|
||||||
|
|
||||||
|
# ── reindex_case_law re-embeds from stored text, no extractor/LLM ───────
|
||||||
|
def test_reindex_case_law_uses_stored_text(monkeypatch):
|
||||||
|
cid = uuid4()
|
||||||
|
calls = {"chunk_embed_store": [], "mark_indexed": []}
|
||||||
|
|
||||||
|
async def _get_case_law(x):
|
||||||
|
return {"id": cid, "full_text": "טקסט שמור של ההחלטה"}
|
||||||
|
monkeypatch.setattr(ingest.db, "get_case_law", _get_case_law)
|
||||||
|
|
||||||
|
async def _ces(case_law_id, text, page_offsets, page_count, progress):
|
||||||
|
calls["chunk_embed_store"].append((case_law_id, text))
|
||||||
|
return 5
|
||||||
|
monkeypatch.setattr(ingest, "_chunk_embed_store", _ces)
|
||||||
|
|
||||||
|
async def _mark(x):
|
||||||
|
calls["mark_indexed"].append(x)
|
||||||
|
monkeypatch.setattr(ingest.db, "mark_indexed", _mark)
|
||||||
|
|
||||||
|
out = _run(ingest.reindex_case_law(cid))
|
||||||
|
assert out["chunks"] == 5 and out["reindexed"] is True
|
||||||
|
assert calls["chunk_embed_store"][0][1] == "טקסט שמור של ההחלטה"
|
||||||
|
assert calls["mark_indexed"] == [cid]
|
||||||
|
|
||||||
|
|
||||||
|
def test_reindex_case_law_missing_row_raises(monkeypatch):
|
||||||
|
async def _none(x): return None
|
||||||
|
monkeypatch.setattr(ingest.db, "get_case_law", _none)
|
||||||
|
with pytest.raises(ValueError, match="not found"):
|
||||||
|
_run(ingest.reindex_case_law(uuid4()))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run to verify failure**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_reindex_on_change.py -v`
|
||||||
|
Expected: FAIL — `AttributeError: ... no attribute '_content_hash'` / `mark_indexed` / `reindex_case_law`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/tests/test_reindex_on_change.py
|
||||||
|
git commit -m "test(reindex): failing tests for content-hash re-index (FU-3)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: V23 + hash helpers + content_hash at write
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/services/db.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Ensure `hashlib` import + add `_content_hash`**
|
||||||
|
|
||||||
|
READ the top imports of db.py. If `import hashlib` is absent, add it. Add this helper near `_canonical_case_number` (~line 1227):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _content_hash(text: str) -> str:
|
||||||
|
"""SHA-256 hex of the text — deterministic content fingerprint (FU-3/GAP-09).
|
||||||
|
|
||||||
|
Empty/None → "" (a row with no text has no content fingerprint).
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add `SCHEMA_V23_SQL` after `SCHEMA_V22_SQL` + wire it**
|
||||||
|
|
||||||
|
READ near `SCHEMA_V22_SQL` and `_run_schema_migrations`. Add after the V22 block:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ── V23: case_law content/indexed hashes — re-index on content change (GAP-09) ──
|
||||||
|
# content_hash = SHA-256 of current full_text (written at the create boundary).
|
||||||
|
# indexed_hash = the content_hash the CURRENT chunks/embeddings were built from
|
||||||
|
# (set by mark_indexed after a successful store). Stale ⇔ content_hash IS
|
||||||
|
# DISTINCT FROM indexed_hash. embedding can't be a GENERATED column (needs an
|
||||||
|
# API call), so freshness is enforced by detection + reindex_case_law + health-check.
|
||||||
|
SCHEMA_V23_SQL = """
|
||||||
|
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS content_hash text NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS indexed_hash text;
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
After `await conn.execute(SCHEMA_V22_SQL)` add `await conn.execute(SCHEMA_V23_SQL)`; bump the log line to `v1-v23`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write `content_hash` in the two case_law create functions**
|
||||||
|
|
||||||
|
In `create_external_case_law` and `create_internal_committee_decision` (db.py ~2610-2760), the `INSERT ... ON CONFLICT ... DO UPDATE` was built in FU-2a. For EACH:
|
||||||
|
1. Add `content_hash` to the INSERT column list (append after the last data column, before the closing `)`).
|
||||||
|
2. Add a matching `$N` placeholder in VALUES (next number after the current max).
|
||||||
|
3. Add `content_hash = EXCLUDED.content_hash` to the `DO UPDATE SET` clause.
|
||||||
|
4. Append `_content_hash(full_text)` as the LAST positional arg in the `conn.fetchrow(..., <args>)` call (matching the new `$N`).
|
||||||
|
|
||||||
|
CRITICAL: the new placeholder number must equal `(current highest $N) + 1`, and the new arg must be appended LAST in the args tuple in the SAME order. Read the current SQL + args carefully and count. After editing, verify param count = placeholder count (Step 5 import check will catch a gross mismatch; the DB smoke in Task 6 confirms at runtime).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Write `content_hash` in `create_case`**
|
||||||
|
|
||||||
|
In `create_case` (db.py ~1130-1165), the INSERT into `cases` — add `content_hash`? NO: `cases` is a different table (active appeal cases), and FU-3's scope is `case_law` (the corpus). Do NOT alter `create_case` or the `cases` table here. (The spec §3 mentioned create_case for normalization in FU-2a; for FU-3 hashing, scope is `case_law` only. Skip create_case.)
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add `mark_indexed`, `list_stale_case_law`, `recompute_content_hashes` (after `get_case_law`, ~line 2547)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def mark_indexed(case_law_id: UUID) -> None:
|
||||||
|
"""Mark a case_law row's embeddings as built from its current content (FU-3).
|
||||||
|
|
||||||
|
Sets indexed_hash := content_hash. Call AFTER a successful chunk+embed+store.
|
||||||
|
"""
|
||||||
|
pool = await get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
await conn.execute(
|
||||||
|
"UPDATE case_law SET indexed_hash = content_hash WHERE id = $1",
|
||||||
|
case_law_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_stale_case_law(limit: int = 500) -> list[dict]:
|
||||||
|
"""case_law rows whose embeddings are stale vs current content (GAP-09/INV-G6)."""
|
||||||
|
pool = await get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""SELECT id, case_number, source_kind
|
||||||
|
FROM case_law
|
||||||
|
WHERE coalesce(full_text, '') <> ''
|
||||||
|
AND content_hash IS DISTINCT FROM indexed_hash
|
||||||
|
ORDER BY created_at LIMIT $1""",
|
||||||
|
limit,
|
||||||
|
)
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def recompute_content_hashes() -> dict:
|
||||||
|
"""Backfill (FU-3): set content_hash for all rows; set indexed_hash=content_hash
|
||||||
|
only where chunks already exist (those are already embedded). Rows with text but
|
||||||
|
no chunks get indexed_hash=NULL → surface as stale. Hash-only; no re-embed."""
|
||||||
|
pool = await get_pool()
|
||||||
|
updated = 0
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch("SELECT id, full_text FROM case_law")
|
||||||
|
for r in rows:
|
||||||
|
ch = _content_hash(r["full_text"] or "")
|
||||||
|
has_chunks = await conn.fetchval(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM precedent_chunks WHERE case_law_id = $1)",
|
||||||
|
r["id"])
|
||||||
|
await conn.execute(
|
||||||
|
"UPDATE case_law SET content_hash = $2, "
|
||||||
|
"indexed_hash = CASE WHEN $3 THEN $2 ELSE indexed_hash END WHERE id = $1",
|
||||||
|
r["id"], ch, bool(has_chunks))
|
||||||
|
updated += 1
|
||||||
|
return {"updated": updated}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run the helper tests**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_reindex_on_change.py -k "content_hash or mark_indexed" -v`
|
||||||
|
Expected: `test_content_hash_*` (3) + `test_mark_indexed_executes_update` PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/db.py
|
||||||
|
git commit -m "feat(reindex): V23 content/indexed hashes + helpers + write content_hash (GAP-09, FU-3)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: `reindex_case_law` + mark_indexed on ingest
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/services/ingest.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Call `mark_indexed` after successful chunk+embed+store in `ingest_document`**
|
||||||
|
|
||||||
|
READ `ingest_document` — find the line `stored_chunks = await _chunk_embed_store(case_law_id, raw_text, page_offsets, page_count, progress)` (~line 184). Immediately AFTER it, add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await db.mark_indexed(case_law_id)
|
||||||
|
```
|
||||||
|
(After a fresh ingest, chunks were just built from the current text → indexed_hash = content_hash.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add `reindex_case_law` (append to ingest.py)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def reindex_case_law(
|
||||||
|
case_law_id: "UUID | str",
|
||||||
|
progress: ProgressCb | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Re-chunk + re-embed an existing case_law row from its STORED full_text (GAP-09).
|
||||||
|
|
||||||
|
No re-extract / no re-OCR (uses the stored text — see feedback_no_reocr_retrofit)
|
||||||
|
and no LLM/CLI (only chunker + voyage embeddings), so it is safe to run anywhere.
|
||||||
|
Idempotent: store_precedent_chunks(_hierarchical) is DELETE-then-INSERT.
|
||||||
|
"""
|
||||||
|
progress = progress or _noop_progress
|
||||||
|
cid = case_law_id if isinstance(case_law_id, UUID) else UUID(str(case_law_id))
|
||||||
|
row = await db.get_case_law(cid)
|
||||||
|
if not row:
|
||||||
|
raise ValueError(f"case_law not found: {cid}")
|
||||||
|
text = (row.get("full_text") or "").strip()
|
||||||
|
if not text:
|
||||||
|
raise ValueError("case_law has no stored full_text to re-index")
|
||||||
|
stored = await _chunk_embed_store(cid, text, None, 0, progress)
|
||||||
|
await db.mark_indexed(cid)
|
||||||
|
await progress("completed", 100, f"הוטמע מחדש: {stored} chunks")
|
||||||
|
return {"status": "completed", "case_law_id": str(cid), "chunks": stored, "reindexed": True}
|
||||||
|
```
|
||||||
|
(`UUID`, `db`, `_chunk_embed_store`, `_noop_progress`, `ProgressCb` are already in ingest.py.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run reindex tests**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_reindex_on_change.py -v`
|
||||||
|
Expected: ALL pass (incl `test_reindex_case_law_uses_stored_text`, `test_reindex_case_law_missing_row_raises`).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/ingest.py
|
||||||
|
git commit -m "feat(reindex): reindex_case_law from stored text + mark_indexed on ingest (GAP-09, FU-3)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Health-check drift count
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/services/metrics.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `stale_embedding_case_law` count**
|
||||||
|
|
||||||
|
READ metrics.py — the aggregation that holds `non_searchable_case_law` / `cases_with_stale_blocks` (added in FU-2a/FU-7). Add a sibling, mirroring the exact pattern:
|
||||||
|
|
||||||
|
```python
|
||||||
|
stale_embedding_case_law = await conn.fetchval(
|
||||||
|
"SELECT COUNT(*) FROM case_law "
|
||||||
|
"WHERE coalesce(full_text,'') <> '' AND content_hash IS DISTINCT FROM indexed_hash")
|
||||||
|
```
|
||||||
|
and expose it in the returned summary dict: `"stale_embedding_case_law": stale_embedding_case_law`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Smoke-import + commit**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -c "from legal_mcp.services import metrics; print('clean')"`
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/metrics.py
|
||||||
|
git commit -m "feat(reindex): health-check stale_embedding_case_law count (GAP-09, FU-3)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: MCP tool `precedent_reindex`
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/tools/precedent_library.py`, `mcp-server/src/legal_mcp/server.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the tool function in precedent_library.py (mirror `precedent_extract_metadata`)**
|
||||||
|
|
||||||
|
READ `precedent_extract_metadata` (tools/precedent_library.py ~205-216) for the `_ok`/`_err`/UUID pattern. Add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def precedent_reindex(case_law_id: str) -> str:
|
||||||
|
"""re-chunk + re-embed פסיקה קיימת מה-full_text השמור (FU-3/GAP-09).
|
||||||
|
|
||||||
|
לתיקון drift של embeddings או אחרי שינוי-תוכן. אינו מריץ OCR/LLM — רק
|
||||||
|
chunking + voyage embeddings. idempotent (מוחק ובונה chunks מחדש).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cid = UUID(case_law_id)
|
||||||
|
except ValueError:
|
||||||
|
return _err("case_law_id לא תקין")
|
||||||
|
try:
|
||||||
|
from legal_mcp.services import ingest
|
||||||
|
result = await ingest.reindex_case_law(cid)
|
||||||
|
except Exception as e:
|
||||||
|
return _err(str(e))
|
||||||
|
return _ok(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Register in server.py (mirror the precedent tools' `@mcp.tool()` registration)**
|
||||||
|
|
||||||
|
READ server.py — find where `precedent_extract_metadata` (or another `precedent_*` tool) is registered with `@mcp.tool()` and delegated to `tools.precedent_library`. Add an equivalent registration for `precedent_reindex` following the identical pattern (decorator + delegation + the same import style). Report the exact registration block you added.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Smoke-import + commit**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -c "from legal_mcp.tools import precedent_library; import legal_mcp.server; print('clean')"`
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/tools/precedent_library.py mcp-server/src/legal_mcp/server.py
|
||||||
|
git commit -m "feat(reindex): precedent_reindex MCP tool (GAP-09, FU-3)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Backfill + full suite + DB smoke + lint + TaskMaster
|
||||||
|
|
||||||
|
- [ ] **Step 1: Full offline suite**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/ -q`
|
||||||
|
Expected: all pass (FU-1/2a/7 + new FU-3). If a pre-existing test that calls `ingest_document` breaks because `mark_indexed` isn't stubbed, fix that fixture to stub `db.mark_indexed` (same pattern as the FU-2a `recompute_searchable` fixture fix). Report.
|
||||||
|
|
||||||
|
- [ ] **Step 2: DB smoke + backfill (real Postgres — applies V23, runs backfill)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai && set -a && source ~/.env 2>/dev/null && set +a
|
||||||
|
cd mcp-server && .venv/bin/python -c "
|
||||||
|
import asyncio
|
||||||
|
from legal_mcp.services import db
|
||||||
|
async def main():
|
||||||
|
await db.get_pool() # applies V23
|
||||||
|
pool = await db.get_pool()
|
||||||
|
async with pool.acquire() as c:
|
||||||
|
cols = await c.fetchval(\"SELECT count(*) FROM information_schema.columns WHERE table_name='case_law' AND column_name IN ('content_hash','indexed_hash')\")
|
||||||
|
print('V23 columns present:', cols, '(expect 2)')
|
||||||
|
res = await db.recompute_content_hashes()
|
||||||
|
print('backfill:', res)
|
||||||
|
stale = await db.list_stale_case_law()
|
||||||
|
print('stale after backfill:', len(stale))
|
||||||
|
asyncio.run(main())
|
||||||
|
" 2>&1 | grep -vE 'INFO|WARNING|httpx|deprecat|command not found|\^\^\^' | tail -5
|
||||||
|
```
|
||||||
|
Expected: `V23 columns present: 2`, backfill updated ~129, `stale after backfill:` a small number (rows with text but no chunks, e.g. cited_only). Report the stale count.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Lint**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m ruff check src/legal_mcp/services/db.py src/legal_mcp/services/ingest.py 2>/dev/null; echo "exit=$?"`
|
||||||
|
Expected: clean or "ruff not available".
|
||||||
|
|
||||||
|
- [ ] **Step 4: TaskMaster** — controller marks #61 + subtask 61.1 done (61.2 already cancelled), verifies via MCP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Notes
|
||||||
|
|
||||||
|
- **GAP-09** → content_hash detection (Task 2) + reindex_case_law (Task 3) + drift health-check (Task 4) + MCP tool (Task 5).
|
||||||
|
- **No re-OCR:** reindex uses stored `full_text` only (Task 3) — honors feedback_no_reocr_retrofit.
|
||||||
|
- **Backfill is hash-only** (Task 6 Step 2) — no re-embed, no API cost; existing vectors untouched.
|
||||||
|
- **#61.2 closed** (not-applicable, in the spec commit) — no multimodal backfill task here.
|
||||||
|
- **Scope:** `case_law` only — `create_case`/`cases` table NOT touched (Task 2 Step 4).
|
||||||
|
- **Type consistency:** `_content_hash(text)->str`, `mark_indexed(case_law_id)`, `reindex_case_law(id)->{chunks,reindexed}`, `list_stale_case_law()`, `recompute_content_hashes()->{updated}` — names identical across tasks + tests.
|
||||||
|
- **Param-count risk** (Task 2 Step 3): the FU-2a upsert SQL must get exactly one new placeholder + one new arg per function; verified at runtime by the Task 6 DB smoke (a mismatch raises immediately).
|
||||||
521
docs/superpowers/plans/2026-05-30-fu7-audit-provenance.md
Normal file
521
docs/superpowers/plans/2026-05-30-fu7-audit-provenance.md
Normal file
@@ -0,0 +1,521 @@
|
|||||||
|
# FU-7: Audit-Trail + Provenance — 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:** Turn `audit_log` into an end-to-end audit trail, attach source-provenance to generated blocks, enforce citation→corpus resolution, and flag DOCX↔blocks drift — all forward-only, no data migration.
|
||||||
|
|
||||||
|
**Architecture:** Reuse `audit_log.log_action` with a `details` JSONB payload (X5 §4 — no new table) via a non-fatal `log_action_safe` wrapper. Provenance is an append-only `write_block` audit event carrying the source ids that fed the generation. GAP-17 drift is a deterministic `cases.blocks_stale` flag (V22) set at the known divergence points + a health-check count — not a fragile DOCX→blocks reparse. GAP-20 is a structural `case_law_id` resolver surfaced as a QA warning.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.12, asyncpg, PostgreSQL@localhost:5433, pytest offline, `.venv` at `mcp-server/.venv`.
|
||||||
|
|
||||||
|
**Spec:** [docs/superpowers/specs/2026-05-30-fu7-audit-provenance-design.md](../specs/2026-05-30-fu7-audit-provenance-design.md)
|
||||||
|
|
||||||
|
**Run tests:** `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_audit_provenance.py -v`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/services/audit.py` — add `log_action_safe(...)`.
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/services/db.py` — V22 migration (`cases.blocks_stale`), `mark_blocks_stale`, `resolve_citation_case_law_ids`.
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/tools/documents.py` — audit in `document_upload`, `extract_claims`.
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/services/block_writer.py` — collect source ids; audit `write_block`; clear `blocks_stale` on save.
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/tools/drafting.py` — audit `export_docx`; set/clear `blocks_stale` in `export_docx`/`revise_draft`/`apply_user_edit`.
|
||||||
|
- **Modify** QA path (`services/qa_validator.py`) — citation→corpus warning.
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/services/metrics.py` — `cases_with_stale_blocks` count.
|
||||||
|
- **Create** `mcp-server/tests/test_audit_provenance.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Failing tests
|
||||||
|
|
||||||
|
**Files:** Create `mcp-server/tests/test_audit_provenance.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""FU-7: audit-trail + provenance (offline, monkeypatched I/O)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from legal_mcp.services import audit, db
|
||||||
|
|
||||||
|
|
||||||
|
def _run(coro):
|
||||||
|
return asyncio.run(coro)
|
||||||
|
|
||||||
|
|
||||||
|
# ── GAP-18: log_action_safe is non-fatal ───────────────────────────────
|
||||||
|
def test_log_action_safe_swallows_db_error(monkeypatch):
|
||||||
|
async def _boom(*a, **k):
|
||||||
|
raise RuntimeError("db down")
|
||||||
|
monkeypatch.setattr(audit, "log_action", _boom)
|
||||||
|
# must NOT raise
|
||||||
|
_run(audit.log_action_safe("write_block", details={"x": 1}))
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_action_safe_forwards_args(monkeypatch):
|
||||||
|
seen = {}
|
||||||
|
async def _capture(action, case_id=None, document_id=None, details=None, user="system"):
|
||||||
|
seen.update(action=action, details=details)
|
||||||
|
monkeypatch.setattr(audit, "log_action", _capture)
|
||||||
|
_run(audit.log_action_safe("export_docx", details={"path": "/x"}))
|
||||||
|
assert seen["action"] == "export_docx" and seen["details"] == {"path": "/x"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── GAP-20: structural citation resolver ────────────────────────────────
|
||||||
|
def test_resolve_citation_case_law_ids_splits(monkeypatch):
|
||||||
|
good = uuid4()
|
||||||
|
bad = uuid4()
|
||||||
|
|
||||||
|
class _Conn:
|
||||||
|
async def fetchval(self, q, cid):
|
||||||
|
return cid == good
|
||||||
|
async def __aenter__(self): return self
|
||||||
|
async def __aexit__(self, *a): return False
|
||||||
|
|
||||||
|
class _Pool:
|
||||||
|
def acquire(self): return _Conn()
|
||||||
|
|
||||||
|
async def _pool():
|
||||||
|
return _Pool()
|
||||||
|
monkeypatch.setattr(db, "get_pool", _pool)
|
||||||
|
|
||||||
|
out = _run(db.resolve_citation_case_law_ids([good, bad]))
|
||||||
|
assert good in out["resolved"] and bad in out["unresolved"]
|
||||||
|
|
||||||
|
|
||||||
|
# ── GAP-17: blocks_stale helper ────────────────────────────────────────
|
||||||
|
def test_mark_blocks_stale_executes_update(monkeypatch):
|
||||||
|
seen = {}
|
||||||
|
|
||||||
|
class _Conn:
|
||||||
|
async def execute(self, q, *a):
|
||||||
|
seen["q"] = q; seen["args"] = a
|
||||||
|
async def __aenter__(self): return self
|
||||||
|
async def __aexit__(self, *a): return False
|
||||||
|
|
||||||
|
class _Pool:
|
||||||
|
def acquire(self): return _Conn()
|
||||||
|
|
||||||
|
async def _pool(): return _Pool()
|
||||||
|
monkeypatch.setattr(db, "get_pool", _pool)
|
||||||
|
|
||||||
|
cid = uuid4()
|
||||||
|
_run(db.mark_blocks_stale(cid, True))
|
||||||
|
assert "blocks_stale" in seen["q"] and seen["args"][0] is True and seen["args"][1] == cid
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run to verify failure**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_audit_provenance.py -v`
|
||||||
|
Expected: FAIL — `AttributeError: ... has no attribute 'log_action_safe'` / `resolve_citation_case_law_ids` / `mark_blocks_stale`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/tests/test_audit_provenance.py
|
||||||
|
git commit -m "test(audit): failing tests for audit-trail + provenance (FU-7)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: V22 migration + core helpers
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/services/audit.py`, `mcp-server/src/legal_mcp/services/db.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `log_action_safe` to audit.py (after `log_action`)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def log_action_safe(
|
||||||
|
action: str,
|
||||||
|
case_id: "UUID | None" = None,
|
||||||
|
document_id: "UUID | None" = None,
|
||||||
|
details: dict | None = None,
|
||||||
|
user: str = "system",
|
||||||
|
) -> None:
|
||||||
|
"""Non-fatal audit: never let an audit-log failure break the caller's action.
|
||||||
|
|
||||||
|
The authoritative integrity trail is git (X5 §2.1); audit_log is the
|
||||||
|
'who/what/when' observability layer, so a write failure is logged as a
|
||||||
|
warning and swallowed.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await log_action(action, case_id=case_id, document_id=document_id,
|
||||||
|
details=details, user=user)
|
||||||
|
except Exception as e: # noqa: BLE001 — observability must not break the op
|
||||||
|
logger.warning("audit log_action failed (non-fatal) for %s: %s", action, e)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add `SCHEMA_V22_SQL` after `SCHEMA_V21_SQL` in db.py + wire it**
|
||||||
|
|
||||||
|
READ db.py near `SCHEMA_V21_SQL` (~line 1097-1133). Add after the V21 block:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ── V22: cases.blocks_stale — DOCX↔blocks drift flag (GAP-17 / INV-EX1) ──
|
||||||
|
# Set true when revise_draft/apply_user_edit make active_draft_path the live
|
||||||
|
# source-of-truth without re-syncing decision_blocks; cleared when blocks are
|
||||||
|
# re-exported or re-saved. Surfaced by health-check. Source-of-truth remains
|
||||||
|
# decision_blocks — this only flags known drift (no fragile DOCX→blocks reparse).
|
||||||
|
SCHEMA_V22_SQL = """
|
||||||
|
ALTER TABLE cases ADD COLUMN IF NOT EXISTS blocks_stale boolean NOT NULL DEFAULT false;
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
After `await conn.execute(SCHEMA_V21_SQL)` add `await conn.execute(SCHEMA_V22_SQL)` and bump the log line to `v1-v22`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add `mark_blocks_stale` + `resolve_citation_case_law_ids` to db.py (near the case helpers, after `get_active_draft_path`)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def mark_blocks_stale(case_id: UUID, stale: bool) -> None:
|
||||||
|
"""Flag/clear DOCX↔blocks drift for a case (GAP-17)."""
|
||||||
|
pool = await get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
await conn.execute(
|
||||||
|
"UPDATE cases SET blocks_stale = $1, updated_at = now() WHERE id = $2",
|
||||||
|
stale, case_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_citation_case_law_ids(ids) -> dict:
|
||||||
|
"""Structural citation→corpus resolution (GAP-20 / INV-AUD3).
|
||||||
|
|
||||||
|
Given case_law_id values referenced by a decision's citations/provenance,
|
||||||
|
split into resolvable (exist in case_law) vs unresolvable.
|
||||||
|
"""
|
||||||
|
resolved, unresolved = [], []
|
||||||
|
pool = await get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
for cid in ids:
|
||||||
|
try:
|
||||||
|
exists = await conn.fetchval(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM case_law WHERE id = $1)", cid)
|
||||||
|
except Exception:
|
||||||
|
exists = False
|
||||||
|
(resolved if exists else unresolved).append(cid)
|
||||||
|
return {"resolved": resolved, "unresolved": unresolved}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run Task-1 tests for these helpers**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_audit_provenance.py -v`
|
||||||
|
Expected: all 4 tests PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/audit.py mcp-server/src/legal_mcp/services/db.py
|
||||||
|
git commit -m "feat(audit): log_action_safe + V22 blocks_stale + citation resolver (FU-7)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: GAP-18 — audit calls on upload / extract_claims / export
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/tools/documents.py`, `mcp-server/src/legal_mcp/tools/drafting.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: `document_upload` — audit after processing (documents.py)**
|
||||||
|
|
||||||
|
READ `document_upload` (lines ~14-94). It computes `case_id`, `doc` (with `doc["id"]`), `actual_doc_type`, and `result` (with `result["classification"]`). Ensure `from legal_mcp.services import audit` is imported (add if missing). Immediately BEFORE the final `return json.dumps({...})`, add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await audit.log_action_safe(
|
||||||
|
"document_upload", case_id=case_id, document_id=UUID(doc["id"]),
|
||||||
|
details={"title": title, "doc_type": actual_doc_type},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: `extract_claims` — audit before return (documents.py)**
|
||||||
|
|
||||||
|
In `extract_claims` (lines ~300-348), before the final `return json.dumps(results, ...)`, add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await audit.log_action_safe(
|
||||||
|
"extract_claims", case_id=case_id,
|
||||||
|
details={"docs_processed": len(docs), "results": len(results)},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: `export_docx` — audit after export (drafting.py)**
|
||||||
|
|
||||||
|
READ `export_docx` in `drafting.py` (around lines 384-439). It resolves `case_id`, builds `path`, and calls `db.set_active_draft_path(case_id, path)`. Ensure `audit` is imported. After the `set_active_draft_path` call, add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await audit.log_action_safe(
|
||||||
|
"export_docx", case_id=case_id,
|
||||||
|
details={"path": str(path)},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify imports**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -c "from legal_mcp.tools import documents, drafting; print('clean')"`
|
||||||
|
Expected: `clean`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/tools/documents.py mcp-server/src/legal_mcp/tools/drafting.py
|
||||||
|
git commit -m "feat(audit): log document_upload/extract_claims/export_docx (GAP-18, FU-7)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: GAP-19 — block→source provenance
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/services/block_writer.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Make `_build_precedents_context` also return the case_law ids it used**
|
||||||
|
|
||||||
|
READ `_build_precedents_context` (lines ~671-716). Change the `caselaw_rows` SELECT to also fetch `cl.id`:
|
||||||
|
replace `"""SELECT cl.case_number, cl.case_name, cl.court, cl.summary, cl.key_quote,` with
|
||||||
|
`"""SELECT cl.id, cl.case_number, cl.case_name, cl.court, cl.summary, cl.key_quote,`.
|
||||||
|
Collect ids and change the function to return a tuple. At the function's two `return` points:
|
||||||
|
- replace `return "\n\n".join(parts) if parts else "(אין תקדימים)"` with
|
||||||
|
`return ("\n\n".join(parts) if parts else "(אין תקדימים)"), case_law_ids`
|
||||||
|
- ensure `case_law_ids = []` is initialized at the top, and inside the caselaw loop append `r["id"]` (str(r["id"])).
|
||||||
|
|
||||||
|
If there is an early/exception return path that returns a bare string, make it return `("(אין תקדימים)", [])` too.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update the caller in `write_block` + collect document/claim ids**
|
||||||
|
|
||||||
|
READ `write_block` (lines ~280-394). Line ~321 currently:
|
||||||
|
`precedents_context = await _build_precedents_context(case_id, block_id)`
|
||||||
|
Change to:
|
||||||
|
`precedents_context, _precedent_case_law_ids = await _build_precedents_context(case_id, block_id)`
|
||||||
|
|
||||||
|
Add a helper `_collect_block_sources` (after `_build_result`, ~line 408):
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def _collect_block_sources(case_id: UUID, block_id: str) -> dict:
|
||||||
|
"""Deterministic source ids available to a block's generation (GAP-19).
|
||||||
|
|
||||||
|
document_ids: case documents matching the block's allowed doc-types.
|
||||||
|
claim_ids: extracted claims for the case. (case_law_ids are captured
|
||||||
|
separately from the precedent search inside write_block.)
|
||||||
|
"""
|
||||||
|
allowed = _BLOCK_DOC_TYPES.get(block_id, [])
|
||||||
|
docs = await db.list_documents(case_id)
|
||||||
|
if allowed:
|
||||||
|
docs = [d for d in docs if d.get("doc_type") in allowed]
|
||||||
|
claims = await db.get_claims(case_id)
|
||||||
|
return {
|
||||||
|
"document_ids": [str(d["id"]) for d in docs],
|
||||||
|
"claim_ids": [str(c["id"]) for c in claims],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In `write_block`, just before the final `return _build_result(block_id, content, block_cfg)` (the non-template path, ~line 394), build the sources and attach to the result:
|
||||||
|
|
||||||
|
```python
|
||||||
|
sources = await _collect_block_sources(case_id, block_id)
|
||||||
|
sources["case_law_ids"] = _precedent_case_law_ids
|
||||||
|
result = _build_result(block_id, content, block_cfg)
|
||||||
|
result["sources"] = sources
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
(For the template path return at ~line 308, attach an empty sources dict: `r = _build_result(...); r["sources"] = {"document_ids": [], "claim_ids": [], "case_law_ids": []}; return r`.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write the provenance audit in `write_and_store_block` and `save_block_content`**
|
||||||
|
|
||||||
|
In `write_and_store_block` (~line 1039), after `await store_block(UUID(decision["id"]), result)`, add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await audit.log_action_safe(
|
||||||
|
"write_block", case_id=case_id,
|
||||||
|
details={
|
||||||
|
"decision_id": str(decision["id"]),
|
||||||
|
"block_id": block_id,
|
||||||
|
"model_used": result.get("model_used"),
|
||||||
|
"generation_type": result.get("generation_type"),
|
||||||
|
"sources": result.get("sources", {}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await db.mark_blocks_stale(case_id, False)
|
||||||
|
```
|
||||||
|
|
||||||
|
In `save_block_content` (~line 905), after `await store_block(...)` add the same `mark_blocks_stale(case_id, False)` (a saved block means DB blocks are current). Ensure `from legal_mcp.services import audit` is imported in block_writer.py (add if missing).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Smoke-import + targeted check**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -c "from legal_mcp.services import block_writer; print('clean')"`
|
||||||
|
Expected: `clean`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/block_writer.py
|
||||||
|
git commit -m "feat(audit): block→source provenance via write_block audit event (GAP-19, FU-7)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: GAP-20 — citation→corpus validation as QA warning
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/services/qa_validator.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Read the QA validator structure**
|
||||||
|
|
||||||
|
READ `mcp-server/src/legal_mcp/services/qa_validator.py` — find the function that runs the QA checks and returns findings (look for the list of checks / findings dicts with severity like `warning`/`critical`). Identify the findings structure (keys, how a check is appended).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add a citation-resolution check**
|
||||||
|
|
||||||
|
Add a check that gathers `case_law_id`s referenced by the decision's provenance/citations and resolves them. Concretely, add a function in qa_validator.py:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def _check_citation_resolution(case_id, decision_id) -> list[dict]:
|
||||||
|
"""GAP-20/INV-AUD3: every cited case_law_id must resolve to the corpus.
|
||||||
|
|
||||||
|
Reads case_law_ids from the decision's write_block audit provenance
|
||||||
|
(audit_log details.sources.case_law_ids) and verifies each resolves.
|
||||||
|
Unresolvable ids → non-blocking warning + audit('citation_unresolved').
|
||||||
|
"""
|
||||||
|
from legal_mcp.services import db, audit
|
||||||
|
from uuid import UUID
|
||||||
|
rows = await audit.get_audit_log(case_id=case_id, action="write_block", limit=200)
|
||||||
|
ids = set()
|
||||||
|
for r in rows:
|
||||||
|
details = r.get("details") or {}
|
||||||
|
if isinstance(details, str):
|
||||||
|
import json as _json
|
||||||
|
try: details = _json.loads(details)
|
||||||
|
except (ValueError, TypeError): details = {}
|
||||||
|
for raw in (details.get("sources") or {}).get("case_law_ids", []):
|
||||||
|
try: ids.add(UUID(str(raw)))
|
||||||
|
except (ValueError, TypeError): pass
|
||||||
|
if not ids:
|
||||||
|
return []
|
||||||
|
res = await db.resolve_citation_case_law_ids(list(ids))
|
||||||
|
findings = []
|
||||||
|
if res["unresolved"]:
|
||||||
|
await audit.log_action_safe(
|
||||||
|
"citation_unresolved", case_id=case_id,
|
||||||
|
details={"unresolved": [str(x) for x in res["unresolved"]]},
|
||||||
|
)
|
||||||
|
findings.append({
|
||||||
|
"check": "citation_resolution",
|
||||||
|
"severity": "warning",
|
||||||
|
"passed": False,
|
||||||
|
"message": f"{len(res['unresolved'])} ציטוטים אינם פתירים לקורפוס — דורש אימות יו\"ר",
|
||||||
|
})
|
||||||
|
return findings
|
||||||
|
```
|
||||||
|
|
||||||
|
Then wire `_check_citation_resolution` into the validator's main run function so its findings are appended to the result list (match the existing findings shape — adjust the dict keys to the validator's actual schema discovered in Step 1). It must be a **warning**, never a critical gate (does not block export).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Smoke-import**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -c "from legal_mcp.services import qa_validator; print('clean')"`
|
||||||
|
Expected: `clean`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/services/qa_validator.py
|
||||||
|
git commit -m "feat(qa): citation→corpus resolution as non-blocking warning (GAP-20, FU-7)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: GAP-17 — blocks_stale wiring + health-check
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/tools/drafting.py`, `mcp-server/src/legal_mcp/services/metrics.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Set `blocks_stale=true` in `revise_draft` and `apply_user_edit`**
|
||||||
|
|
||||||
|
READ `revise_draft` (~647-733) and `apply_user_edit` (~569-613) in drafting.py. Each ends by calling `db.set_active_draft_path(case_id, ...)`. Immediately after that call in EACH function, add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await db.mark_blocks_stale(case_id, True)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Clear `blocks_stale=false` in `export_docx`**
|
||||||
|
|
||||||
|
In `export_docx` (after the `set_active_draft_path` + the audit added in Task 3), add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await db.mark_blocks_stale(case_id, False)
|
||||||
|
```
|
||||||
|
(export_docx renders FROM the blocks, so the DOCX matches blocks → not stale.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Health-check count in metrics.py**
|
||||||
|
|
||||||
|
READ `mcp-server/src/legal_mcp/services/metrics.py` — find the aggregation that already runs counts (the one FU-2a added `non_searchable_case_law` to). Add a sibling count:
|
||||||
|
|
||||||
|
```python
|
||||||
|
cases_with_stale_blocks = await conn.fetchval(
|
||||||
|
"SELECT COUNT(*) FROM cases WHERE blocks_stale")
|
||||||
|
```
|
||||||
|
and expose it in the returned summary dict as `"cases_with_stale_blocks": cases_with_stale_blocks`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Smoke-import**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -c "from legal_mcp.tools import drafting; from legal_mcp.services import metrics; print('clean')"`
|
||||||
|
Expected: `clean`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/src/legal_mcp/tools/drafting.py mcp-server/src/legal_mcp/services/metrics.py
|
||||||
|
git commit -m "feat(audit): blocks_stale drift flag + health-check visibility (GAP-17, FU-7)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Full suite + DB smoke + lint + TaskMaster
|
||||||
|
|
||||||
|
- [ ] **Step 1: Full offline suite**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/ -q`
|
||||||
|
Expected: all pass (FU-1/2a + new FU-7 tests). Report the summary line. If a pre-existing test fails because a newly-audited function now calls `audit`/`mark_blocks_stale` without a stub, fix that test's fixture to stub the new boundary (same pattern as the FU-2a `recompute_searchable` fixture fix).
|
||||||
|
|
||||||
|
- [ ] **Step 2: DB smoke (real Postgres — applies V22, exercises helpers)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai && set -a && source ~/.env 2>/dev/null && set +a
|
||||||
|
cd mcp-server && .venv/bin/python -c "
|
||||||
|
import asyncio, uuid
|
||||||
|
from legal_mcp.services import db, audit
|
||||||
|
async def main():
|
||||||
|
await db.get_pool() # applies V22
|
||||||
|
pool = await db.get_pool()
|
||||||
|
async with pool.acquire() as c:
|
||||||
|
col = await c.fetchval(\"SELECT 1 FROM information_schema.columns WHERE table_name='cases' AND column_name='blocks_stale'\")
|
||||||
|
print('V22 blocks_stale present:', bool(col))
|
||||||
|
# citation resolver: random id is unresolved
|
||||||
|
out = await db.resolve_citation_case_law_ids([uuid.uuid4()])
|
||||||
|
print('resolver unresolved count:', len(out['unresolved']))
|
||||||
|
# log_action_safe never raises
|
||||||
|
await audit.log_action_safe('fu7_smoke', details={'ok': True})
|
||||||
|
print('log_action_safe ok')
|
||||||
|
asyncio.run(main())
|
||||||
|
" 2>&1 | grep -vE 'INFO|WARNING|httpx|deprecat|command not found|\^\^\^' | tail -5
|
||||||
|
```
|
||||||
|
Expected: `V22 blocks_stale present: True`, `resolver unresolved count: 1`, `log_action_safe ok`. (Optionally clean the smoke row: `DELETE FROM audit_log WHERE action='fu7_smoke'`.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Lint**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m ruff check src/legal_mcp/services/audit.py src/legal_mcp/services/db.py src/legal_mcp/services/block_writer.py 2>/dev/null; echo "exit=$?"`
|
||||||
|
Expected: clean or "ruff not available".
|
||||||
|
|
||||||
|
- [ ] **Step 4: Mark TaskMaster #65 done** — controller edits `.taskmaster/tasks/tasks.json` + verifies via MCP get_task.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Notes
|
||||||
|
|
||||||
|
- **GAP-18** → Task 3 (+ write_block audit in Task 4). **GAP-19** → Task 4 (provenance event). **GAP-20** → Task 5 (resolver + QA warning). **GAP-17** → Tasks 2+6 (V22 flag + wiring + health).
|
||||||
|
- **No new table** (audit_log reused, X5 §4). **No data migration** (V22 additive; provenance forward-only).
|
||||||
|
- **Non-fatal audit:** all calls via `log_action_safe`. **GAP-20 is warning-only** (never a critical gate — doesn't block export, consistent with FU-6 gates).
|
||||||
|
- **Type consistency:** `log_action_safe`, `mark_blocks_stale(case_id, stale)`, `resolve_citation_case_law_ids(ids)->{resolved,unresolved}`, `result["sources"]={document_ids,claim_ids,case_law_ids}` — names identical across tasks + tests.
|
||||||
|
- **Offline-test limit:** real audit_log INSERT / V22 verified by Task 7 Step 2 smoke; offline tests cover the pure wrappers/resolver logic.
|
||||||
254
docs/superpowers/plans/2026-05-30-system-spec-set.md
Normal file
254
docs/superpowers/plans/2026-05-30-system-spec-set.md
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
# System Spec-Set (Sub-Project 1) 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:** Author the living system spec-set under `docs/spec/` that canonically defines the *עוזר משפטי* system and its invariants ("what is correct"), each invariant backed by ≥3 authoritative sources.
|
||||||
|
|
||||||
|
**Architecture:** A `00-constitution.md` keystone (mission, global invariants, engineering rules, invariant template, verification protocol, index) + lifecycle-organized domain files (`01-ingest` … `07-learning`) + cross-cutting files (`X1`…`X5`). Existing docs are cited as verified sources, never duplicated. This is documentation, not code: the "test" is the **verification gate** — every invariant carries ≥3 verified sources or is marked `⚠ UNVERIFIED` and escalated to the chair (never decided solo).
|
||||||
|
|
||||||
|
**Tech Stack:** Markdown. Sources verified via WebSearch/WebFetch + primary texts (Nevo for Israeli statutes). Design basis: [docs/superpowers/specs/2026-05-30-system-spec-design.md](../specs/2026-05-30-system-spec-design.md).
|
||||||
|
|
||||||
|
**Branch:** `system-spec` (already created; design doc committed at `a5b22da`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conventions for every file (apply in each task)
|
||||||
|
|
||||||
|
- **Invariant template** (use verbatim structure):
|
||||||
|
```
|
||||||
|
### INV-<DOMAIN><n>: <short title>
|
||||||
|
**כלל:** <one crisp normative statement — what MUST hold>
|
||||||
|
**מקורות:** <≥3 authorities> | סטטוס: verified / ⚠ UNVERIFIED
|
||||||
|
**אכיפה:** <where/how enforced — schema / write-validation / health-check / human gate>
|
||||||
|
**הפרה ידועה:** <example from the system if any → links to audit; else "—">
|
||||||
|
```
|
||||||
|
- **Language:** Hebrew prose, English for technical terms and source names (matches project docs + RTL preference).
|
||||||
|
- **Length target:** ≤ ~500 lines/file. If exceeding, that domain needs splitting — note it, don't cram.
|
||||||
|
- **Citing existing docs:** reference (e.g., `block-schema.md`) as a *source to verify*; if it contradicts the ≥3 authorities, record a one-line audit-finding rather than silently trusting it.
|
||||||
|
- **Cross-links:** link sibling spec files by relative path; link global invariants as `00-constitution.md#inv-g<n>`.
|
||||||
|
|
||||||
|
## Per-file verification gate (the "test")
|
||||||
|
|
||||||
|
A file passes only when ALL hold (this checklist is a literal step in each task):
|
||||||
|
1. Every `INV-*` has either ≥3 named authoritative sources (`verified`) or is marked `⚠ UNVERIFIED` with an escalation note.
|
||||||
|
2. No placeholder text (`TBD`/`TODO`/"להשלים").
|
||||||
|
3. All cross-links resolve to a real file/anchor.
|
||||||
|
4. Consistent with `00-constitution.md` (no invariant contradicts a global invariant).
|
||||||
|
5. ≤ ~500 lines.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0 — Scaffold
|
||||||
|
|
||||||
|
### Task 0: Create the spec directory
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `docs/spec/README.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create `docs/spec/` with a short README**
|
||||||
|
|
||||||
|
Write `docs/spec/README.md`:
|
||||||
|
```markdown
|
||||||
|
# ספ המערכת — עוזר משפטי (Living System Spec)
|
||||||
|
|
||||||
|
זהו מקור-האמת הקנוני ל"מהו תקין" במערכת. שער-הכניסה: [00-constitution.md](00-constitution.md).
|
||||||
|
כל invariant מגובה ב-≥3 מקורות סמכותיים; פריט לא-מאומת מסומן ⚠ UNVERIFIED ומועלה ליו"ר.
|
||||||
|
|
||||||
|
מבנה: 00 חוקה · 01–07 מחזור-חיים · X1–X5 חוצי-שלבים. ראה אינדקס מלא בחוקה.
|
||||||
|
בסיס-עיצוב: docs/superpowers/specs/2026-05-30-system-spec-design.md
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/spec/README.md
|
||||||
|
git commit -m "docs(spec): scaffold docs/spec/ living spec-set"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1 — Keystone (REVIEW CHECKPOINT after)
|
||||||
|
|
||||||
|
### Task 1: `00-constitution.md` — the keystone
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `docs/spec/00-constitution.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the constitution** with these sections (content is already determined by the approved design):
|
||||||
|
|
||||||
|
1. **ייעוד** — paste the confirmed mission paragraph from the design doc §2.
|
||||||
|
2. **עקרונות-עבודה** — the 4 work principles (design doc §3): don't assume existing is correct; 3-source protocol; research→draft; collaboration model.
|
||||||
|
3. **תבנית-invariant** — the template from "Conventions" above.
|
||||||
|
4. **פרוטוקול-אימות** — `verified` vs `⚠ UNVERIFIED`; escalation to chair; never decide solo.
|
||||||
|
5. **Invariants גלובליים G1–G11** — each written with the full template. Content + sources from design doc §6 / §9:
|
||||||
|
|
||||||
|
- **INV-G1 מזהה קנוני מנורמל בכתיבה** — SSOT/normalization · Codd 1NF (CACM 13(6), 1970) · Kleppmann DDIA. אכיפה: normalization-on-write in the ingest path + `X1-identifiers.md`. הפרה ידועה: tolerant `_normalize_case_number` on read only; `8126-25` vs `8126-03-25`.
|
||||||
|
- **INV-G2 מקור-אמת יחיד, אין מסלולים מקבילים מתפצלים** — Kleppmann (system of record) · Fowler (Canonical Data Model) · SSOT. אכיפה: one canonical ingest path; siblings share it. הפרה ידועה: `ingest_precedent` vs `ingest_internal_decision` asymmetry.
|
||||||
|
- **INV-G3 ingest אחיד ו-idempotent (upsert על מפתח דטרמיניסטי)** — Kleppmann · Stripe/CDC idempotency · ISO 8000. אכיפה: `01-ingest.md` unified path.
|
||||||
|
- **INV-G4 חוזה-שלמות לפני "שמיש/ניתן-לחיפוש"** — ISO 8000 · DAMA-UK (completeness) · ISO 15489 (reliability). אכיפה: write-validation + health-check; `02-data-model.md`. הפרה ידועה: ערן סופר 8046/24 indexed with empty headnote/summary/tags.
|
||||||
|
- **INV-G5 metadata מלא לכל פריט מואנדקס + הפרדת-קורפוס בכל query** — Pinecone (multitenancy) · RAG attribution (Lewis et al.) · ISO 8000. אכיפה: `03-retrieval.md`. הפרה ידועה: task #56 halacha_filters source_kind leak.
|
||||||
|
- **INV-G6 re-index בכל שינוי תוכן** — Pinecone · Weaviate · RAG freshness. אכיפה: ingest/update path.
|
||||||
|
- **INV-G7 מיזוג RRF לא סכום-ציונים** — Elastic (RRF) · Weaviate · OpenSearch/Azure (corrob.). אכיפה: retrieval fusion (already implemented — codified).
|
||||||
|
- **INV-G8 איכות-אחזור נמדדת (precision+recall)** — Manning IR textbook · RAG eval literature · (Elastic eval guidance). אכיפה: eval harness in `03-retrieval.md`.
|
||||||
|
- **INV-G9 עקיבוּת-מקור + audit-trail ל-AI** — CEPEJ (user control) · NCSC · ISO 15489. אכיפה: `X5-audit-provenance.md`.
|
||||||
|
- **INV-G10 המערכת מסייעת; שערים אנושיים = invariant** — NCSC ("never replace human judgment") · CEPEJ · FJC. אכיפה: `05-qa-review.md` human gates.
|
||||||
|
- **INV-G11 תוכן החלטה מנומקת** (רקע ניטרלי · ללא כפילות · מענה לטענות המפסיד · מבחן-השופט · טענות מקוריות) — FJC Writing Manual · South Bucks [2004] UKHL 33 · חוק לתיקון סדרי המינהל (החלטות והנמקות) תשי"ט-1958. אכיפה: `04-analysis-writing.md` + `05-qa-review.md`.
|
||||||
|
|
||||||
|
6. **כללי-הנדסה** — סימטריה · נרמול-לא-תיקון-תסמין · quality-at-source (Fowler/Data-Mesh) · אין בליעה שקטה.
|
||||||
|
7. **אינדקס** — table linking all spec files (00, 01–07, X1–X5) with one-line purpose each.
|
||||||
|
8. **נספח מקורות** — paste the full source appendix from design doc §9.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the per-file verification gate** (the 5-point checklist above). Fix inline.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/spec/00-constitution.md
|
||||||
|
git commit -m "docs(spec): 00-constitution — mission, 11 global invariants, engineering rules"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: REVIEW CHECKPOINT** — present `00-constitution.md` to חיים. Do not start Phase 2 until approved. If the constitution's framing changes, the domain files adapt to it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 — Lifecycle domain files
|
||||||
|
|
||||||
|
> Each task: (a) targeted research to verify domain-specific invariants to ≥3 sources (global invariants already verified — reuse their sources; only NEW domain claims need fresh sourcing); (b) draft the file; (c) run the verification gate; (d) commit. Group review checkpoint at end of Phase 2.
|
||||||
|
|
||||||
|
### Task 2: `01-ingest.md` — unified intake contract
|
||||||
|
|
||||||
|
**Files:** Create `docs/spec/01-ingest.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Document the **target single ingest path** for all three intake kinds (case documents / external precedent / internal-committee decisions). Describe the canonical pipeline: stage file → extract text → chunk → embed → store → queue metadata extraction → queue halacha extraction → set statuses. State which steps are **uniform across all kinds** (this is the fix for the asymmetry).
|
||||||
|
- [ ] **Step 2:** Define domain invariants applying INV-G2/G3/G4/G6 to ingest, e.g.:
|
||||||
|
- **INV-ING1:** every intake kind flows through the same canonical ingest function; a new kind extends it via parameters, never a parallel function. (sources: INV-G2 set)
|
||||||
|
- **INV-ING2:** ingest is idempotent on the canonical identifier (re-ingest = upsert, no duplicate row/chunks). (sources: INV-G3 set)
|
||||||
|
- **INV-ING3:** metadata extraction is queued for *every* kind that has extractable metadata — not conditional per path. (sources: INV-G4 set; הפרה ידועה: internal path skipped `request_metadata_extraction`)
|
||||||
|
- [ ] **Step 3:** Cite current reality as audit-findings (the 8 documented asymmetries from the design research) — as `הפרה ידועה` lines, not as "correct."
|
||||||
|
- [ ] **Step 4:** Run verification gate. **Step 5:** Commit `docs(spec): 01-ingest unified intake contract`.
|
||||||
|
|
||||||
|
### Task 3: `02-data-model.md` — entities + completeness contract
|
||||||
|
|
||||||
|
**Files:** Create `docs/spec/02-data-model.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Enumerate the canonical entities (cases, case_law, documents, chunks, halachot, chair_feedback, …) — name, purpose, key fields. Mark this as the **target** model (verify field names against current schema during execution; divergences → audit-findings).
|
||||||
|
- [ ] **Step 2:** Define the **completeness contract per entity** — the mandatory-field set that makes a record "usable/searchable" (INV-G4). For `case_law`: e.g., canonical case_number, case_name, court, practice_area, source_kind, + (for searchable) ≥1 chunk and non-empty metadata. State explicitly that records failing the contract are flagged, not silently searchable.
|
||||||
|
- **INV-DM1:** a case_law row is "searchable" only when its completeness contract is satisfied. (sources: ISO 8000 · DAMA-UK · ISO 15489)
|
||||||
|
- **INV-DM2:** each entity has exactly one canonical identifier; no field stores a full citation as the identifier. (sources: INV-G1 set; הפרה ידועה: citation-as-case_number for סופר entries)
|
||||||
|
- [ ] **Step 3:** Run gate. **Step 4:** Commit `docs(spec): 02-data-model entities + completeness contract`.
|
||||||
|
|
||||||
|
### Task 4: `03-retrieval.md` — corpora + retrieval invariants
|
||||||
|
|
||||||
|
**Files:** Create `docs/spec/03-retrieval.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Document the 3 corpora + their search tools (source_kind mapping) and the hybrid/RRF design. (Reuse research from design §9 RAG sources — already verified.)
|
||||||
|
- [ ] **Step 2:** Define invariants (apply INV-G5/G6/G7/G8/G9):
|
||||||
|
- **INV-RET1:** corpus separation enforced on 100% of query paths (chunks AND halachot filters). (Pinecone · ISO · RAG; הפרה ידועה: task #56)
|
||||||
|
- **INV-RET2:** no item indexed without complete required metadata + resolvable source locator. (INV-G5 set)
|
||||||
|
- **INV-RET3:** heterogeneous retrievers fused by RRF, never raw-score sum. (Elastic · Weaviate)
|
||||||
|
- **INV-RET4:** retrieval quality measured by a standing precision+recall eval harness on a fixed labeled query set. (Manning · RAG eval)
|
||||||
|
- **INV-RET5:** every returned span is attributable to its source. (CEPEJ · RAG)
|
||||||
|
- [ ] **Step 3:** Run gate. **Step 4:** Commit `docs(spec): 03-retrieval corpora + retrieval invariants`.
|
||||||
|
|
||||||
|
### Task 5: `04-analysis-writing.md` — claims, 12 blocks, Dafna style
|
||||||
|
|
||||||
|
**Files:** Create `docs/spec/04-analysis-writing.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Reference (cite, don't duplicate) `block-schema.md`, `decision-methodology.md`, `skills/decision/SKILL.md` as sources; summarize the 12-block model + claims extraction at spec altitude.
|
||||||
|
- [ ] **Step 2:** Verify the Israeli reasoned-decision sources (design doc §8 open items #1–#3): confirm exact section of חוק 1958 (תשכ"ט-1969 amendment) on Nevo; confirm/locate ברק-ארז citation; confirm בג"ץ 143/56 / עע"ם 2994/21. Mark each `verified` or `⚠ UNVERIFIED` + escalate.
|
||||||
|
- [ ] **Step 3:** Define invariants from INV-G11:
|
||||||
|
- **INV-WR1:** block ו (background) is neutral — no judgment words, no party quotes. (FJC · חובת הנמקה)
|
||||||
|
- **INV-WR2:** no duplication — block י references prior blocks, does not restate facts. (FJC §non-duplication)
|
||||||
|
- **INV-WR3:** every losing-side principal argument is addressed. (FJC · South Bucks adequacy)
|
||||||
|
- **INV-WR4:** block ז = original claims only; supplements go to block ח. (project rule; cite corpus-analysis)
|
||||||
|
- **INV-WR5:** judge-unfamiliar-with-case test — decision is self-contained and traceable. (FJC · South Bucks)
|
||||||
|
- [ ] **Step 4:** Run gate. **Step 5:** Commit `docs(spec): 04-analysis-writing — 12 blocks + reasoned-decision invariants`.
|
||||||
|
|
||||||
|
### Task 6: `05-qa-review.md` — QA gates + human gates
|
||||||
|
|
||||||
|
**Files:** Create `docs/spec/05-qa-review.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Document the existing automated QA gates (`validate_decision`: neutral_background, claims_coverage, weight_compliance, structural_integrity, no_duplication, sequential_numbering) — as the QA contract (verify against `qa_validator.py` at execution).
|
||||||
|
- [ ] **Step 2:** Define human-gate invariants (INV-G10):
|
||||||
|
- **INV-QA1:** halacha approval is a manual chair decision; auto-extracted halachot are `pending_review` until the chair approves. (NCSC · CEPEJ · project rule)
|
||||||
|
- **INV-QA2:** outcome selection and chair feedback are human gates, never automated. (NCSC · CEPEJ · FJC)
|
||||||
|
- **INV-QA3:** a decision cannot be exported while critical QA gates fail. (FJC · validate_decision design)
|
||||||
|
- [ ] **Step 3:** Run gate. **Step 4:** Commit `docs(spec): 05-qa-review — QA + human gates`.
|
||||||
|
|
||||||
|
### Task 7: `06-export.md` — DOCX export contract
|
||||||
|
|
||||||
|
**Files:** Create `docs/spec/06-export.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Reference `skills/dafna-decision-template/SKILL.md`; document the export contract: line classification, dash policy, placeholder handling, template styles. Define:
|
||||||
|
- **INV-EX1:** export is deterministic from the stored decision blocks (single source = DB blocks; the DOCX is derived). (INV-G2 derived-data set)
|
||||||
|
- **INV-EX2:** export preserves source traceability where required. (INV-G9)
|
||||||
|
- [ ] **Step 2:** Run gate. **Step 3:** Commit `docs(spec): 06-export DOCX contract`.
|
||||||
|
|
||||||
|
### Task 8: `07-learning.md` — Hermes, lessons, feedback loop
|
||||||
|
|
||||||
|
**Files:** Create `docs/spec/07-learning.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Document the learning loop: Hermes curator (post-export analysis), `docs/legal-decision-lessons.md`, chair-feedback weekly analysis. Define:
|
||||||
|
- **INV-LRN1:** curator proposes; changes to SKILL.md/lessons.md require manual chair approval. (INV-G10; project rule)
|
||||||
|
- **INV-LRN2:** quality accountability sits at the source (ingest/authoring), not downstream. (Fowler/Data-Mesh)
|
||||||
|
- [ ] **Step 2:** Run gate. **Step 3:** Commit `docs(spec): 07-learning loop`.
|
||||||
|
|
||||||
|
- [ ] **Phase 2 REVIEW CHECKPOINT** — present `01`–`07` to חיים for review before Phase 3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3 — Cross-cutting files (final REVIEW after)
|
||||||
|
|
||||||
|
### Task 9: `X1-identifiers.md` — canonical identifier model
|
||||||
|
|
||||||
|
**Files:** Create `docs/spec/X1-identifiers.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Define the canonical case_number model: the normalized written form, the relationship `cases.case_number` vs `case_law.case_number`, and citation formats. Specify **normalize-on-write** (INV-G1), with tolerant-match-on-read as a *secondary* convenience, not the primary mechanism.
|
||||||
|
- **INV-ID1:** case_number is normalized to canonical form at write time. (SSOT · Codd · Kleppmann)
|
||||||
|
- **INV-ID2:** no entity uses a full citation string as its identifier. (INV-G1; הפרה ידועה: סופר entries)
|
||||||
|
- [ ] **Step 2:** Run gate. **Step 3:** Commit `docs(spec): X1-identifiers canonical model`.
|
||||||
|
|
||||||
|
### Task 10: `X2-multi-company.md`
|
||||||
|
|
||||||
|
**Files:** Create `docs/spec/X2-multi-company.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Document CMP (1xxx) / CMPA (8xxx), 14 agents (7×2), and the sync rules (cite `sync_agents_across_companies.py`, `HEARTBEAT.md`). Define:
|
||||||
|
- **INV-MC1:** any agent-config change in master must be synced to the mirror company via the API sync script. (project rule)
|
||||||
|
- [ ] **Step 2:** Run gate. **Step 3:** Commit `docs(spec): X2-multi-company`.
|
||||||
|
|
||||||
|
### Task 11: `X3-integration-deploy.md`
|
||||||
|
|
||||||
|
**Files:** Create `docs/spec/X3-integration-deploy.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Document Paperclip integration (wakeup via API not DB; comment routing via CEO; outbound case-status webhook) and the deploy model (Coolify dockerimage for legal-ai; pm2 for paperclip/chat-service). Define:
|
||||||
|
- **INV-INT1:** Paperclip wakeup goes through `POST /api/agents/{id}/wakeup` with `payload.issueId`, never a direct DB insert. (project rule; cite memory reference)
|
||||||
|
- **INV-INT2:** legal-ai code changes require commit→push→Coolify deploy; no local uvicorn. (project rule)
|
||||||
|
- [ ] **Step 2:** Run gate. **Step 3:** Commit `docs(spec): X3-integration-deploy`.
|
||||||
|
|
||||||
|
### Task 12: `X4-agents.md`
|
||||||
|
|
||||||
|
**Files:** Create `docs/spec/X4-agents.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Map the domain agents (ceo, researcher, analyst, writer, qa, proofreader, exporter, hermes) — role + which spec files each must read. Reserve a section for the **process agents** (sub-project 5: add-feature / fix-feature / spec-guardian) to be defined later. Define:
|
||||||
|
- **INV-AG1:** every agent reads `00-constitution.md` first and the relevant domain spec before acting. (governance rule)
|
||||||
|
- [ ] **Step 2:** Run gate. **Step 3:** Commit `docs(spec): X4-agents map`.
|
||||||
|
|
||||||
|
### Task 13: `X5-audit-provenance.md`
|
||||||
|
|
||||||
|
**Files:** Create `docs/spec/X5-audit-provenance.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Define the audit-trail + provenance requirements (INV-G9): logging of AI-assisted generation, traceability of every cited authority/source in a decision back to the corpus, record integrity over time.
|
||||||
|
- **INV-AUD1:** every AI-assisted artifact records what sources/data produced it. (CEPEJ user-control · NCSC · ISO 15489)
|
||||||
|
- **INV-AUD2:** record integrity — a stored decision/record is complete and unaltered except via tracked, attributed changes. (ISO 15489 §5.2.2.3)
|
||||||
|
- [ ] **Step 2:** Run gate. **Step 3:** Commit `docs(spec): X5-audit-provenance`.
|
||||||
|
|
||||||
|
- [ ] **FINAL REVIEW** — present the complete spec-set to חיים. On approval, sub-project 1 is done; proceed to sub-project 2 (Audit) in its own spec→plan cycle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review (run after writing this plan)
|
||||||
|
|
||||||
|
- **Spec coverage:** every design-doc section maps to a task — mission/principles → Task 1; G1–G11 → Task 1 + applied in 2–13; spec-set structure → Tasks 0–13; verification protocol → conventions + gate; open legal items → Task 5 Step 2. ✓
|
||||||
|
- **Placeholder scan:** domain-file invariants are enumerated with IDs + sources, not "define later"; the only deferred content is the process-agents section (Task 12) which is explicitly sub-project 5, and the legal `⚠ UNVERIFIED` items (Task 5) which are an intentional escalation, not a placeholder. ✓
|
||||||
|
- **Type/name consistency:** invariant IDs are unique (G1–G11, ING1–3, DM1–2, RET1–5, WR1–5, QA1–3, EX1–2, LRN1–2, ID1–2, MC1, INT1–2, AG1, AUD1–2); file names consistent with design doc §5. ✓
|
||||||
|
```
|
||||||
@@ -0,0 +1,401 @@
|
|||||||
|
# FU-2b: Internal Identifier Reconciliation — 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:** Build a reversible, chair-gated migration script that rewrites `internal_committee` `case_number` values currently holding a full citation into the canonical normalized bare number (X1: trim · prefix-strip · `/`→`-`, month preserved), leaving `citation_formatted` untouched.
|
||||||
|
|
||||||
|
**Architecture:** A standalone `scripts/` migration (not editable-service code), `--dry-run` by default. Dry-run emits a reconciliation table (CSV + Hebrew Markdown) for chair review; `--apply --approved <csv>` writes a backup then updates only chair-approved rows. Extraction is deterministic (single number-token regex) — no LLM. The production apply runs only AFTER Dafna approves the table.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.12, asyncpg, PostgreSQL@localhost:5433, pytest offline, `.venv` at `mcp-server/.venv`.
|
||||||
|
|
||||||
|
**Spec:** [docs/superpowers/specs/2026-05-31-fu2b-identifier-reconciliation-design.md](../specs/2026-05-31-fu2b-identifier-reconciliation-design.md)
|
||||||
|
|
||||||
|
**Run script:** `PY=/home/chaim/legal-ai/mcp-server/.venv/bin/python; $PY scripts/fu2b_reconcile_internal_case_numbers.py` (dry-run)
|
||||||
|
**Run tests:** `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_fu2b_reconcile.py -v`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- **Create** `scripts/fu2b_reconcile_internal_case_numbers.py` — the migration (pure `_extract_bare` + reconciliation builder + table/backup/revert writers + argparse `--dry-run`/`--apply`).
|
||||||
|
- **Create** `mcp-server/tests/test_fu2b_reconcile.py` — offline tests for `_extract_bare` + consistency flagging (imports the script module via sys.path).
|
||||||
|
- **Modify** `scripts/SCRIPTS.md` — register the new script (CLAUDE.md rule).
|
||||||
|
- **Artifact (produced, committed for review)** `data/audit/fu2b-reconciliation-<ts>.md` — the chair table from the dry-run.
|
||||||
|
|
||||||
|
No service code changes; no schema change. FK-safe (all `case_law` FKs use `id` UUID — verified).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Failing tests for `_extract_bare`
|
||||||
|
|
||||||
|
**Files:** Create `mcp-server/tests/test_fu2b_reconcile.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""FU-2b: deterministic bare-number extraction (offline)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Load the migration script as a module (it lives in scripts/, not a package).
|
||||||
|
_SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "fu2b_reconcile_internal_case_numbers.py"
|
||||||
|
_spec = importlib.util.spec_from_file_location("fu2b_reconcile", _SCRIPT)
|
||||||
|
fu2b = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(fu2b)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("raw,expected_bare", [
|
||||||
|
("ערר (ועדות ערר - תכנון ובנייה ירושלים) 403/17 אהרון ברק נ'", "403-17"),
|
||||||
|
("ערר (...) 8136-10-24 שחר שות'", "8136-10-24"), # month preserved
|
||||||
|
("בל\"מ (...) 1028/20 חלוואני ריאד", "1028-20"),
|
||||||
|
("8047/23", "8047-23"), # already-bare-ish
|
||||||
|
("ערר 81002-01-21", "81002-01-21"),
|
||||||
|
])
|
||||||
|
def test_extract_bare_single_token(raw, expected_bare):
|
||||||
|
bare, flag = fu2b._extract_bare(raw)
|
||||||
|
assert bare == expected_bare
|
||||||
|
assert flag == "OK"
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_bare_no_number():
|
||||||
|
bare, flag = fu2b._extract_bare("ערר אדלר נ' הוועדה")
|
||||||
|
assert bare is None and flag == "NO_NUMBER"
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_bare_multiple_numbers_flagged():
|
||||||
|
# Two case-number-shaped tokens → ambiguous, must NOT auto-pick.
|
||||||
|
bare, flag = fu2b._extract_bare("ערר 403/17 ו-1024/24 מאוחדים")
|
||||||
|
assert bare is None and flag == "MULTI_NUMBER"
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_bare_preserves_month_not_padding():
|
||||||
|
# Month kept exactly; 2-part stays 2-part (no invented month).
|
||||||
|
assert fu2b._extract_bare("ערר 8126/24 פלוני")[0] == "8126-24"
|
||||||
|
assert fu2b._extract_bare("ערר 8126-03-25 פלוני")[0] == "8126-03-25"
|
||||||
|
|
||||||
|
|
||||||
|
def test_consistency_flag_when_bare_absent_from_citation():
|
||||||
|
# proposed bare must appear in citation_formatted, else MISMATCH.
|
||||||
|
assert fu2b._consistency_flag("403-17", "ערר (...) 403/17 אהרון ברק") == "OK"
|
||||||
|
assert fu2b._consistency_flag("403-17", "ערר (...) 1975/24 מישהו אחר") == "MISMATCH"
|
||||||
|
assert fu2b._consistency_flag("403-17", "") == "NO_CITATION"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run to verify failure**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_fu2b_reconcile.py -v`
|
||||||
|
Expected: FAIL — `FileNotFoundError`/`ModuleNotFoundError` (script doesn't exist) or `AttributeError: _extract_bare`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/tests/test_fu2b_reconcile.py
|
||||||
|
git commit -m "test(fu2b): failing tests for bare-number extraction (FU-2b)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: The migration script (dry-run + apply + backup)
|
||||||
|
|
||||||
|
**Files:** Create `scripts/fu2b_reconcile_internal_case_numbers.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the script**
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""FU-2b — reconcile internal_committee case_number → canonical bare number.
|
||||||
|
|
||||||
|
Rewrites case_number values that currently hold a full citation into the
|
||||||
|
canonical normalized bare number (X1: trim · prefix-strip · '/'→'-', month
|
||||||
|
preserved). citation_formatted is the display field and is left untouched.
|
||||||
|
|
||||||
|
DETERMINISTIC — no LLM. Extraction takes the single case-number-shaped token
|
||||||
|
from the value; 0 or >1 tokens are flagged for chair review, never guessed.
|
||||||
|
|
||||||
|
Usage (must use the mcp-server venv — asyncpg/pgvector vendored there):
|
||||||
|
PY=/home/chaim/legal-ai/mcp-server/.venv/bin/python
|
||||||
|
|
||||||
|
# Dry-run (default): builds the reconciliation table for chair review.
|
||||||
|
$PY scripts/fu2b_reconcile_internal_case_numbers.py
|
||||||
|
|
||||||
|
# Apply ONLY the chair-approved rows (after Dafna's review), backup first:
|
||||||
|
$PY scripts/fu2b_reconcile_internal_case_numbers.py --apply \
|
||||||
|
--approved data/audit/fu2b-approved-<ts>.csv
|
||||||
|
|
||||||
|
Scope: source_kind='internal_committee' only (external → #68/FU-2c). FK-safe:
|
||||||
|
all case_law FKs reference case_law.id (UUID), not case_number.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(REPO_ROOT / "mcp-server" / "src"))
|
||||||
|
|
||||||
|
if "POSTGRES_URL" not in os.environ:
|
||||||
|
os.environ["POSTGRES_URL"] = (
|
||||||
|
f"postgres://{os.environ.get('POSTGRES_USER','legal_ai')}:"
|
||||||
|
f"{os.environ.get('POSTGRES_PASSWORD','')}@"
|
||||||
|
f"{os.environ.get('POSTGRES_HOST','127.0.0.1')}:"
|
||||||
|
f"{os.environ.get('POSTGRES_PORT','5433')}/"
|
||||||
|
f"{os.environ.get('POSTGRES_DB','legal_ai')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
AUDIT_DIR = REPO_ROOT / "data" / "audit"
|
||||||
|
_TOKEN_RE = re.compile(r"[0-9]{2,6}(?:[-/][0-9]{1,2}){1,2}")
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_bare(case_number: str) -> tuple[str | None, str]:
|
||||||
|
"""Return (canonical_bare, flag). flag ∈ {OK, NO_NUMBER, MULTI_NUMBER}.
|
||||||
|
|
||||||
|
Deterministic: finds case-number-shaped tokens (NNNN/YY or NNNN-MM-YY).
|
||||||
|
Exactly one → normalize '/'→'-' (month preserved, none invented). 0 or >1
|
||||||
|
→ None + flag (chair decides; never guess).
|
||||||
|
"""
|
||||||
|
tokens = _TOKEN_RE.findall(case_number or "")
|
||||||
|
if len(tokens) == 1:
|
||||||
|
return tokens[0].replace("/", "-"), "OK"
|
||||||
|
if not tokens:
|
||||||
|
return None, "NO_NUMBER"
|
||||||
|
return None, "MULTI_NUMBER"
|
||||||
|
|
||||||
|
|
||||||
|
def _consistency_flag(bare: str | None, citation_formatted: str) -> str:
|
||||||
|
"""OK if bare appears in citation_formatted; MISMATCH if not; NO_CITATION if empty."""
|
||||||
|
if not citation_formatted:
|
||||||
|
return "NO_CITATION"
|
||||||
|
if not bare:
|
||||||
|
return "NO_NUMBER"
|
||||||
|
# compare against the citation with separators unified, to match 403/17 vs 403-17
|
||||||
|
cf = citation_formatted.replace("/", "-")
|
||||||
|
return "OK" if bare in cf else "MISMATCH"
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_reconciliation() -> list[dict]:
|
||||||
|
from legal_mcp.services import db
|
||||||
|
pool = await db.get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"SELECT id, case_number, proceeding_type, coalesce(citation_formatted,'') AS cf "
|
||||||
|
"FROM case_law WHERE source_kind='internal_committee' ORDER BY case_number")
|
||||||
|
# detect dup serials across proceeding_type for a DUP_CHECK flag
|
||||||
|
out: list[dict] = []
|
||||||
|
for r in rows:
|
||||||
|
bare, flag = _extract_bare(r["case_number"])
|
||||||
|
cons = _consistency_flag(bare, r["cf"])
|
||||||
|
changes = bare is not None and bare != r["case_number"]
|
||||||
|
out.append({
|
||||||
|
"id": str(r["id"]),
|
||||||
|
"current_case_number": r["case_number"],
|
||||||
|
"proposed_bare": bare or "",
|
||||||
|
"proceeding_type": r["proceeding_type"] or "",
|
||||||
|
"citation_formatted": r["cf"],
|
||||||
|
"extract_flag": flag,
|
||||||
|
"consistency": cons,
|
||||||
|
"will_change": "yes" if changes else "no",
|
||||||
|
})
|
||||||
|
# DUP_CHECK: same proposed_bare appearing on >1 row (any proceeding_type)
|
||||||
|
from collections import Counter
|
||||||
|
bare_counts = Counter(d["proposed_bare"] for d in out if d["proposed_bare"])
|
||||||
|
for d in out:
|
||||||
|
if d["proposed_bare"] and bare_counts[d["proposed_bare"]] > 1:
|
||||||
|
d["dup_check"] = "DUP_CHECK"
|
||||||
|
else:
|
||||||
|
d["dup_check"] = ""
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _ts() -> str:
|
||||||
|
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||||
|
|
||||||
|
|
||||||
|
def _write_table(rows: list[dict], ts: str) -> tuple[Path, Path]:
|
||||||
|
AUDIT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
csv_path = AUDIT_DIR / f"fu2b-reconciliation-{ts}.csv"
|
||||||
|
md_path = AUDIT_DIR / f"fu2b-reconciliation-{ts}.md"
|
||||||
|
cols = ["id", "current_case_number", "proposed_bare", "proceeding_type",
|
||||||
|
"citation_formatted", "extract_flag", "consistency", "dup_check", "will_change"]
|
||||||
|
with csv_path.open("w", newline="", encoding="utf-8") as f:
|
||||||
|
w = csv.DictWriter(f, fieldnames=cols)
|
||||||
|
w.writeheader()
|
||||||
|
w.writerows(rows)
|
||||||
|
changing = [r for r in rows if r["will_change"] == "yes"]
|
||||||
|
flagged = [r for r in rows if r["extract_flag"] != "OK" or r["consistency"] == "MISMATCH" or r["dup_check"]]
|
||||||
|
with md_path.open("w", encoding="utf-8") as f:
|
||||||
|
f.write(f"# FU-2b — טבלת-תיאום מזהים (internal_committee) — {ts}\n\n")
|
||||||
|
f.write(f"- סה\"כ רשומות: {len(rows)}\n- ישתנו: {len(changing)}\n- מסומנות לסקירה: {len(flagged)}\n\n")
|
||||||
|
f.write("## דורש הכרעת-יו\"ר (flags)\n\n")
|
||||||
|
f.write("| current_case_number | proposed_bare | proc | flags |\n|---|---|---|---|\n")
|
||||||
|
for r in flagged:
|
||||||
|
fl = " ".join(x for x in [r["extract_flag"] if r["extract_flag"] != "OK" else "",
|
||||||
|
r["consistency"] if r["consistency"] == "MISMATCH" else "",
|
||||||
|
r["dup_check"]] if x)
|
||||||
|
f.write(f"| {r['current_case_number'][:50]} | {r['proposed_bare']} | {r['proceeding_type']} | {fl} |\n")
|
||||||
|
f.write("\n## כל השינויים המוצעים\n\n")
|
||||||
|
f.write("| current_case_number | → proposed_bare | proc |\n|---|---|---|\n")
|
||||||
|
for r in changing:
|
||||||
|
f.write(f"| {r['current_case_number'][:55]} | {r['proposed_bare']} | {r['proceeding_type']} |\n")
|
||||||
|
return csv_path, md_path
|
||||||
|
|
||||||
|
|
||||||
|
async def _apply(approved_csv: Path, ts: str) -> dict:
|
||||||
|
from legal_mcp.services import db
|
||||||
|
with approved_csv.open(encoding="utf-8") as f:
|
||||||
|
approved = [r for r in csv.DictReader(f)
|
||||||
|
if r.get("will_change") == "yes" and r.get("proposed_bare")]
|
||||||
|
if not approved:
|
||||||
|
return {"applied": 0, "note": "no approved changing rows"}
|
||||||
|
AUDIT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
backup = AUDIT_DIR / f"fu2b-backup-{ts}.csv"
|
||||||
|
pool = await db.get_pool()
|
||||||
|
applied = 0
|
||||||
|
with backup.open("w", newline="", encoding="utf-8") as bf:
|
||||||
|
bw = csv.writer(bf)
|
||||||
|
bw.writerow(["id", "old_case_number"])
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
for r in approved:
|
||||||
|
old = await conn.fetchval("SELECT case_number FROM case_law WHERE id=$1", r["id"])
|
||||||
|
if old is None:
|
||||||
|
continue
|
||||||
|
bw.writerow([r["id"], old])
|
||||||
|
await conn.execute(
|
||||||
|
"UPDATE case_law SET case_number=$2 WHERE id=$1 "
|
||||||
|
"AND source_kind='internal_committee'",
|
||||||
|
r["id"], r["proposed_bare"])
|
||||||
|
applied += 1
|
||||||
|
return {"applied": applied, "backup": str(backup)}
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="FU-2b internal case_number reconciliation")
|
||||||
|
parser.add_argument("--apply", action="store_true", help="apply approved changes (default: dry-run)")
|
||||||
|
parser.add_argument("--approved", type=str, help="path to chair-approved CSV (required with --apply)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
ts = _ts()
|
||||||
|
|
||||||
|
if not args.apply:
|
||||||
|
rows = await _build_reconciliation()
|
||||||
|
csv_path, md_path = _write_table(rows, ts)
|
||||||
|
changing = sum(1 for r in rows if r["will_change"] == "yes")
|
||||||
|
flagged = sum(1 for r in rows if r["extract_flag"] != "OK" or r["consistency"] == "MISMATCH" or r["dup_check"])
|
||||||
|
print(f"DRY-RUN: {len(rows)} rows | will_change={changing} | flagged={flagged}")
|
||||||
|
print(f" table: {md_path}")
|
||||||
|
print(f" csv: {csv_path}")
|
||||||
|
print("Review the table with the chair, then run --apply --approved <reviewed.csv>.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not args.approved:
|
||||||
|
print("ERROR: --apply requires --approved <csv> (the chair-reviewed table).", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
result = await _apply(Path(args.approved), ts)
|
||||||
|
print(f"APPLIED: {result}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(asyncio.run(main()))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the unit tests**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_fu2b_reconcile.py -v`
|
||||||
|
Expected: ALL pass (extraction + flags + consistency).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
chmod +x scripts/fu2b_reconcile_internal_case_numbers.py
|
||||||
|
git add scripts/fu2b_reconcile_internal_case_numbers.py
|
||||||
|
git commit -m "feat(fu2b): chair-gated internal case_number reconciliation script (GAP-07/08)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Dry-run against the DB → produce the chair table
|
||||||
|
|
||||||
|
**Files:** Produces `data/audit/fu2b-reconciliation-<ts>.{csv,md}`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run the dry-run**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai && set -a && source ~/.env 2>/dev/null && set +a
|
||||||
|
PY=/home/chaim/legal-ai/mcp-server/.venv/bin/python
|
||||||
|
$PY scripts/fu2b_reconcile_internal_case_numbers.py
|
||||||
|
```
|
||||||
|
Expected output: `DRY-RUN: 56 rows | will_change=~52 | flagged=~1` (the ~1 = the 8047/23 DUP_CHECK pair → 2 rows flagged). Note the exact numbers.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Sanity-check the produced table**
|
||||||
|
|
||||||
|
Open `data/audit/fu2b-reconciliation-<ts>.md`. Verify:
|
||||||
|
- `will_change` rows: each `current_case_number` (full citation) → a clean `proposed_bare` matching the number inside it.
|
||||||
|
- `flagged` section: should contain the `8047-23` DUP_CHECK pair (ערר + בל"מ) and ideally nothing else (0 MULTI_NUMBER, 0 MISMATCH expected per the analysis).
|
||||||
|
- If MULTI_NUMBER / MISMATCH rows appear unexpectedly, STOP and report them (the analysis predicted 0; an unexpected flag means the data changed and needs investigation before chair review).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit the produced table as a review artifact**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add data/audit/fu2b-reconciliation-*.md data/audit/fu2b-reconciliation-*.csv
|
||||||
|
git commit -m "chore(fu2b): dry-run reconciliation table for chair review (GAP-07/08)"
|
||||||
|
```
|
||||||
|
(If `data/audit/` is gitignored, skip the commit and report the path instead — the table still exists on disk for review.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: SCRIPTS.md + PR
|
||||||
|
|
||||||
|
- [ ] **Step 1: Register the script in `scripts/SCRIPTS.md`**
|
||||||
|
|
||||||
|
Add a row to the active-scripts table (match the file's existing table format) describing `fu2b_reconcile_internal_case_numbers.py`: purpose (FU-2b internal case_number reconciliation, GAP-07/08), status (active, chair-gated), usage (dry-run default / `--apply --approved`).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Full suite + commit + push + PR**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/ -q # report summary (expect all pass)
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add scripts/SCRIPTS.md
|
||||||
|
git commit -m "docs(scripts): register fu2b reconciliation script (FU-2b)"
|
||||||
|
git push -u origin fix/fu2b-identifier-reconciliation
|
||||||
|
```
|
||||||
|
Then create the PR via the Gitea REST API (token from `~/.git-credentials`) and merge per the standing PR+merge rule. The PR delivers the **tooling + dry-run table**; the production `--apply` is the separate gated step below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: [HUMAN GATE] Chair review + gated apply (NOT automated)
|
||||||
|
|
||||||
|
> This task is the chair-approval gate. It is NOT executed by an implementer subagent.
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Present `data/audit/fu2b-reconciliation-<ts>.md` to the controller, who presents it to Dafna: the ~52 proposed changes + the `8047-23` ערר/בל"מ DUP_CHECK pair. Dafna confirms the mapping and adjudicates whether 8047/23 is two distinct proceedings (keep both) or a mis-tagged duplicate (manual delete, separate).
|
||||||
|
- [ ] **Step 2:** Save the reviewed table as `data/audit/fu2b-approved-<ts>.csv` (rows Dafna approved; `will_change=yes` only for those).
|
||||||
|
- [ ] **Step 3:** Run the gated apply against the DB:
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai && set -a && source ~/.env && set +a
|
||||||
|
PY=/home/chaim/legal-ai/mcp-server/.venv/bin/python
|
||||||
|
$PY scripts/fu2b_reconcile_internal_case_numbers.py --apply --approved data/audit/fu2b-approved-<ts>.csv
|
||||||
|
```
|
||||||
|
- [ ] **Step 4:** Verify: re-run dry-run → `will_change=0` (idempotent); spot-check `get_case_by_number` still resolves a migrated case; confirm a backup CSV was written (revert path). Mark TaskMaster #67 done.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Notes
|
||||||
|
|
||||||
|
- **GAP-07/08 (internal)** → Task 2 script + Task 3 dry-run + Task 5 gated apply. Canonical form per X1 (month preserved) — `_extract_bare` replaces only `/`→`-` on the single extracted token, never strips/pads a month.
|
||||||
|
- **Reversible:** `_apply` writes `fu2b-backup-<ts>.csv` (id, old_case_number) before each UPDATE.
|
||||||
|
- **Chair gate:** `--apply` requires `--approved <csv>`; production apply is Task 5 (human), not part of the PR merge.
|
||||||
|
- **Determinism / safety:** 0/>1 token → flagged, never guessed; consistency + DUP_CHECK flags surface the 8047 edge.
|
||||||
|
- **Scope:** `source_kind='internal_committee'` only (the UPDATE has the `AND source_kind='internal_committee'` guard); external → #68.
|
||||||
|
- **FK-safe:** verified all 11 `case_law` FKs use `id` (UUID).
|
||||||
|
- **Type consistency:** `_extract_bare(case_number)->(bare|None,flag)`, `_consistency_flag(bare,citation)->str` — names match tests (Task 1) and script (Task 2).
|
||||||
326
docs/superpowers/plans/2026-05-31-fu8a-process-to-code-guards.md
Normal file
326
docs/superpowers/plans/2026-05-31-fu8a-process-to-code-guards.md
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
# FU-8a: Process→Code Guards — 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:** Make two process barriers enforceable in code: `sync_agents_across_companies.py --verify` exits non-zero on any drift (incl. adapter_type mismatch, loud not silent), and a fitness-function test fails the suite if the repo gains raw Paperclip HTTP calls or direct `agent_wakeup_requests` inserts.
|
||||||
|
|
||||||
|
**Architecture:** GAP-21 — extract the drift loop into a pure `build_drift_report(...)` and a pure `_verify_exit_code(...)`, then make `--verify` exit `1` on drift. GAP-22 — a self-contained pytest fitness function that scans `web/`, `mcp-server/src/`, `scripts/` for forbidden Paperclip-access patterns with an explicit allowlist. Both pure-code; repo pre-scanned clean (0 existing violations).
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.12, asyncpg (sync script), pytest offline, `.venv` at `mcp-server/.venv`.
|
||||||
|
|
||||||
|
**Spec:** [docs/superpowers/specs/2026-05-31-fu8a-process-to-code-guards-design.md](../specs/2026-05-31-fu8a-process-to-code-guards-design.md)
|
||||||
|
|
||||||
|
**Run tests:** `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_sync_verify_gate.py tests/test_paperclip_access_guard.py -v`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- **Modify** `scripts/sync_agents_across_companies.py` — extract `build_drift_report(...)` + `_verify_exit_code(...)` (pure); `--verify` exits non-zero on drift; adapter_type mismatch + missing-in-mirror counted as drift.
|
||||||
|
- **Create** `mcp-server/tests/test_sync_verify_gate.py` — offline tests for the two pure functions (imports the script via importlib, like the FU-2b test).
|
||||||
|
- **Create** `mcp-server/tests/test_paperclip_access_guard.py` — the fitness-function guard (scan + fixtures + real-repo assertion).
|
||||||
|
- **Modify** `scripts/SCRIPTS.md` — note the new `--verify` gate semantics.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: GAP-21 — `--verify` becomes an enforceable drift gate
|
||||||
|
|
||||||
|
**Files:** Modify `scripts/sync_agents_across_companies.py`; Create `mcp-server/tests/test_sync_verify_gate.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""FU-8a / GAP-21: sync --verify drift-gate logic (offline)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "sync_agents_across_companies.py"
|
||||||
|
_spec = importlib.util.spec_from_file_location("sync_agents", _SCRIPT)
|
||||||
|
sync = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(sync)
|
||||||
|
|
||||||
|
|
||||||
|
def _agent(name, adapter="claude_code", cfg=None):
|
||||||
|
return {"id": f"id-{name}", "name": name, "adapter_type": adapter,
|
||||||
|
"adapter_config": cfg or {"model": "x"}, "runtime_config": {}, "metadata": {},
|
||||||
|
"budget_monthly_cents": 0, "icon": "", "title": "", "role": "", "agent_api_keys": []}
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_exit_code_clean_is_zero():
|
||||||
|
assert sync._verify_exit_code(plan=[], mismatches=[], missing=[]) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_exit_code_drift_is_nonzero():
|
||||||
|
assert sync._verify_exit_code(plan=[("m", "mi", {"x": 1})], mismatches=[], missing=[]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_exit_code_adapter_mismatch_is_nonzero():
|
||||||
|
# adapter_type mismatch must count as drift (not silent skip)
|
||||||
|
assert sync._verify_exit_code(plan=[], mismatches=["עוזר משפטי"], missing=[]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_exit_code_missing_is_nonzero():
|
||||||
|
assert sync._verify_exit_code(plan=[], mismatches=[], missing=["סוכן"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_drift_report_flags_adapter_mismatch():
|
||||||
|
master = [_agent("A", adapter="claude_code")]
|
||||||
|
mirror_by_name = {"A": _agent("A", adapter="deepseek_local")}
|
||||||
|
rep = sync.build_drift_report(master, mirror_by_name, mirror_skills=set(), only=None)
|
||||||
|
assert "A" in rep["mismatches"]
|
||||||
|
assert rep["plan"] == [] # mismatch short-circuits the diff
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_drift_report_flags_missing_and_plan():
|
||||||
|
master = [_agent("A"), _agent("B")]
|
||||||
|
# A missing in mirror; B present but differing config
|
||||||
|
mirror_by_name = {"B": _agent("B", cfg={"model": "different"})}
|
||||||
|
rep = sync.build_drift_report(master, mirror_by_name, mirror_skills=set(), only=None)
|
||||||
|
assert "A" in rep["missing"]
|
||||||
|
assert any(p[0]["name"] == "B" for p in rep["plan"])
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run to verify it fails**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_sync_verify_gate.py -v`
|
||||||
|
Expected: FAIL — `AttributeError: module 'sync_agents' has no attribute '_verify_exit_code'` / `build_drift_report`.
|
||||||
|
(Note: the script imports `asyncpg` at module top — confirm it imports cleanly under importlib; it does not connect at import time.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the two pure functions**
|
||||||
|
|
||||||
|
In `scripts/sync_agents_across_companies.py`, add ABOVE `async def main()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def build_drift_report(master_agents, mirror_by_name, mirror_skills, only=None) -> dict:
|
||||||
|
"""Pure drift computation (no DB, no printing). Returns:
|
||||||
|
{"plan": [(master, mirror, diff), ...], "mismatches": [name, ...], "missing": [name, ...]}.
|
||||||
|
adapter_type mismatch and missing-in-mirror are recorded as drift, not skipped silently.
|
||||||
|
"""
|
||||||
|
plan, mismatches, missing = [], [], []
|
||||||
|
for m in master_agents:
|
||||||
|
if only and m["name"] != only:
|
||||||
|
continue
|
||||||
|
mirror = mirror_by_name.get(m["name"])
|
||||||
|
if not mirror:
|
||||||
|
missing.append(m["name"])
|
||||||
|
continue
|
||||||
|
if m["adapter_type"] != mirror["adapter_type"]:
|
||||||
|
mismatches.append(m["name"])
|
||||||
|
continue
|
||||||
|
diff = compute_diff(m, mirror, mirror_skills)
|
||||||
|
if diff:
|
||||||
|
plan.append((m, mirror, diff))
|
||||||
|
return {"plan": plan, "mismatches": mismatches, "missing": missing}
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_exit_code(plan, mismatches, missing) -> int:
|
||||||
|
"""0 iff fully in sync; 1 if any drift (needs-sync / adapter mismatch / missing-in-mirror)."""
|
||||||
|
return 1 if (plan or mismatches or missing) else 0
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Rewire `main()`'s drift loop + `--verify` to use them**
|
||||||
|
|
||||||
|
In `main()`, REPLACE the inline drift loop (the `plan = []` block through the `for m in master_agents:` loop that builds `plan`) with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
print(f"=== Drift report ===")
|
||||||
|
report = build_drift_report(master_agents, mirror_by_name, mirror_skills, only=args.only)
|
||||||
|
plan = report["plan"]
|
||||||
|
for name in report["missing"]:
|
||||||
|
print(f" ⚠ {name:14s} — NOT FOUND in mirror (we never auto-create) — DRIFT")
|
||||||
|
for name in report["mismatches"]:
|
||||||
|
m = next(a for a in master_agents if a["name"] == name)
|
||||||
|
mi = mirror_by_name[name]
|
||||||
|
print(f" ❌ {name:14s} — adapter_type mismatch ({m['adapter_type']} vs {mi['adapter_type']}) "
|
||||||
|
f"— DRIFT (apply skips it; fix manually in both companies)")
|
||||||
|
for master, mirror, diff in plan:
|
||||||
|
print_diff(master["name"], diff, master["id"], mirror["id"])
|
||||||
|
```
|
||||||
|
|
||||||
|
And REPLACE the `if args.verify:` block with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if args.verify:
|
||||||
|
code = _verify_exit_code(plan, report["mismatches"], report["missing"])
|
||||||
|
total_drift = len(plan) + len(report["mismatches"]) + len(report["missing"])
|
||||||
|
print(f"\nSummary: {len(plan)} need sync, {len(report['mismatches'])} adapter-mismatch, "
|
||||||
|
f"{len(report['missing'])} missing-in-mirror → {'DRIFT' if code else 'IN SYNC'}")
|
||||||
|
sys.exit(code)
|
||||||
|
```
|
||||||
|
|
||||||
|
(The `--apply` path still uses `plan` and still does NOT touch adapter_type-mismatch agents — only `--verify`'s exit code changes + the loud reporting.)
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run tests + import check**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_sync_verify_gate.py -v` → all PASS.
|
||||||
|
Run: `.venv/bin/python -c "import importlib.util,pathlib; p=pathlib.Path('scripts/sync_agents_across_companies.py'); s=importlib.util.spec_from_file_location('s',p); m=importlib.util.module_from_spec(s); s.loader.exec_module(m); print('imports')"` (from repo root) → `imports`.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add scripts/sync_agents_across_companies.py mcp-server/tests/test_sync_verify_gate.py
|
||||||
|
git commit -m "feat(sync): --verify exits non-zero on drift; adapter mismatch = loud drift (GAP-21, FU-8a)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: GAP-22 — Paperclip-access fitness function
|
||||||
|
|
||||||
|
**Files:** Create `mcp-server/tests/test_paperclip_access_guard.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the guard + its tests**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""FU-8a / GAP-22: fitness function — forbid un-sanctioned Paperclip access.
|
||||||
|
|
||||||
|
Fails if any scanned source (outside the allowlist) reaches the Paperclip API
|
||||||
|
with a raw HTTP client or inserts directly into agent_wakeup_requests. The
|
||||||
|
sanctioned paths are web/paperclip_api.py::pc_request (Python) and scripts/pc.sh
|
||||||
|
(bash); wakeup must go through POST /api/agents/{id}/wakeup.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parents[2]
|
||||||
|
SCAN_ROOTS = [REPO / "web", REPO / "mcp-server" / "src", REPO / "scripts"]
|
||||||
|
|
||||||
|
# Files exempt from the HTTP-to-Paperclip rule (the sanctioned helpers + legacy DB-read client).
|
||||||
|
ALLOWLIST = {
|
||||||
|
REPO / "web" / "paperclip_api.py", # the sanctioned pc_request helper
|
||||||
|
REPO / "scripts" / "pc.sh", # the sanctioned bash wrapper
|
||||||
|
REPO / "web" / "paperclip_client.py", # legacy: DB reads only (no raw http, no wakeup insert)
|
||||||
|
}
|
||||||
|
|
||||||
|
_PC_URL = re.compile(r"PAPERCLIP_API_URL|127\.0\.0\.1:3100|localhost:3100|pc\.nautilus\.marcusgroup\.org")
|
||||||
|
_HTTP_CLIENT = re.compile(r"\bhttpx\b|\brequests\.(get|post|put|patch|delete)\b|\baiohttp\b|\bcurl\b")
|
||||||
|
_WAKEUP_INSERT = re.compile(r"insert\s+into\s+agent_wakeup_requests", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _scan_text(text: str) -> list[str]:
|
||||||
|
"""Return violation reasons for a single file's text."""
|
||||||
|
reasons = []
|
||||||
|
if _WAKEUP_INSERT.search(text):
|
||||||
|
reasons.append("direct INSERT INTO agent_wakeup_requests — use the wakeup API")
|
||||||
|
# raw HTTP to Paperclip: both a paperclip-URL token and an http-client token present
|
||||||
|
if _PC_URL.search(text) and _HTTP_CLIENT.search(text):
|
||||||
|
reasons.append("raw HTTP client + Paperclip URL — use web/paperclip_api.pc_request or scripts/pc.sh")
|
||||||
|
return reasons
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_source_files():
|
||||||
|
for root in SCAN_ROOTS:
|
||||||
|
if not root.exists():
|
||||||
|
continue
|
||||||
|
for ext in ("*.py", "*.sh"):
|
||||||
|
for f in root.rglob(ext):
|
||||||
|
if f in ALLOWLIST or "/.venv/" in str(f) or "/tests/" in str(f):
|
||||||
|
continue
|
||||||
|
yield f
|
||||||
|
|
||||||
|
|
||||||
|
def find_violations() -> list[tuple[str, str]]:
|
||||||
|
out = []
|
||||||
|
for f in _iter_source_files():
|
||||||
|
try:
|
||||||
|
text = f.read_text(encoding="utf-8")
|
||||||
|
except (UnicodeDecodeError, OSError):
|
||||||
|
continue
|
||||||
|
for reason in _scan_text(text):
|
||||||
|
out.append((str(f.relative_to(REPO)), reason))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ── the guard catches positives, ignores sanctioned negatives ──────────
|
||||||
|
def test_scan_flags_raw_http_to_paperclip():
|
||||||
|
bad = 'import httpx\nasync def f():\n await httpx.post(f"{PAPERCLIP_API_URL}/x")\n'
|
||||||
|
assert _scan_text(bad)
|
||||||
|
|
||||||
|
|
||||||
|
def test_scan_flags_wakeup_insert():
|
||||||
|
bad = "await conn.execute('INSERT INTO agent_wakeup_requests (id) VALUES ($1)', x)"
|
||||||
|
assert _scan_text(bad)
|
||||||
|
|
||||||
|
|
||||||
|
def test_scan_ignores_sanctioned_helper_shape():
|
||||||
|
ok = 'url = f"{PAPERCLIP_API_URL}{path}"\n# the only place httpx is allowed for paperclip\n'
|
||||||
|
# this shape WOULD flag if not allowlisted — proving the allowlist is what protects it
|
||||||
|
assert _scan_text(ok) # raw text matches; the file is protected by ALLOWLIST, not by content
|
||||||
|
|
||||||
|
|
||||||
|
def test_scan_ignores_plain_code():
|
||||||
|
assert _scan_text("def add(a, b):\n return a + b\n") == []
|
||||||
|
|
||||||
|
|
||||||
|
# ── the real repo must be clean (pre-scanned 2026-05-31: 0 violations) ──
|
||||||
|
def test_repo_has_no_paperclip_access_violations():
|
||||||
|
violations = find_violations()
|
||||||
|
assert violations == [], "Un-sanctioned Paperclip access found:\n" + "\n".join(
|
||||||
|
f" {f}: {r}" for f, r in violations)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the guard tests**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/test_paperclip_access_guard.py -v`
|
||||||
|
Expected: ALL PASS — including `test_repo_has_no_paperclip_access_violations` (repo is clean).
|
||||||
|
If `test_repo_has_no_paperclip_access_violations` FAILS, it found a real violation: either fix the offending code to use the sanctioned helper, or (if it's a genuine sanctioned location) add it to `ALLOWLIST` with a comment justifying it. Report any such case.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add mcp-server/tests/test_paperclip_access_guard.py
|
||||||
|
git commit -m "feat(guard): fitness function blocking raw Paperclip access (GAP-22, FU-8a)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: SCRIPTS.md + full suite + smoke + PR
|
||||||
|
|
||||||
|
- [ ] **Step 1: Note the `--verify` gate semantics in SCRIPTS.md**
|
||||||
|
|
||||||
|
In `scripts/SCRIPTS.md`, in the `sync_agents_across_companies.py` row, append to its Purpose cell: "**`--verify` יוצא exit≠0 על drift** (כולל adapter_type-mismatch — מדווח רם, נספר כ-drift) — שמיש כ-gate ל-cron/CI (GAP-21/FU-8a)."
|
||||||
|
|
||||||
|
- [ ] **Step 2: Full offline suite**
|
||||||
|
|
||||||
|
Run: `cd ~/legal-ai/mcp-server && .venv/bin/python -m pytest tests/ -q`
|
||||||
|
Expected: all pass (prior suite + the new GAP-21/GAP-22 tests). Report the summary line.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Smoke — run `--verify` against the live Paperclip DB (read-only)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai && set -a && source ~/.env 2>/dev/null && set +a
|
||||||
|
PAPERCLIP_BOARD_API_KEY="${PAPERCLIP_BOARD_API_KEY:-}" \
|
||||||
|
/home/chaim/legal-ai/mcp-server/.venv/bin/python scripts/sync_agents_across_companies.py --verify; echo "exit=$?"
|
||||||
|
```
|
||||||
|
Report the output + exit code. Expected: prints a drift report; `exit=0` if agents are in sync, `exit=1` if drift exists (either is a valid result — it proves the gate works). The script only READS in `--verify` (no mutation).
|
||||||
|
(If the script needs `PAPERCLIP_DB_URL`/board key and they're absent, report that the smoke needs the Paperclip env; the offline unit tests already validate the gate logic.)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit + PR**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/legal-ai
|
||||||
|
git add scripts/SCRIPTS.md
|
||||||
|
git commit -m "docs(scripts): note sync --verify drift-gate semantics (FU-8a)"
|
||||||
|
git push -u origin fix/fu8a-process-to-code-guards
|
||||||
|
```
|
||||||
|
Create the PR via the Gitea REST API (token from `~/.git-credentials`) and merge per the standing PR+merge rule.
|
||||||
|
|
||||||
|
- [ ] **Step 5: TaskMaster #66 → done** (controller; verify via MCP). GAP-23 remains in #69.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Notes
|
||||||
|
|
||||||
|
- **GAP-21** → Task 1: `build_drift_report` + `_verify_exit_code` (pure, tested); `--verify` exits 1 on drift; adapter mismatch loud + counted. `--apply` behavior unchanged.
|
||||||
|
- **GAP-22** → Task 2: fitness function; tested on positive fixtures + sanctioned negatives + the real repo (clean). Allowlist explicit (`paperclip_api.py`, `pc.sh`, legacy `paperclip_client.py`).
|
||||||
|
- **Repo pre-scanned clean** — Task 2 Step 2's repo assertion passes today; if it ever fails, that's the guard doing its job.
|
||||||
|
- **No production-data risk** — pure-code; smoke `--verify` is read-only.
|
||||||
|
- **Type consistency:** `build_drift_report(...)->{plan,mismatches,missing}`, `_verify_exit_code(plan,mismatches,missing)->int`, `find_violations()->[(file,reason)]`, `_scan_text(text)->[reason]` — names match across tasks + tests.
|
||||||
|
- **GAP-23 out of scope** (#69 / FU-8b).
|
||||||
@@ -0,0 +1,504 @@
|
|||||||
|
# X11 Citation Corroboration — Phase 1 (Signal) 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:** Build the citation-corroboration **signal** — classify each incoming citation's *treatment*, link it to the *specific halacha* it supports, and expose per-halacha corroboration — **without yet changing the approval gate** (that is Phase 2).
|
||||||
|
|
||||||
|
**Architecture:** A new schema version (V24) adds a `treatment` column to `precedent_internal_citations` and a `halacha_citation_corroboration` link table. A new service `corroboration.py` does three deterministic-or-LLM steps: (1) classify treatment of a citing passage via Opus 4.8 (INV-COR2), (2) match a citing passage to one halacha via pgvector cosine over `halachot.embedding` (INV-COR3), (3) aggregate distinct positive citations per halacha (INV-COR4). A read-only MCP tool reports the result with full provenance (INV-COR6). Auto-approval (INV-COR4/G10) and enrichment are **Phase 2** (separate plan).
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.12, asyncpg + pgvector, FastMCP (`@mcp.tool()`), `claude_session.query_json(model=, effort=)`, `embeddings.embed_texts`, pytest (offline deterministic tests mirroring `tests/test_fu2b_reconcile.py`).
|
||||||
|
|
||||||
|
**Spec:** [docs/spec/X11-citation-corroboration.md](../../spec/X11-citation-corroboration.md) (INV-COR1–COR6). Prerequisite (clean identity graph) is **done** (Shafer merge + corpus scan).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- Create: `mcp-server/src/legal_mcp/services/corroboration.py` — the corroboration service (classify · match · aggregate · persist). One responsibility: turn the citation graph into per-halacha corroboration rows.
|
||||||
|
- Modify: `mcp-server/src/legal_mcp/services/db.py` — add `SCHEMA_V24_SQL` + helpers `store_corroboration`, `list_corroboration_for_halacha`.
|
||||||
|
- Modify: `mcp-server/src/legal_mcp/server.py` — register read-only MCP tool `halacha_corroboration`.
|
||||||
|
- Create: `mcp-server/tests/test_corroboration.py` — offline deterministic tests (treatment parse, aggregation, independence rule).
|
||||||
|
|
||||||
|
**Treatment vocabulary (INV-COR2):** positive = `{followed, explained}`; negative = `{distinguished, criticized, questioned, overruled}`; neutral = `{mentioned}`. Defined once in `corroboration.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Schema V24 — treatment column + corroboration link table
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mcp-server/src/legal_mcp/services/db.py` (add `SCHEMA_V24_SQL` constant near the other `SCHEMA_V23_SQL`; register it in `_run_schema_migrations`, db.py:54-ish)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the schema constant**
|
||||||
|
|
||||||
|
Add after the `SCHEMA_V23_SQL = """..."""` block:
|
||||||
|
|
||||||
|
```python
|
||||||
|
SCHEMA_V24_SQL = """
|
||||||
|
-- X11: citation corroboration (treatment + halacha-level link)
|
||||||
|
ALTER TABLE precedent_internal_citations
|
||||||
|
ADD COLUMN IF NOT EXISTS treatment TEXT DEFAULT '';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS halacha_citation_corroboration (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
halacha_id UUID NOT NULL REFERENCES halachot(id) ON DELETE CASCADE,
|
||||||
|
citing_case_law_id UUID REFERENCES case_law(id) ON DELETE CASCADE,
|
||||||
|
citing_decision_id UUID REFERENCES decisions(id) ON DELETE SET NULL,
|
||||||
|
source_citation_id UUID NOT NULL, -- the precedent_internal_citations / case_law_citations row
|
||||||
|
treatment TEXT NOT NULL, -- followed/explained/distinguished/criticized/questioned/overruled/mentioned
|
||||||
|
match_score NUMERIC(4,3) DEFAULT 0, -- cosine(context, rule_statement)
|
||||||
|
match_context TEXT DEFAULT '',
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
UNIQUE (halacha_id, source_citation_id)
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_hcc_halacha ON halacha_citation_corroboration(halacha_id);
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Register it in `_run_schema_migrations`**
|
||||||
|
|
||||||
|
In `_run_schema_migrations` (db.py), after `await conn.execute(SCHEMA_V23_SQL)` add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await conn.execute(SCHEMA_V24_SQL)
|
||||||
|
```
|
||||||
|
|
||||||
|
And update the log line to `"Database schema initialized (v1-v24)"`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Apply + verify against the dev DB**
|
||||||
|
|
||||||
|
Run (from `mcp-server/`):
|
||||||
|
```bash
|
||||||
|
DOTENV_PATH=/home/chaim/.env DATA_DIR=/home/chaim/legal-ai/data .venv/bin/python -c "
|
||||||
|
import asyncio; from legal_mcp.services import db
|
||||||
|
async def m():
|
||||||
|
pool=await db.get_pool()
|
||||||
|
cols=await pool.fetch(\"SELECT column_name FROM information_schema.columns WHERE table_name='precedent_internal_citations' AND column_name='treatment'\")
|
||||||
|
t=await pool.fetchval(\"SELECT to_regclass('halacha_citation_corroboration')\")
|
||||||
|
print('treatment col:', bool(cols), '| table:', t)
|
||||||
|
asyncio.run(m())"
|
||||||
|
```
|
||||||
|
Expected: `treatment col: True | table: halacha_citation_corroboration`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
```bash
|
||||||
|
git add mcp-server/src/legal_mcp/services/db.py
|
||||||
|
git commit -m "feat(db): V24 — citation treatment column + halacha corroboration link table (X11)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Treatment classifier (deterministic parse, unit-tested)
|
||||||
|
|
||||||
|
The LLM call classifies a citing passage; the **parse/validate** of its output is the deterministic, unit-tested unit (mirrors `halacha_extractor._coerce_halacha`).
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `mcp-server/src/legal_mcp/services/corroboration.py`
|
||||||
|
- Test: `mcp-server/tests/test_corroboration.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# tests/test_corroboration.py
|
||||||
|
from __future__ import annotations
|
||||||
|
import pytest
|
||||||
|
from legal_mcp.services import corroboration as cor
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("raw,expected", [
|
||||||
|
({"treatment": "followed"}, "followed"),
|
||||||
|
({"treatment": "OVERRULED"}, "overruled"), # case-insensitive
|
||||||
|
({"treatment": "bananas"}, "mentioned"), # unknown -> neutral default
|
||||||
|
({}, "mentioned"), # missing -> neutral default
|
||||||
|
])
|
||||||
|
def test_coerce_treatment(raw, expected):
|
||||||
|
assert cor._coerce_treatment(raw) == expected
|
||||||
|
|
||||||
|
def test_treatment_polarity():
|
||||||
|
assert cor.is_positive("followed") and cor.is_positive("explained")
|
||||||
|
assert cor.is_negative("distinguished") and cor.is_negative("overruled")
|
||||||
|
assert not cor.is_positive("mentioned") and not cor.is_negative("mentioned")
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `cd mcp-server && .venv/bin/python -m pytest tests/test_corroboration.py -q`
|
||||||
|
Expected: FAIL — `ModuleNotFoundError: corroboration` / `_coerce_treatment` undefined.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# src/legal_mcp/services/corroboration.py
|
||||||
|
"""X11 citation corroboration — classify treatment, match to halacha, aggregate.
|
||||||
|
|
||||||
|
Phase 1: builds the SIGNAL only (no approval changes). See docs/spec/X11.
|
||||||
|
All LLM calls go through the local claude_session bridge (Opus 4.8 @ xhigh),
|
||||||
|
same architectural rule as the other extractors (local MCP only).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
from legal_mcp import config
|
||||||
|
from legal_mcp.config import parse_llm_json
|
||||||
|
from legal_mcp.services import claude_session
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TREATMENT_POSITIVE = {"followed", "explained"}
|
||||||
|
TREATMENT_NEGATIVE = {"distinguished", "criticized", "questioned", "overruled"}
|
||||||
|
TREATMENT_NEUTRAL = {"mentioned"}
|
||||||
|
_VALID_TREATMENT = TREATMENT_POSITIVE | TREATMENT_NEGATIVE | TREATMENT_NEUTRAL
|
||||||
|
|
||||||
|
def is_positive(t: str) -> bool: return t in TREATMENT_POSITIVE
|
||||||
|
def is_negative(t: str) -> bool: return t in TREATMENT_NEGATIVE
|
||||||
|
|
||||||
|
def _coerce_treatment(raw: dict) -> str:
|
||||||
|
t = str((raw or {}).get("treatment", "")).strip().lower()
|
||||||
|
return t if t in _VALID_TREATMENT else "mentioned"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `cd mcp-server && .venv/bin/python -m pytest tests/test_corroboration.py -q`
|
||||||
|
Expected: PASS (3 params + polarity).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add the LLM classifier call (integration, not unit-tested)**
|
||||||
|
|
||||||
|
Append to `corroboration.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_TREATMENT_PROMPT = """אתה משפטן בכיר. נתון ציטוט של פסק/החלטה קודמים בתוך החלטה מאוחרת.
|
||||||
|
סווג כיצד ההחלטה המאוחרת **מטפלת** בתקדים המצוטט, לפי אחת מהקטגוריות:
|
||||||
|
- followed — אימצה והחילה את ההלכה.
|
||||||
|
- explained — הסבירה/הזכירה בלי לחלוק.
|
||||||
|
- distinguished — אבחנה (קבעה שלא חל בנסיבות).
|
||||||
|
- criticized — מתחה ביקורת בלי לבטל.
|
||||||
|
- questioned — הטילה ספק.
|
||||||
|
- overruled — דחתה/ביטלה את ההלכה.
|
||||||
|
- mentioned — אזכור-אגב בלי טיפול.
|
||||||
|
החזר JSON בלבד: {"treatment": "<קטגוריה>"}.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def classify_treatment(cited_citation: str, context: str) -> str:
|
||||||
|
"""Return one treatment label for how `context` treats `cited_citation`."""
|
||||||
|
user = f"תקדים מצוטט: {cited_citation}\n\n--- ההקשר המצטט ---\n{context}\n--- סוף ---"
|
||||||
|
try:
|
||||||
|
result = await claude_session.query_json(
|
||||||
|
user, system=_TREATMENT_PROMPT,
|
||||||
|
model=config.HALACHA_EXTRACT_MODEL or None,
|
||||||
|
effort=config.HALACHA_EXTRACT_EFFORT or None,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("classify_treatment failed: %s", e)
|
||||||
|
return "mentioned"
|
||||||
|
return _coerce_treatment(result if isinstance(result, dict) else {})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
```bash
|
||||||
|
git add mcp-server/src/legal_mcp/services/corroboration.py mcp-server/tests/test_corroboration.py
|
||||||
|
git commit -m "feat(corroboration): treatment classifier + polarity (INV-COR2, X11)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Halacha matcher — pgvector cosine, threshold (INV-COR3)
|
||||||
|
|
||||||
|
The matcher links a citing passage to the single best halacha of the cited precedent. The **threshold decision** is the deterministic unit; the pgvector lookup is integration.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mcp-server/src/legal_mcp/services/corroboration.py`
|
||||||
|
- Modify: `mcp-server/src/legal_mcp/services/db.py` (add `nearest_halacha_for_vector`)
|
||||||
|
- Test: `mcp-server/tests/test_corroboration.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test (threshold gate is the unit)**
|
||||||
|
|
||||||
|
Append to `tests/test_corroboration.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_match_accepts_above_threshold():
|
||||||
|
# (halacha_id, similarity) above floor -> accepted
|
||||||
|
assert cor.accept_match(("h1", 0.62), floor=0.50) == "h1"
|
||||||
|
|
||||||
|
def test_match_rejects_below_threshold():
|
||||||
|
# below floor -> None (INV-COR3: don't attach to a different legal point)
|
||||||
|
assert cor.accept_match(("h1", 0.41), floor=0.50) is None
|
||||||
|
|
||||||
|
def test_match_rejects_empty():
|
||||||
|
assert cor.accept_match(None, floor=0.50) is None
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `cd mcp-server && .venv/bin/python -m pytest tests/test_corroboration.py::test_match_accepts_above_threshold -q`
|
||||||
|
Expected: FAIL — `accept_match` undefined.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the threshold gate + env floor**
|
||||||
|
|
||||||
|
Add to `config.py` (near `HALACHA_EXTRACT_*`):
|
||||||
|
```python
|
||||||
|
HALACHA_CORROBORATION_MATCH_FLOOR = float(os.environ.get("HALACHA_CORROBORATION_MATCH_FLOOR", "0.50"))
|
||||||
|
HALACHA_CORROBORATION_MIN_CITES = int(os.environ.get("HALACHA_CORROBORATION_MIN_CITES", "2"))
|
||||||
|
```
|
||||||
|
Add to `corroboration.py`:
|
||||||
|
```python
|
||||||
|
def accept_match(best: tuple[str, float] | None, floor: float = config.HALACHA_CORROBORATION_MATCH_FLOOR) -> str | None:
|
||||||
|
"""Return the halacha_id iff similarity clears the floor (INV-COR3)."""
|
||||||
|
if not best:
|
||||||
|
return None
|
||||||
|
halacha_id, sim = best
|
||||||
|
return halacha_id if sim >= floor else None
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `cd mcp-server && .venv/bin/python -m pytest tests/test_corroboration.py -q`
|
||||||
|
Expected: PASS (all, incl. Task 2).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add the pgvector lookup (integration)**
|
||||||
|
|
||||||
|
Add to `db.py`:
|
||||||
|
```python
|
||||||
|
async def nearest_halacha_for_vector(case_law_id: UUID, vec: list[float]) -> tuple[str, float] | None:
|
||||||
|
"""Best-matching halacha of `case_law_id` for a context embedding (cosine)."""
|
||||||
|
pool = await get_pool()
|
||||||
|
row = await pool.fetchrow(
|
||||||
|
"SELECT id::text AS id, 1 - (embedding <=> $2) AS sim "
|
||||||
|
"FROM halachot WHERE case_law_id = $1 AND embedding IS NOT NULL "
|
||||||
|
"ORDER BY embedding <=> $2 LIMIT 1",
|
||||||
|
case_law_id, vec,
|
||||||
|
)
|
||||||
|
return (row["id"], float(row["sim"])) if row else None
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
```bash
|
||||||
|
git add mcp-server/src/legal_mcp/services/corroboration.py mcp-server/src/legal_mcp/services/db.py mcp-server/src/legal_mcp/config.py mcp-server/tests/test_corroboration.py
|
||||||
|
git commit -m "feat(corroboration): halacha matcher + cosine threshold (INV-COR3, X11)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Aggregator — distinct positive citations, negative-flag (INV-COR4)
|
||||||
|
|
||||||
|
Pure function: given per-citation results for one halacha, count **distinct** positive citing sources and detect any negative treatment.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mcp-server/src/legal_mcp/services/corroboration.py`
|
||||||
|
- Test: `mcp-server/tests/test_corroboration.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Append to `tests/test_corroboration.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _link(src, treatment):
|
||||||
|
return {"source_id": src, "treatment": treatment}
|
||||||
|
|
||||||
|
def test_aggregate_counts_distinct_positive():
|
||||||
|
links = [_link("d1","followed"), _link("d1","explained"), _link("d2","followed")]
|
||||||
|
agg = cor.aggregate(links, min_cites=2)
|
||||||
|
assert agg["positive_sources"] == 2 # d1 counted once (INV-COR4 independence)
|
||||||
|
assert agg["has_negative"] is False
|
||||||
|
assert agg["corroborated"] is True
|
||||||
|
|
||||||
|
def test_aggregate_negative_blocks():
|
||||||
|
links = [_link("d1","followed"), _link("d2","followed"), _link("d3","distinguished")]
|
||||||
|
agg = cor.aggregate(links, min_cites=2)
|
||||||
|
assert agg["has_negative"] is True
|
||||||
|
assert agg["corroborated"] is False # any negative -> not corroborated (INV-COR2)
|
||||||
|
|
||||||
|
def test_aggregate_below_threshold():
|
||||||
|
agg = cor.aggregate([_link("d1","followed")], min_cites=2)
|
||||||
|
assert agg["corroborated"] is False # single source insufficient (INV-COR4)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `cd mcp-server && .venv/bin/python -m pytest tests/test_corroboration.py::test_aggregate_counts_distinct_positive -q`
|
||||||
|
Expected: FAIL — `aggregate` undefined.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement**
|
||||||
|
|
||||||
|
Add to `corroboration.py`:
|
||||||
|
```python
|
||||||
|
def aggregate(links: list[dict], min_cites: int = config.HALACHA_CORROBORATION_MIN_CITES) -> dict:
|
||||||
|
"""Aggregate per-halacha corroboration (INV-COR4/COR2).
|
||||||
|
|
||||||
|
links: [{"source_id": str, "treatment": str}, ...] already matched to ONE halacha.
|
||||||
|
positive_sources = count of DISTINCT source_id whose treatment is positive.
|
||||||
|
has_negative = any negative treatment present.
|
||||||
|
corroborated = positive_sources >= min_cites AND not has_negative.
|
||||||
|
"""
|
||||||
|
positive = {l["source_id"] for l in links if is_positive(l["treatment"])}
|
||||||
|
has_negative = any(is_negative(l["treatment"]) for l in links)
|
||||||
|
return {
|
||||||
|
"positive_sources": len(positive),
|
||||||
|
"has_negative": has_negative,
|
||||||
|
"corroborated": len(positive) >= min_cites and not has_negative,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `cd mcp-server && .venv/bin/python -m pytest tests/test_corroboration.py -q`
|
||||||
|
Expected: PASS (all).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
```bash
|
||||||
|
git add mcp-server/src/legal_mcp/services/corroboration.py mcp-server/tests/test_corroboration.py
|
||||||
|
git commit -m "feat(corroboration): aggregator — distinct positive + negative-flag (INV-COR4, X11)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Orchestration + persistence (build the signal for one precedent)
|
||||||
|
|
||||||
|
Wires Tasks 2–4 over a precedent's incoming citations and stores `halacha_citation_corroboration` rows (INV-COR6 provenance). Integration (no new offline unit).
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mcp-server/src/legal_mcp/services/corroboration.py`
|
||||||
|
- Modify: `mcp-server/src/legal_mcp/services/db.py` (add `store_corroboration`, `incoming_citations_for_precedent`)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add DB helpers**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# db.py
|
||||||
|
async def incoming_citations_for_precedent(case_law_id: UUID) -> list[dict]:
|
||||||
|
"""All incoming citations (both graphs) with their context + source id."""
|
||||||
|
pool = await get_pool()
|
||||||
|
rows = await pool.fetch(
|
||||||
|
"SELECT id::text AS source_id, source_case_law_id::text AS citing_case_law_id, "
|
||||||
|
" NULL::text AS citing_decision_id, match_context AS context "
|
||||||
|
"FROM precedent_internal_citations WHERE cited_case_law_id = $1 "
|
||||||
|
"UNION ALL "
|
||||||
|
"SELECT id::text, NULL, decision_id::text, context_text "
|
||||||
|
"FROM case_law_citations WHERE case_law_id = $1",
|
||||||
|
case_law_id,
|
||||||
|
)
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
async def store_corroboration(halacha_id: str, source_id: str, citing_case_law_id, citing_decision_id, treatment: str, score: float, context: str) -> None:
|
||||||
|
pool = await get_pool()
|
||||||
|
await pool.execute(
|
||||||
|
"INSERT INTO halacha_citation_corroboration "
|
||||||
|
"(halacha_id, citing_case_law_id, citing_decision_id, source_citation_id, treatment, match_score, match_context) "
|
||||||
|
"VALUES ($1,$2,$3,$4,$5,$6,$7) "
|
||||||
|
"ON CONFLICT (halacha_id, source_citation_id) DO UPDATE SET "
|
||||||
|
"treatment=EXCLUDED.treatment, match_score=EXCLUDED.match_score",
|
||||||
|
halacha_id, citing_case_law_id, citing_decision_id, source_id, treatment, score, context,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the orchestrator**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# corroboration.py
|
||||||
|
from uuid import UUID
|
||||||
|
from legal_mcp.services import db, embeddings
|
||||||
|
|
||||||
|
async def build_for_precedent(case_law_id: str | UUID) -> dict:
|
||||||
|
"""For one cited precedent: classify+match+store each incoming citation. Idempotent."""
|
||||||
|
if isinstance(case_law_id, str):
|
||||||
|
case_law_id = UUID(case_law_id)
|
||||||
|
cits = await db.incoming_citations_for_precedent(case_law_id)
|
||||||
|
linked = 0
|
||||||
|
for c in cits:
|
||||||
|
ctx = (c.get("context") or "").strip()
|
||||||
|
if not ctx:
|
||||||
|
continue
|
||||||
|
vecs = await embeddings.embed_texts([ctx], input_type="query")
|
||||||
|
best = await db.nearest_halacha_for_vector(case_law_id, vecs[0])
|
||||||
|
halacha_id = accept_match(best)
|
||||||
|
if not halacha_id:
|
||||||
|
continue
|
||||||
|
treatment = await classify_treatment(c.get("citing_case_law_id") or c.get("citing_decision_id") or "", ctx)
|
||||||
|
await db.store_corroboration(
|
||||||
|
halacha_id, c["source_id"],
|
||||||
|
c.get("citing_case_law_id"), c.get("citing_decision_id"),
|
||||||
|
treatment, best[1], ctx,
|
||||||
|
)
|
||||||
|
linked += 1
|
||||||
|
return {"citations": len(cits), "linked": linked}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Integration smoke-test on Shafer (the fixed precedent, 7 citations)**
|
||||||
|
|
||||||
|
Run (from `mcp-server/`):
|
||||||
|
```bash
|
||||||
|
DOTENV_PATH=/home/chaim/.env DATA_DIR=/home/chaim/legal-ai/data .venv/bin/python -c "
|
||||||
|
import asyncio; from legal_mcp.services import corroboration as cor
|
||||||
|
print(asyncio.run(cor.build_for_precedent('9024da7b-f408-4b6f-808f-c514a83728e4')))"
|
||||||
|
```
|
||||||
|
Expected: `{'citations': 7, 'linked': <0..7>}` and no exception. Inspect `halacha_citation_corroboration` rows for treatment/score sanity.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
```bash
|
||||||
|
git add mcp-server/src/legal_mcp/services/corroboration.py mcp-server/src/legal_mcp/services/db.py
|
||||||
|
git commit -m "feat(corroboration): orchestrator + persistence over both citation graphs (X11)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Read-only MCP tool `halacha_corroboration`
|
||||||
|
|
||||||
|
Exposes per-halacha corroboration + provenance to the chair/agents (INV-COR6). **No approval change** — reporting only.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mcp-server/src/legal_mcp/services/db.py` (add `list_corroboration_for_halacha`)
|
||||||
|
- Modify: `mcp-server/src/legal_mcp/server.py` (register tool)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the DB read**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# db.py
|
||||||
|
async def list_corroboration_for_halacha(halacha_id: UUID) -> list[dict]:
|
||||||
|
pool = await get_pool()
|
||||||
|
rows = await pool.fetch(
|
||||||
|
"SELECT treatment, match_score, match_context, citing_case_law_id::text, "
|
||||||
|
" citing_decision_id::text, created_at "
|
||||||
|
"FROM halacha_citation_corroboration WHERE halacha_id = $1 "
|
||||||
|
"ORDER BY match_score DESC", halacha_id,
|
||||||
|
)
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Register the MCP tool** (copy an existing `@mcp.tool()` idiom in `server.py`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@mcp.tool()
|
||||||
|
async def halacha_corroboration(halacha_id: str) -> dict:
|
||||||
|
"""החזר את ה-corroboration של הלכה: הציטוטים שמתקפים אותה, הטיפול, וסיכום (X11, read-only)."""
|
||||||
|
from uuid import UUID
|
||||||
|
from legal_mcp.services import corroboration as cor, db
|
||||||
|
links = await db.list_corroboration_for_halacha(UUID(halacha_id))
|
||||||
|
agg = cor.aggregate(
|
||||||
|
[{"source_id": (l["citing_case_law_id"] or l["citing_decision_id"] or ""), "treatment": l["treatment"]} for l in links]
|
||||||
|
)
|
||||||
|
return {"halacha_id": halacha_id, "summary": agg, "citations": links}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify the tool loads** (restart MCP server is required to pick up new tools)
|
||||||
|
|
||||||
|
Run: `cd mcp-server && .venv/bin/python -c "from legal_mcp import server; print('halacha_corroboration' in [t.name for t in server.mcp._tool_manager.list_tools()])"`
|
||||||
|
Expected: `True` (adjust the introspection call to the FastMCP version if needed; the point is the import succeeds and the tool registers).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
```bash
|
||||||
|
git add mcp-server/src/legal_mcp/services/db.py mcp-server/src/legal_mcp/server.py
|
||||||
|
git commit -m "feat(mcp): halacha_corroboration read-only tool (INV-COR6, X11)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Out of scope (Phase 2 — separate plan)
|
||||||
|
|
||||||
|
- **Auto-approval wiring (INV-COR4/COR5 + G10 behavior):** make `corroborated == True` set `halachot.review_status='approved'` with `reviewer='corroborated (≥N judicial citations)'`, leaving the uncorroborated/negative tail in the chair gate. **Sensitive** — gated on Dafna validating the signal from Phase 1 first.
|
||||||
|
- **Enrichment (INV-COR3 secondary):** sharpen `rule_statement` from citing framing.
|
||||||
|
- **Backfill:** run `build_for_precedent` across all precedents with halachot + incoming citations.
|
||||||
|
- **Treatment backfill of `case_law_citations.citation_type`** (currently default `'support'`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
**Spec coverage:** INV-COR2 (Task 2 classifier + polarity), INV-COR3 (Task 3 match floor + pgvector), INV-COR4 (Task 4 distinct positive + min_cites), INV-COR6 (Task 5 store provenance + Task 6 report). INV-COR1 (principle, no code) and INV-COR5 (chair-gate preserved) are honored by **not** changing approval in Phase 1 — explicitly deferred to Phase 2. ✔
|
||||||
|
**Placeholders:** none — every code step has concrete content; the LLM-dependent steps carry the exact prompt + I/O contract; tuning values (`MATCH_FLOOR`, `MIN_CITES`) are real env-defaults, not TBDs.
|
||||||
|
**Type consistency:** `accept_match` returns `halacha_id|None`; `aggregate` consumes `[{"source_id","treatment"}]`; `_coerce_treatment`/`is_positive`/`is_negative` share the `_VALID_TREATMENT` sets. `build_for_precedent` uses `accept_match` + `classify_treatment` + `store_corroboration` with matching signatures. ✔
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
# X11 Citation Corroboration — Phase 2 (Wire the approval gate) 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:** Turn the Phase 1 **signal** into an **approval action**. A halacha that is *corroborated* by ≥N distinct positive judicial citations (0 negatives) is auto-approved with citation provenance; a halacha that a later citing court *overruled* is demoted back to the chair gate. Then **backfill** the signal+approval across the whole corpus.
|
||||||
|
|
||||||
|
**Gate cleared:** Phase 1's "Out of scope" deferred auto-approval as *"Sensitive — gated on Dafna validating the signal from Phase 1 first."* Dafna validated the signal and approved enabling it (2026-06-01). This plan builds the **active** wiring (default ON, env-tunable kill-switch).
|
||||||
|
|
||||||
|
**Architecture:** No schema change — Phase 1's `halacha_citation_corroboration` table already holds the provenance. We add:
|
||||||
|
1. a pure decision function `approval_action(agg, has_overruled)` (unit-tested, INV-COR2/COR4),
|
||||||
|
2. DB transitions that move *only* the legal states (`pending_review → approved` on corroboration; `approved → pending_review` on overruled) — never touching `published`/`rejected`,
|
||||||
|
3. `reconcile_approvals(case_law_id)` called at the tail of `build_for_precedent`,
|
||||||
|
4. a corpus `build_all()` backfill driver + a **write** MCP tool `corroboration_rebuild`.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.12, asyncpg, FastMCP, `claude_session` (local Opus 4.8), pytest (offline deterministic).
|
||||||
|
|
||||||
|
**Spec:** [docs/spec/X11-citation-corroboration.md](../../spec/X11-citation-corroboration.md) §4 step 5, §5 (INV-COR2/COR4/COR5/COR6), §6 (INV-G10 amendment).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Invariant mapping (what each rule forces here)
|
||||||
|
|
||||||
|
- **INV-COR4** — auto-approve requires `positive_sources ≥ N` distinct sources ∧ `has_negative == False`. `aggregate()` (Phase 1) already computes this; Phase 2 only *acts* on `corroborated == True`.
|
||||||
|
- **INV-COR2** — negative treatment never approves; **overruled** demotes. We split "negative" (blocks approval — already handled by `aggregate`) from **overruled** (actively *demotes an already-approved* halacha back to the chair).
|
||||||
|
- **INV-COR5** — the chair gate is preserved for the uncorroborated tail and all negatives. We only transition the two legal states; uncorroborated halachot are never touched.
|
||||||
|
- **INV-COR6** — provenance is retained: `reviewer` records the corroboration basis; the `halacha_citation_corroboration` rows remain the auditable evidence.
|
||||||
|
- **INV-G10 (amended §6)** — the human gate's *authority source* is cumulative judicial treatment for the corroborated subset; chair gate stays mandatory for the tail. Auto-approval is therefore not "AI judgment" but recorded human (citing-court) judgment (INV-COR1).
|
||||||
|
|
||||||
|
**Demotion scope decision (precise reading of §4 step 5):** *any* negative blocks auto-approval (via `aggregate.has_negative`), but only **overruled** actively demotes a halacha that is already approved. `distinguished`/`criticized`/`questioned` block new auto-approval but do not un-approve an existing chair/confidence approval — that stronger action is reserved for `overruled`, and surfaced to the chair via the read tool.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Config kill-switch
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/config.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** After `HALACHA_CORROBORATION_MIN_CITES` (config.py:69) add:
|
||||||
|
```python
|
||||||
|
# X11 Phase 2: gate corroboration → approval. Default ON (Dafna validated the
|
||||||
|
# Phase 1 signal, 2026-06-01). Set to "false" to disable the auto-approve/demote
|
||||||
|
# wiring while keeping the signal (Phase 1) intact.
|
||||||
|
HALACHA_CORROBORATION_AUTO_APPROVE = os.environ.get(
|
||||||
|
"HALACHA_CORROBORATION_AUTO_APPROVE", "true"
|
||||||
|
).strip().lower() in ("1", "true", "yes", "on")
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit** `feat(config): HALACHA_CORROBORATION_AUTO_APPROVE kill-switch (X11 Phase 2)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Pure decision function `approval_action` (TDD)
|
||||||
|
|
||||||
|
The whole approval policy distilled to one deterministic, offline-testable function.
|
||||||
|
|
||||||
|
**Files:** Modify `corroboration.py`; Test `tests/test_corroboration.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Failing test** — append to `tests/test_corroboration.py`:
|
||||||
|
```python
|
||||||
|
def test_approval_action_corroborated_approves():
|
||||||
|
agg = {"positive_sources": 2, "has_negative": False, "corroborated": True}
|
||||||
|
assert cor.approval_action(agg, has_overruled=False) == "approve"
|
||||||
|
|
||||||
|
def test_approval_action_overruled_demotes_even_if_corroborated():
|
||||||
|
# overruled wins over a positive count (INV-COR2 strong form)
|
||||||
|
agg = {"positive_sources": 3, "has_negative": True, "corroborated": False}
|
||||||
|
assert cor.approval_action(agg, has_overruled=True) == "demote"
|
||||||
|
|
||||||
|
def test_approval_action_single_source_noop():
|
||||||
|
agg = {"positive_sources": 1, "has_negative": False, "corroborated": False}
|
||||||
|
assert cor.approval_action(agg, has_overruled=False) is None
|
||||||
|
|
||||||
|
def test_approval_action_negative_nonoverruled_noop():
|
||||||
|
# distinguished blocks approval but does not demote (no overruled)
|
||||||
|
agg = {"positive_sources": 2, "has_negative": True, "corroborated": False}
|
||||||
|
assert cor.approval_action(agg, has_overruled=False) is None
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2:** Run to verify FAIL (`approval_action` undefined).
|
||||||
|
|
||||||
|
- [ ] **Step 3:** Implement in `corroboration.py`:
|
||||||
|
```python
|
||||||
|
def approval_action(agg: dict, has_overruled: bool) -> str | None:
|
||||||
|
"""Decide the corroboration→approval action for ONE halacha (INV-COR2/COR4).
|
||||||
|
|
||||||
|
- 'demote' : a later court overruled it → back to the chair gate (overruled
|
||||||
|
outranks any positive count).
|
||||||
|
- 'approve' : corroborated (≥N distinct positives, 0 negatives).
|
||||||
|
- None : leave as-is (single source, non-overruled negative, or tail).
|
||||||
|
"""
|
||||||
|
if has_overruled:
|
||||||
|
return "demote"
|
||||||
|
if agg.get("corroborated"):
|
||||||
|
return "approve"
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4:** Run to verify PASS (all). **Commit** `feat(corroboration): approval_action decision fn (INV-COR2/COR4, X11 Phase 2)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: DB transitions (legal states only)
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/services/db.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Add near `update_halacha` (db.py:3480-ish):
|
||||||
|
```python
|
||||||
|
async def approve_halacha_by_corroboration(
|
||||||
|
halacha_id: UUID, n_sources: int, min_cites: int,
|
||||||
|
) -> bool:
|
||||||
|
"""Approve a halacha on citation corroboration — ONLY if it is currently
|
||||||
|
awaiting the chair (pending_review). Never touches 'published'/'rejected'/
|
||||||
|
already-'approved' (INV-COR5: chair gate preserved for everything else).
|
||||||
|
Returns True iff a row transitioned."""
|
||||||
|
pool = await get_pool()
|
||||||
|
reviewer = f"corroborated ({n_sources} judicial citations ≥ {min_cites})"
|
||||||
|
row = await pool.fetchrow(
|
||||||
|
"UPDATE halachot SET review_status='approved', reviewer=$2, "
|
||||||
|
"reviewed_at=now(), updated_at=now() "
|
||||||
|
"WHERE id=$1 AND review_status='pending_review' RETURNING id",
|
||||||
|
halacha_id, reviewer,
|
||||||
|
)
|
||||||
|
return row is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def demote_halacha_overruled(halacha_id: UUID) -> bool:
|
||||||
|
"""Demote an APPROVED halacha back to the chair gate because a later citing
|
||||||
|
court overruled it (INV-COR2). Only acts on 'approved' → 'pending_review';
|
||||||
|
leaves 'published'/'rejected'/'pending_review' untouched. The reviewer note
|
||||||
|
records why it is back in the queue. Returns True iff a row transitioned."""
|
||||||
|
pool = await get_pool()
|
||||||
|
row = await pool.fetchrow(
|
||||||
|
"UPDATE halachot SET review_status='pending_review', "
|
||||||
|
"reviewer='flagged: overruled by later citation (X11)', "
|
||||||
|
"reviewed_at=NULL, updated_at=now() "
|
||||||
|
"WHERE id=$1 AND review_status='approved' RETURNING id",
|
||||||
|
halacha_id,
|
||||||
|
)
|
||||||
|
return row is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def list_corroboration_grouped(case_law_id: UUID) -> dict[str, list[dict]]:
|
||||||
|
"""Per-halacha corroboration links for a cited precedent, in the
|
||||||
|
{source_id, treatment} shape `aggregate()` consumes. Distinct citing source
|
||||||
|
keyed by case_law/decision id (falls back to the citation row id)."""
|
||||||
|
pool = await get_pool()
|
||||||
|
rows = await pool.fetch(
|
||||||
|
"SELECT hcc.halacha_id::text AS halacha_id, "
|
||||||
|
" COALESCE(hcc.citing_case_law_id::text, hcc.citing_decision_id::text, "
|
||||||
|
" hcc.source_citation_id::text) AS source_id, "
|
||||||
|
" hcc.treatment "
|
||||||
|
"FROM halacha_citation_corroboration hcc "
|
||||||
|
"JOIN halachot h ON h.id = hcc.halacha_id "
|
||||||
|
"WHERE h.case_law_id = $1",
|
||||||
|
case_law_id,
|
||||||
|
)
|
||||||
|
out: dict[str, list[dict]] = {}
|
||||||
|
for r in rows:
|
||||||
|
out.setdefault(r["halacha_id"], []).append(
|
||||||
|
{"source_id": r["source_id"], "treatment": r["treatment"]}
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
async def precedents_with_halachot_and_incoming_citations() -> list[str]:
|
||||||
|
"""case_law ids that have at least one halacha AND at least one incoming
|
||||||
|
citation (either graph) — the backfill target set."""
|
||||||
|
pool = await get_pool()
|
||||||
|
rows = await pool.fetch(
|
||||||
|
"SELECT c.id::text FROM case_law c "
|
||||||
|
"WHERE EXISTS (SELECT 1 FROM halachot h WHERE h.case_law_id=c.id) "
|
||||||
|
" AND (EXISTS (SELECT 1 FROM precedent_internal_citations p "
|
||||||
|
" WHERE p.cited_case_law_id=c.id) "
|
||||||
|
" OR EXISTS (SELECT 1 FROM case_law_citations cc "
|
||||||
|
" WHERE cc.case_law_id=c.id))",
|
||||||
|
)
|
||||||
|
return [r["id"] for r in rows]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit** `feat(db): corroboration approve/demote transitions + backfill query (X11 Phase 2)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: `reconcile_approvals` + wire into `build_for_precedent` + `build_all`
|
||||||
|
|
||||||
|
**Files:** Modify `corroboration.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Add to `corroboration.py`:
|
||||||
|
```python
|
||||||
|
async def reconcile_approvals(case_law_id: str | UUID) -> dict:
|
||||||
|
"""Apply the corroboration→approval policy for every halacha of a precedent.
|
||||||
|
No-op (returns disabled) when the kill-switch is off. INV-COR2/COR4/COR5."""
|
||||||
|
if not config.HALACHA_CORROBORATION_AUTO_APPROVE:
|
||||||
|
return {"approved": 0, "demoted": 0, "disabled": True}
|
||||||
|
if isinstance(case_law_id, str):
|
||||||
|
case_law_id = UUID(case_law_id)
|
||||||
|
grouped = await db.list_corroboration_grouped(case_law_id)
|
||||||
|
approved = demoted = 0
|
||||||
|
for halacha_id, links in grouped.items():
|
||||||
|
agg = aggregate(links)
|
||||||
|
has_overruled = any(l["treatment"] == "overruled" for l in links)
|
||||||
|
action = approval_action(agg, has_overruled)
|
||||||
|
if action == "approve":
|
||||||
|
if await db.approve_halacha_by_corroboration(
|
||||||
|
UUID(halacha_id), agg["positive_sources"],
|
||||||
|
config.HALACHA_CORROBORATION_MIN_CITES,
|
||||||
|
):
|
||||||
|
approved += 1
|
||||||
|
elif action == "demote":
|
||||||
|
if await db.demote_halacha_overruled(UUID(halacha_id)):
|
||||||
|
demoted += 1
|
||||||
|
return {"approved": approved, "demoted": demoted, "disabled": False}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2:** At the end of `build_for_precedent`, replace the `return` with:
|
||||||
|
```python
|
||||||
|
appr = await reconcile_approvals(case_law_id)
|
||||||
|
return {"citations": len(cits), "linked": linked,
|
||||||
|
"approved": appr["approved"], "demoted": appr["demoted"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3:** Add the corpus driver:
|
||||||
|
```python
|
||||||
|
async def build_all() -> dict:
|
||||||
|
"""Backfill: build the signal + apply approvals for every precedent that has
|
||||||
|
halachot and incoming citations. Idempotent (ON CONFLICT on the link table;
|
||||||
|
transitions only fire on the legal state)."""
|
||||||
|
ids = await db.precedents_with_halachot_and_incoming_citations()
|
||||||
|
totals = {"precedents": 0, "citations": 0, "linked": 0,
|
||||||
|
"approved": 0, "demoted": 0}
|
||||||
|
for cid in ids:
|
||||||
|
r = await build_for_precedent(cid)
|
||||||
|
totals["precedents"] += 1
|
||||||
|
for k in ("citations", "linked", "approved", "demoted"):
|
||||||
|
totals[k] += r.get(k, 0)
|
||||||
|
logger.info("corroboration backfill %s: %s", cid, r)
|
||||||
|
return totals
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit** `feat(corroboration): reconcile_approvals + build_all backfill (X11 Phase 2)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Write MCP tool `corroboration_rebuild`
|
||||||
|
|
||||||
|
**Files:** Modify `mcp-server/src/legal_mcp/server.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Add near `halacha_corroboration` (server.py:926):
|
||||||
|
```python
|
||||||
|
@mcp.tool()
|
||||||
|
async def corroboration_rebuild(case_law_id: str = "") -> dict:
|
||||||
|
"""בנה/רענן את ה-corroboration ויישם אישור-אוטומטי. ריק = כל הקורפוס (backfill);
|
||||||
|
מזהה-תקדים = תקדים בודד. כותב halacha_citation_corroboration + מעדכן review_status
|
||||||
|
(corroborated→approved, overruled→pending_review). X11 Phase 2."""
|
||||||
|
from legal_mcp.services import corroboration as cor
|
||||||
|
if case_law_id.strip():
|
||||||
|
return await cor.build_for_precedent(case_law_id.strip())
|
||||||
|
return await cor.build_all()
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2:** Verify import/registration:
|
||||||
|
```bash
|
||||||
|
cd mcp-server && .venv/bin/python -c "from legal_mcp import server; print('corroboration_rebuild' in [t.name for t in server.mcp._tool_manager.list_tools()])"
|
||||||
|
```
|
||||||
|
Expected `True`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit** `feat(mcp): corroboration_rebuild write tool (X11 Phase 2)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Backfill the corpus + verify
|
||||||
|
|
||||||
|
- [ ] **Step 1:** Snapshot approved/pending counts before.
|
||||||
|
- [ ] **Step 2:** Run `build_all()` from the venv (`DOTENV_PATH=/home/chaim/.env DATA_DIR=…`). Expect ~12 precedents, no exception, a small number of `approved`/`demoted`.
|
||||||
|
- [ ] **Step 3:** Verify: every halacha approved-by-corroboration has `reviewer LIKE 'corroborated %'`; no `published`/`rejected` changed; corroboration rows carry treatment+score. Spot-check one approved halacha via `halacha_corroboration`.
|
||||||
|
- [ ] **Step 4: Commit** any data-audit note under `data/audit/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Out of scope (Phase 2 backlog — deliberately deferred)
|
||||||
|
|
||||||
|
- **Enrichment (INV-COR3 secondary):** sharpen `rule_statement` from citing framing — **proposal-only**, must not silently rewrite an approved rule. Bigger design; separate plan.
|
||||||
|
- **Treatment backfill of `case_law_citations.citation_type`** (default `'support'`) — orthogonal to corroboration (which classifies treatment fresh per citation into its own column).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
**Spec coverage:** INV-COR2 (overruled demote split from generic negative-block; Task 2/3/4), INV-COR4 (acts only on `corroborated`; Task 2/4), INV-COR5 (only legal-state transitions, tail untouched; Task 3 WHERE clauses), INV-COR6 (reviewer provenance + retained link rows; Task 3), INV-G10 amended (authority = citing courts; not AI; Task 2 comment). ✔
|
||||||
|
**Safety:** kill-switch default ON but env-disable-able; transitions are directional and bounded by `review_status` WHERE clauses (cannot touch chair-final states); demotion moves toward *more* human review. ✔
|
||||||
|
**Idempotency:** link table `ON CONFLICT` (Phase 1); approve only fires on `pending_review`, demote only on `approved` → re-runs converge. ✔
|
||||||
150
docs/superpowers/specs/2026-05-30-fu1-unified-ingest-design.md
Normal file
150
docs/superpowers/specs/2026-05-30-fu1-unified-ingest-design.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# FU-1 — איחוד מסלול-הקליטה (Unified Ingest Path) — עיצוב
|
||||||
|
|
||||||
|
**סטטוס:** מאושר-לעיצוב · **תאריך:** 2026-05-30 · **ענף:** TBD (ייפתח בביצוע)
|
||||||
|
**מכסה:** GAP-01, GAP-02, GAP-04, GAP-05 · **מספק:** INV-ING1, INV-ING3, INV-G2, INV-G4
|
||||||
|
**מקורות:** [docs/spec/01-ingest.md](../../spec/01-ingest.md), [docs/spec/gap-audit.md](../../spec/gap-audit.md) · **משימה:** TaskMaster #59 (legal-ai)
|
||||||
|
**סוג-עבודה:** pure-code · **מיגרציה:** אין (אומת מול DB 2026-05-30 — ראה §6)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. הבעיה
|
||||||
|
|
||||||
|
שתי פונקציות-קליטה מקבילות לישויות-אחיות, שמשכפלות את צעדי 2–10 של הפייפליין ומתפצלות
|
||||||
|
בפרטים:
|
||||||
|
|
||||||
|
- `services/precedent_library.py::ingest_precedent` (פסיקה חיצונית, `source_kind='external_upload'`)
|
||||||
|
- `services/internal_decisions.py::ingest_internal_decision` (החלטות-ועדה, `source_kind='internal_committee'`)
|
||||||
|
|
||||||
|
מסלולים מקבילים גוררים drift — צעד שקיים באחד וחסר באחר. הביטוי הקונקרטי: **GAP-02** —
|
||||||
|
המסלול הפנימי מתזמן רק `request_halacha_extraction` ולא `request_metadata_extraction`,
|
||||||
|
ולכן החלטות-ועדה נקלטו בלי metadata. שש אסימטריות נוספות (GAP-01/04/05) מתועדות ב-01-ingest §4.
|
||||||
|
|
||||||
|
## 2. ההכרעה האדריכלית (מאומתת)
|
||||||
|
|
||||||
|
**Template Method skeleton + Strategy via config object.** פונקציה קנונית אחת מריצה את
|
||||||
|
שלד-הפייפליין (סדר-צעדים אכיף); כל מה שמשתנה לפי סוג נישא ב-config object מוזרק (`IntakeSpec`).
|
||||||
|
שתי הפונקציות הציבוריות נשמרות כ-API בעל-שם ומאצילות לליבה.
|
||||||
|
|
||||||
|
| החלטה | נימוק | מקורות (≥3) |
|
||||||
|
|-------|--------|-------------|
|
||||||
|
| מסלול קנוני יחיד (לא 2 מקבילים, לא ליבה-משותפת בין 2 כניסות) | Template Method: "אלגוריתמים כמעט-זהים עם הבדלים קטנים" → שלד אחד אוכף סדר-צעדים, צעד-חסר נעשה בלתי-אפשרי | refactoring.guru (Template Method); SourceMaking (Strategy); ADF parameterized-pipelines |
|
||||||
|
| שמירת `ingest_precedent`/`ingest_internal_decision` כ-API ציבורי | Fowler FlagArgument: "separate methods communicate more clearly"; ליבה משותפת מוסתרת כשהלוגיקה שזורה | martinfowler.com/bliki/FlagArgument; ardalis; luzkan smells |
|
||||||
|
| ווריאציה ב-config object (`IntakeSpec`), לא boolean-flags | flag-argument הוא code smell; config object נותן בהירות + הרחבה | Fowler; ardalis; dev.to flag-anti-pattern |
|
||||||
|
| `validate` כ-callable, `enum_fields` כ-data | callable להטרוגני (Strategy idiomatic ב-Python first-class), data להומוגני ("אל תכפה הכל ל-strategy") | Strategy/Wikipedia; Functional-Strategy dev.to; Strategy-in-Python Medium |
|
||||||
|
| `create_record` כ-callable מוזרק, לא `if source_kind` | Replace Conditional with Polymorphism + factory injection ("tell, don't ask") | refactoring.guru (Replace-Conditional); code-maze (Factory+DI); c-sharpcorner |
|
||||||
|
|
||||||
|
## 3. מבנה מודולים
|
||||||
|
|
||||||
|
**מודול חדש:** `mcp-server/src/legal_mcp/services/ingest.py`
|
||||||
|
|
||||||
|
```
|
||||||
|
services/ingest.py ← חדש (בית המסלול הקנוני)
|
||||||
|
├── IntakeSpec (frozen dataclass — מתאר-הסוג)
|
||||||
|
├── async ingest_document(spec, *, file_path|text, inputs, progress) ← ליבה: צעדים 1–10
|
||||||
|
├── _stage_file(src, root, subdir) (אחיד — מאוחד משני הקבצים)
|
||||||
|
├── _coerce_date / _safe_filename (אחיד — היום משוכפל)
|
||||||
|
└── _embed_pages(case_law_id, pdf, n) (עובר מ-precedent_library.py — צעד 7 אחיד)
|
||||||
|
```
|
||||||
|
|
||||||
|
**API ציבורי — חתימה ללא שינוי לקוראים:**
|
||||||
|
- `precedent_library.py::ingest_precedent(...)` → בונה `_EXTERNAL_SPEC`, קורא `ingest.ingest_document(...)`.
|
||||||
|
- `internal_decisions.py::ingest_internal_decision(...)` → בונה `_INTERNAL_SPEC`, קורא `ingest.ingest_document(...)`.
|
||||||
|
|
||||||
|
**לא זז (גבול FU-2):** `db.create_external_case_law` / `db.create_internal_committee_decision`
|
||||||
|
נשארות נפרדות; מנותבות דרך `IntakeSpec.create_record`. כל שאר הפונקציות בשני קבצי-השירות
|
||||||
|
(search_*, migrate_*, reextract_*, process_pending_extractions, enrich_*) **לא נוגעים בהן**.
|
||||||
|
|
||||||
|
**הקוראים שלא משתנים:** MCP tools (`tools/precedent_library.py`, `tools/internal_decisions.py`)
|
||||||
|
וה-HTTP API ב-`web/` ממשיכים לקרוא לאותן שתי פונקציות ציבוריות.
|
||||||
|
|
||||||
|
## 4. ה-IntakeSpec
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class IntakeSpec:
|
||||||
|
source_kind: str # 'external_upload' | 'internal_committee'
|
||||||
|
id_field: str # 'citation' | 'case_number' (לוג/שגיאות)
|
||||||
|
staging_root: Path # PRECEDENT_LIBRARY_DIR | INTERNAL_DECISIONS_DIR
|
||||||
|
staging_subdir: Callable[[dict], str] # inputs → subdir (source_type | district | 'other')
|
||||||
|
validate: Callable[[dict], None] # מרים ValueError (citation-guard / chair_name-חובה)
|
||||||
|
enum_fields: dict[str, frozenset[str]] # נאכף לשני הסוגים (GAP-04)
|
||||||
|
derive: Callable[[dict], dict] # שדות-נגזרים (district, proceeding_type); identity לחיצוני
|
||||||
|
display_name_fallback: str # שם-השדה כשחסר case_name ('citation'|'case_number')
|
||||||
|
create_record: Callable[..., Awaitable[dict]] # create_external_case_law | create_internal_committee_decision
|
||||||
|
```
|
||||||
|
|
||||||
|
הליבה `ingest_document` **לא יודעת** איזה סוג רץ — רק מפעילה את ה-hooks בנקודות מוגדרות.
|
||||||
|
|
||||||
|
## 5. הפייפליין הקנוני (צעדים 1–10, לפי 01-ingest §2)
|
||||||
|
|
||||||
|
סדר-הביצוע בפועל (ה-DB-create מוקדם — נדרש `case_law_id` לפני אחסון chunks; תואם את הקוד הקיים):
|
||||||
|
|
||||||
|
| # | צעד | אחיד? | מקור-וריאציה |
|
||||||
|
|---|------|-------|---------------|
|
||||||
|
| 1 | ולידציית-קלט + enums | מנגנון אחיד | `spec.validate` + `spec.enum_fields` |
|
||||||
|
| 2 | גזירת-שדות | מנגנון אחיד | `spec.derive` (identity לחיצוני) |
|
||||||
|
| 3 | Stage file | מנגנון אחיד | `spec.staging_root` + `spec.staging_subdir` |
|
||||||
|
| 4 | Extract text (טקסט-ריק = כשל מדווח) | ✅ מלא | — (internal גם מקבל `text` ישיר, בלי קובץ) |
|
||||||
|
| 5 | Strip Nevo preamble | ✅ מלא | — |
|
||||||
|
| 6 | **DB create → `case_law_id`** (ספציפי-לסוג) | מנותב | `spec.create_record` (+ `display_name_fallback`) |
|
||||||
|
| 7 | Chunk (hierarchical/flat לפי `PARENT_DOC_RETRIEVAL_ENABLED`) | ✅ מלא | — (flag, לא סוג) |
|
||||||
|
| 8 | Embed children + Store chunks | ✅ מלא | — |
|
||||||
|
| 9 | **Multimodal page-image embed** (flag+PDF+page_count>0) | ✅ מלא | — (**GAP-05 fix**: היה רק בחיצוני) |
|
||||||
|
| 10 | **Queue metadata extraction** | ✅ מלא | — (**GAP-02 fix**: היה רק בחיצוני) |
|
||||||
|
| 11 | Queue halacha extraction | ✅ מלא | — |
|
||||||
|
| 12 | Set statuses (extraction=completed, halacha=pending) | ✅ מלא | — |
|
||||||
|
|
||||||
|
> הערה: 01-ingest §2 ממספר 1–10 בלי למנות מפורשות את ה-DB-create; כאן הוא צעד 6 כי הוא קודם
|
||||||
|
> ל-chunking בקוד בפועל. שגיאת-עיבוד אחרי create → `extraction_status=failed` (כמו היום).
|
||||||
|
|
||||||
|
**אילוץ `claude_session`:** הליבה רק **מתזמנת** (`request_*_extraction` — כתיבת-DB טהורה).
|
||||||
|
אין import של `halacha_extractor`/`precedent_metadata_extractor` במסלול-הקליטה — נשמר כפי שהיום.
|
||||||
|
|
||||||
|
## 6. שינויי-התנהגות וסיכון
|
||||||
|
|
||||||
|
| שינוי | השפעה | סיכון |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| **GAP-02**: internal עכשיו מתזמן metadata | החלטות-ועדה חדשות יקבלו headnote/summary/tags | אין — תיקון; רשומות קיימות כבר מלאות (0/56 חסר) |
|
||||||
|
| **GAP-04**: ולידציית-enums על internal | קלט עם practice_area לא-חוקי יידחה בקליטה | נמוך — כל 56 הקיימות חוקיות; בודקים שקוראי-internal מעבירים ערכים חוקיים |
|
||||||
|
| **GAP-05 multimodal**: internal PDF עכשיו מטמיע עמודים | החלטות-ועדה חדשות PDF יקבלו page-images | אין (non-fatal); קיימות → backfill ב-**TaskMaster 61.2 (FU-3)** |
|
||||||
|
| **GAP-05 fallback/staging/derive/guard**: מאוחדים | התנהגות זהה, מסלול אחד | אין — citation-guard נשמר ב-`_EXTERNAL_SPEC.validate` |
|
||||||
|
|
||||||
|
**אין מיגרציה (אומת מול DB 2026-05-30):** internal_committee = 56 רשומות; metadata חסר = **0**;
|
||||||
|
enums לא-חוקיים = **0**; multimodal: 14/56 יש (42 חסר → FU-3 #61.2). הריפקטור משנה רק התנהגות
|
||||||
|
*קדימה*; אינו נוגע בנתונים שמורים.
|
||||||
|
|
||||||
|
**Drift מתועד (זניח, מכוון — מסקירת-קוד סופית):**
|
||||||
|
- **empty-chunks early-return:** כשה-chunker מחזיר ריק על טקסט לא-ריק (נדיר), המקור הציב
|
||||||
|
`halacha_status=completed` ויצא בלי לתזמן; הקנוני נופל הלאה ומתזמן את שני החילוצים עם
|
||||||
|
`halacha_status=pending`. עקבי עם INV-ING3 (תיזמון אחיד) — שיפור, לא רגרסיה.
|
||||||
|
- **thumbnails של multimodal** להחלטות-ועדה יושבים תחת `precedent-library/thumbnails/`
|
||||||
|
(ממופתח לפי `case_law_id`) — מכוון, מתועד ב-docstring של `spec_thumb_dir`.
|
||||||
|
- **`queue_halachot`** הוסר כליל (wrapper + `migrate_from_style_corpus`) — הדגל איבד משמעות
|
||||||
|
תחת INV-ING3; אומת שאין caller שמעביר אותו.
|
||||||
|
|
||||||
|
## 7. אסטרטגיית בדיקה
|
||||||
|
|
||||||
|
pytest offline עם monkeypatch לכל גבולות-ה-I/O (db, embeddings, chunker, extractor) — כתבנית
|
||||||
|
[tests/test_search_domain_scope.py](../../../mcp-server/tests/test_search_domain_scope.py)
|
||||||
|
ו-[tests/test_precedent_corpus_isolation.py](../../../mcp-server/tests/test_precedent_corpus_isolation.py).
|
||||||
|
קובץ חדש: `mcp-server/tests/test_unified_ingest.py`. רץ עם `.venv` המקומי.
|
||||||
|
|
||||||
|
מקרי-בדיקה (TDD — נכשלים לפני, עוברים אחרי):
|
||||||
|
1. **regression GAP-02** — `ingest_internal_decision` מתזמן גם metadata **וגם** halacha (לוכד את הבאג המקורי).
|
||||||
|
2. שני הסוגים זורמים דרך `ingest.ingest_document` (לא דרך גוף-קוד נפרד).
|
||||||
|
3. ולידציית-enum דוחה `practice_area` לא-חוקי בשני הסוגים (GAP-04).
|
||||||
|
4. citation-guard עדיין חוסם ציטוט `ערר`/`בל"מ` במסלול החיצוני.
|
||||||
|
5. staging-subdir נפתר נכון (source_type לחיצוני, district לפנימי, 'other' ל-fallback).
|
||||||
|
6. מסלול-`text` (פנימי, בלי קובץ) ומסלול-`file_path` שניהם עובדים.
|
||||||
|
7. multimodal מותנה flag+PDF+page_count — **לא** בסוג-ה-intake; PDF פנימי → מטמיע, text → לא.
|
||||||
|
8. fallback לשם-תצוגה: חסר case_name → נופל למזהה הקנוני הנכון לכל סוג.
|
||||||
|
9. אידמפוטנטיות-חתימה: ערכי-החזרה של שתי הפונקציות הציבוריות נשמרים (תאימות-קוראים).
|
||||||
|
|
||||||
|
## 8. סדר-ביצוע
|
||||||
|
|
||||||
|
1. כתיבת `test_unified_ingest.py` (אדום).
|
||||||
|
2. `services/ingest.py` — `IntakeSpec` + `ingest_document` + הזזת `_embed_pages` + helpers אחידים.
|
||||||
|
3. `_EXTERNAL_SPEC` + צמצום `ingest_precedent` ל-wrapper.
|
||||||
|
4. `_INTERNAL_SPEC` + צמצום `ingest_internal_decision` ל-wrapper.
|
||||||
|
5. הרצת הבדיקות (ירוק) + lint.
|
||||||
|
6. בדיקת-עשן: import של שני קבצי-השירות + ה-MCP tools (ללא שבירת חתימות).
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# FU-2a — Idempotent Ingest + Write-Time Normalization + `searchable` Flag — עיצוב
|
||||||
|
|
||||||
|
**סטטוס:** מאושר-לעיצוב · **תאריך:** 2026-05-30 · **ענף:** TBD
|
||||||
|
**מכסה:** GAP-03, GAP-06, GAP-13 · **מספק:** INV-ING2, INV-G3, INV-G1, INV-ID1, INV-DM1
|
||||||
|
**מקורות:** [01-ingest.md](../../spec/01-ingest.md), [02-data-model.md](../../spec/02-data-model.md), [X1-identifiers.md](../../spec/X1-identifiers.md), [gap-audit.md](../../spec/gap-audit.md)
|
||||||
|
**משימה:** TaskMaster #60 · **תלוי ב:** FU-1 (#59) · **סוג:** pure-code (schema-additive)
|
||||||
|
**מיגרציה:** אין מיגרציית-מזהים (GAP-07/08 פוצלו ל-#67 / FU-2b). דגל `searchable` נגזר ו-recompute-בלבד.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. היקף ומה מחוץ להיקף
|
||||||
|
|
||||||
|
FU-2 פוצל (החלטת-יו"ר 2026-05-30) לאחר שבדיקת-DB גילתה נתונים מבולגנים מהצפוי:
|
||||||
|
~52/56 רשומות `internal_committee` מחזיקות **ציטוט מלא** ב-`case_number`, יש ≥1 כפילות
|
||||||
|
(`8047-23`), ו-GAP-07 ("with-month canonical") דורש את המספר הרשמי שהוקצה — ידע-יו"ר.
|
||||||
|
|
||||||
|
- **בהיקף (FU-2a, כאן):** GAP-03 (upsert idempotent), GAP-06 (נרמול-בכתיבה), GAP-13 (`searchable`).
|
||||||
|
הכל **pure-code / schema-additive**, משנה התנהגות *קדימה*, אפס מוטציה של מזהים קיימים.
|
||||||
|
- **מחוץ להיקף (FU-2b, #67):** GAP-07 (תיאום מזהים מעורבים), GAP-08 (ניקוי ציטוט-כמזהה) —
|
||||||
|
מיגרציית-נתונים שמערבת dedup, סקירת-יו"ר per-record, גיבוי, reversibility.
|
||||||
|
|
||||||
|
**אינטראקציה FU-2a↔FU-2b (מתועד):** נרמול-בכתיבה חל רק על כתיבות *חדשות*. רשומות-עבר עם
|
||||||
|
ציטוט-מלא לא משתנות עד FU-2b. קליטה-חוזרת של רשומת-עבר מבולגנת תיצור רשומה *נקייה* חדשה
|
||||||
|
(לא תתנגש על המחרוזת השונה) — FU-2b יאחד. זה עקבי עם forward-only של FU-1.
|
||||||
|
|
||||||
|
## 2. הכרעות אדריכליות (מאומתות ≥3 מקורות)
|
||||||
|
|
||||||
|
| החלטה | נימוק | מקורות |
|
||||||
|
|-------|--------|--------|
|
||||||
|
| `INSERT … ON CONFLICT DO UPDATE` במקום SELECT-then-INSERT/UPDATE | אטומי, נטול race תחת Read-Committed; ה-SELECT-then-write הוא read-modify-write קלאסי | PostgreSQL INSERT docs; QueryPlane; on-systems.tech |
|
||||||
|
| **לחזור על predicate של ה-partial-index ב-ON CONFLICT** | V15 משתמש ב-partial unique indexes; Postgres דורש את ה-predicate ב-conflict target | PostgreSQL INSERT docs (§ON CONFLICT); QueryPlane gotchas |
|
||||||
|
| נרמול case_number **בכתיבה**, type-aware | נרמול הוא אחריות-גבול-קלט; ערכים שווי-משמעות → פלט זהה. פסיקה-חיצונית: הציטוט *הוא* המזהה → לא לחתוך | DDD value-objects (Medium/dev.to); gojko.net |
|
||||||
|
| דגל `searchable` **materialized** ונגזר-מחדש, לא מוסק בכל query | reify את חוזה-השלמות; חייב להיות נראה ל-health-check (לא הסקה סמויה) | DevIQ MISU; functional-architecture.org; Stemmler |
|
||||||
|
|
||||||
|
## 3. הקבצים
|
||||||
|
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/services/db.py`:
|
||||||
|
- `create_external_case_law` — להמיר ל-`ON CONFLICT` (target: `(case_number) WHERE source_kind <> 'internal_committee'`); זה גם מטפל בקידום `cited_only`→`external_upload` (אותו partial-index). לא לחתוך את ה-citation (זהו המזהה).
|
||||||
|
- `create_internal_committee_decision` — להמיר ל-`ON CONFLICT (case_number, proceeding_type) WHERE source_kind = 'internal_committee'`; לנרמל `case_number` בכניסה.
|
||||||
|
- `create_case` — לנרמל `case_number` בכניסה (כתיבה).
|
||||||
|
- הוספת helper `_canonical_case_number(s)` (שם מפורש; עוטף את הטרנספורם הדטרמיניסטי trim·prefix-strip·/→- של X1). `_normalize_case_number` הקיים (read-time) נשאר כ-shim.
|
||||||
|
- מיגרציית-schema **V21**: `ALTER TABLE case_law ADD COLUMN searchable boolean NOT NULL DEFAULT false`.
|
||||||
|
- פונקציה `recompute_searchable(case_law_id|all)` — נגזרת מחוזה-השלמות; נקראת בסיום-קליטה ובסיום-חילוץ-metadata.
|
||||||
|
- **Modify** `mcp-server/src/legal_mcp/services/ingest.py` — בסיום הצלחת הקליטה, לקרוא `db.recompute_searchable(case_law_id)` (אחיד לכל סוג; אחרי setting statuses).
|
||||||
|
- **Test** `mcp-server/tests/test_idempotent_ingest.py` (חדש) — offline, monkeypatched.
|
||||||
|
|
||||||
|
**גבול:** אין שינוי לחתימות הציבוריות של `ingest_precedent`/`ingest_internal_decision` (FU-1).
|
||||||
|
הנרמול וה-upsert יושבים בשכבת-ה-DB (גבול-הכתיבה), שקופים לקוראים.
|
||||||
|
|
||||||
|
## 4. נרמול type-aware (GAP-06)
|
||||||
|
|
||||||
|
`_canonical_case_number(s)` — דטרמיניסטי, תואם X1 §1, **לא מוסיף/מסיר חודש**:
|
||||||
|
```
|
||||||
|
trim → strip prefix לפני הספרה הראשונה → להחליף '/' ב-'-'
|
||||||
|
```
|
||||||
|
|
||||||
|
| נקודת-כתיבה | מדיניות | נימוק |
|
||||||
|
|--------------|---------|--------|
|
||||||
|
| `create_internal_committee_decision` | `_canonical_case_number(case_number)` | המזהה הקנוני = מספר-בסיס מנורמל |
|
||||||
|
| `create_case` | `_canonical_case_number(case_number)` | תיק פעיל — אותו כלל |
|
||||||
|
| `create_external_case_law` | `.strip()` בלבד (ללא prefix-strip) | פסיקה חיצונית: ה-citation הוא המזהה הקנוני (X1 §1); חיתוך היה הורס אותו |
|
||||||
|
|
||||||
|
> נרמול מטפל ב-prefix+separator בלבד. קלט שהוא ציטוט-מלא (party names, נבו) **לא** מנוקה ל-bare
|
||||||
|
> ע"י הנרמול — זה GAP-08/FU-2b. FU-2a מבטיח שקלט נקי-יחסית נשמר בצורה קנונית.
|
||||||
|
|
||||||
|
## 5. Idempotent upsert (GAP-03)
|
||||||
|
|
||||||
|
שתי פונקציות-ה-create עוברות מ-SELECT-then-INSERT/UPDATE ל-`INSERT … ON CONFLICT … DO UPDATE`,
|
||||||
|
עם **חזרה על ה-predicate** של ה-partial-index (V15):
|
||||||
|
|
||||||
|
- **internal:** `ON CONFLICT (case_number, proceeding_type) WHERE source_kind = 'internal_committee' DO UPDATE SET …`
|
||||||
|
- **external:** `ON CONFLICT (case_number) WHERE source_kind <> 'internal_committee' DO UPDATE SET …`
|
||||||
|
— מחליף את לוגיקת ה-SELECT הקיימת, **כולל** קידום `cited_only`→`external_upload` (אותה partial-
|
||||||
|
index חלה על שניהם; ה-DO UPDATE מקדם את source_kind וממלא שדות חסרים).
|
||||||
|
|
||||||
|
**`DO UPDATE` ממוקד:** רק שדות-קלט לא-ריקים דורסים (לשמר ערכים קיימים; `COALESCE(EXCLUDED.x, case_law.x)`),
|
||||||
|
ולא לדרוס מטא-דאטה שמולא ע"י חילוץ-LLM. אם ל-`case_law` יש טריגרי-`updated_at` — לסנן עם `WHERE`
|
||||||
|
על שינוי בפועל (gotcha מהמחקר). re-embed בקליטה-חוזרת = INV-ING4, שייך ל-FU-3 — כאן רק upsert-הרשומה.
|
||||||
|
|
||||||
|
## 6. דגל `searchable` (GAP-13)
|
||||||
|
|
||||||
|
עמודה חדשה `case_law.searchable boolean NOT NULL DEFAULT false`. **נגזרת** מחוזה-השלמות
|
||||||
|
(02-data-model §2a / INV-DM1), לא מוסקת ב-query:
|
||||||
|
|
||||||
|
```
|
||||||
|
searchable = (
|
||||||
|
case_number/citation קנוני לא-ריק
|
||||||
|
AND case_name<>'' AND practice_area<>'' AND source_kind<>''
|
||||||
|
AND EXISTS(precedent_chunk עם embedding NOT NULL)
|
||||||
|
AND extraction_status='completed'
|
||||||
|
AND (headnote<>'' OR summary<>'' OR jsonb_array_length(subject_tags)>0)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `recompute_searchable(case_law_id)` נקראת בסיום-קליטה (ingest.py) ובסיום `precedent_metadata_extractor`.
|
||||||
|
- **Backfill (recompute-בלבד, הפיך):** מיגרציה V21 מריצה `recompute_searchable(all)` פעם אחת על רשומות
|
||||||
|
קיימות. זו גזירה הפיכה (ניתן להריץ שוב כל רגע) — אינה נוגעת במזהים, לא חלק מ-FU-2b.
|
||||||
|
- שכבת-החיפוש (`search_*`) תסונן ל-`searchable=true` — **שינוי-התנהגות מתועד** (ראה §7).
|
||||||
|
- health-check יחשוף `count(*) FILTER (WHERE NOT searchable)` (זרע ל-GAP-14/FU-5).
|
||||||
|
|
||||||
|
## 7. שינויי-התנהגות וסיכון
|
||||||
|
|
||||||
|
| שינוי | השפעה | סיכון |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| upsert ON CONFLICT | קליטה-חוזרת = update אטומי, לא כפילות; קידום cited_only נשמר | נמוך — מאומת מול partial-index הקיים |
|
||||||
|
| נרמול-בכתיבה (internal/cases) | קלט חדש נשמר כ-bare מנורמל | נמוך — type-aware; external לא נחתך |
|
||||||
|
| `searchable` מסנן חיפוש | רשומות שלא עומדות בחוזה-השלמות **לא יוחזרו** | ⚠️ בינוני — backfill עלול לסמן רשומות-עבר כ-non-searchable. **אימות:** להריץ recompute ב-dry-run ולדווח כמה ירדו מהחיפוש *לפני* הפעלת הסינון |
|
||||||
|
| backfill searchable | דגל נגזר על רשומות קיימות | נמוך — הפיך, recompute-בלבד, לא נוגע במזהים |
|
||||||
|
|
||||||
|
**אזהרת-backlog:** ה-rows עם ציטוט-מלא-כמזהה (FU-2b) עשויים בכל זאת לעמוד בחוזה-השלמות (יש להם
|
||||||
|
chunks+metadata), כך שהסינון לא בהכרח מפיל אותם. ה-dry-run ב-§7 יכמת זאת לפני הפעלה.
|
||||||
|
|
||||||
|
## 8. אסטרטגיית בדיקה
|
||||||
|
|
||||||
|
`tests/test_idempotent_ingest.py` — offline, monkeypatch ל-DB pool (או בדיקת-SQL מול sqlite-fallback אם קיים בפרויקט; אחרת monkeypatch כמו FU-1). מקרים:
|
||||||
|
1. `_canonical_case_number`: `"ערר 8137/24"`→`"8137-24"`, `"8126-03-25"`→`"8126-03-25"` (חודש נשמר), `" עע\"מ 1/20 "`→`"1-20"`.
|
||||||
|
2. נרמול type-aware: internal מנרמל; external **לא** חותך citation.
|
||||||
|
3. upsert: קליטה כפולה של אותו (case_number, proceeding_type) internal = רשומה אחת (לא שתיים).
|
||||||
|
4. upsert: קידום `cited_only`→`external_upload` על אותו case_number = עדכון, לא כפילות.
|
||||||
|
5. `DO UPDATE` ממוקד: מטא-דאטה קיים לא נדרס ע"י קלט ריק (COALESCE).
|
||||||
|
6. `recompute_searchable`: רשומה מלאה→true; חסרת-embedding/metadata/extraction→false.
|
||||||
|
7. ingest קורא recompute_searchable בסיום (שני הסוגים).
|
||||||
|
|
||||||
|
> בדיקת ON CONFLICT האמיתית דורשת Postgres. אם אין מסלול-בדיקה מול DB אמיתי בפרויקט,
|
||||||
|
> הבדיקות יאמתו את בניית-ה-SQL ואת הלוגיקה הטהורה (normalize, completeness predicate) ב-offline,
|
||||||
|
> ושכבת-ה-SQL תיבדק ב-smoke מול ה-DB המקומי (5433) ידנית בסיום, מתועד בתוכנית.
|
||||||
|
|
||||||
|
## 9. סדר-ביצוע
|
||||||
|
|
||||||
|
1. בדיקות אדומות (`test_idempotent_ingest.py`).
|
||||||
|
2. `_canonical_case_number` + נרמול-בכתיבה ב-3 פונקציות ה-create.
|
||||||
|
3. המרת שתי create ל-`ON CONFLICT … DO UPDATE` (עם predicate חוזר + COALESCE ממוקד).
|
||||||
|
4. מיגרציה V21: עמודה `searchable` + `recompute_searchable` + backfill recompute.
|
||||||
|
5. קריאה ל-`recompute_searchable` מ-ingest.py; חשיפת `count FILTER (WHERE NOT searchable)` ב-health-check.
|
||||||
|
6. **dry-run** של backfill מול DB 5433 → לדווח כמה רשומות יסומנו `searchable=false` ומאילו source_kind.
|
||||||
|
7. **שער החלטה (gated):** סינון `searchable=true` בשכבת-החיפוש מופעל **רק אם** ה-dry-run מראה
|
||||||
|
שאף רשומה לגיטימית לא יורדת מהחיפוש. אם רשומות-עבר לגיטימיות היו נופלות (למשל מבולגנות-FU-2b
|
||||||
|
שעדיין שמישות) — לדחות את הפעלת-הסינון לפולואו-אפ אחרי FU-2b, ולהשאיר את העמודה+health-check בלבד.
|
||||||
|
(להציף את ממצא ה-dry-run למשתמש לפני הפעלה — שינוי חיפוש הוא פעולה גלויה.)
|
||||||
|
8. בדיקות ירוקות + smoke מול DB מקומי + lint.
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# FU-3 — Re-Index on Content Change — עיצוב
|
||||||
|
|
||||||
|
**סטטוס:** מאושר-לעיצוב · **תאריך:** 2026-05-30 · **ענף:** TBD
|
||||||
|
**מכסה:** GAP-09 · **מספק:** INV-DM3, INV-G6, INV-ING4 (freshness) · **משימה:** TaskMaster #61
|
||||||
|
**תלוי ב:** FU-1 (#59) · **סוג:** pure-code + backfill-hash זול (אפס re-embed בריצה רגילה)
|
||||||
|
**מיגרציה:** V23 additive (2 עמודות-hash) + backfill-hash דטרמיניסטי הפיך. אין re-embed המוני.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. הבעיה (מאומת בקוד)
|
||||||
|
|
||||||
|
`embedding` אינו עמודת `GENERATED` (בניגוד ל-tsvectors שמתעדכנים אוטומטית בשינוי-תוכן). חילוץ
|
||||||
|
embedding דורש קריאת-API, ולכן אי-אפשר להפוך אותו ל-GENERATED. הממצא של מיפוי-הקוד:
|
||||||
|
|
||||||
|
- **re-ingest דרך `ingest_document` כבר מבצע re-index נכון** — `_chunk_embed_store` רץ ללא-תנאי
|
||||||
|
ו-`store_precedent_chunks(_hierarchical)` הן DELETE-then-INSERT. אז המסלול המלא תקין.
|
||||||
|
- **3 פערים אמיתיים:** (א) אין **גילוי שינוי-תוכן** (אין `content_hash`/`updated_at` ב-case_law);
|
||||||
|
(ב) אין **נקודת re-index עצמאית** — כדי להטמיע מחדש חייבים לקלוט מחדש את ה**קובץ**, אך רשומות
|
||||||
|
רבות (למשל 42 החלטות-ועדה) נקלטו מ-`text` בלי קובץ; (ג) אין **גילוי-drift** בין תוכן ל-embeddings.
|
||||||
|
|
||||||
|
**אכיפת INV-G6** ("re-index בכל שינוי-תוכן") כשהטמעה אינה GENERATED = **גילוי (hash) + כלי-reindex
|
||||||
|
מתוכן-שמור + health-check** — בדיוק כדפוס ה-drift של FU-7 (detect-don't-auto-magic).
|
||||||
|
|
||||||
|
## 2. הכרעות אדריכליות (מאומתות ≥3 מקורות)
|
||||||
|
|
||||||
|
| החלטה | נימוק | מקורות |
|
||||||
|
|-------|--------|--------|
|
||||||
|
| `content_hash` (SHA-256 של full_text) לגילוי-שינוי, לא timestamp | hash תוכן הוא הדפוס המומלץ כשאין timestamp מהימן; דטרמיניסטי + collision-safe | Hash-based change detection (DeepWiki); Andy Dote content-hash; moby#9391 |
|
||||||
|
| re-index **מ-full_text שמור**, לא מ-re-extract/re-OCR | OCR לא-דטרמיניסטי; להשתמש בטקסט השמור (תואם [[feedback_no_reocr_retrofit]]) | RAG re-embed-on-edit (Medium); particula incremental update |
|
||||||
|
| detect→re-embed **רק שהשתנה** (לא rebuild מלא) + health-check staleness | incremental sync; ניטור recall כשהאינדקס מתיישן | apxml RAG updates; Pinecone/Weaviate (gap-audit) |
|
||||||
|
| backfill = hash בלבד (לא re-embed) — רשומות קיימות כבר מוטמעות | זול, הפיך, אפס עלות-API; re-embed רק כשתוכן באמת השתנה | — (נגזר מהמצב: 80 רשומות כבר embedded) |
|
||||||
|
|
||||||
|
## 3. הקבצים
|
||||||
|
|
||||||
|
- **Modify** `services/db.py`: V23 (`content_hash`, `indexed_hash` ב-case_law); `_content_hash(text)`;
|
||||||
|
כתיבת `content_hash` בכניסת `create_external_case_law`/`create_internal_committee_decision`/`create_case`;
|
||||||
|
`mark_indexed(case_law_id)` (מעתיק content_hash→indexed_hash); `recompute_content_hashes()` (backfill);
|
||||||
|
`list_stale_case_law()` (drift query).
|
||||||
|
- **Modify** `services/ingest.py`: אחרי `_chunk_embed_store` המוצלח → `mark_indexed(case_law_id)`; הוספת
|
||||||
|
`reindex_case_law(case_law_id)` — טוען row, chunk+embed+store מ-full_text שמור, ואז `mark_indexed`.
|
||||||
|
- **Modify** `services/metrics.py`: חשיפת `stale_embedding_case_law` count.
|
||||||
|
- **Add** MCP tool `precedent_reindex(case_law_id)` (wrapper דק ל-`ingest.reindex_case_law`) — מאפשר
|
||||||
|
הפעלה ידנית; voyage-API בלבד (אין CLI/LLM → בטוח גם בקונטיינר).
|
||||||
|
- **Test** `tests/test_reindex_on_change.py` (חדש).
|
||||||
|
|
||||||
|
**גבול:** אין שינוי לחתימות ציבוריות. `reindex_case_law` הוא **תוסף**; המסלול הקיים לא משתנה.
|
||||||
|
|
||||||
|
## 4. content_hash + indexed_hash
|
||||||
|
|
||||||
|
- `_content_hash(text) -> str`: `hashlib.sha256(text.encode()).hexdigest()`; על `""`/None → `""`.
|
||||||
|
- `content_hash` = hash של ה-full_text **הנוכחי**, נכתב בכל כתיבת-row (ב-create_*; גבול-הכתיבה כמו נרמול FU-2a).
|
||||||
|
- `indexed_hash` = ה-hash שעליו נבנו ה-chunks/embeddings **הנוכחיים**, נכתב ב-`mark_indexed` אחרי
|
||||||
|
store מוצלח (ב-ingest + ב-reindex).
|
||||||
|
- **טרי** ⇔ `content_hash = indexed_hash`. **stale** ⇔ `content_hash IS DISTINCT FROM indexed_hash`
|
||||||
|
(כולל indexed_hash=NULL = "מעולם לא הוטמע מהתוכן הזה").
|
||||||
|
|
||||||
|
## 5. `reindex_case_law(case_law_id)` (GAP-09 enforcement)
|
||||||
|
|
||||||
|
```
|
||||||
|
load case_law row → full_text (שמור)
|
||||||
|
→ _chunk_embed_store(case_law_id, full_text, page_offsets=None, ...) # אותו מסלול קנוני
|
||||||
|
→ mark_indexed(case_law_id) # indexed_hash = content_hash
|
||||||
|
return {chunks, reindexed: true}
|
||||||
|
```
|
||||||
|
- **לא** קורא ל-extractor/OCR ולא ל-LLM — רק chunk (טקסט שמור) + embed (voyage) + store. תואם
|
||||||
|
[[feedback_no_reocr_retrofit]] ו-claude_session (אין CLI).
|
||||||
|
- multimodal: מדלג (page-images דורשים PDF; רשומות-טקסט אין להן — ראה §7). אם בעתיד יש קובץ — המסלול
|
||||||
|
המלא של ingest מטפל.
|
||||||
|
- idempotent (store = DELETE-then-INSERT; mark_indexed דטרמיניסטי).
|
||||||
|
|
||||||
|
## 6. גילוי-drift + health-check
|
||||||
|
|
||||||
|
- `list_stale_case_law()` → רשומות עם full_text לא-ריק ו-`content_hash IS DISTINCT FROM indexed_hash`.
|
||||||
|
- health-check (metrics.py) חושף `stale_embedding_case_law` count (INV-G6 observability; אחות ל-
|
||||||
|
`non_searchable_case_law`/`cases_with_stale_blocks` מ-FU-2a/FU-7).
|
||||||
|
|
||||||
|
## 7. #61.2 (multimodal backfill) — נסגר כלא-ישים
|
||||||
|
|
||||||
|
בדיקת-DB (2026-05-30): 42 החלטות-ועדה ללא page-images — **כולן** `document_id=NULL` ו-full_text
|
||||||
|
קיים, ואין PDF מקור בדיסק (`data/internal-decisions/` מכיל קובץ אחד). page-images דורשים **רינדור
|
||||||
|
PDF**; לרשומות-טקסט אין PDF → **בלתי-אפשרי**. לכן #61.2 נסגר כ-not-applicable. (אם יועלה PDF לאחת —
|
||||||
|
מסלול-ה-ingest הרגיל יטפל ב-multimodal.) FU-3 core מטמיע-מחדש את ה**טקסט** של כל 42 במידת-הצורך.
|
||||||
|
|
||||||
|
## 8. שינויי-התנהגות וסיכון
|
||||||
|
|
||||||
|
| שינוי | השפעה | סיכון |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| content_hash בכתיבה | כל קליטה חדשה נושאת hash; טרי-מעצם-הקליטה | נמוך — additive |
|
||||||
|
| mark_indexed ב-ingest | רשומות חדשות = טרי (content=indexed) | נמוך |
|
||||||
|
| reindex_case_law | re-embed מתוכן שמור; עלות-API לפי-בקשה | נמוך — תוסף, ידני/מבוקר; לא רץ אוטומטית בהמוני |
|
||||||
|
| backfill hashes | content_hash לכולם; indexed_hash=content רק אם יש chunks, אחרת NULL | נמוך — הפיך, אפס re-embed |
|
||||||
|
| health-check stale count | חשיפת drift | נמוך — read-only |
|
||||||
|
|
||||||
|
## 9. אסטרטגיית בדיקה
|
||||||
|
|
||||||
|
`tests/test_reindex_on_change.py` — offline, monkeypatch. מקרים:
|
||||||
|
1. `_content_hash`: דטרמיניסטי; `""`/None→`""`; טקסט שונה→hash שונה.
|
||||||
|
2. stale-predicate: content≠indexed → stale; שווים → טרי; indexed=NULL → stale.
|
||||||
|
3. `mark_indexed` מריץ UPDATE שמעתיק content_hash→indexed_hash (monkeypatch conn).
|
||||||
|
4. `reindex_case_law`: טוען full_text, קורא _chunk_embed_store ו-mark_indexed (monkeypatch), לא קורא extractor/LLM.
|
||||||
|
5. create_* כותב content_hash (monkeypatch — assert ה-hash מועבר ל-INSERT/upsert).
|
||||||
|
|
||||||
|
> בדיקות-DB אמיתיות (V23, backfill, drift query) — smoke מול DB מקומי (5433) בסיום, כמו FU-2a/FU-7.
|
||||||
|
|
||||||
|
## 10. סדר-ביצוע
|
||||||
|
|
||||||
|
1. בדיקות אדומות.
|
||||||
|
2. V23 (`content_hash`,`indexed_hash`) + `_content_hash` + `mark_indexed` + כתיבת content_hash ב-create_*.
|
||||||
|
3. `reindex_case_law` ב-ingest.py + קריאת `mark_indexed` אחרי `_chunk_embed_store` בקליטה.
|
||||||
|
4. `list_stale_case_law` + health-check `stale_embedding_case_law`.
|
||||||
|
5. MCP tool `precedent_reindex`.
|
||||||
|
6. backfill (DB smoke): `recompute_content_hashes()` — content_hash לכולם, indexed_hash=content אם יש chunks.
|
||||||
|
7. בדיקות ירוקות + smoke מול DB + lint + סגירת #61.2 + TaskMaster #61.
|
||||||
122
docs/superpowers/specs/2026-05-30-fu7-audit-provenance-design.md
Normal file
122
docs/superpowers/specs/2026-05-30-fu7-audit-provenance-design.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# FU-7 — Audit-Trail + Provenance — עיצוב
|
||||||
|
|
||||||
|
**סטטוס:** מאושר-לעיצוב · **תאריך:** 2026-05-30 · **ענף:** TBD
|
||||||
|
**מכסה:** GAP-17, GAP-18, GAP-19, GAP-20 · **מספק:** INV-AUD1, INV-AUD2, INV-AUD3, INV-EX1, INV-G9
|
||||||
|
**מקורות:** [X5-audit-provenance.md](../../spec/X5-audit-provenance.md), [06-export.md](../../spec/06-export.md), [gap-audit.md](../../spec/gap-audit.md)
|
||||||
|
**משימה:** TaskMaster #65 · **תלוי ב:** FU-1 (#59) · **סוג:** pure-code (schema-additive קל)
|
||||||
|
**מיגרציה:** אין. כל השינויים forward-only; backfill קל אופציונלי (provenance של בלוקים קיימים לא נאכף רטרואקטיבית).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. מטרה והיקף
|
||||||
|
|
||||||
|
X5 §4 קובע את המנגנון הקנוני: **שימוש חוזר ב-`audit_log.log_action` עם `details` JSONB** —
|
||||||
|
לא טבלה חדשה (כלל-הנדסה "סימטריה"). FU-7 ממיר את `audit_log` מ"כמעט-ריק" ל-audit-trail מקצה-לקצה,
|
||||||
|
מוסיף provenance בלוק→מקורות, אוכף ציטוט→קורפוס, ומגלה drift בין DOCX-החי לבלוקים.
|
||||||
|
|
||||||
|
| GAP | בעיה (מאומת בקוד) | יעד FU-7 |
|
||||||
|
|-----|--------------------|----------|
|
||||||
|
| GAP-18 | `log_action` נכתב רק ב-`case_subtype_override` (cases.py:203) | קריאות `log_action` ב-4 פעולות משנות-מצב: upload, extract_claims, write_block, export |
|
||||||
|
| GAP-19 | `decision_blocks` נושא `model_used` בלבד — אין קישור לקטעי-מקור | רשומת provenance ב-`audit_log.details` עם source ids שהזינו את הגנרציה |
|
||||||
|
| GAP-20 | אין אכיפה שציטוט פתיר לקורפוס | ולידציה דטרמיניסטית של `case_law_id` בציטוטים → flag לבלתי-פתירים |
|
||||||
|
| GAP-17 | `active_draft_path` הופך SoT אחרי revise/apply בלי re-sync לבלוקים | דגל `blocks_stale` דטרמיניסטי + חשיפת drift ב-health-check (לא re-sync שביר) |
|
||||||
|
|
||||||
|
## 2. הכרעות אדריכליות (מאומתות ≥3 מקורות)
|
||||||
|
|
||||||
|
| החלטה | נימוק | מקורות |
|
||||||
|
|-------|--------|--------|
|
||||||
|
| provenance כ-**event ב-`audit_log` append-only** (details payload), לא עמודה/טבלה חדשה | דפוס lineage בוגר: entity-key + event-type + actor + source-ids; X5 §4 (סימטריה) | Snowflake data-lineage; OvalEdge provenance; DesignGurus append-only audit |
|
||||||
|
| GAP-17 = **detect + flag**, מקור-אמת=בלוקים, לא auto-resync | auto-remediation דורש rollback אמין; reparse DOCX→blocks שביר (edits שוברים מבנה) | Flux GitOps drift; Terraform drift (env0); Spacelift |
|
||||||
|
| GAP-20 = **ולידציה מבנית** של `case_law_id` פתיר, לא NLP של ציטוט חופשי | NLP-ציטוט עברי חופף ל-`extract_internal_citations` הקיים; INV-AUD3 מנוסח סביב פתירוּת `case_law_id` | X5 INV-AUD3; RAG attribution (Lewis 2020); ISO 8000 |
|
||||||
|
| audit כ-**non-fatal** (כשל-audit מתעד warning, לא מפיל פעולה) | git הוא שכבת-השלמות (X5 §2.1); audit_log הוא observability "מי/מה/מתי" | X5 §2.1; דפוס audit fire-safe |
|
||||||
|
|
||||||
|
## 3. הקבצים
|
||||||
|
|
||||||
|
- **Modify** `tools/audit.py` — אין שינוי לחתימת `log_action`; להוסיף helper `log_action_safe(...)` שעוטף ב-try/except (warning, non-fatal) כדי שכשל-audit לא יפיל את הפעולה.
|
||||||
|
- **Modify** `tools/documents.py` — `document_upload` (~:14) + `extract_claims` (~:300): קריאת `log_action_safe`.
|
||||||
|
- **Modify** `services/block_writer.py` — `write_block`/`store_block` (~:1010): לאסוף source ids מ-context builders + לכתוב audit `write_block` עם provenance.
|
||||||
|
- **Modify** `tools/drafting.py` — `export_docx` (~:384): audit `export_docx`; `revise_draft` (~:647) + `apply_user_edit` (~:569): סימון `blocks_stale=true`.
|
||||||
|
- **Modify** `services/db.py` — מיגרציה V22: עמודת `cases.blocks_stale boolean DEFAULT false`; helper `mark_blocks_stale(case_id, val)`; helper `resolve_citation_case_law_ids(ids)` (בדיקת קיום); helper `audit_provenance_query` (קריאה — לא חובה).
|
||||||
|
- **Modify** `services/qa_validator.py` (או היכן שרץ QA) — בדיקת ציטוט→קורפוס: לכל `case_law_id` בציטוטי-הבלוק, אם לא פתיר → ממצא-QA (warning) + audit `citation_unresolved`.
|
||||||
|
- **Modify** health-check (metrics.py / processing_status) — חשיפת `cases_with_stale_blocks` count.
|
||||||
|
- **Test** `tests/test_audit_provenance.py` (חדש) — offline, monkeypatched.
|
||||||
|
|
||||||
|
**גבול:** אין שינוי לחתימות ציבוריות; אין מיגרציית-נתונים. provenance של בלוקים *קיימים* לא נאכף
|
||||||
|
רטרואקטיבית (forward-only) — תואם FU-1/FU-2a.
|
||||||
|
|
||||||
|
## 4. GAP-18 — audit על כל פעולה משנה-מצב
|
||||||
|
|
||||||
|
`log_action_safe(action, case_id=, document_id=, details=, user=)` — עטיפת `log_action` ב-try/except
|
||||||
|
(כשל → `logger.warning`, ה-action ממשיך). נקודות-הקריאה:
|
||||||
|
|
||||||
|
| פעולה | action | details |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| document_upload | `"document_upload"` | `{title, doc_type, classification}` |
|
||||||
|
| extract_claims | `"extract_claims"` | `{docs_processed, claims_count}` |
|
||||||
|
| write_block (GAP-19) | `"write_block"` | `{decision_id, block_id, model_used, generation_type, source_document_ids, retrieved_case_law_ids, claim_ids}` |
|
||||||
|
| export_docx | `"export_docx"` | `{path, file_size, block_count}` |
|
||||||
|
|
||||||
|
## 5. GAP-19 — provenance בלוק→מקורות
|
||||||
|
|
||||||
|
`write_block` כבר אוסף הקשר מ-`_build_source_context` (document chunks), `_build_precedents_context`
|
||||||
|
(`para_results`/`caselaw_rows` → `case_law_id`s), `_build_claims_context` (claim ids). היעד: לאסוף את
|
||||||
|
המזהים הללו ל-dict `sources = {document_ids, case_law_ids, claim_ids}` ולכלול אותו ברשומת ה-audit
|
||||||
|
`write_block` (§4). כך `audit_log` עונה "מאיזו פסיקה/מסמך נולד הבלוק" — בלי עמודה/טבלה חדשה.
|
||||||
|
מפתח-הקישור: `details.decision_id`+`details.block_id` (audit_log עצמו keyed ב-case_id/document_id).
|
||||||
|
|
||||||
|
## 6. GAP-20 — ציטוט→קורפוס נאכף
|
||||||
|
|
||||||
|
`resolve_citation_case_law_ids(ids) -> {resolved: [...], unresolved: [...]}` — בדיקת `EXISTS` מול
|
||||||
|
`case_law`. בנקודת ה-QA (לפני export, משתלב עם שערי FU-6): לאסוף את כל ה-`case_law_id` מציטוטי-הבלוקים
|
||||||
|
(`decision_paragraphs.citations` אם מאוכלס, אחרת מ-provenance של §5), ולהריץ resolve. בלתי-פתירים →
|
||||||
|
**ממצא-QA (warning, לא חוסם-קריטי)** + audit `citation_unresolved`. אכיפה מבנית בלבד (case_law_id),
|
||||||
|
לא חילוץ-NLP של ציטוט חופשי.
|
||||||
|
|
||||||
|
> **הערה:** `decision_paragraphs` אינו מאוכלס כיום ע"י אף כלי (ממצא Explore). לכן ולידציית-הציטוט
|
||||||
|
> פועלת על ה-`case_law_id`s שנרשמו ב-provenance (§5); אם/כאשר decision_paragraphs יאוכלס — אותה
|
||||||
|
> ולידציה חלה עליו. זה שומר את ה-GAP סגור בלי לבנות צינור-ציטוטים חדש (מחוץ-להיקף).
|
||||||
|
|
||||||
|
## 7. GAP-17 — drift בין DOCX-חי לבלוקים
|
||||||
|
|
||||||
|
מקור-אמת = `decision_blocks` (INV-EX1). אחרי `revise_draft`/`apply_user_edit` שהופכים את
|
||||||
|
`active_draft_path` ל-SoT-בפועל בלי re-sync, מסמנים `cases.blocks_stale=true` (חוזה מפורש: "הבלוקים
|
||||||
|
ידועים כלא-מסונכרנים מול ה-DOCX-החי"). `export_docx` מ-blocks מאפס `blocks_stale=false` (הבלוקים שוב SoT).
|
||||||
|
health-check חושף `cases_with_stale_blocks`. **לא** מבצעים reparse DOCX→blocks (שביר).
|
||||||
|
|
||||||
|
| נקודה | פעולה על blocks_stale |
|
||||||
|
|-------|------------------------|
|
||||||
|
| revise_draft / apply_user_edit | `= true` (DOCX-חי חרג מהבלוקים) |
|
||||||
|
| export_docx (מ-blocks) | `= false` (בלוקים = SoT שוב) |
|
||||||
|
| write_block / save_block_content | `= false` (בלוק עודכן ב-DB) |
|
||||||
|
|
||||||
|
## 8. שינויי-התנהגות וסיכון
|
||||||
|
|
||||||
|
| שינוי | השפעה | סיכון |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| audit על 4 פעולות | audit_log מתמלא; observability | נמוך — non-fatal, לא משנה תוצאת-פעולה |
|
||||||
|
| provenance ב-write_block audit | רשומת מקור לכל גנרציה חדשה | נמוך — forward-only; בלוקים קיימים לא מושפעים |
|
||||||
|
| ציטוט-QA warning | ציטוט בלתי-פתיר מסומן לאימות-יו"ר | נמוך — warning, לא חוסם export (לא קריטי) |
|
||||||
|
| `blocks_stale` flag | חשיפת drift; אינו חוסם | נמוך — דגל אינפורמטיבי; V22 additive |
|
||||||
|
|
||||||
|
## 9. אסטרטגיית בדיקה
|
||||||
|
|
||||||
|
`tests/test_audit_provenance.py` — offline, monkeypatch DB pool. מקרים:
|
||||||
|
1. `log_action_safe` בולע כשל-DB (warning) ולא מרים.
|
||||||
|
2. כל אחת מ-4 הפעולות קוראת ל-audit עם ה-action הנכון (monkeypatch log_action, assert call).
|
||||||
|
3. write_block audit כולל `source_document_ids`/`retrieved_case_law_ids` מה-context.
|
||||||
|
4. `resolve_citation_case_law_ids`: מפריד resolved/unresolved נכון (monkeypatch EXISTS).
|
||||||
|
5. ציטוט בלתי-פתיר → ממצא-QA warning (לא חוסם-קריטי).
|
||||||
|
6. `blocks_stale`: revise/apply → true; export-from-blocks → false.
|
||||||
|
7. health-check חושף `cases_with_stale_blocks`.
|
||||||
|
|
||||||
|
> בדיקות-DB אמיתיות (audit_log INSERT, V22, EXISTS) — smoke מול DB מקומי (5433) בסיום, כמו FU-2a.
|
||||||
|
|
||||||
|
## 10. סדר-ביצוע
|
||||||
|
|
||||||
|
1. בדיקות אדומות.
|
||||||
|
2. `log_action_safe` + מיגרציה V22 (`blocks_stale`) + helpers (`mark_blocks_stale`, `resolve_citation_case_law_ids`).
|
||||||
|
3. GAP-18: 4 קריאות audit (upload, extract_claims, export_docx + write_block בסיס).
|
||||||
|
4. GAP-19: איסוף source ids ב-write_block → provenance ב-audit.
|
||||||
|
5. GAP-20: ולידציית-ציטוט ב-QA + audit `citation_unresolved`.
|
||||||
|
6. GAP-17: `blocks_stale` ב-revise/apply/export/write_block + health-check.
|
||||||
|
7. בדיקות ירוקות + smoke מול DB + lint + TaskMaster.
|
||||||
168
docs/superpowers/specs/2026-05-30-system-spec-design.md
Normal file
168
docs/superpowers/specs/2026-05-30-system-spec-design.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# מסמך-עיצוב אב — ספ המערכת והשכבה החסרה (System Spec & Integrity Layer)
|
||||||
|
|
||||||
|
**תאריך:** 2026-05-30
|
||||||
|
**סטטוס:** עיצוב מאושר (Design approved) — ממתין לכתיבת קבצי הספ
|
||||||
|
**בעלים:** חיים מרכוס
|
||||||
|
**הקשר:** מהלך-יסוד להגדרת "מהו תקין" במערכת *עוזר משפטי*, ולסגירת כשל-שורש חוזר.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. הבעיה — כשל-השורש החוזר
|
||||||
|
|
||||||
|
מה שנחווה כ"כל פעם משהו אחר לא מדויק" אינו אוסף תקלות אקראיות אלא **כשל אחד שחוזר בתחפושות**. ראיות שצפו (30.5.2026):
|
||||||
|
|
||||||
|
| תסמין | שורש |
|
||||||
|
|--------|------|
|
||||||
|
| `8126-25` לא נמצא (האמיתי `8126-03-25`); קומיט "tolerant case_number lookup" | אין מפתח קנוני — מתקנים תסמין בקריאה |
|
||||||
|
| 3 החלטות "סופר" ב-3 פורמטים שונים (`8126/24`, ציטוט-מלא-כ-case_number) | אין חוזה-נתונים אחיד |
|
||||||
|
| ערן סופר 8046/24 עלתה בלי metadata (headnote/summary/tags ריקים) | מסלול ה-ingest הפנימי לא מתזמן חילוץ metadata — אסימטרי למסלול החיצוני |
|
||||||
|
| 10/19 הלכות מאושרות, התגלה במקרה | שער ידני שקוף בלי נראות backlog |
|
||||||
|
| משימות #56, #57 | אי-עקביות בין רכיבים (דליפה חוצת-קורפוסים, chunker) |
|
||||||
|
|
||||||
|
**אבחנה:** המערכת גדלה בקצב *הוספת יכולות* מהר יותר מקצב *שמירת עקביות* — מסלולים/כלים/קורפוסים מקבילים שנוספים בבידוד ומתפצלים (drift), בלי שכבה שמגדירה ואוכפת "תקין". כל פגם מתגלה בדיעבד, אחד-אחד.
|
||||||
|
|
||||||
|
**התרופה:** לא לתקן 10 דברים — להוסיף **שכבה אחת חסרה**: חוקה + חוזה-שלמות + בדיקת-בריאות אחת + איחוד מסלולי ה-ingest. זה הופך כשל מ"מתפרץ במקום אקראי" ל"נחסם בכניסה, גלוי בדשבורד".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. ייעוד המערכת (מאושר ע"י חיים)
|
||||||
|
|
||||||
|
> מערכת AI שמסייעת ליו"ר ועדת הערר לתכנון ובנייה (מחוז ירושלים, עו"ד דפנה תמיר) לנסח **החלטות מעין-שיפוטיות כתובות ומנומקות** — מסמכים משפטיים פורמליים שעומדים לביקורת שיפוטית — תוך שמירה על **הקול, השיקול והאחריות של היו"ר**.
|
||||||
|
|
||||||
|
- **משרת:** יו"ר הוועדה (משתמש-על) והסוכנים הפועלים בשמה.
|
||||||
|
- **מחזור-חיים:** ניהול תיקים → בסיס ידע (3 קורפוסים) → אחזור סמנטי (RAG) → סיוע-כתיבה (12 בלוקים, סגנון דפנה) → ייצוא DOCX.
|
||||||
|
- **3 סוגי עררים:** רישוי ובנייה (1xxx, חם), היטל השבחה (8xxx, קר), פיצויים ס'197 (9xxx, קר).
|
||||||
|
- **ה"למה" העמוק:** המערכת מסייעת — היו"ר מכריעה (שערים קריטיים ידניים בכוונה); מנוע צבירת-ידע (לומד מהחלטות סופיות ומפידבק); רב-חברתי (CMP/CMPA).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. עקרונות-עבודה למהלך
|
||||||
|
|
||||||
|
1. **אסור להניח שהקיים תקין.** כל מה שמופה בקוד/בקורפוס = "טענה לבדיקה", לא "אמת". "תקין" נגזר ממקורות חיצוניים, לא מהמערכת שתחת חשד.
|
||||||
|
2. **פרוטוקול אימות 3-מקורות:** כל invariant/חוק בספ מגובה ב-**≥3 מקורות סמכותיים מוכרים** בעלי ידע מקצועי מוכח. כשאין 3 → מסומן `⚠ UNVERIFIED` ומועלה לחיים, לא מוכרע לבד.
|
||||||
|
3. **מנגנון:** מחקר עצמאי → טיוטה לביקורת.
|
||||||
|
4. **מודל-שיתוף:** על החלטות טכניות/אדריכליות אני חוקר ומכריע מקצועית ומציג תוצאה מוגמרת. שואל את חיים רק במקום שבו *הוא* הסמכות — כוונה, עדיפויות עסקיות, עובדות משפטיות-דומייניות.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. פירוק ל-5 תת-פרויקטים (לפי תלות)
|
||||||
|
|
||||||
|
| # | תת-פרויקט | תוצר | תלות |
|
||||||
|
|---|-----------|------|------|
|
||||||
|
| 1 | **ספ המערכת + חוקה** | spec-set ב-`docs/spec/` המגדיר מודל קנוני + invariants | — |
|
||||||
|
| 2 | **מפת הפערים (Audit)** | סריקה אמפירית מול הספ → רשימת משימות | תת-פרויקט 1 |
|
||||||
|
| 3 | **שכבת שלמות-נתונים** | חוזה-שלמות באכיפת-קוד + בדיקת-בריאות אחת + **איחוד מסלולי ingest** | 1, 2 |
|
||||||
|
| 4 | **בדיקה חוזרת** | הרצת בריאות/audit אחרי התיקון | 3 |
|
||||||
|
| 5 | **סוכני-תהליך** | add-feature / fix-feature / spec-guardian — מכירים את הספ, "עושים שיעורי בית", לומדים ומתעדכנים | 1 (3) |
|
||||||
|
|
||||||
|
כל תת-פרויקט יקבל מחזור spec→plan→implementation משלו. מסמך זה מפרט את **תת-פרויקט 1** במלואו ומקבע את ההחלטות העקרוניות לכולם.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. מבנה הספ-set (תת-פרויקט 1)
|
||||||
|
|
||||||
|
מיקום: **`docs/spec/`** (ספ חי). ארגון קבצי-תחום: **לפי מחזור-חיים** (גישה A) — חושף ישירות אסימטריות-זרימה.
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/spec/
|
||||||
|
├── 00-constitution.md ← ייעוד · invariants גלובליים · כללי-הנדסה · אינדקס · תבנית-invariant · פרוטוקול-אימות
|
||||||
|
│ ── מחזור-החיים ──
|
||||||
|
├── 01-ingest.md ← קליטה מאוחדת: מסמכי-תיק / פסיקה חיצונית / החלטות-ועדה — חוזה מסלול-יחיד
|
||||||
|
├── 02-data-model.md ← אחסון: ישויות (cases, case_law, documents, chunks, halachot…) + חוזה-שלמות לכל ישות
|
||||||
|
├── 03-retrieval.md ← 3 קורפוסים + כלי-חיפוש · hybrid/RRF · attribution · eval harness · invariants
|
||||||
|
├── 04-analysis-writing.md ← חילוץ טענות · 12 בלוקים · סגנון דפנה (מצטט block-schema.md וכו')
|
||||||
|
├── 05-qa-review.md ← שערי QA + שערים אנושיים (אישור הלכה, בחירת תוצאה, פידבק) כ-invariant
|
||||||
|
├── 06-export.md ← ייצוא DOCX לפי תבנית דפנה
|
||||||
|
├── 07-learning.md ← Hermes · לקחים · לולאת פידבק היו"ר · צמיחת קורפוס (quality-at-source)
|
||||||
|
│ ── חוצי-שלבים ──
|
||||||
|
├── X1-identifiers.md ← מודל מזהים קנוני: נרמול case_number **בכתיבה** · cases מול case_law · פורמטי ציטוט
|
||||||
|
├── X2-multi-company.md ← CMP/CMPA · 14 סוכנים · כללי sync
|
||||||
|
├── X3-integration-deploy.md ← Paperclip (wakeup, ניתוב comments, webhooks) · Coolify/pm2
|
||||||
|
├── X4-agents.md ← מפת הסוכנים (דומיין + סוכני-התהליך מתת-פרויקט 5)
|
||||||
|
└── X5-audit-provenance.md ← audit-trail לשימוש ב-AI · עקיבוּת כל מקור מצוטט · שלמות-רשומה (CEPEJ/NCSC/ISO 15489)
|
||||||
|
```
|
||||||
|
|
||||||
|
**עקרונות:** כל קובץ עצמאי, ממוקד, agent-readable, יעד ≤~500 שורות (תפיחה = סימן לפיצול). `00-constitution.md` = שער-כניסה יחיד. מסמכים קיימים (`architecture.md`, `product-specification.md`, `block-schema.md`…) לא נמחקים ולא משוכפלים — מצוטטים כ"מקור" ומאומתים מול הסמכויות; סתירה = ממצא ל-audit.
|
||||||
|
|
||||||
|
### תבנית-invariant (מבנה אחיד לכל חוק בספ)
|
||||||
|
```
|
||||||
|
### INV-<תחום><מספר>: <כותרת קצרה>
|
||||||
|
**כלל:** <ניסוח נורמטיבי חד — מה חייב להתקיים>
|
||||||
|
**מקורות:** <≥3 סמכויות> | סטטוס: verified / ⚠ UNVERIFIED
|
||||||
|
**אכיפה:** <היכן/איך נאכף — schema, ולידציית-כתיבה, בדיקת-בריאות, שער>
|
||||||
|
**הפרה ידועה:** <דוגמה מהמערכת, אם יש — מקשר ל-audit>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. ה-Invariants הגלובליים (לב `00-constitution.md`)
|
||||||
|
|
||||||
|
כל אחד מגובה ב-≥3 סמכויות (פירוט ב-§9). אלה החוקים שמייבשים את כשל-השורש:
|
||||||
|
|
||||||
|
| # | Invariant | סמכויות |
|
||||||
|
|---|-----------|---------|
|
||||||
|
| **G1** | מזהה קנוני, **מנורמל בכתיבה** (לא תיקון-סלחני בקריאה בלבד) | SSOT/normalization · Codd 1NF · Kleppmann |
|
||||||
|
| **G2** | מקור-אמת יחיד; **אין מסלולי-קוד מקבילים שמתפצלים** — אחים חולקים מסלול קנוני אחד; derived data משוחזר | Kleppmann (system of record) · Fowler (canonical model) · SSOT |
|
||||||
|
| **G3** | ingest **אחיד ו-idempotent** (upsert על מפתח דטרמיניסטי) | Kleppmann · Stripe/CDC idempotency · ISO 8000 |
|
||||||
|
| **G4** | **חוזה-שלמות:** שדות חובה מולאו לפני שרשומה "שמישה/ניתנת-לחיפוש"; נבדק מול spec מפורש | ISO 8000 · DAMA (completeness) · ISO 15489 (reliability) |
|
||||||
|
| **G5** | metadata מלא לכל פריט מואנדקס + **הפרדת-קורפוס נאכפת בכל מסלול-query** | Pinecone (multitenancy) · RAG attribution · ISO 8000 |
|
||||||
|
| **G6** | **re-index בכל שינוי תוכן** (אין embeddings מיושנים) | Pinecone · Weaviate · RAG freshness |
|
||||||
|
| **G7** | מיזוג **לפי דירוג (RRF)**, לא סכום-ציונים גולמי בין retrievers | Elastic · Weaviate · OpenSearch/Azure (corrob.) |
|
||||||
|
| **G8** | איכות-אחזור **נמדדת (precision+recall)**, לא מונחת | Manning (IR textbook) · RAG eval literature |
|
||||||
|
| **G9** | כל פלט **עקיב למקורו** + audit-trail לשימוש ב-AI | CEPEJ (user control) · NCSC · ISO 15489 |
|
||||||
|
| **G10** | המערכת מסייעת; **שערים אנושיים** (אישור הלכה/תוצאה/פידבק) הם invariant, לא רשות | NCSC · CEPEJ · FJC |
|
||||||
|
| **G11** | **תוכן החלטה מנומקת:** רקע ניטרלי · ללא כפילות · מענה לטענות המפסיד · מבחן-השופט · טענות מקוריות | FJC (Writing Manual) · South Bucks (adequacy) · חוק 1958 (חובת הנמקה) |
|
||||||
|
|
||||||
|
### כללי-הנדסה (constitution — מונעים הישנות)
|
||||||
|
- **סימטריה:** אסור להוסיף מסלול מקביל ליכולת קיימת — מרחיבים את המסלול הקנוני. (נגזר מ-G2)
|
||||||
|
- **נרמול לא תיקון-תסמין:** מתקנים נתון במקור (קנוני), לא מטליאים בקריאה. (נגזר מ-G1)
|
||||||
|
- **Quality-at-source:** שלמות נאכפת קרוב ככל האפשר לקליטה. (Fowler/Data-Mesh)
|
||||||
|
- **אין בליעה שקטה:** רשומה חסרה מסומנת ומדווחת, לא מתקבלת בשקט. (תואם feedback קיים)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. פרוטוקול-אימות ומודל-שיתוף (ייכנס ל-`00-constitution.md`)
|
||||||
|
|
||||||
|
- כל invariant נושא `מקורות` + `סטטוס: verified / ⚠ UNVERIFIED`.
|
||||||
|
- `⚠ UNVERIFIED` (פחות מ-3 מקורות) → לא מוכרע לבד; מועלה לחיים.
|
||||||
|
- החלטות טכניות → מחקר עצמאי + הכרעה מקצועית + הצגת תוצאה. שאלה לחיים רק במקום שהוא הסמכות.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. פריטים פתוחים — אימות-מקור-ראשוני נדרש
|
||||||
|
(החוקר אימת מסגרת; הפריטים הישראליים דורשים אימות לפני ציטוט כ-סמכות, בשלב כתיבת `04`/`05`/`X5`)
|
||||||
|
1. מספר הסעיף המדויק בחוק לתיקון סדרי המינהל (החלטות והנמקות) תשי"ט-1958 (וכן תיקון תשכ"ט-1969).
|
||||||
|
2. ציטוט מדויק מ-ברק-ארז, *משפט מינהלי*.
|
||||||
|
3. אסמכתאות פסיקה: בג"ץ 143/56; עע"ם 2994/21 (מעמד ועדת ערר כגוף תכנוני-מקצועי).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. נספח מקורות סמכותיים (מאומתים במחקר 30.5.2026)
|
||||||
|
|
||||||
|
**ממשל-AI שיפוטי + מבנה החלטה מנומקת**
|
||||||
|
- NCSC / JTC — *Court Technology Standards* + *Principles & Practices for AI Use in Courts*. https://www.ncsc.org/our-centers-projects/joint-technology-committee/court-technology-standards
|
||||||
|
- Federal Judicial Center — *Judicial Writing Manual* (2d ed.). https://www.fjc.gov/content/judicial-writing-manual-pocket-guide-judges-second-edition
|
||||||
|
- Council of Europe / CEPEJ — *European Ethical Charter on the use of AI in judicial systems* (2018).
|
||||||
|
- *South Buckinghamshire DC v Porter (No 2)* [2004] UKHL 33 (adequacy of reasons). https://publications.parliament.uk/pa/ld200304/ldjudgmt/jd040701/south-1.htm
|
||||||
|
- חוק לתיקון סדרי המינהל (החלטות והנמקות), תשי"ט-1958. https://www.nevo.co.il/law_html/law00/98603.htm
|
||||||
|
- Kevin D. Ashley — *Artificial Intelligence and Legal Analytics* (CUP).
|
||||||
|
|
||||||
|
**אחזור / RAG / IR**
|
||||||
|
- Lewis et al. (2020) — *Retrieval-Augmented Generation* (NeurIPS). https://arxiv.org/abs/2005.11401
|
||||||
|
- Manning, Raghavan & Schütze — *Introduction to Information Retrieval* (CUP, 2008). https://nlp.stanford.edu/IR-book/
|
||||||
|
- Elastic — *Reciprocal Rank Fusion*. https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion
|
||||||
|
- Pinecone — *Implement multitenancy*. https://docs.pinecone.io/guides/index-data/implement-multitenancy
|
||||||
|
- Weaviate — *Hybrid Search Explained*. https://weaviate.io/blog/hybrid-search-explained
|
||||||
|
|
||||||
|
**שלמות-נתונים / איכות / רשומות**
|
||||||
|
- DAMA-DMBOK2 + DAMA-UK — *Six Primary Dimensions for Data Quality* (2013).
|
||||||
|
- ISO 8000 — Data quality (8000-8/61/110).
|
||||||
|
- ISO 15489-1:2016 — Records management (authenticity/reliability/integrity/usability).
|
||||||
|
- Martin Kleppmann — *Designing Data-Intensive Applications* (O'Reilly, 2017).
|
||||||
|
- E.F. Codd — Relational model & normalization (CACM 13(6), 1970).
|
||||||
|
- Martin Fowler — Canonical Data Model / Data Mesh (quality-at-source).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. השלב הבא
|
||||||
|
לאחר ביקורת חיים על מסמך זה → invoke `writing-plans` לבניית תוכנית-יישום מפורטת לתת-פרויקט 1 (כתיבת קבצי הספ-set, החל מ-`00-constitution.md`).
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# FU-2b — תיאום מזהי `case_number` (Identifier Reconciliation) — עיצוב
|
||||||
|
|
||||||
|
**סטטוס:** מאושר-לעיצוב · **תאריך:** 2026-05-31 · **ענף:** TBD
|
||||||
|
**מכסה:** GAP-07, GAP-08 (scope: `internal_committee` בלבד) · **מספק:** INV-ID1, INV-ID2, INV-DM2
|
||||||
|
**משימה:** TaskMaster #67 · **תלוי ב:** FU-2a (#60, פונקציית הנרמול) · **סוג:** **data-migration + chair-gate**
|
||||||
|
**מחוץ-להיקף:** external_upload → **#68 / FU-2c** (נתונים סותרים, ראה §1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. הבעיה והיקף (מאומת מול DB, 2026-05-31)
|
||||||
|
|
||||||
|
`internal_committee` הוא הקורפוס שבו `case_number` חייב להיות **מספר-ועדה מנורמל** (X1 §1), אך
|
||||||
|
~52/56 רשומות מחזיקות **ציטוט-מלא** בשדה-המזהה (GAP-08 — "החלטות סופר"), בניגוד ל-INV-ID2
|
||||||
|
(ציטוט = שדה-תצוגה נגזר, לעולם לא מזהה).
|
||||||
|
|
||||||
|
**ממצאי-נתונים שמעצבים את המיגרציה:**
|
||||||
|
- **חילוץ דטרמיניסטי ונקי:** כל 56 הרשומות → בדיוק token-מספר אחד (regex `[0-9]{2,6}(?:[-/][0-9]{1,2}){1,2}`). 0 רב-משמעיים, 0 בלתי-פתירים.
|
||||||
|
- **עקביות מושלמת:** ב-55/56 המספר המחולץ **מופיע** ב-`citation_formatted`; **0 סתירות**. (1 רשומה בלי citation_formatted — כבר bare.)
|
||||||
|
- **0 התנגשויות-מפתח** על (bare, proceeding_type) → **אין dedup**.
|
||||||
|
- **אין בעיית with/without-month:** ה"צורות הכפולות" (1024-24 מול 1024-25 וכו') הן **שנים שונות** = תיקים שונים, לא padding.
|
||||||
|
- **edge יחיד ליו"ר:** `8047/23` קיים פעמיים — אחת `proceeding_type=ערר`, אחת `בל"מ` (48 chunks כל אחת). לפי X1 אלו **שתי רשומות מובחנות** (ערר מול בל"מ), אך זהות chunk-count מצדיקה אימות-יו"ר שאינן כפילות מתויגת-שגוי.
|
||||||
|
|
||||||
|
**external מופרד (#68):** ב-external נמצאה **סתירה** (`case_number=25226-04-25` מול
|
||||||
|
`citation_formatted=1975/24`) — ה-citation_formatted נוצר בנפרד ואינו ground-truth אמין; דורש
|
||||||
|
טיפול נפרד. בנוסף, זהות פסיקה-חיצונית היא טבעית הציטוט (אין מספר-ועדה). מחוץ ל-FU-2b.
|
||||||
|
|
||||||
|
## 2. ההכרעה (מבוססת X1 + ממצאי-נתונים)
|
||||||
|
|
||||||
|
הצורה הקנונית של `case_number` ל-internal = **trim · prefix-strip · `/`→`-`** על המספר הרשמי,
|
||||||
|
**בלי להמציא/להסיר חודש** (X1 §1; מקורות: Codd 1NF · Kleppmann DDIA · SSOT — verified ב-X1).
|
||||||
|
המיגרציה **דטרמיניסטית** (לא LLM): מחלצת את ה-token המספרי היחיד ומנרמלת. הציטוט כבר חי
|
||||||
|
ב-`citation_formatted` — אין מה לנגוע בו.
|
||||||
|
|
||||||
|
**דפוס-בטיחות (chair-gated reversible migration):** גיבוי-לפני-שינוי → dry-run שמפיק טבלת-תיאום
|
||||||
|
→ **שער-אישור-יו"ר** → apply מפורש → אימות. זהו דפוס סטנדרטי למיגרציה בלתי-הפיכה על נתוני-ייצור.
|
||||||
|
|
||||||
|
## 3. הרכיבים
|
||||||
|
|
||||||
|
- **סקריפט** `scripts/fu2b_reconcile_internal_case_numbers.py` (לא MCP tool — מיגרציה חד-פעמית מבוקרת):
|
||||||
|
- `--dry-run` (ברירת-מחדל): מפיק טבלת-תיאום `data/audit/fu2b-reconciliation-<ts>.csv` +
|
||||||
|
`.md` קריא ליו"ר. עמודות: `id, current_case_number, proposed_bare, proceeding_type,
|
||||||
|
citation_formatted, consistency_ok, flag`.
|
||||||
|
- `--apply`: דורש קובץ-אישור (ראה §4); מגבה ואז מבצע.
|
||||||
|
- מעבד **רק** `source_kind='internal_committee'` ו**רק** רשומות שבהן `proposed_bare != case_number`
|
||||||
|
(idempotent — already-bare לא נוגעים).
|
||||||
|
- **חילוץ:** `_extract_bare(case_number) -> str|None` — regex token יחיד + `_canonical_case_number`
|
||||||
|
(מ-FU-2a, db.py) לנרמול הסופי. אם 0 או >1 tokens → `None` + flag `NEEDS_CHAIR`.
|
||||||
|
- **consistency guard:** אם `proposed_bare` **לא** מופיע ב-`citation_formatted` → flag `MISMATCH` (לא
|
||||||
|
יוחל אוטומטית; ליו"ר). (כיום 0 כאלה, אך הסקריפט בודק בזמן-ריצה.)
|
||||||
|
- **גיבוי:** לפני apply, כתיבת `data/audit/fu2b-backup-<ts>.csv` = `(id, old_case_number)` לכל רשומה
|
||||||
|
שתשונה → revert-script טריוויאלי.
|
||||||
|
- **edge 8047/23:** הסקריפט **לא** ממזג; מסמן את הזוג ב-flag `DUP_CHECK` בטבלה. ההכרעה (מובחנות מול
|
||||||
|
כפילות) היא של היו"ר; אם כפילות — מחיקה ידנית נפרדת (לא חלק מה-apply הדטרמיניסטי).
|
||||||
|
|
||||||
|
## 4. שער-אישור-היו"ר (chair gate)
|
||||||
|
|
||||||
|
1. הרצת `--dry-run` → טבלת-תיאום (`.md`) + סיכום (כמה ישתנו, אילו flags).
|
||||||
|
2. **הצגה לדפנה**: הטבלה (52 שורות: ציטוט-נוכחי → bare מוצע) + ה-edge של 8047/23. היא מסמנת
|
||||||
|
שורות שגויות (אם יש) ומכריעה על 8047/23.
|
||||||
|
3. תיקון flags לפי הערותיה (אם יש), ואז `--apply --approved data/audit/fu2b-approved-<ts>.csv`
|
||||||
|
(קובץ-האישור = הטבלה לאחר סקירתה; הסקריפט מחיל רק שורות שאושרו).
|
||||||
|
4. אימות אחרי apply: כל internal `case_number` תואם regex bare; 0 ציטוטים בשדה-המזהה;
|
||||||
|
`search`/`get_case_by_number` עדיין פותרים (FU-2a tolerant-read + הנרמול).
|
||||||
|
|
||||||
|
## 5. אינטראקציה עם FU-2a (forward-consistency)
|
||||||
|
|
||||||
|
FU-2a `_canonical_case_number` מנרמל prefix+separator אך **אינו מחלץ מספר מתוך ציטוט-מלא**. לכן
|
||||||
|
אם קליטה עתידית תעביר ציטוט-מלא כ-`case_number`, ייווצר שוב מזהה מלוכלך. **הערכת-סיכון:** נמוכה —
|
||||||
|
טופס-ההעלאה וה-MCP tool מעבירים שדה-`case_number` נפרד (בד"כ נקי). **החלטה:** FU-2b הוא ניקוי-נתונים
|
||||||
|
בלבד; הקשחת-כתיבה (חילוץ-token גם ב-create) **לא בהיקף** — תיפתח רק אם יתגלה caller שמעביר ציטוט.
|
||||||
|
(מתועד; לא לשנות התנהגות-כתיבה בלי ראיה.)
|
||||||
|
|
||||||
|
## 6. שינויי-התנהגות וסיכון
|
||||||
|
|
||||||
|
| שינוי | השפעה | סיכון |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| `case_number` של ~52 internal → bare | חיפוש exact-match על המספר עובד; (case_number,proceeding_type) נקי | נמוך — דטרמיניסטי, גיבוי, שער-יו"ר, 0 collisions |
|
||||||
|
| 8047/23 edge | אולי מחיקת רשומה כפולה | בינוני — **רק** בהחלטת-יו"ר, מחיקה ידנית נפרדת, לא ב-apply האוטומטי |
|
||||||
|
| citation_formatted | **לא משתנה** (כבר תקין) | אין |
|
||||||
|
| FK/relations | `case_law_relations`/`precedent_internal_citations` מפנים ל-`id` (UUID), לא ל-case_number | אין — שינוי case_number לא שובר קשרים |
|
||||||
|
| chunks/embeddings | מפתח-זר `case_law_id` (UUID) — לא תלוי ב-case_number | אין — re-index לא נדרש |
|
||||||
|
|
||||||
|
## 7. אסטרטגיית בדיקה
|
||||||
|
|
||||||
|
- **בדיקות-יחידה offline** (`tests/test_fu2b_reconcile.py`): `_extract_bare` — token יחיד→bare מנורמל;
|
||||||
|
ציטוט מלא→המספר הנכון (דוגמאות אמיתיות: `"ערר (...) 403/17 אהרון ברק..."`→`403-17`,
|
||||||
|
`"...8136-10-24 שחר..."`→`8136-10-24` חודש נשמר); 0/רב-token→None+flag; consistency guard.
|
||||||
|
- **dry-run מול DB מקומי**: הטבלה מופקת, מספר-השורות-לשינוי = ~52, 0 MISMATCH, 1 DUP_CHECK (8047).
|
||||||
|
- **apply בסביבת-בדיקה**: על עותק/תיק-בדיקה — אימות idempotency (הרצה שנייה = 0 שינויים) + revert מהגיבוי.
|
||||||
|
- ה-apply בייצור רץ **רק אחרי אישור-יו"ר** (לא חלק מה-CI/PR; ידני ומבוקר).
|
||||||
|
|
||||||
|
## 8. סדר-ביצוע
|
||||||
|
|
||||||
|
1. בדיקות אדומות ל-`_extract_bare` + consistency guard.
|
||||||
|
2. `_extract_bare` + הסקריפט (`--dry-run` בלבד תחילה) + הפקת טבלת-תיאום + גיבוי.
|
||||||
|
3. בדיקות ירוקות + dry-run מול DB → הפקת הטבלה.
|
||||||
|
4. **עצירה: הצגת הטבלה + 8047/23 ליו"ר (דפנה)** — שער-אישור.
|
||||||
|
5. (אחרי אישור) מימוש `--apply --approved` + אימות + revert-script.
|
||||||
|
6. הרצת apply בייצור (מבוקר) + אימות-אחרי + TaskMaster #67.
|
||||||
|
|
||||||
|
> צעדים 1–3 לא דורשים את דפנה (אני מכין הכל). צעד 4 הוא שער-האישור. צעדים 5–6 אחרי אישורה.
|
||||||
92
docs/superpowers/specs/2026-05-31-fu5-eval-harness-design.md
Normal file
92
docs/superpowers/specs/2026-05-31-fu5-eval-harness-design.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# FU-5 — Retrieval Eval Harness + Backlog Visibility (design)
|
||||||
|
|
||||||
|
**Task:** #63 (legal-ai tag) · **Covers:** GAP-11, GAP-14 · **Provides:** INV-RET4, G8, INV-QA1, G10
|
||||||
|
**Status:** approved 2026-05-31 (gold-set strategy = hybrid, chair decision). Technical architecture
|
||||||
|
decided per `feedback_research_architecture_decisions` (chair adjudicates domain, not architecture).
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
1. **GAP-11 (INV-RET4/G8):** retrieval quality is never measured. Only `telemetry.log_search_bg`
|
||||||
|
records queries (observation, not evaluation). No gold-set, no precision/recall. Every RRF-weight
|
||||||
|
/ `k` / embedder change is tuned "by feel".
|
||||||
|
2. **GAP-14 (INV-QA1/G10):** the halacha review backlog (`review_status='pending_review'`) is
|
||||||
|
invisible — the 10/19-approved gap was found by accident. The human gate has no visibility.
|
||||||
|
|
||||||
|
## Two independent units
|
||||||
|
|
||||||
|
### Unit A — Retrieval eval harness (GAP-11)
|
||||||
|
|
||||||
|
**Existing leverage:** `search_relevance_feedback` already captures a real ground-truth signal —
|
||||||
|
when a finalized decision cites a precedent, `infer_relevance_from_citations` marks it
|
||||||
|
`relevance_score=3` against the `search_logs` where it appeared (telemetry.py). This bootstraps the
|
||||||
|
gold-set without hand-labeling.
|
||||||
|
|
||||||
|
**A1. Gold-set — versioned file `data/eval/gold-set.jsonl`** (single SoT; reviewable/diffable/
|
||||||
|
chair-editable). One JSON object per line:
|
||||||
|
```json
|
||||||
|
{"id":"g001","query":"...","practice_area":"betterment_levy",
|
||||||
|
"corpus":"precedent_library|internal_decisions",
|
||||||
|
"relevant_case_law_ids":["uuid",...],"source":"bootstrap|chair","note":""}
|
||||||
|
```
|
||||||
|
|
||||||
|
**A2. Bootstrap generator — `scripts/eval_gold_bootstrap.py`** (host-side, mcp-server venv):
|
||||||
|
reads `search_relevance_feedback` (score=3) ⨝ `search_logs`, groups by normalized query →
|
||||||
|
relevant `case_law_id` set, emits `source=bootstrap` entries. Idempotent: re-run regenerates the
|
||||||
|
bootstrap section; never overwrites `source=chair` rows. **Chair gate:** Dafna reviews the file,
|
||||||
|
corrects/augments, promotes entries to `source=chair`.
|
||||||
|
|
||||||
|
**A3. Harness — `scripts/eval_retrieval.py`** (host-side, mcp-server venv; needs POSTGRES + VOYAGE):
|
||||||
|
runs the **production retrieval path** (same service functions the MCP search tools call) for each
|
||||||
|
gold query, computes per-query **precision@k, recall@k, MRR, nDCG@k** (k∈{5,10}); relevant = gold
|
||||||
|
ids. Aggregates mean overall + per corpus + per practice_area. Writes
|
||||||
|
`data/eval/eval-report-<ts>.{json,md}`, prints a summary, and a delta vs the committed
|
||||||
|
`data/eval/baseline.json`. `--update-baseline` rewrites the snapshot.
|
||||||
|
|
||||||
|
**"CI gate" — realized as discipline, not automation.** Retrieval needs the prod DB + Voyage API;
|
||||||
|
no CI runner has that access. The gate is: re-runnable harness + committed `baseline.json` + a
|
||||||
|
documented "run before/after any retrieval-layer change, attach the delta" rule (SCRIPTS.md). A true
|
||||||
|
automated CI gate would require a separate frozen corpus fixture — out of scope, noted as future.
|
||||||
|
|
||||||
|
**Scope:** the two precedent corpora (`search_precedent_library` + `search_internal_decisions`),
|
||||||
|
where the citation signal exists. `search_decisions`/`search_case_documents` return case-document
|
||||||
|
chunks (not `case_law`) and carry no citation ground-truth — deliberately out of scope.
|
||||||
|
|
||||||
|
**Metrics rationale:** precision@k + recall@k are spec-required (INV-RET4). MRR (first-relevant
|
||||||
|
rank) and nDCG@k (graded, position-weighted) are standard IR complements (Manning et al., 2008) —
|
||||||
|
nDCG matches the telemetry docstring's stated nDCG@10 aspiration.
|
||||||
|
|
||||||
|
### Unit B — Backlog visibility (GAP-14) — pure code
|
||||||
|
|
||||||
|
Expose the halacha review backlog where health is already surfaced:
|
||||||
|
- **`metrics.get_dashboard()`** (mcp-server/src/legal_mcp/services/metrics.py) — add
|
||||||
|
`halacha_backlog: {pending_review, approved, rejected, published, total, oldest_pending_at}` from
|
||||||
|
`halachot.review_status` + `min(created_at) where pending_review`. Surfaces through the
|
||||||
|
`get_metrics` MCP tool (agents + dashboard).
|
||||||
|
- **`/api/system/diagnostics`** (web/app.py) — add the same `halacha_backlog` block to the health
|
||||||
|
snapshot.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Unit | Kind | Deploy |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| `scripts/eval_gold_bootstrap.py` | A2 | new, host-side | none |
|
||||||
|
| `scripts/eval_retrieval.py` | A3 | new, host-side | none |
|
||||||
|
| `data/eval/gold-set.jsonl` | A1 | data (on disk; chair-reviewed) | none |
|
||||||
|
| `data/eval/baseline.json` | A3 | committed snapshot | none |
|
||||||
|
| `mcp-server/src/legal_mcp/services/metrics.py` | B | edit `get_dashboard` | Coolify |
|
||||||
|
| `web/app.py` | B | edit diagnostics | Coolify |
|
||||||
|
| `scripts/SCRIPTS.md` | A | doc | none |
|
||||||
|
|
||||||
|
## Test strategy
|
||||||
|
|
||||||
|
- Bootstrap: idempotent (re-run = same bootstrap rows; chair rows untouched); 0 chair rows clobbered.
|
||||||
|
- Harness: metric math unit-verified offline on a synthetic (ranking, relevant-set) fixture
|
||||||
|
(precision@k / recall@k / MRR / nDCG@k against hand-computed values) before any DB run.
|
||||||
|
- Unit B: `get_metrics` (no case_number) returns `halacha_backlog` with counts summing to total;
|
||||||
|
diagnostics endpoint returns the same block. Verified against prod counts.
|
||||||
|
|
||||||
|
## Chair gate (domain — the only thing requiring Dafna)
|
||||||
|
|
||||||
|
After bootstrap produces `gold-set.jsonl`, Dafna reviews: are these queries representative, and are
|
||||||
|
the marked precedents the *correct* answers? Her edits make the gold-set authoritative. Until then
|
||||||
|
the baseline is "provisional (bootstrap-only)".
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# FU-8a — מחסומי-תהליך → מחסומי-קוד (Process Barriers → Code Guards) — עיצוב
|
||||||
|
|
||||||
|
**סטטוס:** מאושר-לעיצוב · **תאריך:** 2026-05-31 · **ענף:** TBD
|
||||||
|
**מכסה:** GAP-21, GAP-22 · **מספק:** INV-MC1, INV-INT1, INV-INT3 · **משימה:** TaskMaster #66
|
||||||
|
**תלוי ב:** — · **סוג:** pure-code · **מחוץ-להיקף:** GAP-23 (חיווט ספ→סוכנים) → #69 / FU-8b.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. הבעיה
|
||||||
|
|
||||||
|
שני מחסומים שהיום נשענים על **נוהל אנושי** ולא על **קוד**, ולכן ניתנים להפרה שקטה:
|
||||||
|
|
||||||
|
- **GAP-21 (INV-MC1):** סנכרון-סוכנים חוצה-חברות (`sync_agents_across_companies.py`) ידני ולא-נאכף.
|
||||||
|
ב-`--verify` (script:397) הוא **יוצא 0 גם כשיש drift**, ו-adapter_type-mismatch (script:388)
|
||||||
|
מודפס כ-"SKIPPING" ו**נבלע** — אין סיגנל-כשל שניתן לתלות בו gate.
|
||||||
|
- **GAP-22 (INV-INT1/INT3):** אין מחסום-קוד נגד עקיפת ה-helpers המאושרים של Paperclip — קריאת
|
||||||
|
`httpx`/`requests` גולמית ל-API של Paperclip (במקום `web/paperclip_api.pc_request`) או `INSERT`
|
||||||
|
ישיר ל-`agent_wakeup_requests` (במקום ה-wakeup API). היום זה כלל-נוהל ב-CLAUDE.md/HEARTBEAT בלבד.
|
||||||
|
|
||||||
|
## 2. ההכרעה (מאומתת ≥3 מקורות)
|
||||||
|
|
||||||
|
**Architectural fitness functions** — הופכים החלטת-ארכיטקטורה מ"הסכמה חברתית" ל**כלל נאכף שנתפס
|
||||||
|
ברגע ההפרה**, כ-assertion דמוי-טסט ב-CI. שני המחסומים מיושמים ככאלה:
|
||||||
|
|
||||||
|
| החלטה | נימוק | מקורות |
|
||||||
|
|-------|--------|--------|
|
||||||
|
| GAP-21: `--verify` יוצא **non-zero על drift**; adapter_type-mismatch = **drift** (לא silent skip) + דיווח רם | drift-verify הוא gate רק אם הוא יוצא non-zero (Terraform 0/2); "alert, don't skip silently" | InfoQ fitness-functions; Firefly CI-drift; Spacelift drift |
|
||||||
|
| GAP-22: **fitness-function (pytest שסורק את ה-repo)**, לא import-linter | הכלל הוא דפוס-*שימוש*/מחרוזת (http ל-URL, מחרוזת-SQL), לא גבול-import; fitness-function כ-test-assertion ב-CI הוא הכלי | InfoQ; Lukas Niessen; aipatternbook |
|
||||||
|
| לרוץ ב-**חבילת-הטסטים הקיימת** (לא CI חדש) | אין CI-lint בפרויקט (רק deploy.yaml); חבילת ה-pytest היא שער-האיכות הקיים | (נגזר מהמצב) |
|
||||||
|
|
||||||
|
## 3. הרכיבים
|
||||||
|
|
||||||
|
- **Modify** `scripts/sync_agents_across_companies.py` (GAP-21):
|
||||||
|
- `--verify` יחזיר **exit 1** כש-`plan` לא-ריק **או** כשיש adapter_type-mismatch (כיום `return` שקט).
|
||||||
|
- adapter_type-mismatch: נספר ל-`mismatches` ומדווח רם (`❌`), לא רק "SKIPPING"; נכלל בסיגנל-הכשל
|
||||||
|
של `--verify`. (ה-skip עצמו ב-`--apply` נשמר — לא מסנכרנים adapter_type אוטומטית — אבל `--verify`
|
||||||
|
**נכשל** כדי לאלץ טיפול ידני.)
|
||||||
|
- **Create** `mcp-server/tests/test_paperclip_access_guard.py` (GAP-22) — fitness-function שסורק את
|
||||||
|
עץ-המקור (`web/`, `mcp-server/src/`, `scripts/`, `plugin-legal-ai/` אם רלוונטי) ו**נכשל** אם נמצא:
|
||||||
|
1. קריאת-HTTP גולמית ל-Paperclip — `httpx`/`requests`/`aiohttp` עם `PAPERCLIP_API_URL` או
|
||||||
|
`localhost:3100`/`pc.nautilus` — **מחוץ** ל-`web/paperclip_api.py` (ה-helper המאושר).
|
||||||
|
2. `INSERT INTO agent_wakeup_requests` (כל קובץ) — חייב לעבור דרך wakeup API.
|
||||||
|
3. `curl ... $PAPERCLIP_API_URL` ב-shell — מחוץ ל-`scripts/pc.sh`.
|
||||||
|
- מימוש: סריקת-טקסט ממוקדת (regex) עם **allowlist** מפורש (הקבצים המאושרים) + הודעת-כשל שמסבירה
|
||||||
|
את ה-helper הנכון. (AST מלא מיותר — הדפוסים הם מחרוזות-URL/SQL, לא מבנה-קוד.)
|
||||||
|
- **Create** `scripts/check_paperclip_access.py` (GAP-22, אופציונלי-דק) — wrapper הניתן להרצה ידנית/CI
|
||||||
|
שמריץ את אותה לוגיקת-סריקה (מייבא מהטסט או חולק helper); exit non-zero על הפרה. (אם הטסט מספיק —
|
||||||
|
לדלג, YAGNI.)
|
||||||
|
|
||||||
|
## 4. שינויי-התנהגות וסיכון
|
||||||
|
|
||||||
|
| שינוי | השפעה | סיכון |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| `--verify` יוצא 1 על drift | הופך ל-gate שמיש (אפשר לתלות בו cron/CI) | נמוך — לא משנה `--apply`; משנה רק exit-code של verify |
|
||||||
|
| adapter_type-mismatch רם + נכלל ב-fail | drift של adapter לא נבלע | נמוך — דיווח; ה-skip ב-apply נשמר |
|
||||||
|
| fitness-function guard | הפרה עתידית תיכשל בטסטים | נמוך — **ה-repo נסרק 2026-05-31: 0 הפרות קיימות** (אין httpx גולמי ל-Paperclip, אין INSERT ל-agent_wakeup_requests, אין curl גולמי). הגדר הוא גדר-קדימה נקי, אפס תיקוני-קוד קיימים |
|
||||||
|
| allowlist | קבצים מאושרים (paperclip_api.py, pc.sh) פטורים | נמוך — מפורש ומתועד |
|
||||||
|
|
||||||
|
## 5. אסטרטגיית בדיקה
|
||||||
|
|
||||||
|
- **GAP-22 fitness-function** נבדק על עצמו: הטסט מאתר דפוס-הפרה מוזרק (fixture עם httpx ל-Paperclip)
|
||||||
|
ומאשר שהוא נתפס; ומאשר שה-helpers המאושרים (paperclip_api.py) **לא** מסומנים. כלומר הטסט בודק
|
||||||
|
את הסורק על דוגמאות חיוביות+שליליות, ואז מריץ אותו על ה-repo האמיתי (חייב לעבור — אחרת יש הפרה
|
||||||
|
קיימת לתקן).
|
||||||
|
- **GAP-21** — בדיקת-יחידה ללוגיקת ה-exit/mismatch: מתוך master/mirror סינתטיים, `--verify` עם drift
|
||||||
|
מחזיר 1; ללא drift מחזיר 0; adapter_type-mismatch → 1 + הודעה. (refactor של לוגיקת-ההכרעה לפונקציה
|
||||||
|
טהורה `_verify_exit_code(plan, mismatches)` שניתנת לבדיקה offline בלי DB/Paperclip.)
|
||||||
|
- חבילה מלאה ירוקה; smoke: `sync...py --verify` מול ה-state הנוכחי (לדווח אם drift קיים).
|
||||||
|
|
||||||
|
## 6. סדר-ביצוע
|
||||||
|
|
||||||
|
1. בדיקות אדומות: `_verify_exit_code` (GAP-21) + סורק ה-guard על fixtures (GAP-22).
|
||||||
|
2. GAP-21: refactor `--verify` ל-`_verify_exit_code` + ספירת mismatches + exit 1 + דיווח רם.
|
||||||
|
3. GAP-22: סורק (`tests/test_paperclip_access_guard.py`) + allowlist; **הרצה על ה-repo** וטיפול בהפרות קיימות (תיקון או allowlist מנומק).
|
||||||
|
4. (אופציונלי) `scripts/check_paperclip_access.py` אם רוצים הרצה עצמאית.
|
||||||
|
5. חבילה ירוקה + smoke (`--verify`) + SCRIPTS.md (אם נוסף סקריפט) + PR+merge + TaskMaster #66.
|
||||||
|
|
||||||
|
> **GAP-23 (#69)** — חיווט הספ ל-HEARTBEAT/סוכנים — מחוץ-להיקף (משנה התנהגות-ייצור, דורש החלטה).
|
||||||
@@ -21,6 +21,19 @@ dependencies = [
|
|||||||
"uvicorn[standard]>=0.30.0",
|
"uvicorn[standard]>=0.30.0",
|
||||||
"httpx>=0.27.0",
|
"httpx>=0.27.0",
|
||||||
"infisicalsdk>=1.0.0",
|
"infisicalsdk>=1.0.0",
|
||||||
|
"aioboto3>=13.0.0", # X14 object storage (MinIO/S3) — services/storage.py
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
# Tier-1 court-verdict fetch (X13) — host-only. The container can't run a
|
||||||
|
# browser, so these are NOT in the base deps; install on the host venv with
|
||||||
|
# `pip install -e ".[court-fetch]" && python -m camoufox fetch`. faster-whisper
|
||||||
|
# is only for the explicit-PDF-download reCAPTCHA fallback (the primary
|
||||||
|
# image-API path needs no solving).
|
||||||
|
court-fetch = [
|
||||||
|
"camoufox>=0.4.11",
|
||||||
|
"faster-whisper>=1.0.0",
|
||||||
|
"h2>=4.0.0", # Tier-0 supremedecisions uses httpx http2
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
13
mcp-server/src/legal_mcp/chat_service/__init__.py
Normal file
13
mcp-server/src/legal_mcp/chat_service/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"""legal-chat-service — host-side SSE bridge to ``claude`` CLI.
|
||||||
|
|
||||||
|
Runs as a pm2-managed process on the host (port 127.0.0.1:8770 by default).
|
||||||
|
The legal-ai FastAPI container proxies chat requests to it via
|
||||||
|
``host.docker.internal:8770``.
|
||||||
|
|
||||||
|
Why a separate service:
|
||||||
|
The chat needs real-time streaming + multi-turn session continuation
|
||||||
|
(``claude --resume <session_id>``). The container can't run the
|
||||||
|
claude CLI (no binary, no claude.ai credentials). Splitting this out
|
||||||
|
keeps the architectural rule of ``claude_session.py`` intact while
|
||||||
|
enabling the new chat feature for free (no API key).
|
||||||
|
"""
|
||||||
210
mcp-server/src/legal_mcp/chat_service/server.py
Normal file
210
mcp-server/src/legal_mcp/chat_service/server.py
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
"""HTTP+SSE bridge from FastAPI (in container) to local claude CLI.
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
POST /chat/start — body: {prompt, system?, resume_session_id?}
|
||||||
|
returns SSE stream of events from
|
||||||
|
``claude_session.query_streaming``.
|
||||||
|
REQUIRES Authorization: Bearer <secret>.
|
||||||
|
GET /health — liveness probe (no auth — used by FastAPI for status).
|
||||||
|
|
||||||
|
Run with pm2:
|
||||||
|
pm2 start scripts/legal-chat-service.config.cjs
|
||||||
|
|
||||||
|
Standalone for dev:
|
||||||
|
cd ~/legal-ai/mcp-server
|
||||||
|
LEGAL_CHAT_SHARED_SECRET=... .venv/bin/python -m legal_mcp.chat_service.server \
|
||||||
|
--port 8770 --host 10.0.1.1
|
||||||
|
|
||||||
|
Security posture
|
||||||
|
----------------
|
||||||
|
1. Bind defaults to ``10.0.1.1`` — the host's docker0 bridge gateway.
|
||||||
|
Containers on docker bridges (including the legal-ai container, which
|
||||||
|
sits on the ``coolify`` network but routes to docker0 at the host)
|
||||||
|
can reach this address; processes outside the host cannot. Binding to
|
||||||
|
``0.0.0.0`` is permitted but discouraged (relies on the cloud-level
|
||||||
|
firewall as the sole perimeter).
|
||||||
|
2. ``/chat/start`` requires a ``Authorization: Bearer <LEGAL_CHAT_SHARED_SECRET>``
|
||||||
|
header. The secret is loaded from the environment; without it set,
|
||||||
|
the server refuses to start (no fallback to "open" mode, by design —
|
||||||
|
the claude CLI it spawns can run arbitrary tool calls, so an
|
||||||
|
unauthenticated /chat/start is RCE-equivalent).
|
||||||
|
3. ``/health`` is intentionally unauthenticated so the FastAPI proxy
|
||||||
|
can probe liveness with no token. It returns only a static OK and
|
||||||
|
never spawns subprocesses, so it can't be abused.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
# Run-via-CLI bootstrap so ``python -m legal_mcp.chat_service.server``
|
||||||
|
# works even when the package isn't installed (it is in the venv, but
|
||||||
|
# this safeguard keeps the entrypoint robust).
|
||||||
|
_pkg_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
if _pkg_root not in sys.path:
|
||||||
|
sys.path.insert(0, _pkg_root)
|
||||||
|
|
||||||
|
from legal_mcp.services import claude_session # noqa: E402
|
||||||
|
|
||||||
|
logger = logging.getLogger("legal_chat_service")
|
||||||
|
|
||||||
|
|
||||||
|
# Loaded once at startup. Validated to be non-empty in main(); the handler
|
||||||
|
# uses a constant-time compare to avoid timing oracles on a short input.
|
||||||
|
_SHARED_SECRET: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
async def health(request: web.Request) -> web.Response:
|
||||||
|
return web.json_response({"ok": True, "service": "legal-chat-service"})
|
||||||
|
|
||||||
|
|
||||||
|
def _check_bearer(request: web.Request) -> web.Response | None:
|
||||||
|
"""Validate ``Authorization: Bearer <secret>``. Returns 401 response on failure."""
|
||||||
|
auth = request.headers.get("Authorization", "")
|
||||||
|
expected = "Bearer " + _SHARED_SECRET
|
||||||
|
# ``compare_digest`` defends against timing attacks. Strings of different
|
||||||
|
# length still leak length, but for a 43-char urlsafe token that's
|
||||||
|
# uninteresting and the auth scheme prefix anchors it anyway.
|
||||||
|
import hmac
|
||||||
|
if not auth or not hmac.compare_digest(auth, expected):
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "unauthorized: missing or invalid Bearer token"},
|
||||||
|
status=401,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def chat_start(request: web.Request) -> web.StreamResponse:
|
||||||
|
"""Drive ``claude_session.query_streaming`` and forward events as SSE.
|
||||||
|
|
||||||
|
Request body (JSON):
|
||||||
|
prompt: str — required, user message
|
||||||
|
system: str | None — system instructions (ignored if resuming)
|
||||||
|
resume_session_id: str | None — continue a prior CLI session
|
||||||
|
timeout: int = 3600 — hard timeout for the subprocess
|
||||||
|
"""
|
||||||
|
unauth = _check_bearer(request)
|
||||||
|
if unauth is not None:
|
||||||
|
return unauth
|
||||||
|
|
||||||
|
try:
|
||||||
|
body = await request.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return web.json_response({"error": "invalid JSON body"}, status=400)
|
||||||
|
|
||||||
|
prompt = body.get("prompt") or ""
|
||||||
|
if not prompt.strip():
|
||||||
|
return web.json_response({"error": "prompt is required"}, status=400)
|
||||||
|
system = body.get("system")
|
||||||
|
resume_session_id = body.get("resume_session_id")
|
||||||
|
timeout = int(body.get("timeout") or 3600)
|
||||||
|
|
||||||
|
response = web.StreamResponse(
|
||||||
|
status=200,
|
||||||
|
reason="OK",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
"Cache-Control": "no-cache, no-transform",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
# X-Accel-Buffering=no defeats nginx/traefik buffering — the
|
||||||
|
# FastAPI container proxies via httpx and forwards bytes as
|
||||||
|
# they arrive, but the inner header is harmless and makes
|
||||||
|
# browser-direct testing easier.
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await response.prepare(request)
|
||||||
|
|
||||||
|
async def send_event(payload: dict[str, Any]) -> None:
|
||||||
|
line = f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
||||||
|
await response.write(line.encode("utf-8"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
async for event in claude_session.query_streaming(
|
||||||
|
prompt,
|
||||||
|
system=system,
|
||||||
|
resume_session_id=resume_session_id,
|
||||||
|
timeout=timeout,
|
||||||
|
):
|
||||||
|
await send_event(event)
|
||||||
|
if event.get("type") == "done" or event.get("type") == "error":
|
||||||
|
break
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# Client disconnected — bail cleanly.
|
||||||
|
logger.info("chat_start: client disconnected")
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("chat_start: streaming failed")
|
||||||
|
try:
|
||||||
|
await send_event({"type": "error", "message": str(e)})
|
||||||
|
except ConnectionResetError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
await response.write_eof()
|
||||||
|
except ConnectionResetError:
|
||||||
|
pass
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def build_app() -> web.Application:
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_get("/health", health)
|
||||||
|
app.router.add_post("/chat/start", chat_start)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="legal-chat-service")
|
||||||
|
parser.add_argument("--port", type=int, default=8770)
|
||||||
|
parser.add_argument(
|
||||||
|
"--host", default="10.0.1.1",
|
||||||
|
help=(
|
||||||
|
"bind address. Default 10.0.1.1 = docker0 bridge gateway — "
|
||||||
|
"reachable from containers, invisible to non-host networks. "
|
||||||
|
"Use 127.0.0.1 for host-local dev; do not bind 0.0.0.0 "
|
||||||
|
"without a separate perimeter firewall."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument("--log-level", default="INFO")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=args.log_level.upper(),
|
||||||
|
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
||||||
|
)
|
||||||
|
|
||||||
|
secret = os.environ.get("LEGAL_CHAT_SHARED_SECRET", "").strip()
|
||||||
|
if not secret:
|
||||||
|
logger.error(
|
||||||
|
"LEGAL_CHAT_SHARED_SECRET is empty; refusing to start. "
|
||||||
|
"Set it in /home/chaim/.legal-chat-service.env (loaded by "
|
||||||
|
"pm2) and mirror it as a Coolify env var on the legal-ai app."
|
||||||
|
)
|
||||||
|
return 2
|
||||||
|
if len(secret) < 24:
|
||||||
|
logger.error(
|
||||||
|
"LEGAL_CHAT_SHARED_SECRET is too short (got %d chars); "
|
||||||
|
"refusing to start. Use >=32 chars (e.g. python3 -c "
|
||||||
|
"'import secrets; print(secrets.token_urlsafe(32))').",
|
||||||
|
len(secret),
|
||||||
|
)
|
||||||
|
return 2
|
||||||
|
global _SHARED_SECRET
|
||||||
|
_SHARED_SECRET = secret
|
||||||
|
|
||||||
|
app = build_app()
|
||||||
|
logger.info("legal-chat-service listening on %s:%d", args.host, args.port)
|
||||||
|
web.run_app(app, host=args.host, port=args.port, print=lambda _msg: None)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -42,6 +42,42 @@ POSTGRES_URL = os.environ.get(
|
|||||||
# Redis
|
# Redis
|
||||||
REDIS_URL = os.environ.get("REDIS_URL", "redis://127.0.0.1:6380/0")
|
REDIS_URL = os.environ.get("REDIS_URL", "redis://127.0.0.1:6380/0")
|
||||||
|
|
||||||
|
# Claude CLI — model + effort for halacha extraction.
|
||||||
|
# All LLM calls go through the local `claude -p` CLI (claude_session.py).
|
||||||
|
# By default the CLI uses the developer's session default model with no
|
||||||
|
# explicit effort. For halacha extraction we pin Opus 4.8 @ xhigh: the
|
||||||
|
# 2026-05-31 A/B (scripts/ab_halacha_opus48.py) showed it cuts over-extraction
|
||||||
|
# (~124→51 on שטיין) at 100% quote-verification with honest confidence
|
||||||
|
# calibration. Env-overridable so the model/effort can be tuned without a
|
||||||
|
# code change (set to "" to fall back to the CLI default). Other extractors
|
||||||
|
# (claims, metadata, block-writing, QA) keep the CLI default unless similarly
|
||||||
|
# pinned.
|
||||||
|
HALACHA_EXTRACT_MODEL = os.environ.get("HALACHA_EXTRACT_MODEL", "claude-opus-4-8")
|
||||||
|
HALACHA_EXTRACT_EFFORT = os.environ.get("HALACHA_EXTRACT_EFFORT", "xhigh")
|
||||||
|
# Digest (X12) metadata extraction is a simpler, high-volume task (concept tag,
|
||||||
|
# headline, underlying citation, tags from a one-page summary) — Sonnet is the
|
||||||
|
# speed/cost sweet-spot here, unlike halacha extraction which pins Opus. Tune via env.
|
||||||
|
DIGEST_EXTRACT_MODEL = os.environ.get("DIGEST_EXTRACT_MODEL", "claude-sonnet-4-6")
|
||||||
|
# Effort for BULK queue-drain extraction (process_pending over many precedents).
|
||||||
|
# xhigh is the quality sweet-spot for a single precedent but very slow at scale
|
||||||
|
# (a 64-chunk case ≈ 20 min). Bulk drains use a lighter effort to cut wall-clock;
|
||||||
|
# interactive single re-extraction keeps HALACHA_EXTRACT_EFFORT (xhigh). Tune via
|
||||||
|
# env (set to 'xhigh' to make bulk match single, or 'medium' for max speed).
|
||||||
|
HALACHA_BULK_EXTRACT_EFFORT = os.environ.get("HALACHA_BULK_EXTRACT_EFFORT", "high")
|
||||||
|
# Concurrent chunks WITHIN a single extraction. Each `claude -p` @ xhigh holds
|
||||||
|
# ~300MB RSS + heavy CPU; cross-process overlap (agent retries) on top of this
|
||||||
|
# froze the box on 2026-05-31 (hard reboot). A global advisory lock now caps
|
||||||
|
# the system to ONE extraction at a time; this caps the chunks within it.
|
||||||
|
HALACHA_CHUNK_CONCURRENCY = int(os.environ.get("HALACHA_CHUNK_CONCURRENCY", "3"))
|
||||||
|
HALACHA_CORROBORATION_MATCH_FLOOR = float(os.environ.get("HALACHA_CORROBORATION_MATCH_FLOOR", "0.50"))
|
||||||
|
HALACHA_CORROBORATION_MIN_CITES = int(os.environ.get("HALACHA_CORROBORATION_MIN_CITES", "2"))
|
||||||
|
# X11 Phase 2: gate corroboration → approval. Default ON (Dafna validated the
|
||||||
|
# Phase 1 signal, 2026-06-01). Set to "false" to disable the auto-approve/demote
|
||||||
|
# wiring while keeping the Phase 1 signal intact.
|
||||||
|
HALACHA_CORROBORATION_AUTO_APPROVE = os.environ.get(
|
||||||
|
"HALACHA_CORROBORATION_AUTO_APPROVE", "true"
|
||||||
|
).strip().lower() in ("1", "true", "yes", "on")
|
||||||
|
|
||||||
# Voyage AI
|
# Voyage AI
|
||||||
VOYAGE_API_KEY = os.environ.get("VOYAGE_API_KEY", "")
|
VOYAGE_API_KEY = os.environ.get("VOYAGE_API_KEY", "")
|
||||||
VOYAGE_MODEL = os.environ.get("VOYAGE_MODEL", "voyage-law-2")
|
VOYAGE_MODEL = os.environ.get("VOYAGE_MODEL", "voyage-law-2")
|
||||||
@@ -87,6 +123,20 @@ MULTIMODAL_TEXT_WEIGHT = float(
|
|||||||
# concentrate weight at top ranks; higher values flatten the curve.
|
# concentrate weight at top ranks; higher values flatten the curve.
|
||||||
MULTIMODAL_RRF_K = int(os.environ.get("MULTIMODAL_RRF_K", "60"))
|
MULTIMODAL_RRF_K = int(os.environ.get("MULTIMODAL_RRF_K", "60"))
|
||||||
|
|
||||||
|
# BM25/lexical hybrid — fuse ``ts_rank_cd`` over ``content_tsv``/
|
||||||
|
# ``rule_tsv`` (DB schema V12) with the semantic cosine layer via RRF.
|
||||||
|
# Recovers recall on exact-string queries that voyage embeddings blur
|
||||||
|
# (e.g. case-number citations like "1461/20", "317/10"; rare planning
|
||||||
|
# vocabulary). Hebrew uses the ``simple`` text-search config — no
|
||||||
|
# stemmer needed, and numeric/punctuation tokens stay intact. When
|
||||||
|
# disabled, hybrid search falls back to semantic-only (the previous
|
||||||
|
# behaviour). On by default — the lexical leg is cheap (GIN index) and
|
||||||
|
# only ever *adds* candidates to RRF, it can't down-rank a strong
|
||||||
|
# semantic hit.
|
||||||
|
BM25_HYBRID_ENABLED = (
|
||||||
|
os.environ.get("BM25_HYBRID_ENABLED", "true").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
# Halacha extraction — auto-approve threshold. Halachot with extractor
|
# Halacha extraction — auto-approve threshold. Halachot with extractor
|
||||||
# confidence >= this value are inserted with review_status='approved'
|
# confidence >= this value are inserted with review_status='approved'
|
||||||
# instead of 'pending_review' (so they immediately appear in
|
# instead of 'pending_review' (so they immediately appear in
|
||||||
@@ -98,6 +148,49 @@ HALACHA_AUTO_APPROVE_THRESHOLD = float(
|
|||||||
os.environ.get("HALACHA_AUTO_APPROVE_THRESHOLD", "0.80")
|
os.environ.get("HALACHA_AUTO_APPROVE_THRESHOLD", "0.80")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Halacha dedup-on-insert — within-precedent semantic cosine ceiling. Before
|
||||||
|
# storing a halacha, store_halachot_for_chunk skips it if its rule-embedding has
|
||||||
|
# cosine >= this value against an already-stored halacha of the SAME precedent
|
||||||
|
# (exact normalized supporting_quote is always skipped regardless). 0.93 is the
|
||||||
|
# conservative auto-skip floor: the 2026-06-03 cleanup showed the 0.90-0.95 band
|
||||||
|
# is "almost entirely" same-rule-reworded, but auto-skip is unreviewed so we sit
|
||||||
|
# just above the manual-cleanup 0.90 to avoid dropping a genuinely distinct
|
||||||
|
# principle. Set > 1.0 to disable semantic dedup (exact-quote dedup still runs).
|
||||||
|
HALACHA_DEDUP_COSINE = float(os.environ.get("HALACHA_DEDUP_COSINE", "0.93"))
|
||||||
|
|
||||||
|
# Halacha dedup TAIL band (#82.3) — the [BAND_COSINE, DEDUP_COSINE) range is too
|
||||||
|
# low to auto-skip but suspicious. A halacha whose nearest same-precedent
|
||||||
|
# neighbor sits in this band AND has high LEXICAL overlap (Jaccard/Levenshtein
|
||||||
|
# on rule_statement) is flagged 'near_duplicate' (blocks auto-approve → review),
|
||||||
|
# not skipped — catching paraphrases the cosine threshold misses without
|
||||||
|
# dropping a possibly-distinct principle unreviewed. 0.83 from the same cleanup.
|
||||||
|
HALACHA_DEDUP_BAND_COSINE = float(os.environ.get("HALACHA_DEDUP_BAND_COSINE", "0.83"))
|
||||||
|
|
||||||
|
# Halacha review-queue clustering (#84.2) — when the review queue is requested
|
||||||
|
# with cluster=true, halachot of the SAME precedent whose rule-embeddings are
|
||||||
|
# within this cosine are grouped into ONE review card (canonical + variants), so
|
||||||
|
# the chair judges near-identical principles once instead of repeatedly. Display
|
||||||
|
# only — never merges/deletes. 0.90 = "same principle, reworded".
|
||||||
|
HALACHA_CLUSTER_COSINE = float(os.environ.get("HALACHA_CLUSTER_COSINE", "0.90"))
|
||||||
|
|
||||||
|
# Halacha NLI entailment validator (#81.3) — after extraction, a claude_session
|
||||||
|
# judge checks each halacha's rule_statement is entailed by its supporting_quote.
|
||||||
|
# Non-entailed (neutral/contradiction) → quality flag 'nli_unsupported' that
|
||||||
|
# blocks auto-approve. Runs through the local CLI (zero cost); fails OPEN if the
|
||||||
|
# CLI is unavailable (e.g. container). 'low' effort — entailment is a simple call.
|
||||||
|
HALACHA_NLI_ENABLED = os.environ.get("HALACHA_NLI_ENABLED", "true").lower() == "true"
|
||||||
|
HALACHA_NLI_MODEL = os.environ.get("HALACHA_NLI_MODEL", HALACHA_EXTRACT_MODEL)
|
||||||
|
HALACHA_NLI_EFFORT = os.environ.get("HALACHA_NLI_EFFORT", "low")
|
||||||
|
|
||||||
|
# Halacha over-extraction consolidation (#81.5) — after a precedent finishes
|
||||||
|
# extracting, a claude_session pass folds facets of the SAME legal question
|
||||||
|
# (below the #82 dedup cosine) into one canonical; the rest are marked rejected
|
||||||
|
# (reversible). Cross-chunk safety net for over-splitting. Runs through the local
|
||||||
|
# CLI (zero cost); fails OPEN. 'high' effort — folding needs careful judgment.
|
||||||
|
HALACHA_CONSOLIDATE_ENABLED = os.environ.get("HALACHA_CONSOLIDATE_ENABLED", "true").lower() == "true"
|
||||||
|
HALACHA_CONSOLIDATE_MODEL = os.environ.get("HALACHA_CONSOLIDATE_MODEL", HALACHA_EXTRACT_MODEL)
|
||||||
|
HALACHA_CONSOLIDATE_EFFORT = os.environ.get("HALACHA_CONSOLIDATE_EFFORT", "high")
|
||||||
|
|
||||||
# Google Cloud Vision (OCR for scanned PDFs)
|
# Google Cloud Vision (OCR for scanned PDFs)
|
||||||
GOOGLE_CLOUD_VISION_API_KEY = os.environ.get("GOOGLE_CLOUD_VISION_API_KEY", "")
|
GOOGLE_CLOUD_VISION_API_KEY = os.environ.get("GOOGLE_CLOUD_VISION_API_KEY", "")
|
||||||
|
|
||||||
@@ -109,6 +202,32 @@ EXPORTS_DIR = DATA_DIR / "exports" # legacy exports only
|
|||||||
# Cases directory — flat structure: data/cases/{case_number}/
|
# Cases directory — flat structure: data/cases/{case_number}/
|
||||||
CASES_DIR = DATA_DIR / "cases"
|
CASES_DIR = DATA_DIR / "cases"
|
||||||
|
|
||||||
|
# ── Object storage (X14 / MinIO) ───────────────────────────────────
|
||||||
|
# Single storage layer (services/storage.py) replaces the scattered file
|
||||||
|
# I/O across ~8 services (INV-STG1 / G2). Backend selector:
|
||||||
|
# "filesystem" (default) — disk under DATA_DIR; current behaviour, no change.
|
||||||
|
# "dual" — write disk + S3, read S3→disk fallback (migration).
|
||||||
|
# "s3" — MinIO only.
|
||||||
|
# See docs/spec/X14-storage-minio.md.
|
||||||
|
STORAGE_BACKEND = os.environ.get("STORAGE_BACKEND", "filesystem").strip().lower()
|
||||||
|
# Endpoint reached server-side (internal Docker network: http://minio:9000).
|
||||||
|
MINIO_ENDPOINT = os.environ.get("MINIO_ENDPOINT", "http://minio:9000")
|
||||||
|
# Public endpoint used when MINTING presigned URLs for the browser (INV-STG6) —
|
||||||
|
# the browser cannot resolve the internal hostname. Falls back to the internal
|
||||||
|
# endpoint when unset (e.g. local dev).
|
||||||
|
MINIO_PUBLIC_ENDPOINT = os.environ.get("MINIO_PUBLIC_ENDPOINT", MINIO_ENDPOINT)
|
||||||
|
MINIO_ACCESS_KEY = os.environ.get("MINIO_ACCESS_KEY", "")
|
||||||
|
MINIO_SECRET_KEY = os.environ.get("MINIO_SECRET_KEY", "")
|
||||||
|
MINIO_REGION = os.environ.get("MINIO_REGION", "us-east-1")
|
||||||
|
# Logical bucket → name. Governance boundaries (INV-STG3): documents
|
||||||
|
# (versioned), immutable (versioned + Object-Lock COMPLIANCE for final
|
||||||
|
# decisions, INV-STG4), derived (thumbnails/extracted text — regenerable).
|
||||||
|
MINIO_BUCKET_DOCUMENTS = os.environ.get("MINIO_BUCKET_DOCUMENTS", "legal-documents")
|
||||||
|
MINIO_BUCKET_IMMUTABLE = os.environ.get("MINIO_BUCKET_IMMUTABLE", "legal-immutable")
|
||||||
|
MINIO_BUCKET_DERIVED = os.environ.get("MINIO_BUCKET_DERIVED", "legal-derived")
|
||||||
|
# Default presigned-URL TTL (seconds). SigV4 hard max is 7 days; keep short.
|
||||||
|
MINIO_PRESIGN_TTL = int(os.environ.get("MINIO_PRESIGN_TTL", "900"))
|
||||||
|
|
||||||
|
|
||||||
def find_case_dir(case_number: str) -> Path:
|
def find_case_dir(case_number: str) -> Path:
|
||||||
"""Return the case directory for a given case number."""
|
"""Return the case directory for a given case number."""
|
||||||
@@ -118,6 +237,43 @@ def find_case_dir(case_number: str) -> Path:
|
|||||||
CHUNK_SIZE_TOKENS = 600
|
CHUNK_SIZE_TOKENS = 600
|
||||||
CHUNK_OVERLAP_TOKENS = 100
|
CHUNK_OVERLAP_TOKENS = 100
|
||||||
|
|
||||||
|
# Parent-doc retrieval (TaskMaster #48) — hierarchical chunking + lookup.
|
||||||
|
# When enabled:
|
||||||
|
# - The ingest pipeline emits two tiers of precedent_chunks: small
|
||||||
|
# "child" chunks (~300 tokens) for high-recall semantic/lexical
|
||||||
|
# matching, and larger "parent" chunks (~1500 tokens) that contain
|
||||||
|
# ~5 children each. Children are embedded and indexed; parents
|
||||||
|
# carry the broader text the LLM gets back.
|
||||||
|
# - Search runs against children, then swaps each hit for its parent
|
||||||
|
# row before returning — so the writer sees a coherent passage
|
||||||
|
# instead of a 300-token sliver.
|
||||||
|
#
|
||||||
|
# Off by default: the schema (V17) is safe to apply even when the flag
|
||||||
|
# is false (the chunker still emits single-tier chunks and search just
|
||||||
|
# returns them unchanged). Flip to true ONLY after the corpus has been
|
||||||
|
# re-ingested with the hierarchical chunker — see precedent_library
|
||||||
|
# ingest pipeline + the backfill plan in TaskMaster #48.
|
||||||
|
PARENT_DOC_RETRIEVAL_ENABLED = (
|
||||||
|
os.environ.get("PARENT_DOC_RETRIEVAL_ENABLED", "false").lower() == "true"
|
||||||
|
)
|
||||||
|
# Child chunks are what get embedded + matched. Smaller = higher recall,
|
||||||
|
# more rows. 300 tokens (~600 chars Hebrew) is the empirical sweet spot
|
||||||
|
# referenced in the original parent-doc literature (Anthropic, LlamaIndex).
|
||||||
|
PARENT_DOC_CHILD_SIZE_TOKENS = int(
|
||||||
|
os.environ.get("PARENT_DOC_CHILD_SIZE_TOKENS", "300")
|
||||||
|
)
|
||||||
|
# Parent chunks are what get returned to the LLM. Large enough to hold
|
||||||
|
# a full rule statement plus the surrounding paragraph and any cited
|
||||||
|
# authority. 1500 tokens = ~5 children at 300 each.
|
||||||
|
PARENT_DOC_PARENT_SIZE_TOKENS = int(
|
||||||
|
os.environ.get("PARENT_DOC_PARENT_SIZE_TOKENS", "1500")
|
||||||
|
)
|
||||||
|
# Child overlap — keeps neighbouring children sharing ~50 tokens so a
|
||||||
|
# sentence on a chunk boundary still matches the natural phrasing.
|
||||||
|
PARENT_DOC_CHILD_OVERLAP_TOKENS = int(
|
||||||
|
os.environ.get("PARENT_DOC_CHILD_OVERLAP_TOKENS", "50")
|
||||||
|
)
|
||||||
|
|
||||||
# External service allowlist — case materials may ONLY be sent to these domains
|
# External service allowlist — case materials may ONLY be sent to these domains
|
||||||
ALLOWED_EXTERNAL_SERVICES = {
|
ALLOWED_EXTERNAL_SERVICES = {
|
||||||
"api.voyageai.com", # Voyage AI (embeddings)
|
"api.voyageai.com", # Voyage AI (embeddings)
|
||||||
|
|||||||
7
mcp-server/src/legal_mcp/court_fetch_service/__init__.py
Normal file
7
mcp-server/src/legal_mcp/court_fetch_service/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"""Host-side Tier-1 verdict fetch service (X13).
|
||||||
|
|
||||||
|
Runs on the host under pm2 (it needs a real browser, which the legal-ai
|
||||||
|
container can't run). Drives a Camoufox stealth browser against נט המשפט to
|
||||||
|
download administrative/district-court verdicts the Supreme portal (Tier 0)
|
||||||
|
doesn't carry. See docs/spec/X13-court-fetch.md.
|
||||||
|
"""
|
||||||
314
mcp-server/src/legal_mcp/court_fetch_service/camofox_client.py
Normal file
314
mcp-server/src/legal_mcp/court_fetch_service/camofox_client.py
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
"""Camoufox driver for נט המשפט — calibrated, proven flow (X13, Tier 1).
|
||||||
|
|
||||||
|
Open-source, zero-API-cost: drives a **Camoufox** stealth browser (a Firefox
|
||||||
|
fork with C++ fingerprint spoofing) via its official Python package
|
||||||
|
(``camoufox.async_api``) — in-process, no separate Node server. The full flow
|
||||||
|
was reverse-engineered and validated end-to-end against עת"מ 46111-12-22
|
||||||
|
(2026-06-07): a 34-page verdict PDF retrieved with **no smart-card and no
|
||||||
|
CAPTCHA-solving**.
|
||||||
|
|
||||||
|
The proven path:
|
||||||
|
1. homepage → DOM-click ``btnExternalSearchCases`` ("תיקים לפי מס' תיק מקור").
|
||||||
|
2. Fill the visible header case-locator: ``BamaCaseNumberTextBoxH`` = case
|
||||||
|
number, ``BamaMonthYearTextBoxHT`` = "MM-YY"; click ``SearchHeaderCaseButton``.
|
||||||
|
→ lands on ``FolderCaseDetails/CaseDetails.aspx`` for the case.
|
||||||
|
3. Click the "פסקי דין" sidebar tab → ``Decisions/DecisionList.aspx``.
|
||||||
|
4. Click the document → popup ``Viewer/NGCSViewerPage.aspx?DocumentNumber=…``.
|
||||||
|
5. The viewer renders pages as PNG images via the ``GetImages`` PageMethod —
|
||||||
|
**served without reCAPTCHA** (the reCAPTCHA on the viewer only gates the
|
||||||
|
explicit save/print, which we don't use). Capture the internal
|
||||||
|
``documentNumber`` from the viewer's first ``GetImages`` call, then pull
|
||||||
|
every 4-page batch via ``fetch`` **with header ``X-Requested-With:
|
||||||
|
XMLHttpRequest``** (required — the F5 WAF blocks AJAX calls without it).
|
||||||
|
6. Decode the base64 PNGs → assemble a PDF (Pillow). The existing ingest
|
||||||
|
pipeline OCRs it (Google Vision) → text → corpus.
|
||||||
|
|
||||||
|
Operational requirements (see scripts/legal-court-fetch-service.config.cjs):
|
||||||
|
* a virtual display — Camoufox/Firefox crashes headless on this server
|
||||||
|
without one. Set ``DISPLAY`` to a running Xvfb (e.g. ``:99``).
|
||||||
|
* RAM — a Firefox content process loading the heavy ASP.NET pages needs
|
||||||
|
~0.5–1 GB; keep the box from swapping.
|
||||||
|
|
||||||
|
reCAPTCHA note: ``recaptcha_audio`` (local Whisper) remains as a fallback for
|
||||||
|
the explicit-PDF-download path, but the primary image-API path needs no
|
||||||
|
solving, so it is normally unused.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
NGCS_HOME = "https://www.court.gov.il/ngcs.web.site/homepage.aspx"
|
||||||
|
|
||||||
|
# Headless Camoufox needs a virtual display on this server.
|
||||||
|
_DISPLAY = os.environ.get("DISPLAY", "")
|
||||||
|
_NAV_TIMEOUT_MS = int(float(os.environ.get("COURT_FETCH_BROWSER_TIMEOUT_S", "60")) * 1000)
|
||||||
|
_PAGE_BATCH = 4 # the viewer's GetImages batch size
|
||||||
|
_MAX_PAGES = 400 # hard cap on a single document
|
||||||
|
# Hard wall-clock cap on a single fetch so a hung browser can't pin a Firefox
|
||||||
|
# process forever (anti-leak; INV-CF4 politeness). The async-with cleanup runs
|
||||||
|
# on the resulting CancelledError, tearing the browser down.
|
||||||
|
_FETCH_HARD_TIMEOUT_S = float(os.environ.get("COURT_FETCH_HARD_TIMEOUT_S", "180"))
|
||||||
|
|
||||||
|
|
||||||
|
def _reap_orphan_browsers() -> int:
|
||||||
|
"""Kill any ``camoufox-bin`` orphaned to ``ppid=1`` before we launch.
|
||||||
|
|
||||||
|
Fetching is serial (INV-CF4), so any browser not owned by a live parent is
|
||||||
|
a leftover from a prior crashed/killed fetch. Pure /proc, best-effort —
|
||||||
|
never raises into the fetch path.
|
||||||
|
"""
|
||||||
|
killed = 0
|
||||||
|
try:
|
||||||
|
for pid in os.listdir("/proc"):
|
||||||
|
if not pid.isdigit():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
with open(f"/proc/{pid}/status", "rb") as f:
|
||||||
|
status = f.read().decode("utf-8", "replace")
|
||||||
|
with open(f"/proc/{pid}/cmdline", "rb") as f:
|
||||||
|
cmd = f.read().decode("utf-8", "replace")
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if "camoufox-bin" not in cmd:
|
||||||
|
continue
|
||||||
|
ppid = 0
|
||||||
|
for line in status.splitlines():
|
||||||
|
if line.startswith("PPid:"):
|
||||||
|
try: ppid = int(line.split()[1])
|
||||||
|
except (IndexError, ValueError): pass
|
||||||
|
break
|
||||||
|
if ppid == 1:
|
||||||
|
try:
|
||||||
|
os.kill(int(pid), 9)
|
||||||
|
killed += 1
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if killed:
|
||||||
|
logger.warning("reaped %d orphaned camoufox-bin before fetch", killed)
|
||||||
|
return killed
|
||||||
|
|
||||||
|
|
||||||
|
class CamofoxUnavailable(RuntimeError):
|
||||||
|
"""Camoufox (or its virtual display) isn't available."""
|
||||||
|
|
||||||
|
|
||||||
|
class NgcsFlowError(RuntimeError):
|
||||||
|
"""A step in the נט-המשפט flow failed (navigation / not found / blocked)."""
|
||||||
|
|
||||||
|
|
||||||
|
def is_enabled() -> bool:
|
||||||
|
"""True if the Camoufox package imports (browser binary present)."""
|
||||||
|
try:
|
||||||
|
import camoufox.async_api # noqa: F401
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def health() -> dict:
|
||||||
|
return {"camoufox_import": is_enabled(), "display": _DISPLAY or "(none)"}
|
||||||
|
|
||||||
|
|
||||||
|
async def _fill_visible(page, id_substr: str, value: str) -> bool:
|
||||||
|
for el in await page.locator(f"input[id*='{id_substr}']").all():
|
||||||
|
try:
|
||||||
|
if await el.is_visible() and await el.is_editable():
|
||||||
|
await el.fill(value)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _reach_viewer(page, *, case_number: str, month_year: str):
|
||||||
|
"""Drive home → search → case → פסקי דין → viewer popup. Returns the popup page."""
|
||||||
|
await page.goto(NGCS_HOME, wait_until="domcontentloaded", timeout=_NAV_TIMEOUT_MS)
|
||||||
|
await page.wait_for_timeout(2500)
|
||||||
|
await page.eval_on_selector(
|
||||||
|
"#Header1_UpperMenu1_btnExternalSearchCases", "el => el.click()"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await page.wait_for_load_state("domcontentloaded", timeout=_NAV_TIMEOUT_MS)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await page.wait_for_timeout(4500)
|
||||||
|
|
||||||
|
if not await _fill_visible(page, "BamaCaseNumberTextBoxH", case_number):
|
||||||
|
raise NgcsFlowError("שדה מספר-תיק לא נמצא בעמוד החיפוש")
|
||||||
|
my_filled = False
|
||||||
|
for el in await page.locator("input[id*='BamaMonthYearTextBoxHT']").all():
|
||||||
|
if await el.is_visible():
|
||||||
|
await el.click()
|
||||||
|
await page.keyboard.type(month_year, delay=60)
|
||||||
|
my_filled = True
|
||||||
|
break
|
||||||
|
if not my_filled:
|
||||||
|
raise NgcsFlowError("שדה חודש-שנה לא נמצא")
|
||||||
|
clicked = False
|
||||||
|
for b in await page.locator("[id*='SearchHeaderCaseButton']").all():
|
||||||
|
if await b.is_visible():
|
||||||
|
await b.click()
|
||||||
|
clicked = True
|
||||||
|
break
|
||||||
|
if not clicked:
|
||||||
|
raise NgcsFlowError("כפתור החיפוש לא נמצא")
|
||||||
|
await page.wait_for_timeout(6000)
|
||||||
|
if "CaseDetails" not in page.url:
|
||||||
|
raise NgcsFlowError(
|
||||||
|
f"לא הגענו לעמוד-התיק (URL={page.url[:80]}) — ייתכן שהתיק לא נמצא/לא פתוח לעיון"
|
||||||
|
)
|
||||||
|
|
||||||
|
# פסקי דין tab → DecisionList
|
||||||
|
psak = page.locator("a:has-text('פסקי דין')")
|
||||||
|
opened = False
|
||||||
|
for i in range(await psak.count()):
|
||||||
|
el = psak.nth(i)
|
||||||
|
if await el.is_visible():
|
||||||
|
await el.click()
|
||||||
|
opened = True
|
||||||
|
break
|
||||||
|
if not opened:
|
||||||
|
raise NgcsFlowError("לשונית 'פסקי דין' לא נמצאה בעמוד-התיק")
|
||||||
|
await page.wait_for_timeout(6000)
|
||||||
|
|
||||||
|
# open the verdict document viewer (popup)
|
||||||
|
viewers = page.locator(
|
||||||
|
"a[href*='Viewer'],[onclick*='Viewer'],a[href*='Document'],a:has-text('צפייה')"
|
||||||
|
)
|
||||||
|
async with page.context.expect_page(timeout=15000) as pop:
|
||||||
|
clicked = False
|
||||||
|
for i in range(await viewers.count()):
|
||||||
|
el = viewers.nth(i)
|
||||||
|
if await el.is_visible():
|
||||||
|
await el.click()
|
||||||
|
clicked = True
|
||||||
|
break
|
||||||
|
if not clicked:
|
||||||
|
raise NgcsFlowError("לא נמצא מסמך פסק-דין לצפייה")
|
||||||
|
return await pop.value
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_admin_verdict(
|
||||||
|
*, file_number: str, month: str, year: str, case_number: str, court: str
|
||||||
|
) -> dict:
|
||||||
|
"""Fetch an admin/district court verdict as a PDF. Returns
|
||||||
|
``{content: bytes, filename, source_url, court}``; raises on failure.
|
||||||
|
|
||||||
|
``file_number``/``month``/``year`` are the נט-המשפט triple (e.g. 46111/12/22).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from camoufox.async_api import AsyncCamoufox
|
||||||
|
except Exception as e:
|
||||||
|
raise CamofoxUnavailable(
|
||||||
|
"חבילת camoufox אינה מותקנת/זמינה. הרץ `pip install camoufox` ו-"
|
||||||
|
"`python -m camoufox fetch`. ראה docs/spec/X13-court-fetch.md."
|
||||||
|
) from e
|
||||||
|
if not _DISPLAY:
|
||||||
|
# Headless Firefox crashes here without a virtual display.
|
||||||
|
raise CamofoxUnavailable(
|
||||||
|
"אין DISPLAY — Camoufox דורש Xvfb על שרת ללא מסך. הפעל Xvfb (למשל :99) "
|
||||||
|
"והגדר DISPLAY (ראה pm2 config)."
|
||||||
|
)
|
||||||
|
|
||||||
|
month_year = f"{int(month):02d}-{year[-2:]}"
|
||||||
|
|
||||||
|
# Belt-and-suspenders against browser leaks: kill any orphaned browser from
|
||||||
|
# a prior crashed fetch before we launch a new one (serial → safe).
|
||||||
|
_reap_orphan_browsers()
|
||||||
|
|
||||||
|
async def _run() -> dict:
|
||||||
|
doc_num = {"v": None}
|
||||||
|
|
||||||
|
async def on_resp(resp):
|
||||||
|
if "GetImages" in resp.url and not doc_num["v"]:
|
||||||
|
try:
|
||||||
|
doc_num["v"] = json.loads(resp.request.post_data).get("documentNumber")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async with AsyncCamoufox(
|
||||||
|
headless=True, geoip=False, humanize=True, locale="he-IL"
|
||||||
|
) as browser:
|
||||||
|
page = await browser.new_page()
|
||||||
|
page.context.on("response", lambda r: asyncio.create_task(on_resp(r)))
|
||||||
|
vp = await _reach_viewer(page, case_number=file_number, month_year=month_year)
|
||||||
|
source_url = vp.url
|
||||||
|
await vp.wait_for_timeout(9000)
|
||||||
|
if not doc_num["v"]:
|
||||||
|
raise NgcsFlowError("לא נלכד documentNumber מהצופה (ייתכן שהמסמך לא נטען)")
|
||||||
|
|
||||||
|
# Pull every page batch through fetch() with X-Requested-With (WAF-safe).
|
||||||
|
imgs = await vp.evaluate(
|
||||||
|
"""async (args) => {
|
||||||
|
const [dn, maxPages, batch] = args;
|
||||||
|
const url = window.location.href.split('?')[0] + '/GetImages';
|
||||||
|
const out = {};
|
||||||
|
for (let f = 0; f < maxPages; f += batch) {
|
||||||
|
let d;
|
||||||
|
try {
|
||||||
|
const r = await fetch(url, {method:'POST', credentials:'include',
|
||||||
|
headers:{'Content-Type':'application/json; charset=utf-8',
|
||||||
|
'X-Requested-With':'XMLHttpRequest'},
|
||||||
|
body: JSON.stringify({documentNumber:dn, fromIndex:f, toIndex:f+batch-1})});
|
||||||
|
if (!r.ok) break;
|
||||||
|
const j = await r.json(); d = (j.d !== undefined) ? j.d : j;
|
||||||
|
} catch (e) { break; }
|
||||||
|
if (!Array.isArray(d) || d.length === 0) break;
|
||||||
|
d.forEach((html, k) => { if (html) out[f+k] = html; });
|
||||||
|
if (d.length < batch) break;
|
||||||
|
await new Promise(r => setTimeout(r, 350));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}""",
|
||||||
|
[doc_num["v"], _MAX_PAGES, _PAGE_BATCH],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not imgs:
|
||||||
|
raise NgcsFlowError("לא התקבלו עמודי-מסמך מ-GetImages")
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
pages = []
|
||||||
|
for idx in sorted(imgs, key=lambda x: int(x)):
|
||||||
|
m = re.search(r"base64,([A-Za-z0-9+/=]+)", imgs[idx] or "")
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
pages.append(Image.open(io.BytesIO(base64.b64decode(m.group(1)))).convert("RGB"))
|
||||||
|
if not pages:
|
||||||
|
raise NgcsFlowError("עמודי-המסמך לא ניתנים לפענוח (base64)")
|
||||||
|
|
||||||
|
buf = io.BytesIO()
|
||||||
|
pages[0].save(buf, format="PDF", save_all=True, append_images=pages[1:])
|
||||||
|
content = buf.getvalue()
|
||||||
|
logger.info("נט המשפט: fetched %s — %d pages, %d bytes",
|
||||||
|
case_number, len(pages), len(content))
|
||||||
|
return {
|
||||||
|
"content": content,
|
||||||
|
"filename": f"{case_number}.pdf",
|
||||||
|
"source_url": source_url,
|
||||||
|
"court": court or "בית משפט מחוזי",
|
||||||
|
"pages": len(pages),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hard wall-clock cap: on a hung browser, the timeout cancels _run(); the
|
||||||
|
# async-with __aexit__ tears the browser down, and the reap below sweeps any
|
||||||
|
# process that outlived the cancellation.
|
||||||
|
try:
|
||||||
|
return await asyncio.wait_for(_run(), _FETCH_HARD_TIMEOUT_S)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_reap_orphan_browsers()
|
||||||
|
raise NgcsFlowError(
|
||||||
|
f"אחזור עבר את מגבלת-הזמן ({_FETCH_HARD_TIMEOUT_S:.0f}ש') ובוטל"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
_reap_orphan_browsers()
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""Open-source reCAPTCHA v2 audio-challenge solver (X13, Tier 1).
|
||||||
|
|
||||||
|
Pure open-source, zero-API-cost: switch the reCAPTCHA widget to its **audio**
|
||||||
|
challenge, download the mp3, transcribe it with a **local Whisper** model
|
||||||
|
(``faster-whisper``), and submit the transcript. This is the well-known
|
||||||
|
"Buster"-style technique. It is intentionally a *best-effort* solver —
|
||||||
|
reCAPTCHA actively fights audio solving, so a non-trivial failure rate is
|
||||||
|
expected and handled by the Tier-2 human fallback (INV-CF3), never hidden.
|
||||||
|
|
||||||
|
Model is loaded lazily and cached; ``WHISPER_MODEL`` (default ``small``) and
|
||||||
|
``WHISPER_DEVICE`` (default ``cpu``) tune it. The dependency is optional — if
|
||||||
|
``faster-whisper`` isn't installed, ``transcribe_audio`` raises a clear error
|
||||||
|
so the caller falls back to a human solve rather than crashing the service.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_WHISPER_MODEL_NAME = os.environ.get("WHISPER_MODEL", "small")
|
||||||
|
_WHISPER_DEVICE = os.environ.get("WHISPER_DEVICE", "cpu")
|
||||||
|
_model = None
|
||||||
|
|
||||||
|
|
||||||
|
class AudioSolveUnavailable(RuntimeError):
|
||||||
|
"""faster-whisper isn't installed — cannot solve audio locally."""
|
||||||
|
|
||||||
|
|
||||||
|
def _get_model():
|
||||||
|
global _model
|
||||||
|
if _model is not None:
|
||||||
|
return _model
|
||||||
|
try:
|
||||||
|
from faster_whisper import WhisperModel # type: ignore
|
||||||
|
except ImportError as e:
|
||||||
|
raise AudioSolveUnavailable(
|
||||||
|
"faster-whisper אינו מותקן — לא ניתן לפתור reCAPTCHA אודיו מקומית. "
|
||||||
|
"התקן `pip install faster-whisper` או הסתמך על fallback אנושי (VNC)."
|
||||||
|
) from e
|
||||||
|
logger.info("loading whisper model %s on %s", _WHISPER_MODEL_NAME, _WHISPER_DEVICE)
|
||||||
|
_model = WhisperModel(
|
||||||
|
_WHISPER_MODEL_NAME, device=_WHISPER_DEVICE, compute_type="int8"
|
||||||
|
)
|
||||||
|
return _model
|
||||||
|
|
||||||
|
|
||||||
|
async def download_audio(audio_url: str) -> bytes:
|
||||||
|
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as c:
|
||||||
|
r = await c.get(audio_url)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.content
|
||||||
|
|
||||||
|
|
||||||
|
def transcribe_audio(mp3_bytes: bytes) -> str:
|
||||||
|
"""Transcribe a reCAPTCHA audio clip to its (English) digit/word phrase.
|
||||||
|
|
||||||
|
Raises ``AudioSolveUnavailable`` if the local model isn't installed.
|
||||||
|
"""
|
||||||
|
model = _get_model()
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=True) as f:
|
||||||
|
f.write(mp3_bytes)
|
||||||
|
f.flush()
|
||||||
|
# reCAPTCHA audio is English regardless of page locale.
|
||||||
|
segments, _info = model.transcribe(f.name, language="en")
|
||||||
|
text = " ".join(seg.text for seg in segments).strip()
|
||||||
|
# Normalise: reCAPTCHA expects the bare phrase, lower-case, no punctuation.
|
||||||
|
cleaned = "".join(ch for ch in text.lower() if ch.isalnum() or ch.isspace())
|
||||||
|
return " ".join(cleaned.split())
|
||||||
|
|
||||||
|
|
||||||
|
async def solve_from_audio_url(audio_url: str) -> str:
|
||||||
|
"""Convenience: download + transcribe an audio-challenge URL."""
|
||||||
|
mp3 = await download_audio(audio_url)
|
||||||
|
return transcribe_audio(mp3)
|
||||||
275
mcp-server/src/legal_mcp/court_fetch_service/server.py
Normal file
275
mcp-server/src/legal_mcp/court_fetch_service/server.py
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
"""Host-side HTTP bridge for Tier-1 verdict fetching (X13).
|
||||||
|
|
||||||
|
Mirrors ``legal_mcp.chat_service.server`` — the proven host-side pattern: an
|
||||||
|
aiohttp app, bound to the docker bridge gateway, Bearer-auth, that does the one
|
||||||
|
thing the container can't (here: drive a real browser against נט המשפט).
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
POST /fetch body {file_number, month, year, case_number, court}
|
||||||
|
→ {ok, content_b64, filename, source_url, court, reason}
|
||||||
|
REQUIRES Authorization: Bearer <COURT_FETCH_SHARED_SECRET>.
|
||||||
|
GET /health liveness (no auth); reports camofox + VNC URL if available.
|
||||||
|
GET /pm2 read-only pm2 status of legal-* / paperclip services (no auth).
|
||||||
|
POST /pm2/control body {name, action: restart|stop|start} → run pm2 on a
|
||||||
|
whitelisted legal-* process. REQUIRES Bearer (mutating).
|
||||||
|
|
||||||
|
Run with pm2:
|
||||||
|
pm2 start scripts/legal-court-fetch-service.config.cjs
|
||||||
|
|
||||||
|
Security posture (identical rationale to legal-chat-service):
|
||||||
|
1. Bind defaults to ``10.0.1.1`` (docker0 bridge gateway) — reachable from
|
||||||
|
the host + containers on docker bridges, invisible to outside networks.
|
||||||
|
2. ``/fetch`` requires a Bearer token (constant-time compare); the service
|
||||||
|
refuses to start without ``COURT_FETCH_SHARED_SECRET`` set.
|
||||||
|
3. ``/health`` is unauthenticated and spawns nothing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
_pkg_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
if _pkg_root not in sys.path:
|
||||||
|
sys.path.insert(0, _pkg_root)
|
||||||
|
|
||||||
|
from legal_mcp.court_fetch_service import camofox_client # noqa: E402
|
||||||
|
|
||||||
|
logger = logging.getLogger("legal_court_fetch_service")
|
||||||
|
|
||||||
|
_SHARED_SECRET: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
async def health(request: web.Request) -> web.Response:
|
||||||
|
info = {"ok": True, "service": "legal-court-fetch-service",
|
||||||
|
"camofox_enabled": camofox_client.is_enabled()}
|
||||||
|
if camofox_client.is_enabled():
|
||||||
|
try:
|
||||||
|
info["camofox"] = await camofox_client.health()
|
||||||
|
except Exception as e: # health must never throw
|
||||||
|
info["camofox_error"] = str(e)
|
||||||
|
return web.json_response(info)
|
||||||
|
|
||||||
|
|
||||||
|
# Background services we surface on the /operations dashboard. pm2 jlist is a
|
||||||
|
# host-only capability (the legal-ai container can't run pm2), so the container's
|
||||||
|
# FastAPI proxies this read-only endpoint over the docker bridge. No secret:
|
||||||
|
# pm2 status (names/cpu/mem) carries nothing sensitive and the bind (10.0.1.1)
|
||||||
|
# is already host/container-only.
|
||||||
|
_PM2_PREFIXES = ("legal-", "paperclip")
|
||||||
|
|
||||||
|
|
||||||
|
def _trim_service(a: dict) -> dict:
|
||||||
|
"""Project a pm2 jlist app entry into the fields the dashboard needs."""
|
||||||
|
env = a.get("pm2_env", {}) or {}
|
||||||
|
return {
|
||||||
|
"name": a.get("name", ""),
|
||||||
|
"status": env.get("status", ""),
|
||||||
|
"restarts": env.get("restart_time", 0),
|
||||||
|
"uptime_ms": env.get("pm_uptime", 0),
|
||||||
|
"cpu": (a.get("monit") or {}).get("cpu", 0),
|
||||||
|
"memory_bytes": (a.get("monit") or {}).get("memory", 0),
|
||||||
|
"cron": env.get("cron_restart") or "",
|
||||||
|
"autorestart": env.get("autorestart", True),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _pm2_run(*args: str, timeout: float = 10) -> tuple[int, bytes, bytes]:
|
||||||
|
"""Run a pm2 subcommand; returns (returncode, stdout, stderr)."""
|
||||||
|
import asyncio as _asyncio
|
||||||
|
|
||||||
|
proc = await _asyncio.create_subprocess_exec(
|
||||||
|
"pm2", *args,
|
||||||
|
stdout=_asyncio.subprocess.PIPE, stderr=_asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
out, err = await _asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||||||
|
return proc.returncode or 0, out, err
|
||||||
|
|
||||||
|
|
||||||
|
async def pm2_status(request: web.Request) -> web.Response:
|
||||||
|
"""Return a trimmed ``pm2 jlist`` for the legal-ai background services."""
|
||||||
|
try:
|
||||||
|
rc, out, err = await _pm2_run("jlist")
|
||||||
|
if rc != 0:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": f"pm2 jlist failed: {err.decode('utf-8','replace')[:200]}"},
|
||||||
|
status=502,
|
||||||
|
)
|
||||||
|
apps = json.loads(out.decode("utf-8", "replace"))
|
||||||
|
except FileNotFoundError:
|
||||||
|
return web.json_response({"error": "pm2 not found on PATH"}, status=502)
|
||||||
|
except Exception as e: # never throw
|
||||||
|
return web.json_response({"error": f"pm2 error: {e}"}, status=502)
|
||||||
|
|
||||||
|
services = [
|
||||||
|
_trim_service(a) for a in apps
|
||||||
|
if any(str(a.get("name", "")).startswith(p) for p in _PM2_PREFIXES)
|
||||||
|
]
|
||||||
|
services.sort(key=lambda s: s["name"])
|
||||||
|
return web.json_response({"services": services})
|
||||||
|
|
||||||
|
|
||||||
|
# Process control (restart/stop/start) for the dashboard's "Windows-services"
|
||||||
|
# panel. Mutating, so it requires the Bearer secret (unlike read-only /pm2).
|
||||||
|
# Whitelisted to ``legal-`` names only — never paperclip or arbitrary processes.
|
||||||
|
_PM2_ACTIONS = {"restart", "stop", "start"}
|
||||||
|
|
||||||
|
# Our own pm2 process name. Restarting/stopping ourselves kills this process
|
||||||
|
# mid-reply, so those self-actions are detached (see pm2_control).
|
||||||
|
_OWN_PM2_NAME = os.environ.get("COURT_FETCH_SERVICE_PM2_NAME", "legal-court-fetch-service")
|
||||||
|
|
||||||
|
|
||||||
|
async def pm2_control(request: web.Request) -> web.Response:
|
||||||
|
"""Run ``pm2 <action> <name>`` for a whitelisted legal-* process."""
|
||||||
|
unauth = _check_bearer(request)
|
||||||
|
if unauth is not None:
|
||||||
|
return unauth
|
||||||
|
try:
|
||||||
|
body = await request.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return web.json_response({"error": "invalid JSON body"}, status=400)
|
||||||
|
|
||||||
|
name = str(body.get("name", "")).strip()
|
||||||
|
action = str(body.get("action", "")).strip()
|
||||||
|
if action not in _PM2_ACTIONS:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": f"action must be one of {sorted(_PM2_ACTIONS)}"}, status=400
|
||||||
|
)
|
||||||
|
if not name.startswith("legal-"):
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "name must be a legal-* process"}, status=403
|
||||||
|
)
|
||||||
|
|
||||||
|
# Self restart/stop kills this process before it can reply (client sees a
|
||||||
|
# dropped connection / 502) even though pm2 does perform the action. Detach
|
||||||
|
# it with a brief delay so the HTTP response flushes first, then report
|
||||||
|
# success optimistically.
|
||||||
|
if name == _OWN_PM2_NAME and action in ("restart", "stop"):
|
||||||
|
import asyncio as _asyncio
|
||||||
|
|
||||||
|
await _asyncio.create_subprocess_shell(f"sleep 1; pm2 {action} {name} --silent")
|
||||||
|
return web.json_response(
|
||||||
|
{"ok": True, "action": action, "deferred": True, "service": None}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rc, out, err = await _pm2_run(action, name, "--silent", timeout=30)
|
||||||
|
if rc != 0:
|
||||||
|
return web.json_response(
|
||||||
|
{"ok": False,
|
||||||
|
"error": f"pm2 {action} {name} failed: "
|
||||||
|
f"{err.decode('utf-8','replace')[:200]}"},
|
||||||
|
status=502,
|
||||||
|
)
|
||||||
|
# Re-read just this process so the UI settles on the real new state.
|
||||||
|
rc2, out2, _ = await _pm2_run("jlist")
|
||||||
|
svc = None
|
||||||
|
if rc2 == 0:
|
||||||
|
for a in json.loads(out2.decode("utf-8", "replace")):
|
||||||
|
if a.get("name") == name:
|
||||||
|
svc = _trim_service(a)
|
||||||
|
break
|
||||||
|
return web.json_response({"ok": True, "action": action, "service": svc})
|
||||||
|
except FileNotFoundError:
|
||||||
|
return web.json_response({"error": "pm2 not found on PATH"}, status=502)
|
||||||
|
except Exception as e: # never throw
|
||||||
|
return web.json_response({"ok": False, "error": f"pm2 error: {e}"}, status=502)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_bearer(request: web.Request) -> web.Response | None:
|
||||||
|
auth = request.headers.get("Authorization", "")
|
||||||
|
expected = "Bearer " + _SHARED_SECRET
|
||||||
|
if not auth or not hmac.compare_digest(auth, expected):
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "unauthorized: missing or invalid Bearer token"}, status=401
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch(request: web.Request) -> web.Response:
|
||||||
|
unauth = _check_bearer(request)
|
||||||
|
if unauth is not None:
|
||||||
|
return unauth
|
||||||
|
try:
|
||||||
|
body = await request.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return web.json_response({"error": "invalid JSON body"}, status=400)
|
||||||
|
|
||||||
|
required = ("file_number", "month", "year")
|
||||||
|
if not all(body.get(k) for k in required):
|
||||||
|
return web.json_response(
|
||||||
|
{"ok": False, "reason": f"missing one of {required}"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await camofox_client.fetch_admin_verdict(
|
||||||
|
file_number=str(body["file_number"]),
|
||||||
|
month=str(body["month"]),
|
||||||
|
year=str(body["year"]),
|
||||||
|
case_number=str(body.get("case_number", "")),
|
||||||
|
court=str(body.get("court", "")),
|
||||||
|
)
|
||||||
|
return web.json_response({
|
||||||
|
"ok": True,
|
||||||
|
"content_b64": base64.b64encode(result["content"]).decode("ascii"),
|
||||||
|
"filename": result.get("filename", ""),
|
||||||
|
"source_url": result.get("source_url", ""),
|
||||||
|
"court": result.get("court", ""),
|
||||||
|
})
|
||||||
|
except (camofox_client.CamofoxUnavailable, camofox_client.NgcsFlowError) as e:
|
||||||
|
# Expected, recoverable failure → orchestrator escalates (INV-CF3).
|
||||||
|
return web.json_response({"ok": False, "reason": str(e)}, status=200)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.exception("fetch failed")
|
||||||
|
return web.json_response({"ok": False, "reason": f"unexpected: {e}"}, status=200)
|
||||||
|
|
||||||
|
|
||||||
|
def build_app() -> web.Application:
|
||||||
|
app = web.Application(client_max_size=64 * 1024 * 1024)
|
||||||
|
app.router.add_get("/health", health)
|
||||||
|
app.router.add_get("/pm2", pm2_status)
|
||||||
|
app.router.add_post("/pm2/control", pm2_control)
|
||||||
|
app.router.add_post("/fetch", fetch)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="legal-court-fetch-service")
|
||||||
|
parser.add_argument("--port", type=int, default=8771)
|
||||||
|
parser.add_argument("--host", default="10.0.1.1",
|
||||||
|
help="bind address; default = docker0 bridge gateway")
|
||||||
|
parser.add_argument("--log-level", default="INFO")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(level=args.log_level.upper(),
|
||||||
|
format="%(asctime)s %(name)s %(levelname)s %(message)s")
|
||||||
|
|
||||||
|
secret = os.environ.get("COURT_FETCH_SHARED_SECRET", "").strip()
|
||||||
|
if not secret:
|
||||||
|
logger.error(
|
||||||
|
"COURT_FETCH_SHARED_SECRET is empty; refusing to start. Set it in "
|
||||||
|
"/home/chaim/.legal-court-fetch-service.env (loaded by pm2) and "
|
||||||
|
"mirror it as a Coolify env var on the legal-ai app."
|
||||||
|
)
|
||||||
|
return 2
|
||||||
|
if len(secret) < 24:
|
||||||
|
logger.error("COURT_FETCH_SHARED_SECRET too short (>=32 chars expected).")
|
||||||
|
return 2
|
||||||
|
global _SHARED_SECRET
|
||||||
|
_SHARED_SECRET = secret
|
||||||
|
|
||||||
|
app = build_app()
|
||||||
|
logger.info("legal-court-fetch-service listening on %s:%d", args.host, args.port)
|
||||||
|
web.run_app(app, host=args.host, port=args.port, print=lambda _m: None)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -53,6 +53,13 @@ mcp = FastMCP(
|
|||||||
from legal_mcp.tools import ( # noqa: E402
|
from legal_mcp.tools import ( # noqa: E402
|
||||||
cases, documents, search, drafting, workflow, precedents,
|
cases, documents, search, drafting, workflow, precedents,
|
||||||
precedent_library as plib,
|
precedent_library as plib,
|
||||||
|
internal_decisions as int_tools,
|
||||||
|
legal_arguments as la_tools,
|
||||||
|
missing_precedents as mp_tools,
|
||||||
|
citations as cit_tools,
|
||||||
|
training_enrichment as train_tools,
|
||||||
|
digests as digest_tools,
|
||||||
|
court_fetch as cf_tools,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -79,10 +86,24 @@ async def case_create(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# INV-TOOL5 / GAP-53: hard cap on list/search result sizes (OWASP API4:2023 —
|
||||||
|
# Unrestricted Resource Consumption). Non-positive is treated as "max", not "all".
|
||||||
|
_MAX_LIMIT = 200
|
||||||
|
|
||||||
|
|
||||||
|
def _clamp_limit(limit: int, hard_max: int = _MAX_LIMIT) -> int:
|
||||||
|
"""Clamp a caller-supplied result limit to [1, hard_max]."""
|
||||||
|
try:
|
||||||
|
n = int(limit)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return hard_max
|
||||||
|
return hard_max if n <= 0 else min(n, hard_max)
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def case_list(status: str = "", limit: int = 50) -> str:
|
async def case_list(status: str = "", limit: int = 50) -> str:
|
||||||
"""רשימת תיקי ערר. סינון אופציונלי לפי סטטוס (new/in_progress/drafted/reviewed/final)."""
|
"""רשימת תיקי ערר. סינון אופציונלי לפי סטטוס (new/in_progress/drafted/reviewed/final)."""
|
||||||
return await cases.case_list(status, limit)
|
return await cases.case_list(status, _clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@@ -103,7 +124,7 @@ async def case_update(
|
|||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
expected_outcome: str = "",
|
expected_outcome: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""עדכון פרטי תיק. expected_outcome: rejection/partial_acceptance/full_acceptance/betterment_levy."""
|
"""עדכון פרטי תיק. expected_outcome: rejection/partial_acceptance/full_acceptance (betterment_levy הוא practice_area, לא תוצאה)."""
|
||||||
return await cases.case_update(
|
return await cases.case_update(
|
||||||
case_number, status, title, subject, notes,
|
case_number, status, title, subject, notes,
|
||||||
hearing_date, decision_date, tags, expected_outcome,
|
hearing_date, decision_date, tags, expected_outcome,
|
||||||
@@ -151,13 +172,33 @@ async def precedent_remove(precedent_id: str) -> str:
|
|||||||
return await precedents.precedent_remove(precedent_id)
|
return await precedents.precedent_remove(precedent_id)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def search_case_precedents(
|
||||||
|
query: str, practice_area: str = "", limit: int = 10,
|
||||||
|
) -> str:
|
||||||
|
"""חיפוש בציטוטי-פסיקה שדפנה צירפה ידנית לתיקים (טבלת case_precedents) —
|
||||||
|
קורפוס "case-attached". זה **לא** ספריית-הפסיקה הסמכותית.
|
||||||
|
|
||||||
|
GAP-49 (INV-TOOL2): שם קודם היה `precedent_search_library` — הפוך וכמעט-זהה
|
||||||
|
ל-`search_precedent_library` (הספרייה הסמכותית), מה שסיכן ציטוט מהמקור הלא-נכון.
|
||||||
|
אל תצטט מכאן כמקור-סמכות ל-CREAC; לזה השתמש ב-`search_precedent_library`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: מחרוזת חיפוש (מול citation ו-quote)
|
||||||
|
practice_area: סינון תחום משפטי (אופציונלי)
|
||||||
|
limit: תקרת תוצאות
|
||||||
|
"""
|
||||||
|
return await precedents.search_case_precedents(query, practice_area, _clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def precedent_search_library(
|
async def precedent_search_library(
|
||||||
query: str, practice_area: str = "", limit: int = 10,
|
query: str, practice_area: str = "", limit: int = 10,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""חיפוש בציטוטים שדפנה צירפה ידנית לתיקים בעבר (case_precedents).
|
"""DEPRECATED (GAP-49) — שם-מטעה. השתמש ב-`search_case_precedents` (ציטוטים
|
||||||
שונה מ-search_precedent_library שמחפש בקורפוס הפסיקה הסמכותית."""
|
מצורפים-לתיק) או ב-`search_precedent_library` (ספריית-הפסיקה הסמכותית).
|
||||||
return await precedents.precedent_search_library(query, practice_area, limit)
|
Alias זמני לתאימות-לאחור — מנתב ל-search_case_precedents."""
|
||||||
|
return await precedents.search_case_precedents(query, practice_area, _clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
# ── External Precedent Library — authoritative case-law corpus ─────
|
# ── External Precedent Library — authoritative case-law corpus ─────
|
||||||
@@ -196,11 +237,20 @@ async def precedent_library_list(
|
|||||||
precedent_level: str = "",
|
precedent_level: str = "",
|
||||||
source_type: str = "",
|
source_type: str = "",
|
||||||
search: str = "",
|
search: str = "",
|
||||||
|
source_kind: str = "external_upload",
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""רשימת הפסיקה בקורפוס הסמכותי, עם פילטרים."""
|
"""רשימת הפסיקה בקורפוס, עם פילטרים.
|
||||||
|
|
||||||
|
source_kind: 'external_upload' (ברירת מחדל — פס"ד בתי משפט) /
|
||||||
|
'internal_committee' (החלטות ועדות ערר ערר/בל"מ שהועלו) /
|
||||||
|
'all_committees' (שתיהן — internal + appeals_committee).
|
||||||
|
החלטות ערר/בל"מ שמעלים נשמרות כ-internal_committee — כדי לראותן
|
||||||
|
ברשימה השתמש ב-source_kind='internal_committee' או 'all_committees'.
|
||||||
|
"""
|
||||||
return await plib.precedent_library_list(
|
return await plib.precedent_library_list(
|
||||||
practice_area, court, precedent_level, source_type, search, limit,
|
practice_area, court, precedent_level, source_type, search,
|
||||||
|
source_kind, _clamp_limit(limit),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -244,10 +294,34 @@ async def precedent_extract_metadata(case_law_id: str) -> str:
|
|||||||
return await plib.precedent_extract_metadata(case_law_id)
|
return await plib.precedent_extract_metadata(case_law_id)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def precedent_reindex(case_law_id: str) -> str:
|
||||||
|
"""re-chunk + re-embed פסיקה קיימת מה-full_text השמור (FU-3/GAP-09). אינו מריץ OCR/LLM — רק chunking + voyage embeddings. idempotent."""
|
||||||
|
return await plib.precedent_reindex(case_law_id)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def style_corpus_enrich(corpus_id: str, overwrite: bool = False) -> str:
|
||||||
|
"""חילוץ מטא-דאטה (summary, outcome, key_principles, appeal_subtype) להחלטה בקורפוס הסגנון של דפנה. ברירת מחדל: ממלא רק שדות ריקים. שלח `overwrite=true` כדי לרענן."""
|
||||||
|
return await train_tools.extract_decision_metadata(corpus_id, overwrite=overwrite)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def style_corpus_pending_enrichment(limit: int = 50) -> str:
|
||||||
|
"""רשימת החלטות בקורפוס הסגנון שעדיין חסרות summary/outcome/key_principles — מועמדות לחילוץ."""
|
||||||
|
return await train_tools.list_corpus_pending_enrichment(_clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def extraction_status() -> str:
|
||||||
|
"""סטטוס תור-החילוץ — כמה פסיקות ממתינות לחילוץ metadata/halacha + גיל הבקשה הוותיקה. read-only (חושף את התור ש-precedent_process_pending מרוקן)."""
|
||||||
|
return await plib.extraction_status()
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def precedent_process_pending(kind: str = "metadata", limit: int = 20) -> str:
|
async def precedent_process_pending(kind: str = "metadata", limit: int = 20) -> str:
|
||||||
"""ריקון תור בקשות חילוץ שנשלחו מ-UI. kind: 'metadata' או 'halacha'. מריץ extractor מקומית עם CLI על כל פריט בתור, ומנקה את הסימון אחרי הצלחה."""
|
"""ריקון תור בקשות חילוץ שנשלחו מ-UI. kind: 'metadata' או 'halacha'. מריץ extractor מקומית עם CLI על כל פריט בתור, ומנקה את הסימון אחרי הצלחה."""
|
||||||
return await plib.precedent_process_pending(kind, limit)
|
return await plib.precedent_process_pending(kind, _clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@@ -264,10 +338,85 @@ async def search_precedent_library(
|
|||||||
"""חיפוש סמנטי בקורפוס הפסיקה הסמכותית. מחזיר הלכות (מאושרות בלבד) + קטעי טקסט. השתמש כש-legal-writer צריך לצטט פסיקה מחייבת בבלוק י (CREAC: rule + explanation)."""
|
"""חיפוש סמנטי בקורפוס הפסיקה הסמכותית. מחזיר הלכות (מאושרות בלבד) + קטעי טקסט. השתמש כש-legal-writer צריך לצטט פסיקה מחייבת בבלוק י (CREAC: rule + explanation)."""
|
||||||
return await plib.search_precedent_library(
|
return await plib.search_precedent_library(
|
||||||
query, practice_area, court, precedent_level, appeal_subtype,
|
query, practice_area, court, precedent_level, appeal_subtype,
|
||||||
None, subject_tag, limit, include_halachot,
|
None, subject_tag, _clamp_limit(limit), include_halachot,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Digests radar (X12) — secondary discovery layer; NOT a citation corpus.
|
||||||
|
@mcp.tool()
|
||||||
|
async def digest_upload(
|
||||||
|
file_path: str,
|
||||||
|
yomon_number: str = "",
|
||||||
|
digest_date: str = "",
|
||||||
|
practice_area: str = "",
|
||||||
|
appeal_subtype: str = "",
|
||||||
|
subject_tags: list[str] | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""העלאת יומון ("כל יום") לקורפוס-הגילוי (radar) + חילוץ מטא-דאטה אוטומטי. היומון הוא מקור-משני המצביע על הפסק המקורי — אינו מצוטט בהחלטה ואינו מחלץ הלכות (INV-DIG1/2). practice_area: rishuy_uvniya / betterment_levy / compensation_197."""
|
||||||
|
return await digest_tools.digest_upload(
|
||||||
|
file_path, yomon_number, digest_date, practice_area,
|
||||||
|
appeal_subtype, subject_tags,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def digest_list(
|
||||||
|
practice_area: str = "",
|
||||||
|
concept_tag: str = "",
|
||||||
|
linked: bool | None = None,
|
||||||
|
search: str = "",
|
||||||
|
limit: int = 100,
|
||||||
|
) -> str:
|
||||||
|
"""רשימת יומונים בקורפוס-הגילוי, עם פילטרים. linked=false → יומונים שהפסק המקורי שלהם עוד לא נקלט לספריית הפסיקה (פער-ידע גלוי, INV-DIG3)."""
|
||||||
|
return await digest_tools.digest_list(
|
||||||
|
practice_area, concept_tag, linked, search, _clamp_limit(limit),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def digest_get(digest_id: str) -> str:
|
||||||
|
"""יומון ספציפי לפי מזהה (כולל מראה-מקום, ניתוח, וקישור לפסק המקורי אם קיים)."""
|
||||||
|
return await digest_tools.digest_get(digest_id)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def digest_link(digest_id: str, case_law_id: str) -> str:
|
||||||
|
"""קישור ידני של יומון לפסק הדין המקורי בספריית הפסיקה (INV-DIG3). idempotent."""
|
||||||
|
return await digest_tools.digest_link(digest_id, case_law_id)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def digest_relink(digest_id: str) -> str:
|
||||||
|
"""ניסיון-קישור מחדש: בודק אם פסק הדין המקורי של היומון כבר בספרייה ומקשר אוטומטית."""
|
||||||
|
return await digest_tools.digest_relink(digest_id)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def digest_delete(digest_id: str) -> str:
|
||||||
|
"""מחיקת יומון מקורפוס-הגילוי."""
|
||||||
|
return await digest_tools.digest_delete(digest_id)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def search_digests(
|
||||||
|
query: str,
|
||||||
|
practice_area: str = "",
|
||||||
|
subject_tag: str = "",
|
||||||
|
concept_tag: str = "",
|
||||||
|
limit: int = 10,
|
||||||
|
) -> str:
|
||||||
|
"""חיפוש סמנטי בקורפוס-הגילוי (יומוני "כל יום") — מצפן-מחקר (radar). מחזיר את היומון הרלוונטי + מראה-המקום של הפסק המקורי. ⚠️ היומון אינו מצוטט בהחלטה (INV-DIG1) — הצטט מהפסק המקורי דרך search_precedent_library. החוקר משתמש בזה בשלב 2ב.0 לפני האימות."""
|
||||||
|
return await digest_tools.search_digests(
|
||||||
|
query, practice_area, subject_tag, concept_tag, _clamp_limit(limit),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def digest_process_pending(limit: int = 20) -> str:
|
||||||
|
"""ריקון תור היומונים שהועלו מה-UI וממתינים לעיבוד-LLM מקומי. מריץ חילוץ-מטא-דאטה + embedding + autolink על כל יומון 'pending' (מקומית עם CLI). חלופת-MCP ל-scripts/ingest_digests_batch.py."""
|
||||||
|
return await digest_tools.digest_process_pending(_clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def halacha_review(
|
async def halacha_review(
|
||||||
halacha_id: str,
|
halacha_id: str,
|
||||||
@@ -288,7 +437,7 @@ async def halacha_review(
|
|||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def halachot_pending(limit: int = 100) -> str:
|
async def halachot_pending(limit: int = 100) -> str:
|
||||||
"""תור ההלכות הממתינות לאישור."""
|
"""תור ההלכות הממתינות לאישור."""
|
||||||
return await plib.halachot_pending(limit)
|
return await plib.halachot_pending(_clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
# Documents
|
# Documents
|
||||||
@@ -363,6 +512,28 @@ async def get_claims(
|
|||||||
return await documents.get_claims(case_number, party_role)
|
return await documents.get_claims(case_number, party_role)
|
||||||
|
|
||||||
|
|
||||||
|
# Legal arguments — aggregated (de-duped) propositions
|
||||||
|
@mcp.tool()
|
||||||
|
async def aggregate_claims_to_arguments(
|
||||||
|
case_number: str,
|
||||||
|
force: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""כינוס פרופוזיציות גולמיות (claims) לטיעונים משפטיים מובחנים — ~6-12 לכל צד.
|
||||||
|
|
||||||
|
משתמש ב-Claude headless לסיווג ואיגוד. force=True מוחק טיעונים קיימים לפני חישוב מחדש.
|
||||||
|
"""
|
||||||
|
return await la_tools.aggregate_claims_to_arguments(case_number, force=force)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_legal_arguments(
|
||||||
|
case_number: str,
|
||||||
|
party: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""שליפת טיעונים משפטיים מאוגדים. party: appellant/respondent/committee/permit_applicant (ריק=הכל)."""
|
||||||
|
return await la_tools.get_legal_arguments(case_number, party)
|
||||||
|
|
||||||
|
|
||||||
# References
|
# References
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def extract_references(
|
async def extract_references(
|
||||||
@@ -385,7 +556,7 @@ async def search_decisions(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""חיפוש סמנטי בהחלטות קודמות ובמסמכים — מסונן לפי תחום משפטי."""
|
"""חיפוש סמנטי בהחלטות קודמות ובמסמכים — מסונן לפי תחום משפטי."""
|
||||||
return await search.search_decisions(
|
return await search.search_decisions(
|
||||||
query, limit, section_type, practice_area, appeal_subtype, case_number,
|
query, _clamp_limit(limit), section_type, practice_area, appeal_subtype, case_number,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -396,7 +567,7 @@ async def search_case_documents(
|
|||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""חיפוש סמנטי בתוך מסמכי תיק ספציפי."""
|
"""חיפוש סמנטי בתוך מסמכי תיק ספציפי."""
|
||||||
return await search.search_case_documents(case_number, query, limit)
|
return await search.search_case_documents(case_number, query, _clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@@ -409,7 +580,7 @@ async def find_similar_cases(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""מציאת תיקים דומים על בסיס תיאור — מסונן לפי תחום משפטי."""
|
"""מציאת תיקים דומים על בסיס תיאור — מסונן לפי תחום משפטי."""
|
||||||
return await search.find_similar_cases(
|
return await search.find_similar_cases(
|
||||||
description, limit, practice_area, appeal_subtype, case_number,
|
description, _clamp_limit(limit), practice_area, appeal_subtype, case_number,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -422,6 +593,7 @@ async def search_internal_decisions(
|
|||||||
chair_name: str = "",
|
chair_name: str = "",
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
include_halachot: bool = True,
|
include_halachot: bool = True,
|
||||||
|
include_cited_by: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""חיפוש בהחלטות ועדות ערר לתכנון ובנייה (כל המחוזות).
|
"""חיפוש בהחלטות ועדות ערר לתכנון ובנייה (כל המחוזות).
|
||||||
|
|
||||||
@@ -436,9 +608,13 @@ async def search_internal_decisions(
|
|||||||
chair_name: שם יו"ר הוועדה לסינון. ריק = כל היו"רים
|
chair_name: שם יו"ר הוועדה לסינון. ריק = כל היו"רים
|
||||||
limit: מספר תוצאות מקסימלי
|
limit: מספר תוצאות מקסימלי
|
||||||
include_halachot: האם לכלול הלכות שחולצו
|
include_halachot: האם לכלול הלכות שחולצו
|
||||||
|
include_cited_by: True = הוסף תוצאות עקיפות — לכל hit הוסף גם החלטות
|
||||||
|
שהוא מצטט (מתוך citation graph). שימושי לחיפוש "כל הקשור ל-X"
|
||||||
|
כשרוצים להרחיב מעבר לטקסט המקורי. default False.
|
||||||
"""
|
"""
|
||||||
return await search.search_internal_decisions(
|
return await search.search_internal_decisions(
|
||||||
query, practice_area, appeal_subtype, district, chair_name, limit, include_halachot,
|
query, practice_area, appeal_subtype, district, chair_name, _clamp_limit(limit), include_halachot,
|
||||||
|
include_cited_by=include_cited_by,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -449,13 +625,25 @@ async def get_style_guide() -> str:
|
|||||||
return await drafting.get_style_guide()
|
return await drafting.get_style_guide()
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def style_distance(case_number: str) -> str:
|
||||||
|
"""מדד מרחק-סגנון (T7) — האם הטיוטה מתכנסת לסגנון דפנה: סטיית יחסי-זהב,
|
||||||
|
ספירת אנטי-דפוסים, ושיעור-השינוי draft→final מפנקס-ההתאמה. ללא LLM."""
|
||||||
|
import json as _json
|
||||||
|
from legal_mcp.services import style_distance as _sd
|
||||||
|
result = await _sd.style_distance(case_number)
|
||||||
|
return _json.dumps(result, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def draft_section(
|
async def draft_section(
|
||||||
case_number: str,
|
case_number: str,
|
||||||
section: str,
|
section: str,
|
||||||
instructions: str = "",
|
instructions: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""הרכבת הקשר מלא לניסוח סעיף (עובדות + תקדימים + סגנון)."""
|
"""DEPRECATED (GAP-50/INV-TOOL2) — הרכבת הקשר לניסוח לפי **סעיף** (granularity ישן).
|
||||||
|
העדף את `get_block_context(case_number, block_id)` — הקשר לפי-בלוק, התואם
|
||||||
|
לארכיטקטורת 12-הבלוקים הקנונית. נשמר זמנית לתאימות-לאחור."""
|
||||||
return await drafting.draft_section(case_number, section, instructions)
|
return await drafting.draft_section(case_number, section, instructions)
|
||||||
|
|
||||||
|
|
||||||
@@ -512,6 +700,12 @@ async def extract_appraiser_facts(case_number: str) -> str:
|
|||||||
return await drafting.extract_appraiser_facts(case_number)
|
return await drafting.extract_appraiser_facts(case_number)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_appraiser_facts(case_number: str) -> str:
|
||||||
|
"""קריאת עובדות-השמאי שכבר חולצו (facts + סתירות) — ללא חילוץ-מחדש יקר. ה-get המקביל ל-extract_appraiser_facts."""
|
||||||
|
return await drafting.get_appraiser_facts(case_number)
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def write_interim_draft(case_number: str, instructions: str = "") -> str:
|
async def write_interim_draft(case_number: str, instructions: str = "") -> str:
|
||||||
"""כתיבת ארבעת הבלוקים לטיוטת ביניים (רקע, תכניות+היתרים, טענות, הליכים) — אותו skill וטמפלט."""
|
"""כתיבת ארבעת הבלוקים לטיוטת ביניים (רקע, תכניות+היתרים, טענות, הליכים) — אותו skill וטמפלט."""
|
||||||
@@ -595,7 +789,7 @@ async def set_outcome(
|
|||||||
outcome: str,
|
outcome: str,
|
||||||
reasoning: str = "",
|
reasoning: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""הזנת תוצאה לתיק: rejected (דחייה), accepted (קבלה), partial (קבלה חלקית). אם אין נימוק — מפעיל סיעור מוחות."""
|
"""הזנת תוצאה לתיק: rejection (דחייה), partial_acceptance (קבלה חלקית), full_acceptance (קבלה מלאה). ערכי-legacy ממופים. אם אין נימוק — מפעיל סיעור מוחות."""
|
||||||
return await workflow.set_outcome(case_number, outcome, reasoning)
|
return await workflow.set_outcome(case_number, outcome, reasoning)
|
||||||
|
|
||||||
|
|
||||||
@@ -662,6 +856,205 @@ async def internal_decision_enrich(
|
|||||||
return _json.dumps(result, ensure_ascii=False, indent=2)
|
return _json.dumps(result, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def internal_decision_upload(
|
||||||
|
file_path: str,
|
||||||
|
case_number: str,
|
||||||
|
chair_name: str,
|
||||||
|
district: str,
|
||||||
|
case_name: str = "",
|
||||||
|
court: str = "",
|
||||||
|
decision_date: str = "",
|
||||||
|
practice_area: str = "",
|
||||||
|
appeal_subtype: str = "",
|
||||||
|
subject_tags: list[str] | None = None,
|
||||||
|
summary: str = "",
|
||||||
|
is_binding: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""העלאת החלטה של ועדת ערר (internal_committee) לקורפוס הסמכותי.
|
||||||
|
|
||||||
|
שדות חובה: file_path, case_number, chair_name, district.
|
||||||
|
שמירת ההחלטה עוברת דרך ingest_internal_decision — תויג source_kind='internal_committee' אוטומטית.
|
||||||
|
district תקין: ירושלים / מרכז / תל אביב / צפון / דרום / חיפה / ארצי.
|
||||||
|
|
||||||
|
בניגוד ל-precedent_library_upload (שתמיד שומר external_upload),
|
||||||
|
הכלי הזה הוא הנתיב המוסמך להחלטות ועדת ערר ומכריח chair_name+district.
|
||||||
|
"""
|
||||||
|
return await int_tools.internal_decision_upload(
|
||||||
|
file_path=file_path,
|
||||||
|
case_number=case_number,
|
||||||
|
chair_name=chair_name,
|
||||||
|
district=district,
|
||||||
|
case_name=case_name,
|
||||||
|
court=court,
|
||||||
|
decision_date=decision_date,
|
||||||
|
practice_area=practice_area,
|
||||||
|
appeal_subtype=appeal_subtype,
|
||||||
|
subject_tags=subject_tags,
|
||||||
|
summary=summary,
|
||||||
|
is_binding=is_binding,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Missing precedents (TaskMaster #35) ───────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def missing_precedent_create(
|
||||||
|
citation: str,
|
||||||
|
case_number: str = "",
|
||||||
|
cited_in_document_id: str = "",
|
||||||
|
cited_by_party: str = "unknown",
|
||||||
|
cited_by_party_name: str = "",
|
||||||
|
legal_topic: str = "",
|
||||||
|
legal_issue: str = "",
|
||||||
|
claim_quote: str = "",
|
||||||
|
case_name: str = "",
|
||||||
|
notes: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""תיעוד פסיקה שצוטטה בכתבי הטענות אך אינה בקורפוס.
|
||||||
|
|
||||||
|
שימוש: סוכן המחקר (legal-researcher) קורא לזה כשהוא מזהה ציטוט שלא
|
||||||
|
ניתן לאמת מול הקורפוס. הרשומה נשארת 'open' עד שהיו"ר מעלה את הפסיקה.
|
||||||
|
cited_by_party: appellant / respondent / committee / permit_applicant / unknown.
|
||||||
|
דה-דופ אוטומטי: ציטוט+תיק זהים → מחזיר את הרשומה הקיימת.
|
||||||
|
"""
|
||||||
|
return await mp_tools.missing_precedent_create(
|
||||||
|
citation=citation,
|
||||||
|
case_number=case_number,
|
||||||
|
cited_in_document_id=cited_in_document_id,
|
||||||
|
cited_by_party=cited_by_party,
|
||||||
|
cited_by_party_name=cited_by_party_name,
|
||||||
|
legal_topic=legal_topic,
|
||||||
|
legal_issue=legal_issue,
|
||||||
|
claim_quote=claim_quote,
|
||||||
|
case_name=case_name,
|
||||||
|
notes=notes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def missing_precedent_list(
|
||||||
|
case_number: str = "",
|
||||||
|
status: str = "open",
|
||||||
|
legal_topic: str = "",
|
||||||
|
limit: int = 50,
|
||||||
|
) -> str:
|
||||||
|
"""רשימת פסיקות חסרות לתיק או בכלל. status: open/uploaded/closed/irrelevant.
|
||||||
|
|
||||||
|
שימוש: היו"ר רואה מה ממתין להעלאה; הסוכן מאשר שלא יוצר כפילויות.
|
||||||
|
"""
|
||||||
|
return await mp_tools.missing_precedent_list(
|
||||||
|
case_number=case_number,
|
||||||
|
status=status,
|
||||||
|
legal_topic=legal_topic,
|
||||||
|
limit=_clamp_limit(limit),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def missing_precedent_close(
|
||||||
|
id: str,
|
||||||
|
linked_case_law_id: str = "",
|
||||||
|
notes: str = "",
|
||||||
|
status: str = "closed",
|
||||||
|
) -> str:
|
||||||
|
"""סגירת רשומת פסיקה חסרה לאחר העלאה לקורפוס.
|
||||||
|
|
||||||
|
status: closed (הועלה ונקשר) / uploaded (הועלה, ממתין לקישור) /
|
||||||
|
irrelevant (היו"ר החליט שזה לא רלוונטי לקורפוס).
|
||||||
|
"""
|
||||||
|
return await mp_tools.missing_precedent_close(
|
||||||
|
id=id,
|
||||||
|
linked_case_law_id=linked_case_law_id,
|
||||||
|
notes=notes,
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Court verdict auto-fetch (X13) ────────────────────────────────
|
||||||
|
@mcp.tool()
|
||||||
|
async def court_verdict_fetch(citation: str) -> str:
|
||||||
|
"""אחזור אוטומטי של פסק-דין בית-משפט מנט המשפט/פורטל-העליון וקליטה לקורפוס.
|
||||||
|
|
||||||
|
מסווג את הציטוט (עליון→Tier0 / מנהלי→Tier1 / ערר→skip), מוריד וקולט דרך
|
||||||
|
צינור-הקליטה הקנוני. דוגמה: 'עת"מ 46111-12-22'. כלי מקומי בלבד."""
|
||||||
|
return await cf_tools.court_verdict_fetch(citation)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def court_fetch_status(case_number: str = "", status_filter: str = "") -> str:
|
||||||
|
"""סטטוס תור-אחזור הפסיקה. case_number לפריט יחיד, או status_filter (pending/failed/manual/done)."""
|
||||||
|
return await cf_tools.court_fetch_status(case_number, status_filter)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def court_fetch_drain(limit: int = 10) -> str:
|
||||||
|
"""ריקון תור-אחזור הפסיקה — מוריד וקולט jobs ממתינים שהיומונים מילאו, וקושר חזרה ליומון. מקומי בלבד."""
|
||||||
|
return await cf_tools.court_fetch_drain(limit)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Internal citations graph (TaskMaster #34) ─────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def extract_internal_citations(
|
||||||
|
case_law_id: str = "",
|
||||||
|
chair_name: str = "",
|
||||||
|
limit: int = 0,
|
||||||
|
) -> str:
|
||||||
|
"""חילוץ ציטוטים פנימיים מהחלטות ועדת ערר ושמירה ב-citation graph.
|
||||||
|
|
||||||
|
משתמש בדפוסי regex עבריים ("ונפנה ל…", "כפי שקבעתי…", "ראה החלטתי…")
|
||||||
|
לזיהוי הפניות בין החלטות. אם case_law_id סופק — מריץ על שורה אחת
|
||||||
|
(שימושי אחרי upload). אם chair_name סופק — מריץ על כל ההחלטות של
|
||||||
|
אותו יו"ר. אם שניהם ריקים — מריץ על כל ה-internal_committee corpus.
|
||||||
|
|
||||||
|
איידמפוטנטי: ניתן להריץ שוב ושוב בלי כפילויות. ציטוטים שמופנים
|
||||||
|
להחלטות שעדיין לא בקורפוס נשמרים כ-unlinked (cited_case_law_id=NULL)
|
||||||
|
ויראו ב-list_internal_citations כשהיו"ר יחליט אם להעלות אותן.
|
||||||
|
"""
|
||||||
|
return await cit_tools.extract_internal_citations(
|
||||||
|
case_law_id=case_law_id,
|
||||||
|
chair_name=chair_name,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def list_internal_citations(
|
||||||
|
case_law_id: str = "",
|
||||||
|
linked_only: bool = False,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> str:
|
||||||
|
"""רשימת ציטוטים יוצאים מהחלטה (מה ההחלטה מצטטת).
|
||||||
|
|
||||||
|
משתמש לקבלת תמונה של בסיס הפסיקה שהחלטה הסתמכה עליו.
|
||||||
|
linked_only=True מסנן רק ציטוטים שזוהו ב-case_law של הקורפוס.
|
||||||
|
"""
|
||||||
|
return await cit_tools.list_internal_citations(
|
||||||
|
case_law_id=case_law_id,
|
||||||
|
linked_only=linked_only,
|
||||||
|
limit=_clamp_limit(limit),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def list_incoming_citations(
|
||||||
|
case_law_id: str = "",
|
||||||
|
limit: int = 50,
|
||||||
|
) -> str:
|
||||||
|
"""רשימת ציטוטים נכנסים אל החלטה (אילו החלטות מצטטות אותה).
|
||||||
|
|
||||||
|
שימוש: רוצים לדעת אילו החלטות של דפנה (או של ועדות אחרות) הסתמכו
|
||||||
|
על פסק דין מסוים — מעבירים את ה-case_law_id של פסק הדין.
|
||||||
|
"""
|
||||||
|
return await cit_tools.list_incoming_citations(
|
||||||
|
case_law_id=case_law_id,
|
||||||
|
limit=_clamp_limit(limit),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def record_chair_feedback(
|
async def record_chair_feedback(
|
||||||
case_number: str,
|
case_number: str,
|
||||||
@@ -681,9 +1074,33 @@ async def list_chair_feedback(
|
|||||||
case_number: str = "",
|
case_number: str = "",
|
||||||
category: str = "",
|
category: str = "",
|
||||||
unresolved_only: bool = True,
|
unresolved_only: bool = True,
|
||||||
|
limit: int = 100,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""הצגת הערות יו"ר שתועדו — אפשר לסנן לפי תיק, קטגוריה, מטופלות."""
|
"""הצגת הערות יו"ר שתועדו — אפשר לסנן לפי תיק, קטגוריה, מטופלות."""
|
||||||
return await workflow.list_chair_feedback(case_number, category, unresolved_only)
|
return await workflow.list_chair_feedback(case_number, category, unresolved_only, _clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def halacha_corroboration(halacha_id: str) -> dict:
|
||||||
|
"""החזר את ה-corroboration של הלכה: הציטוטים שמתקפים אותה, הטיפול, וסיכום (X11, read-only)."""
|
||||||
|
from uuid import UUID
|
||||||
|
from legal_mcp.services import corroboration as cor, db
|
||||||
|
links = await db.list_corroboration_for_halacha(UUID(halacha_id))
|
||||||
|
agg = cor.aggregate(
|
||||||
|
[{"source_id": (l["citing_case_law_id"] or l["citing_decision_id"] or ""), "treatment": l["treatment"]} for l in links]
|
||||||
|
)
|
||||||
|
return {"halacha_id": halacha_id, "summary": agg, "citations": links}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def corroboration_rebuild(case_law_id: str = "") -> dict:
|
||||||
|
"""בנה/רענן את ה-corroboration ויישם אישור-אוטומטי. ריק = כל הקורפוס (backfill);
|
||||||
|
מזהה-תקדים = תקדים בודד. כותב halacha_citation_corroboration ומעדכן review_status
|
||||||
|
(corroborated→approved, overruled→pending_review). X11 Phase 2."""
|
||||||
|
from legal_mcp.services import corroboration as cor
|
||||||
|
if case_law_id.strip():
|
||||||
|
return await cor.build_for_precedent(case_law_id.strip())
|
||||||
|
return await cor.build_all()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Output: data/cases/{case_number}/exports/ניתוח-משפטי-v{N}.docx
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -34,7 +35,7 @@ from docx.text.paragraph import Paragraph
|
|||||||
from docx.text.run import Run
|
from docx.text.run import Run
|
||||||
|
|
||||||
from legal_mcp import config
|
from legal_mcp import config
|
||||||
from legal_mcp.services import db, research_md
|
from legal_mcp.services import db, research_md, storage
|
||||||
|
|
||||||
|
|
||||||
def _mark_run_rtl(run: Run) -> None:
|
def _mark_run_rtl(run: Run) -> None:
|
||||||
@@ -494,10 +495,19 @@ async def build_analysis_docx(case_number: str) -> Path:
|
|||||||
continue
|
continue
|
||||||
_emit_content_line(doc, raw)
|
_emit_content_line(doc, raw)
|
||||||
|
|
||||||
# Save versioned
|
# Save versioned through the storage layer (INV-STG1). export_dir.mkdir +
|
||||||
|
# the glob in _next_version still read disk (correct under filesystem/dual;
|
||||||
|
# storage-native versioning is a cutover concern). out_path is always under
|
||||||
|
# DATA_DIR, so the bytes land exactly where they did before.
|
||||||
export_dir = case_dir / "exports"
|
export_dir = case_dir / "exports"
|
||||||
export_dir.mkdir(parents=True, exist_ok=True)
|
export_dir.mkdir(parents=True, exist_ok=True)
|
||||||
version = _next_version(export_dir)
|
version = _next_version(export_dir)
|
||||||
out_path = export_dir / f"ניתוח-משפטי-v{version}.docx"
|
out_path = export_dir / f"ניתוח-משפטי-v{version}.docx"
|
||||||
doc.save(str(out_path))
|
buf = io.BytesIO()
|
||||||
|
doc.save(buf)
|
||||||
|
await storage.put_bytes(
|
||||||
|
out_path.relative_to(config.DATA_DIR).as_posix(), buf.getvalue(),
|
||||||
|
bucket=storage.Bucket.DOCUMENTS,
|
||||||
|
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
)
|
||||||
return out_path
|
return out_path
|
||||||
|
|||||||
@@ -250,8 +250,19 @@ async def extract_appraiser_facts(case_id: UUID) -> dict:
|
|||||||
|
|
||||||
conflicts = await db.detect_appraiser_conflicts(case_id)
|
conflicts = await db.detect_appraiser_conflicts(case_id)
|
||||||
|
|
||||||
|
# Don't swallow extractor failures: if every appraisal errored and no
|
||||||
|
# facts were extracted, surface that as a distinct status instead of
|
||||||
|
# the misleading "completed, 0 facts" we used to return — the caller
|
||||||
|
# (and the UI) need to know that nothing actually ran.
|
||||||
|
all_errored = (
|
||||||
|
total_facts == 0
|
||||||
|
and by_doc
|
||||||
|
and all(d.get("status") == "error" for d in by_doc)
|
||||||
|
)
|
||||||
|
status = "extraction_failed" if all_errored else "completed"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "completed",
|
"status": status,
|
||||||
"appraisal_count": len(appraisals),
|
"appraisal_count": len(appraisals),
|
||||||
"total_facts": total_facts,
|
"total_facts": total_facts,
|
||||||
"conflicts": conflicts,
|
"conflicts": conflicts,
|
||||||
|
|||||||
371
mcp-server/src/legal_mcp/services/argument_aggregator.py
Normal file
371
mcp-server/src/legal_mcp/services/argument_aggregator.py
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
"""כינוס פרופוזיציות לטיעונים משפטיים מובחנים — argument de-duplication.
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. ``claims_extractor`` extracts ~20-30 raw propositions per litigation
|
||||||
|
brief into the ``claims`` table.
|
||||||
|
2. This module groups those raw propositions, per party, into 6-12
|
||||||
|
distinct legal arguments via Claude headless (`claude_session`).
|
||||||
|
3. The result is stored in ``legal_arguments`` plus ``legal_argument_
|
||||||
|
propositions`` (M:M join) so we keep traceability back to the source
|
||||||
|
claims.
|
||||||
|
|
||||||
|
Manually de-duping 184 propositions in 3 cases yielded 82 arguments
|
||||||
|
(~24/case) — see ``data/cases/{1017,1018,1019}-03-26/documents/research/
|
||||||
|
legal-arguments.md`` for the gold standard.
|
||||||
|
|
||||||
|
**Architectural constraint**: ``claude_session`` only works from the local
|
||||||
|
MCP server (Claude CLI is not installed in the FastAPI container). Calls
|
||||||
|
from ``web/`` must go through MCP tools; calls from MCP tools land here
|
||||||
|
directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from legal_mcp.services import claude_session, db
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Allowed enum values mirror the DB CHECK constraints.
|
||||||
|
ALLOWED_PARTIES = {"appellant", "respondent", "committee", "permit_applicant", "unknown"}
|
||||||
|
ALLOWED_PRIORITIES = {"threshold", "substantive", "procedural", "relief"}
|
||||||
|
|
||||||
|
# Hebrew labels for the prompt (Claude needs context in the same
|
||||||
|
# language as the source material).
|
||||||
|
PARTY_LABELS_HE = {
|
||||||
|
"appellant": "עוררים",
|
||||||
|
"respondent": "משיבים",
|
||||||
|
"committee": "ועדה מקומית",
|
||||||
|
"permit_applicant": "מבקשי היתר",
|
||||||
|
"unknown": "צד לא מזוהה",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
AGGREGATE_PROMPT_TEMPLATE = """אתה מנתח כתבי טענות בתחום תכנון ובנייה (ועדת ערר).
|
||||||
|
|
||||||
|
לפניך {n} פרופוזיציות גולמיות שחולצו ממסמכי {party_he} בתיק ערר.
|
||||||
|
מטרתך: לקבץ אותן ל-{target_min}-{target_max} **טיעונים משפטיים מובחנים**
|
||||||
|
(ארגומנטים אמיתיים, לא חזרה מילולית של הפרופוזיציות).
|
||||||
|
|
||||||
|
## כללי איגוד:
|
||||||
|
1. **טיעון אמיתי = רעיון משפטי אחד** — לא רשימה של פרופוזיציות, אלא טענה משפטית עצמאית.
|
||||||
|
2. **מקבצים פרופוזיציות שתומכות באותו רעיון משפטי** — גם אם הניסוח שלהן שונה.
|
||||||
|
3. **מפרידים בין סוגי טענות**:
|
||||||
|
- **threshold** = טענות סף (זכות עמידה, סמכות, מועדים, שיהוי)
|
||||||
|
- **substantive** = טענות מהותיות (תחולת חוק, פרשנות, חישוב)
|
||||||
|
- **procedural** = פגמי הליך (פרסום, פרוטוקול, ניגוד עניינים)
|
||||||
|
- **relief** = סעדים מבוקשים / סיכומים
|
||||||
|
4. **כותרת קצרה ובהירה** — תיאורית, לא משפטית מפורטת. 5-15 מילים.
|
||||||
|
5. **גוף הטיעון בפסקה אחת** — 3-7 שורות עברית, נאמן למקור.
|
||||||
|
6. **שמירת ה-claim_ids המקוריים** — לכל טיעון, רשום אילו פרופוזיציות תומכות בו.
|
||||||
|
|
||||||
|
## פלט:
|
||||||
|
החזר JSON בלבד (ללא markdown, ללא הסברים), array של אובייקטים:
|
||||||
|
```
|
||||||
|
[
|
||||||
|
{{
|
||||||
|
"title": "כותרת קצרה של הטיעון",
|
||||||
|
"body": "גוף הטיעון בפסקה אחת",
|
||||||
|
"topic": "סוגיה משפטית קצרה (לדוגמה: 'זכות עמידה', 'תחולת תמ\\"א 38')",
|
||||||
|
"priority": "threshold|substantive|procedural|relief",
|
||||||
|
"claim_ids": ["uuid-1", "uuid-2"]
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## הפרופוזיציות:
|
||||||
|
{propositions_json}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _build_prompt(party: str, propositions: list[dict]) -> str:
|
||||||
|
"""Compose the per-party aggregation prompt."""
|
||||||
|
n = len(propositions)
|
||||||
|
# Conservative target: ~1 argument per 2-3 propositions, clamped 4-12.
|
||||||
|
target_min = max(4, n // 4)
|
||||||
|
target_max = max(target_min + 1, min(12, n // 2 + 1))
|
||||||
|
|
||||||
|
party_he = PARTY_LABELS_HE.get(party, party)
|
||||||
|
# Strip noise from propositions for the prompt — Claude only needs
|
||||||
|
# the id and the text to do the grouping.
|
||||||
|
compact = [
|
||||||
|
{"id": str(p["id"]), "text": p["claim_text"]}
|
||||||
|
for p in propositions
|
||||||
|
]
|
||||||
|
propositions_json = json.dumps(compact, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
return AGGREGATE_PROMPT_TEMPLATE.format(
|
||||||
|
n=n,
|
||||||
|
party_he=party_he,
|
||||||
|
target_min=target_min,
|
||||||
|
target_max=target_max,
|
||||||
|
propositions_json=propositions_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_argument(raw: dict, fallback_topic: str = "") -> dict | None:
|
||||||
|
"""Validate & normalize a single argument dict from Claude.
|
||||||
|
|
||||||
|
Returns None if the row is unusable (missing required fields).
|
||||||
|
"""
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return None
|
||||||
|
title = (raw.get("title") or "").strip()
|
||||||
|
body = (raw.get("body") or "").strip()
|
||||||
|
if not title or not body:
|
||||||
|
return None
|
||||||
|
priority = raw.get("priority", "substantive")
|
||||||
|
if priority not in ALLOWED_PRIORITIES:
|
||||||
|
priority = "substantive"
|
||||||
|
topic = (raw.get("topic") or fallback_topic or "").strip() or None
|
||||||
|
claim_ids_raw = raw.get("claim_ids") or []
|
||||||
|
claim_ids: list[UUID] = []
|
||||||
|
if isinstance(claim_ids_raw, list):
|
||||||
|
for cid in claim_ids_raw:
|
||||||
|
try:
|
||||||
|
claim_ids.append(UUID(str(cid)))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
return {
|
||||||
|
"title": title,
|
||||||
|
"body": body,
|
||||||
|
"topic": topic,
|
||||||
|
"priority": priority,
|
||||||
|
"claim_ids": claim_ids,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _aggregate_party(
|
||||||
|
party: str, propositions: list[dict],
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Ask Claude to group one party's propositions; return normalized rows."""
|
||||||
|
if not propositions:
|
||||||
|
return []
|
||||||
|
prompt = _build_prompt(party, propositions)
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw_result = await claude_session.query_json(prompt)
|
||||||
|
except RuntimeError as e:
|
||||||
|
# Surface CLI-unavailable specifically so the caller can report
|
||||||
|
# cleanly instead of crashing the whole job.
|
||||||
|
raise RuntimeError(
|
||||||
|
f"argument_aggregator: claude_session.query_json failed for party "
|
||||||
|
f"'{party}': {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
if not isinstance(raw_result, list):
|
||||||
|
logger.warning(
|
||||||
|
"argument_aggregator: Claude returned non-list (%s) for party '%s'",
|
||||||
|
type(raw_result).__name__, party,
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
out: list[dict] = []
|
||||||
|
for entry in raw_result:
|
||||||
|
norm = _normalize_argument(entry)
|
||||||
|
if norm:
|
||||||
|
out.append(norm)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
async def aggregate_claims_to_arguments(
|
||||||
|
case_id: UUID, force: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
"""For a given case, group existing claims into distinct legal arguments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
case_id: The case UUID.
|
||||||
|
force: If True, delete existing ``legal_arguments`` for the case
|
||||||
|
before aggregating. Otherwise short-circuit if any rows exist.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A summary dict:
|
||||||
|
``{"status": "completed"|"skipped"|"no_claims"|"llm_unavailable",
|
||||||
|
"by_party": {party: count}, "total": int, "message": ...}``
|
||||||
|
"""
|
||||||
|
pool = await db.get_pool()
|
||||||
|
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
existing = await conn.fetchval(
|
||||||
|
"SELECT COUNT(*) FROM legal_arguments WHERE case_id = $1",
|
||||||
|
case_id,
|
||||||
|
)
|
||||||
|
if existing and not force:
|
||||||
|
return {
|
||||||
|
"status": "skipped",
|
||||||
|
"message": f"Found {existing} existing arguments. Use force=True to re-run.",
|
||||||
|
"total": existing,
|
||||||
|
}
|
||||||
|
|
||||||
|
if force and existing:
|
||||||
|
await conn.execute(
|
||||||
|
"DELETE FROM legal_arguments WHERE case_id = $1", case_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pull all claims for this case, grouped by party.
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""SELECT id, party_role, claim_text, claim_index, source_document
|
||||||
|
FROM claims
|
||||||
|
WHERE case_id = $1
|
||||||
|
ORDER BY party_role, claim_index""",
|
||||||
|
case_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
return {
|
||||||
|
"status": "no_claims",
|
||||||
|
"message": "No claims found for this case. Run extract_claims first.",
|
||||||
|
"total": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Group propositions by party.
|
||||||
|
by_party: dict[str, list[dict]] = {}
|
||||||
|
for r in rows:
|
||||||
|
party = r["party_role"]
|
||||||
|
# Map deprecated 'appraiser' or unknown labels to 'unknown'.
|
||||||
|
if party not in ALLOWED_PARTIES:
|
||||||
|
party = "unknown"
|
||||||
|
by_party.setdefault(party, []).append(dict(r))
|
||||||
|
|
||||||
|
party_counts: dict[str, int] = {}
|
||||||
|
inserted = 0
|
||||||
|
errors: list[str] = []
|
||||||
|
|
||||||
|
for party, props in by_party.items():
|
||||||
|
try:
|
||||||
|
arguments = await _aggregate_party(party, props)
|
||||||
|
except RuntimeError as e:
|
||||||
|
# Most likely cause: Claude CLI not installed (running from
|
||||||
|
# the container). Don't crash — record the gap and continue.
|
||||||
|
msg = str(e)
|
||||||
|
if "Claude CLI not found" in msg:
|
||||||
|
return {
|
||||||
|
"status": "llm_unavailable",
|
||||||
|
"message": (
|
||||||
|
"Claude CLI not available. This service must run from "
|
||||||
|
"the local MCP server (not the FastAPI container)."
|
||||||
|
),
|
||||||
|
"total": 0,
|
||||||
|
}
|
||||||
|
errors.append(f"{party}: {msg}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not arguments:
|
||||||
|
party_counts[party] = 0
|
||||||
|
continue
|
||||||
|
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
async with conn.transaction():
|
||||||
|
for idx, arg in enumerate(arguments):
|
||||||
|
arg_id = await conn.fetchval(
|
||||||
|
"""INSERT INTO legal_arguments
|
||||||
|
(case_id, party, argument_index, argument_title,
|
||||||
|
argument_body, legal_topic, priority)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
RETURNING id""",
|
||||||
|
case_id,
|
||||||
|
party,
|
||||||
|
idx + 1,
|
||||||
|
arg["title"],
|
||||||
|
arg["body"],
|
||||||
|
arg["topic"],
|
||||||
|
arg["priority"],
|
||||||
|
)
|
||||||
|
for cid in arg["claim_ids"]:
|
||||||
|
try:
|
||||||
|
await conn.execute(
|
||||||
|
"""INSERT INTO legal_argument_propositions
|
||||||
|
(argument_id, claim_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT DO NOTHING""",
|
||||||
|
arg_id, cid,
|
||||||
|
)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
# Likely FK violation if the LLM hallucinated
|
||||||
|
# a claim_id. Log and continue.
|
||||||
|
logger.warning(
|
||||||
|
"argument_aggregator: skipped bad claim_id %s for arg %s: %s",
|
||||||
|
cid, arg_id, e,
|
||||||
|
)
|
||||||
|
inserted += 1
|
||||||
|
party_counts[party] = len(arguments)
|
||||||
|
|
||||||
|
result: dict = {
|
||||||
|
"status": "completed",
|
||||||
|
"total": inserted,
|
||||||
|
"by_party": party_counts,
|
||||||
|
"propositions_processed": len(rows),
|
||||||
|
}
|
||||||
|
if errors:
|
||||||
|
result["errors"] = errors
|
||||||
|
result["status"] = "completed_with_errors"
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def get_legal_arguments(
|
||||||
|
case_id: UUID, party: str = "",
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Return aggregated legal arguments for a case, optionally filtered by party.
|
||||||
|
|
||||||
|
Each row includes ``supporting_claims`` (list of source claim_ids).
|
||||||
|
"""
|
||||||
|
pool = await db.get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
if party and party in ALLOWED_PARTIES:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""SELECT id, case_id, party, argument_index, argument_title,
|
||||||
|
argument_body, legal_topic, priority, cited_precedents,
|
||||||
|
created_at, updated_at
|
||||||
|
FROM legal_arguments
|
||||||
|
WHERE case_id = $1 AND party = $2
|
||||||
|
ORDER BY priority, argument_index""",
|
||||||
|
case_id, party,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""SELECT id, case_id, party, argument_index, argument_title,
|
||||||
|
argument_body, legal_topic, priority, cited_precedents,
|
||||||
|
created_at, updated_at
|
||||||
|
FROM legal_arguments
|
||||||
|
WHERE case_id = $1
|
||||||
|
ORDER BY party, priority, argument_index""",
|
||||||
|
case_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pull supporting claims (id + full text) for each argument in one
|
||||||
|
# round-trip. ``supporting_claims`` stays id-only for backwards compat
|
||||||
|
# (counts, MCP consumers); ``supporting_propositions`` carries the text
|
||||||
|
# so the UI can show the raw propositions without an extra fetch.
|
||||||
|
arg_ids = [r["id"] for r in rows]
|
||||||
|
supporting: dict[UUID, list[str]] = {}
|
||||||
|
propositions: dict[UUID, list[dict]] = {}
|
||||||
|
if arg_ids:
|
||||||
|
joins = await conn.fetch(
|
||||||
|
"""SELECT lap.argument_id, lap.claim_id,
|
||||||
|
c.claim_text, c.source_document, c.claim_index
|
||||||
|
FROM legal_argument_propositions lap
|
||||||
|
JOIN claims c ON c.id = lap.claim_id
|
||||||
|
WHERE lap.argument_id = ANY($1::uuid[])
|
||||||
|
ORDER BY c.claim_index""",
|
||||||
|
arg_ids,
|
||||||
|
)
|
||||||
|
for j in joins:
|
||||||
|
supporting.setdefault(j["argument_id"], []).append(str(j["claim_id"]))
|
||||||
|
propositions.setdefault(j["argument_id"], []).append({
|
||||||
|
"id": str(j["claim_id"]),
|
||||||
|
"text": j["claim_text"],
|
||||||
|
"source_document": j["source_document"],
|
||||||
|
})
|
||||||
|
|
||||||
|
out: list[dict] = []
|
||||||
|
for r in rows:
|
||||||
|
d = dict(r)
|
||||||
|
d["id"] = str(d["id"])
|
||||||
|
d["case_id"] = str(d["case_id"])
|
||||||
|
d["supporting_claims"] = supporting.get(r["id"], [])
|
||||||
|
d["supporting_propositions"] = propositions.get(r["id"], [])
|
||||||
|
out.append(d)
|
||||||
|
return out
|
||||||
@@ -44,6 +44,26 @@ async def log_action(
|
|||||||
json.dumps(details or {}, ensure_ascii=False)[:200])
|
json.dumps(details or {}, ensure_ascii=False)[:200])
|
||||||
|
|
||||||
|
|
||||||
|
async def log_action_safe(
|
||||||
|
action: str,
|
||||||
|
case_id: "UUID | None" = None,
|
||||||
|
document_id: "UUID | None" = None,
|
||||||
|
details: dict | None = None,
|
||||||
|
user: str = "system",
|
||||||
|
) -> None:
|
||||||
|
"""Non-fatal audit: never let an audit-log failure break the caller's action.
|
||||||
|
|
||||||
|
The authoritative integrity trail is git (X5 §2.1); audit_log is the
|
||||||
|
'who/what/when' observability layer, so a write failure is logged as a
|
||||||
|
warning and swallowed.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await log_action(action, case_id=case_id, document_id=document_id,
|
||||||
|
details=details, user=user)
|
||||||
|
except Exception as e: # noqa: BLE001 — observability must not break the op
|
||||||
|
logger.warning("audit log_action failed (non-fatal) for %s: %s", action, e)
|
||||||
|
|
||||||
|
|
||||||
async def get_audit_log(
|
async def get_audit_log(
|
||||||
case_id: UUID | None = None,
|
case_id: UUID | None = None,
|
||||||
action: str | None = None,
|
action: str | None = None,
|
||||||
|
|||||||
@@ -19,8 +19,14 @@ from datetime import date
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from legal_mcp import config
|
from legal_mcp import config
|
||||||
from legal_mcp.services import db, embeddings, claude_session
|
from legal_mcp.services import db, embeddings, claude_session, audit
|
||||||
from legal_mcp.services.lessons import get_content_checklist, get_methodology_summary
|
from legal_mcp.services.lessons import (
|
||||||
|
OUTCOME_LABELS_HE,
|
||||||
|
PRACTICE_AREA_OVERRIDES,
|
||||||
|
canonical_outcome,
|
||||||
|
get_content_checklist,
|
||||||
|
get_methodology_summary,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -242,8 +248,12 @@ BLOCK_PROMPTS = {
|
|||||||
## חומרי מקור:
|
## חומרי מקור:
|
||||||
{source_context}
|
{source_context}
|
||||||
|
|
||||||
## פסיקה רלוונטית (צטט מכאן ומהידע הכללי שלך):
|
## דוגמאות-סגנון מהחלטות דפנה — מבנה וקול בלבד:
|
||||||
{precedents_context}
|
⚠️ אלה דוגמאות ל**איך** דפנה כותבת (מבנה, קצב, תנועות-הנמקה, ביטויים) — **לא מקור-תוכן**. הכלל המבחין: נוסחה/בוילרפלייט קבוע (פתיח דוקטרינלי, תבנית-סיום) → מותר להעתיק; ניתוח/טענות ספציפיים → **הכלל את הדפוס והתאם לתיק שלפניך**, אל תעתיק; מהות משפטית (הלכה/עובדה) מתיק אחר → **אסור** להעתיק.
|
||||||
|
{daphna_style_exemplars}
|
||||||
|
|
||||||
|
## פסיקה רלוונטית לציטוט (צטט מכאן ומהידע הכללי שלך):
|
||||||
|
{case_law_citations}
|
||||||
|
|
||||||
## סגנון דפנה:
|
## סגנון דפנה:
|
||||||
{style_context}""",
|
{style_context}""",
|
||||||
@@ -270,10 +280,11 @@ BLOCK_PROMPTS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Discussion structure by outcome
|
# Discussion structure by outcome
|
||||||
|
# GAP-51: keyed by canonical outcomes (rejection/partial_acceptance/full_acceptance).
|
||||||
STRUCTURE_GUIDANCE = {
|
STRUCTURE_GUIDANCE = {
|
||||||
"rejected": "דחייה — שכבות הגנה (concentric circles): טענה ראשית → נדחית, טענה חלופית → נדחית, חיזוק.",
|
"rejection": "דחייה — שכבות הגנה (concentric circles): טענה ראשית → נדחית, טענה חלופית → נדחית, חיזוק.",
|
||||||
"accepted": "קבלה — נימוק-נימוק: כל נימוק = CREAC מלא, בניית שכנוע הדרגתי.",
|
"full_acceptance": "קבלה — נימוק-נימוק: כל נימוק = CREAC מלא, בניית שכנוע הדרגתי.",
|
||||||
"partial": "קבלה חלקית — מיפוי מתחים: מה מתקבל ולמה, מה נדחה ולמה, איזון.",
|
"partial_acceptance": "קבלה חלקית — מיפוי מתחים: מה מתקבל ולמה, מה נדחה ולמה, איזון.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -305,7 +316,9 @@ async def write_block(
|
|||||||
# Template blocks
|
# Template blocks
|
||||||
if block_id in TEMPLATE_WRITERS:
|
if block_id in TEMPLATE_WRITERS:
|
||||||
content = TEMPLATE_WRITERS[block_id](case, decision)
|
content = TEMPLATE_WRITERS[block_id](case, decision)
|
||||||
return _build_result(block_id, content, block_cfg)
|
r = _build_result(block_id, content, block_cfg)
|
||||||
|
r["sources"] = {"document_ids": [], "claim_ids": [], "case_law_ids": []}
|
||||||
|
return r
|
||||||
|
|
||||||
# AI-generated blocks
|
# AI-generated blocks
|
||||||
prompt_template = BLOCK_PROMPTS.get(block_id)
|
prompt_template = BLOCK_PROMPTS.get(block_id)
|
||||||
@@ -318,15 +331,22 @@ async def write_block(
|
|||||||
claims_context = await _build_claims_context(case_id)
|
claims_context = await _build_claims_context(case_id)
|
||||||
direction_context = _build_direction_context(decision)
|
direction_context = _build_direction_context(decision)
|
||||||
plans_context = await _build_plans_context(case_id)
|
plans_context = await _build_plans_context(case_id)
|
||||||
precedents_context = await _build_precedents_context(case_id, block_id)
|
daphna_style_exemplars, case_law_citations, _precedent_case_law_ids = (
|
||||||
style_context = await _build_style_context()
|
await _build_precedents_context(case_id, block_id)
|
||||||
|
)
|
||||||
|
style_context = await _build_style_context(case.get("practice_area", ""))
|
||||||
discussion_context = await _build_previous_blocks_context(case_id, decision)
|
discussion_context = await _build_previous_blocks_context(case_id, decision)
|
||||||
appraiser_facts_context = await _build_appraiser_facts_context(case_id)
|
appraiser_facts_context = await _build_appraiser_facts_context(case_id)
|
||||||
appraiser_conflicts_context = await _build_appraiser_conflicts_context(case_id)
|
appraiser_conflicts_context = await _build_appraiser_conflicts_context(case_id)
|
||||||
post_hearing_context = await _build_post_hearing_context(case_id)
|
post_hearing_context = await _build_post_hearing_context(case_id)
|
||||||
|
|
||||||
outcome = (decision or {}).get("outcome", "rejected")
|
outcome = canonical_outcome((decision or {}).get("outcome", "rejection"))
|
||||||
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
||||||
|
if case.get("practice_area") == "betterment_levy":
|
||||||
|
structure_guidance = (
|
||||||
|
structure_guidance + " | היטל השבחה: "
|
||||||
|
+ " ".join(PRACTICE_AREA_OVERRIDES["betterment_levy"]["discussion_rules"])
|
||||||
|
).strip()
|
||||||
|
|
||||||
# Content checklist — tells block-yod WHAT topics to cover
|
# Content checklist — tells block-yod WHAT topics to cover
|
||||||
content_checklist = ""
|
content_checklist = ""
|
||||||
@@ -349,7 +369,8 @@ async def write_block(
|
|||||||
claims_context=claims_context,
|
claims_context=claims_context,
|
||||||
direction_context=direction_context,
|
direction_context=direction_context,
|
||||||
plans_context=plans_context,
|
plans_context=plans_context,
|
||||||
precedents_context=precedents_context,
|
daphna_style_exemplars=daphna_style_exemplars,
|
||||||
|
case_law_citations=case_law_citations,
|
||||||
style_context=style_context,
|
style_context=style_context,
|
||||||
discussion_context=discussion_context,
|
discussion_context=discussion_context,
|
||||||
structure_guidance=structure_guidance,
|
structure_guidance=structure_guidance,
|
||||||
@@ -391,7 +412,11 @@ async def write_block(
|
|||||||
timeout = claude_session.LONG_TIMEOUT if model_key == "opus" else claude_session.DEFAULT_TIMEOUT
|
timeout = claude_session.LONG_TIMEOUT if model_key == "opus" else claude_session.DEFAULT_TIMEOUT
|
||||||
content = await claude_session.query(prompt, timeout=timeout)
|
content = await claude_session.query(prompt, timeout=timeout)
|
||||||
|
|
||||||
return _build_result(block_id, content, block_cfg)
|
sources = await _collect_block_sources(case_id, block_id)
|
||||||
|
sources["case_law_ids"] = _precedent_case_law_ids
|
||||||
|
result = _build_result(block_id, content, block_cfg)
|
||||||
|
result["sources"] = sources
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _build_result(block_id: str, content: str, block_cfg: dict) -> dict:
|
def _build_result(block_id: str, content: str, block_cfg: dict) -> dict:
|
||||||
@@ -408,11 +433,32 @@ def _build_result(block_id: str, content: str, block_cfg: dict) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _collect_block_sources(case_id: UUID, block_id: str) -> dict:
|
||||||
|
"""Deterministic source ids available to a block's generation (GAP-19).
|
||||||
|
|
||||||
|
document_ids: case documents matching the block's allowed doc-types.
|
||||||
|
claim_ids: extracted claims for the case. (case_law_ids are captured
|
||||||
|
separately from the precedent search inside write_block.)
|
||||||
|
"""
|
||||||
|
allowed = _BLOCK_DOC_TYPES.get(block_id, []) # [] = all docs; None = no source docs
|
||||||
|
if allowed is None:
|
||||||
|
docs = [] # mirror _build_source_context: this block consumes no raw source docs
|
||||||
|
else:
|
||||||
|
docs = await db.list_documents(case_id)
|
||||||
|
if allowed:
|
||||||
|
docs = [d for d in docs if d.get("doc_type") in allowed]
|
||||||
|
claims = await db.get_claims(case_id)
|
||||||
|
return {
|
||||||
|
"document_ids": [str(d["id"]) for d in docs],
|
||||||
|
"claim_ids": [str(c["id"]) for c in claims],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── Context builders ──────────────────────────────────────────────
|
# ── Context builders ──────────────────────────────────────────────
|
||||||
|
|
||||||
def _build_case_context(case: dict, decision: dict | None) -> str:
|
def _build_case_context(case: dict, decision: dict | None) -> str:
|
||||||
outcome = (decision or {}).get("outcome", "")
|
outcome = canonical_outcome((decision or {}).get("outcome", ""))
|
||||||
outcome_heb = {"rejected": "דחייה", "accepted": "קבלה", "partial": "קבלה חלקית"}.get(outcome, "")
|
outcome_heb = OUTCOME_LABELS_HE.get(outcome, "")
|
||||||
return f"""- מספר תיק: {case['case_number']}
|
return f"""- מספר תיק: {case['case_number']}
|
||||||
- כותרת: {case.get('title', '')}
|
- כותרת: {case.get('title', '')}
|
||||||
- עוררים: {', '.join(case.get('appellants', []))}
|
- עוררים: {', '.join(case.get('appellants', []))}
|
||||||
@@ -668,33 +714,64 @@ async def _build_post_hearing_context(case_id: UUID) -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
async def _build_precedents_context(case_id: UUID, block_id: str) -> str:
|
async def _build_precedents_context(
|
||||||
"""Search for similar precedent paragraphs from other decisions and case law."""
|
case_id: UUID, block_id: str,
|
||||||
parts = []
|
) -> tuple[str, str, list[str]]:
|
||||||
|
"""Two SEPARATE streams (INV-LRN5 — keep style apart from substance):
|
||||||
|
1. style_exemplars — Dafna's own block-level paragraphs (HOW she writes; structure/voice).
|
||||||
|
2. case_law_citations — precedent case-law (substantive material to quote).
|
||||||
|
Returns (style_exemplars, case_law_citations, case_law_ids).
|
||||||
|
"""
|
||||||
|
style_parts: list[str] = []
|
||||||
|
caselaw_parts: list[str] = []
|
||||||
|
case_law_ids: list[str] = []
|
||||||
|
# block → golden-ratio section, for targeted exemplar retrieval (T2)
|
||||||
|
_BLOCK_SECTION = {
|
||||||
|
"block-vav": "background", "block-zayin": "claims",
|
||||||
|
"block-yod": "discussion", "block-yod-alef": "summary",
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
case = await db.get_case(case_id)
|
case = await db.get_case(case_id)
|
||||||
case_number = case.get("case_number", "") if case else ""
|
case_number = case.get("case_number", "") if case else ""
|
||||||
subject = case.get("subject", "") if case else ""
|
subject = case.get("subject", "") if case else ""
|
||||||
|
practice_area = case.get("practice_area", "") if case else ""
|
||||||
|
decision = await db.get_decision_by_case(case_id)
|
||||||
|
outcome = (decision or {}).get("outcome", "")
|
||||||
query = f"דיון משפטי בנושא {subject}" if subject else "דיון משפטי ועדת ערר"
|
query = f"דיון משפטי בנושא {subject}" if subject else "דיון משפטי ועדת ערר"
|
||||||
query_emb = await embeddings.embed_query(query)
|
query_emb = await embeddings.embed_query(query)
|
||||||
|
section = _BLOCK_SECTION.get(block_id)
|
||||||
|
|
||||||
# Search 1: paragraph_embeddings (from other decisions by Dafna)
|
# Stream 1a (PRIMARY): Dafna's own block-level prose from her corpus
|
||||||
|
# (style_exemplars) — matched by section + outcome + practice_area (T2/T3).
|
||||||
|
if section:
|
||||||
|
exemplars = await db.search_style_exemplars(
|
||||||
|
query_embedding=query_emb, section=section,
|
||||||
|
outcome=outcome or None, practice_area=practice_area or None, limit=6,
|
||||||
|
)
|
||||||
|
exemplars = [e for e in exemplars if e.get("decision_number", "") != case_number]
|
||||||
|
for e in exemplars[:4]:
|
||||||
|
style_parts.append(
|
||||||
|
f"[דוגמת-סגנון (מבנה/קול בלבד — התאם, אל תעתיק תוכן) — "
|
||||||
|
f"{e.get('decision_number', '?')}, {section}, "
|
||||||
|
f"outcome={e.get('outcome') or '—'}]\n{e['paragraph_text'][:1100]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stream 1b: paragraphs from pipeline cases (legacy path; may be empty)
|
||||||
para_results = await db.search_similar_paragraphs(
|
para_results = await db.search_similar_paragraphs(
|
||||||
query_embedding=query_emb, limit=10, block_type="block-yod",
|
query_embedding=query_emb, limit=10, block_type="block-yod",
|
||||||
)
|
)
|
||||||
# Filter out same case
|
|
||||||
para_results = [r for r in para_results if r.get("case_number", "") != case_number]
|
para_results = [r for r in para_results if r.get("case_number", "") != case_number]
|
||||||
for r in para_results[:4]:
|
for r in para_results[:2]:
|
||||||
parts.append(
|
style_parts.append(
|
||||||
f"[החלטת {r.get('case_number', '?')} — {r.get('case_title', '')}, "
|
f"[דוגמת-סגנון — החלטת {r.get('case_number', '?')} "
|
||||||
f"בלוק {r.get('block_type', '')}]\n{r['content'][:500]}"
|
f"{r.get('case_title', '')}, בלוק {r.get('block_type', '')}]\n{r['content'][:500]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Search 2: case_law_embeddings (precedent case law)
|
# Stream 2: case_law_embeddings — substantive precedent (citations)
|
||||||
pool = await db.get_pool()
|
pool = await db.get_pool()
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
caselaw_rows = await conn.fetch(
|
caselaw_rows = await conn.fetch(
|
||||||
"""SELECT cl.case_number, cl.case_name, cl.court, cl.summary, cl.key_quote,
|
"""SELECT cl.id, cl.case_number, cl.case_name, cl.court, cl.summary, cl.key_quote,
|
||||||
1 - (cle.embedding <=> $1) AS score
|
1 - (cle.embedding <=> $1) AS score
|
||||||
FROM case_law_embeddings cle
|
FROM case_law_embeddings cle
|
||||||
JOIN case_law cl ON cl.id = cle.case_law_id
|
JOIN case_law cl ON cl.id = cle.case_law_id
|
||||||
@@ -703,9 +780,10 @@ async def _build_precedents_context(case_id: UUID, block_id: str) -> str:
|
|||||||
query_emb,
|
query_emb,
|
||||||
)
|
)
|
||||||
for r in caselaw_rows[:3]:
|
for r in caselaw_rows[:3]:
|
||||||
|
case_law_ids.append(str(r["id"]))
|
||||||
text = r["key_quote"] or r["summary"] or ""
|
text = r["key_quote"] or r["summary"] or ""
|
||||||
if text:
|
if text:
|
||||||
parts.append(
|
caselaw_parts.append(
|
||||||
f"[פסיקה: {r['case_number']} {r['case_name']} ({r.get('court', '')})] "
|
f"[פסיקה: {r['case_number']} {r['case_name']} ({r.get('court', '')})] "
|
||||||
f"score={r['score']:.3f}\n{text[:400]}"
|
f"score={r['score']:.3f}\n{text[:400]}"
|
||||||
)
|
)
|
||||||
@@ -713,16 +791,63 @@ async def _build_precedents_context(case_id: UUID, block_id: str) -> str:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Failed to fetch precedents: %s", e)
|
logger.warning("Failed to fetch precedents: %s", e)
|
||||||
|
|
||||||
return "\n\n".join(parts) if parts else "(אין תקדימים)"
|
return (
|
||||||
|
"\n\n".join(style_parts) if style_parts else "(אין דוגמאות-סגנון)",
|
||||||
|
"\n\n".join(caselaw_parts) if caselaw_parts else "(אין פסיקה רלוונטית)",
|
||||||
|
case_law_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _build_style_context() -> str:
|
# Cache for the abstract voice profile (read once per process).
|
||||||
"""Build comprehensive style guide from DB patterns + SKILL.md rules.
|
_VOICE_FINGERPRINT_CACHE: str | None = None
|
||||||
|
|
||||||
Per Anthropic: explicit style instructions reduce generic output.
|
# Style-acquisition policy (INV-LRN5): how to USE the style material below.
|
||||||
|
_COPY_POLICY = """## מדיניות-סגנון (איך להשתמש בחומר שלהלן) — חובה:
|
||||||
|
**היעד: לכתוב בקול ובשיטה של דפנה — לא להעתיק.** הפרופיל שלהלן הוא ההכללה של *איך* דפנה כותבת; הַחֵל אותו על העובדות של התיק שלפניך.
|
||||||
|
- **תוכן קבוע/נוסחאי** (פתיח דוקטרינלי, תבנית-סיום, ביטויי-מעבר) → מותר להשתמש כלשונו.
|
||||||
|
- **ניתוח/טענות ספציפיים** → הכלל את הדפוס והתאם לתיק; אל תעתיק ניסוח מתיק אחר.
|
||||||
|
- **מהות משפטית (הלכה/עובדה/תקדים) מתיק אחר** → אסור לגרור לכאן; המהות באה מחומרי-המקור והפסיקה של *התיק הזה* בלבד.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _load_voice_fingerprint() -> str:
|
||||||
|
"""Load the abstract authorial-style profile (daphna-voice-fingerprint.md).
|
||||||
|
|
||||||
|
This is the PRIMARY style channel (Authorial Style Profiling): the generalized
|
||||||
|
'how Dafna writes', injected so the writer adapts it rather than copying exemplars.
|
||||||
|
Read-only consumption of a learning artifact (Writing↔Acquisition separation).
|
||||||
|
"""
|
||||||
|
global _VOICE_FINGERPRINT_CACHE
|
||||||
|
if _VOICE_FINGERPRINT_CACHE is not None:
|
||||||
|
return _VOICE_FINGERPRINT_CACHE
|
||||||
|
try:
|
||||||
|
path = config.DATA_DIR.parent / "docs" / "daphna-voice-fingerprint.md"
|
||||||
|
_VOICE_FINGERPRINT_CACHE = path.read_text(encoding="utf-8")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("voice-fingerprint not loaded: %s", e)
|
||||||
|
_VOICE_FINGERPRINT_CACHE = ""
|
||||||
|
return _VOICE_FINGERPRINT_CACHE
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_style_context(practice_area: str = "") -> str:
|
||||||
|
"""Build comprehensive style guide: abstract voice profile (primary) +
|
||||||
|
SKILL.md rules + DB patterns + accumulated chair learnings.
|
||||||
|
|
||||||
|
Per Anthropic: explicit style instructions reduce generic output. The voice
|
||||||
|
fingerprint is the primary abstract-profile channel (T0 / INV-LRN4-5).
|
||||||
|
Accumulated learnings (T15) — the chair's /methodology edits and /training
|
||||||
|
decision_lessons — are appended LAST and marked authoritative, so everything
|
||||||
|
we have learned to date reaches the writer (not just hardcoded defaults).
|
||||||
"""
|
"""
|
||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
|
# Copy-policy first, then the abstract voice profile (the PRIMARY channel).
|
||||||
|
lines.append(_COPY_POLICY)
|
||||||
|
fingerprint = _load_voice_fingerprint()
|
||||||
|
if fingerprint:
|
||||||
|
lines.append("## פרופיל-הקול של דפנה (טביעת-אצבע — המנגנון המרכזי):\n")
|
||||||
|
lines.append(fingerprint)
|
||||||
|
|
||||||
# Core style rules (from SKILL.md analysis)
|
# Core style rules (from SKILL.md analysis)
|
||||||
lines.append("""## כללי סגנון דפנה תמיר — חובה:
|
lines.append("""## כללי סגנון דפנה תמיר — חובה:
|
||||||
|
|
||||||
@@ -780,6 +905,41 @@ async def _build_style_context() -> str:
|
|||||||
for item in items[:8]:
|
for item in items[:8]:
|
||||||
lines.append(f"- {item['pattern_text']}")
|
lines.append(f"- {item['pattern_text']}")
|
||||||
|
|
||||||
|
# ── למידה מצטברת (T15) — עריכות היו"ר ב-/methodology + לקחי /training ──
|
||||||
|
# גובר על ברירות-המחדל לעיל. כך כל מה שלמדנו עד היום מגיע לכותב.
|
||||||
|
learned: list[str] = []
|
||||||
|
try:
|
||||||
|
for cat, label in (
|
||||||
|
("golden_ratios", "יחסי-זהב (אחוזי-סעיפים)"),
|
||||||
|
("discussion_rules", "כללי-דיון"),
|
||||||
|
("content_checklists", "צ׳קליסטים"),
|
||||||
|
("transition_phrases", "ביטויי-מעבר"),
|
||||||
|
("anti_patterns", "אנטי-דפוסים (להימנע)"),
|
||||||
|
):
|
||||||
|
ov = await db.get_methodology_overrides(cat)
|
||||||
|
if ov:
|
||||||
|
learned.append(f"\n**{label} — ערכי היו\"ר (גוברים על ברירת-המחדל):**")
|
||||||
|
for k, v in ov.items():
|
||||||
|
learned.append(f"- {k}: {json.dumps(v, ensure_ascii=False)}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("methodology overrides not loaded: %s", e)
|
||||||
|
try:
|
||||||
|
lessons = await db.get_recent_decision_lessons(limit=15, practice_area=practice_area)
|
||||||
|
if lessons:
|
||||||
|
learned.append("\n**לקחים מהחלטות קודמות (decision_lessons):**")
|
||||||
|
for ls in lessons:
|
||||||
|
src = ls.get("decision_number") or ls.get("source") or ""
|
||||||
|
learned.append(f"- [{ls.get('category', '')}] {ls['lesson_text']}" + (f" ({src})" if src else ""))
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("decision_lessons not loaded: %s", e)
|
||||||
|
|
||||||
|
if learned:
|
||||||
|
lines.append(
|
||||||
|
"\n## ⭐ למידה מצטברת — חובה, גובר על כל ברירת-מחדל לעיל "
|
||||||
|
"(עריכות היו\"ר ב-/methodology + לקחי /training):"
|
||||||
|
)
|
||||||
|
lines.extend(learned)
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -841,15 +1001,22 @@ async def get_block_context(case_id: UUID, block_id: str, instructions: str = ""
|
|||||||
claims_context = await _build_claims_context(case_id)
|
claims_context = await _build_claims_context(case_id)
|
||||||
direction_context = _build_direction_context(decision)
|
direction_context = _build_direction_context(decision)
|
||||||
plans_context = await _build_plans_context(case_id)
|
plans_context = await _build_plans_context(case_id)
|
||||||
precedents_context = await _build_precedents_context(case_id, block_id)
|
daphna_style_exemplars, case_law_citations, _ = (
|
||||||
style_context = await _build_style_context()
|
await _build_precedents_context(case_id, block_id)
|
||||||
|
)
|
||||||
|
style_context = await _build_style_context(case.get("practice_area", ""))
|
||||||
discussion_context = await _build_previous_blocks_context(case_id, decision)
|
discussion_context = await _build_previous_blocks_context(case_id, decision)
|
||||||
appraiser_facts_context = await _build_appraiser_facts_context(case_id)
|
appraiser_facts_context = await _build_appraiser_facts_context(case_id)
|
||||||
appraiser_conflicts_context = await _build_appraiser_conflicts_context(case_id)
|
appraiser_conflicts_context = await _build_appraiser_conflicts_context(case_id)
|
||||||
post_hearing_context = await _build_post_hearing_context(case_id)
|
post_hearing_context = await _build_post_hearing_context(case_id)
|
||||||
|
|
||||||
outcome = (decision or {}).get("outcome", "rejected")
|
outcome = canonical_outcome((decision or {}).get("outcome", "rejection"))
|
||||||
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
||||||
|
if case.get("practice_area") == "betterment_levy":
|
||||||
|
structure_guidance = (
|
||||||
|
structure_guidance + " | היטל השבחה: "
|
||||||
|
+ " ".join(PRACTICE_AREA_OVERRIDES["betterment_levy"]["discussion_rules"])
|
||||||
|
).strip()
|
||||||
|
|
||||||
# Content checklist + methodology for block-yod
|
# Content checklist + methodology for block-yod
|
||||||
content_checklist = ""
|
content_checklist = ""
|
||||||
@@ -868,7 +1035,8 @@ async def get_block_context(case_id: UUID, block_id: str, instructions: str = ""
|
|||||||
claims_context=claims_context,
|
claims_context=claims_context,
|
||||||
direction_context=direction_context,
|
direction_context=direction_context,
|
||||||
plans_context=plans_context,
|
plans_context=plans_context,
|
||||||
precedents_context=precedents_context,
|
daphna_style_exemplars=daphna_style_exemplars,
|
||||||
|
case_law_citations=case_law_citations,
|
||||||
style_context=style_context,
|
style_context=style_context,
|
||||||
discussion_context=discussion_context,
|
discussion_context=discussion_context,
|
||||||
structure_guidance=structure_guidance,
|
structure_guidance=structure_guidance,
|
||||||
@@ -896,7 +1064,8 @@ async def get_block_context(case_id: UUID, block_id: str, instructions: str = ""
|
|||||||
"source_documents": source_context,
|
"source_documents": source_context,
|
||||||
"claims": claims_context,
|
"claims": claims_context,
|
||||||
"direction": direction_context,
|
"direction": direction_context,
|
||||||
"precedents": precedents_context,
|
"precedents": case_law_citations,
|
||||||
|
"style_exemplars": daphna_style_exemplars,
|
||||||
"style_guide": style_context,
|
"style_guide": style_context,
|
||||||
"previous_blocks": discussion_context,
|
"previous_blocks": discussion_context,
|
||||||
}
|
}
|
||||||
@@ -919,36 +1088,39 @@ async def save_block_content(case_id: UUID, block_id: str, content: str) -> dict
|
|||||||
result["generation_type"] = "claude-code"
|
result["generation_type"] = "claude-code"
|
||||||
result["model_used"] = "claude-code"
|
result["model_used"] = "claude-code"
|
||||||
|
|
||||||
await store_block(UUID(decision["id"]), result)
|
await store_block(UUID(decision["id"]), result) # store_block syncs the file (#35)
|
||||||
|
await db.mark_blocks_stale(case_id, False)
|
||||||
# Also write/update the draft file on disk
|
|
||||||
await _update_draft_file(case_id, UUID(decision["id"]))
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
async def _update_draft_file(case_id: UUID, decision_id: UUID) -> None:
|
async def _update_draft_file(decision_id: UUID) -> None:
|
||||||
"""Rebuild drafts/decision.md from all blocks in DB."""
|
"""Rebuild drafts/decision.md from all blocks in DB — the single
|
||||||
from pathlib import Path
|
regenerate-draft hook (lessons #35 / GAP-88). Called after EVERY
|
||||||
|
decision_blocks mutation (store_block, renumber) so the on-disk file never
|
||||||
case = await db.get_case(case_id)
|
drifts from the DB. legal-qa validates against the DB; export and the chair
|
||||||
if not case:
|
read the file — keeping them identical kills the "QA fails twice on the same
|
||||||
return
|
already-fixed issue" loop (CMPA-62). Resolves case from decision_id so no
|
||||||
|
caller has to thread case_id through."""
|
||||||
case_dir = config.find_case_dir(case["case_number"])
|
|
||||||
draft_dir = case_dir / "drafts"
|
|
||||||
draft_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
pool = await db.get_pool()
|
pool = await db.get_pool()
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
|
case_row = await conn.fetchrow(
|
||||||
|
"SELECT c.case_number FROM decisions d JOIN cases c ON c.id = d.case_id "
|
||||||
|
"WHERE d.id = $1",
|
||||||
|
decision_id,
|
||||||
|
)
|
||||||
|
if not case_row:
|
||||||
|
return
|
||||||
rows = await conn.fetch(
|
rows = await conn.fetch(
|
||||||
"SELECT content FROM decision_blocks WHERE decision_id = $1 AND content != '' ORDER BY block_index",
|
"SELECT content FROM decision_blocks WHERE decision_id = $1 AND content != '' ORDER BY block_index",
|
||||||
decision_id,
|
decision_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
draft_dir = config.find_case_dir(case_row["case_number"]) / "drafts"
|
||||||
|
draft_dir.mkdir(parents=True, exist_ok=True)
|
||||||
draft_path = draft_dir / "decision.md"
|
draft_path = draft_dir / "decision.md"
|
||||||
draft_path.write_text("\n\n".join(row["content"] for row in rows if row["content"]), encoding="utf-8")
|
draft_path.write_text("\n\n".join(row["content"] for row in rows if row["content"]), encoding="utf-8")
|
||||||
logger.info("Draft file updated: %s (%d blocks)", draft_path, len(rows))
|
logger.info("Draft file synced: %s (%d blocks)", draft_path, len(rows))
|
||||||
|
|
||||||
|
|
||||||
# ── Renumbering ───────────────────────────────────────────────────
|
# ── Renumbering ───────────────────────────────────────────────────
|
||||||
@@ -1002,6 +1174,11 @@ async def renumber_all_blocks(decision_id: UUID) -> dict:
|
|||||||
)
|
)
|
||||||
updated += 1
|
updated += 1
|
||||||
|
|
||||||
|
# #35 — renumber mutates content via raw UPDATE (bypasses store_block), so
|
||||||
|
# sync the draft file here too, otherwise the file keeps stale numbering.
|
||||||
|
if updated:
|
||||||
|
await _update_draft_file(decision_id)
|
||||||
|
|
||||||
return {"total_paragraphs": current_num - 1, "blocks_updated": updated}
|
return {"total_paragraphs": current_num - 1, "blocks_updated": updated}
|
||||||
|
|
||||||
|
|
||||||
@@ -1034,6 +1211,9 @@ async def store_block(decision_id: UUID, block_result: dict) -> None:
|
|||||||
block_result["model_used"],
|
block_result["model_used"],
|
||||||
block_result["temperature"],
|
block_result["temperature"],
|
||||||
)
|
)
|
||||||
|
# #35 — regenerate the on-disk draft on every persist so DB and file stay
|
||||||
|
# identical (legal-qa reads DB; export/chair read the file).
|
||||||
|
await _update_draft_file(decision_id)
|
||||||
|
|
||||||
|
|
||||||
async def write_and_store_block(
|
async def write_and_store_block(
|
||||||
@@ -1049,4 +1229,15 @@ async def write_and_store_block(
|
|||||||
|
|
||||||
result = await write_block(case_id, block_id, instructions)
|
result = await write_block(case_id, block_id, instructions)
|
||||||
await store_block(UUID(decision["id"]), result)
|
await store_block(UUID(decision["id"]), result)
|
||||||
|
await audit.log_action_safe(
|
||||||
|
"write_block", case_id=case_id,
|
||||||
|
details={
|
||||||
|
"decision_id": str(decision["id"]),
|
||||||
|
"block_id": block_id,
|
||||||
|
"model_used": result.get("model_used"),
|
||||||
|
"generation_type": result.get("generation_type"),
|
||||||
|
"sources": result.get("sources", {}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await db.mark_blocks_stale(case_id, False)
|
||||||
return result
|
return result
|
||||||
|
|||||||
121
mcp-server/src/legal_mcp/services/bulletin_library.py
Normal file
121
mcp-server/src/legal_mcp/services/bulletin_library.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
"""Ingest a monthly "עו"ד על נדל"ן" bulletin into the digests radar (X12).
|
||||||
|
|
||||||
|
A bulletin PDF is multi-topic: it EXPLODES into several digest rows — one per
|
||||||
|
case-law pointer (digest_kind='decision') and one per article (digest_kind=
|
||||||
|
'article'), all tagged publication='עו"ד על נדל"ן' to distinguish them from the
|
||||||
|
daily "כל יום" issues. This reuses the existing radar (no parallel corpus — G2):
|
||||||
|
the case pointers join search_digests / the /digests page and autolink to the
|
||||||
|
underlying ruling exactly like a daily digest; articles are deep-context only.
|
||||||
|
|
||||||
|
LOCAL-ONLY (LLM split + embedding) — host scripts/MCP, never the container path.
|
||||||
|
Idempotent: each item's content_hash (hash of its analysis_text) is the dedup
|
||||||
|
key, so re-running a bulletin skips already-ingested items.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from legal_mcp.services import db, embeddings, extractor
|
||||||
|
from legal_mcp.services import bulletin_splitter, digest_library
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PUBLICATION = 'עו"ד על נדל"ן'
|
||||||
|
SOURCE_FIRM = "צבי שוב + רונית אלפר, עורכי דין"
|
||||||
|
|
||||||
|
|
||||||
|
async def _store_and_embed(digest_row: dict) -> None:
|
||||||
|
"""Compute + store the single radar embedding for a freshly created item."""
|
||||||
|
emb_text = digest_library._embedding_text(digest_row)
|
||||||
|
if not emb_text:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
vecs = await embeddings.embed_texts([emb_text], input_type="document")
|
||||||
|
if vecs:
|
||||||
|
await db.store_digest_embedding(digest_row["id"], vecs[0])
|
||||||
|
except Exception as e: # §6 — surfaced, not swallowed
|
||||||
|
logger.warning("bulletin item embedding failed for %s: %s", digest_row.get("id"), e)
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_item(*, analysis_text: str, kind: str, concept_tag: str,
|
||||||
|
headline: str, summary: str, citation: str, court: str,
|
||||||
|
practice_area: str, subject_tags: list[str], src: str) -> dict | None:
|
||||||
|
"""Create one digest row from a bulletin item. Returns the row, or None if it
|
||||||
|
already exists (idempotent skip) or the insert raced on content_hash."""
|
||||||
|
content_hash = db._content_hash(analysis_text)
|
||||||
|
if await db.get_digest_by_content_hash(content_hash):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return await db.create_digest(
|
||||||
|
analysis_text=analysis_text,
|
||||||
|
publication=PUBLICATION,
|
||||||
|
source_firm=SOURCE_FIRM,
|
||||||
|
concept_tag=concept_tag,
|
||||||
|
headline_holding=headline,
|
||||||
|
summary=summary,
|
||||||
|
underlying_citation=citation,
|
||||||
|
underlying_court=court,
|
||||||
|
practice_area=practice_area,
|
||||||
|
subject_tags=subject_tags,
|
||||||
|
source_document_path=src,
|
||||||
|
extraction_status="completed",
|
||||||
|
digest_kind=kind,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# uq_digests_content_hash race (concurrent run) → treat as already-present.
|
||||||
|
if "uq_digests_content_hash" in str(e):
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def ingest_bulletin(file_path: str, model: str | None = None) -> dict:
|
||||||
|
"""Split a bulletin PDF into digest rows (case pointers + articles).
|
||||||
|
|
||||||
|
Returns counts: {cases, articles, created, skipped, linked}. Idempotent.
|
||||||
|
"""
|
||||||
|
path = str(file_path)
|
||||||
|
raw_text, _pages, _meta = await extractor.extract_text(path)
|
||||||
|
split = await bulletin_splitter.split(raw_text, model=model)
|
||||||
|
cases, articles = split.get("cases", []), split.get("articles", [])
|
||||||
|
|
||||||
|
out = {"file": Path(path).name, "cases": len(cases), "articles": len(articles),
|
||||||
|
"created": 0, "skipped": 0, "linked": 0}
|
||||||
|
|
||||||
|
for c in cases:
|
||||||
|
# analysis_text bundles the pointer's substance → stable per-item hash.
|
||||||
|
atext = "\n".join(p for p in (
|
||||||
|
c["concept_tag"], c["headline_holding"], c["summary"], c["underlying_citation"]
|
||||||
|
) if p).strip()
|
||||||
|
row = await _create_item(
|
||||||
|
analysis_text=atext, kind="decision", concept_tag=c["concept_tag"],
|
||||||
|
headline=c["headline_holding"], summary=c["summary"],
|
||||||
|
citation=c["underlying_citation"], court=c["underlying_court"],
|
||||||
|
practice_area=c["practice_area"], subject_tags=c["subject_tags"], src=path,
|
||||||
|
)
|
||||||
|
if row is None:
|
||||||
|
out["skipped"] += 1
|
||||||
|
continue
|
||||||
|
out["created"] += 1
|
||||||
|
await _store_and_embed(row)
|
||||||
|
linked = await digest_library.try_autolink(row["id"], c["underlying_citation"])
|
||||||
|
if linked:
|
||||||
|
out["linked"] += 1
|
||||||
|
|
||||||
|
for a in articles:
|
||||||
|
# The article body is the substance; prefix authors into the summary.
|
||||||
|
body = a["body"] or a["summary"]
|
||||||
|
summary = (f"מאת {a['authors']}. " if a["authors"] else "") + (a["summary"] or "")
|
||||||
|
atext = "\n".join(p for p in (a["title"], summary, body) if p).strip()
|
||||||
|
row = await _create_item(
|
||||||
|
analysis_text=atext, kind="article", concept_tag=a["title"],
|
||||||
|
headline=a["title"], summary=summary, citation="", court="",
|
||||||
|
practice_area=a["practice_area"], subject_tags=a["subject_tags"], src=path,
|
||||||
|
)
|
||||||
|
if row is None:
|
||||||
|
out["skipped"] += 1
|
||||||
|
continue
|
||||||
|
out["created"] += 1
|
||||||
|
await _store_and_embed(row)
|
||||||
|
|
||||||
|
return out
|
||||||
147
mcp-server/src/legal_mcp/services/bulletin_splitter.py
Normal file
147
mcp-server/src/legal_mcp/services/bulletin_splitter.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
"""Split a monthly "עו"ד על נדל"ן" bulletin into typed radar items (X12).
|
||||||
|
|
||||||
|
The monthly bulletin (a SEPARATE publication from the daily "כל יום" digest) is
|
||||||
|
multi-topic: it bundles a featured ARTICLE, a list of legislative updates, and a
|
||||||
|
set of CASE-LAW pointers grouped by topic. The chair chose to catalog the
|
||||||
|
**case-law pointers** (each → a digest, like the daily issue) and the
|
||||||
|
**articles** (deep-context background) — legislative updates are skipped.
|
||||||
|
|
||||||
|
This module is the LLM splitter only. ``bulletin_library.ingest_bulletin`` turns
|
||||||
|
its output into digest rows. Like the daily extractor it is LOCAL-ONLY (claude
|
||||||
|
CLI) and MUST NOT be imported from the FastAPI container path.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from legal_mcp import config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_VALID_PRACTICE_AREAS = {"rishuy_uvniya", "betterment_levy", "compensation_197"}
|
||||||
|
|
||||||
|
BULLETIN_SPLIT_PROMPT = """\
|
||||||
|
אתה מקבל טקסט מלא של **עלון חודשי "עו"ד על נדל"ן"** (פרסום מקצועי רב-נושאי בתחום
|
||||||
|
תכנון ובנייה, מקרקעין, היטל השבחה, פיצויים והתחדשות עירונית). פצל אותו לפריטים.
|
||||||
|
|
||||||
|
העלון בנוי משלושה חלקים: (א) **מאמר** מקצועי ארוך אחד או יותר; (ב) **עדכוני חקיקה**
|
||||||
|
(תיקוני-חוק, אישורי-תכניות, חוזרים) — **התעלם מהם, אל תחלץ**; (ג) **עדכוני פסיקה**
|
||||||
|
מקובצים לפי נושא — כל פריט = מראה-מקום של פסק דין/החלטה + שורת-תקציר.
|
||||||
|
|
||||||
|
**אל תמציא** — חלץ רק מה שמופיע בטקסט. שדה חסר → מחרוזת ריקה.
|
||||||
|
|
||||||
|
## פלט נדרש
|
||||||
|
החזר JSON אחד (object), ללא markdown:
|
||||||
|
|
||||||
|
{
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"underlying_citation": "מראה-המקום המלא של הפסק כפי שמופיע, מילה במילה (למשל 'ערר 8018-02-22 הועדה המקומית בת ים נ' קבוצת מזרחי ובניו השקעות בע\\"מ'). השדה הקריטי.",
|
||||||
|
"concept_tag": "הנושא/הכותרת שתחתיה מופיע הפריט (למשל 'היטל השבחה', 'הפקעות', 'פירוק שיתוף').",
|
||||||
|
"headline_holding": "שורת-התקציר/הכותרת של הפריט — מה נקבע/השאלה (למשל 'חוסר וודאות בין תכנית קודמת לבין ההקלה').",
|
||||||
|
"summary": "תקציר ניטרלי קצר אם יש פירוט נוסף בגוף; אחרת חזור על headline_holding.",
|
||||||
|
"underlying_court": "הערכאה אם מצוינת (למשל 'בית המשפט המחוזי', 'ועדת ערר').",
|
||||||
|
"practice_area": "אחד מ: 'rishuy_uvniya' / 'betterment_levy' / 'compensation_197' — אם ברור מהנושא; אחרת ריק.",
|
||||||
|
"subject_tags": ["2-5 תגיות snake_case בעברית"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"articles": [
|
||||||
|
{
|
||||||
|
"title": "כותרת המאמר (למשל 'הפקעת קרקעות כיום - על המחוקק לתקן את העיוות שנוצר').",
|
||||||
|
"authors": "שמות המחברים (למשל 'עו\\"ד צבי שוב, עו\\"ד רונית אלפר').",
|
||||||
|
"summary": "2-4 משפטים: על מה המאמר ומה הטענה המרכזית.",
|
||||||
|
"body": "הטקסט המלא של המאמר (כל הפסקאות), לצורך embedding וחיפוש-עומק.",
|
||||||
|
"practice_area": "אחד מ-3 אם ברור; אחרת ריק.",
|
||||||
|
"subject_tags": ["2-5 תגיות snake_case"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
## כללים
|
||||||
|
1. **underlying_citation** — חלץ במלואו ובדיוק; הוא הגשר לפסק. פריט-פסיקה בלי מראה-מקום ברור → דלג עליו.
|
||||||
|
2. **cases** — כל מצביעי-הפסיקה בעלון, גם אם תחת נושאים שונים. אל תאחד פריטים נפרדים.
|
||||||
|
3. **articles** — רק מאמרי-עומק (לא רשימת עדכונים). body = הטקסט המלא.
|
||||||
|
4. **עדכוני חקיקה/אישורי-תכניות/חוזרים — לא לחלץ כלל.**
|
||||||
|
5. אם אין מאמר או אין פסיקה — החזר מערך ריק לאותו מפתח.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_str(d: dict, key: str) -> str:
|
||||||
|
v = d.get(key)
|
||||||
|
return v.strip() if isinstance(v, str) else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_tags(d: dict) -> list[str]:
|
||||||
|
tags = d.get("subject_tags")
|
||||||
|
if not isinstance(tags, list):
|
||||||
|
return []
|
||||||
|
return [str(t).strip() for t in tags if str(t).strip()][:8]
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_pa(d: dict) -> str:
|
||||||
|
pa = _norm_str(d, "practice_area")
|
||||||
|
return pa if pa in _VALID_PRACTICE_AREAS else ""
|
||||||
|
|
||||||
|
|
||||||
|
async def split(raw_text: str, model: str | None = None) -> dict:
|
||||||
|
"""Return ``{"cases": [...], "articles": [...]}`` extracted from a bulletin.
|
||||||
|
|
||||||
|
Empty lists on any failure (surfaced as a warning, never raised) so the
|
||||||
|
batch keeps going. Each item is type-normalized; malformed items are dropped.
|
||||||
|
"""
|
||||||
|
from legal_mcp.services import claude_session
|
||||||
|
|
||||||
|
text = (raw_text or "").strip()
|
||||||
|
if not text:
|
||||||
|
return {"cases": [], "articles": []}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await claude_session.query_json(
|
||||||
|
text,
|
||||||
|
system=BULLETIN_SPLIT_PROMPT,
|
||||||
|
model=(model or config.DIGEST_EXTRACT_MODEL or None),
|
||||||
|
tools="", # pure text→JSON; disable tools (avoids error_max_turns)
|
||||||
|
)
|
||||||
|
except Exception as e: # §6 — surfaced, not swallowed
|
||||||
|
logger.warning("bulletin_splitter: query failed: %s", e)
|
||||||
|
return {"cases": [], "articles": []}
|
||||||
|
|
||||||
|
if not isinstance(result, dict):
|
||||||
|
logger.warning("bulletin_splitter: expected dict, got %s", type(result).__name__)
|
||||||
|
return {"cases": [], "articles": []}
|
||||||
|
|
||||||
|
cases: list[dict] = []
|
||||||
|
for c in result.get("cases") or []:
|
||||||
|
if not isinstance(c, dict):
|
||||||
|
continue
|
||||||
|
citation = _norm_str(c, "underlying_citation")
|
||||||
|
if not citation: # rule 1: no anchor → skip
|
||||||
|
continue
|
||||||
|
cases.append({
|
||||||
|
"underlying_citation": citation,
|
||||||
|
"concept_tag": _norm_str(c, "concept_tag"),
|
||||||
|
"headline_holding": _norm_str(c, "headline_holding"),
|
||||||
|
"summary": _norm_str(c, "summary") or _norm_str(c, "headline_holding"),
|
||||||
|
"underlying_court": _norm_str(c, "underlying_court"),
|
||||||
|
"practice_area": _norm_pa(c),
|
||||||
|
"subject_tags": _norm_tags(c),
|
||||||
|
})
|
||||||
|
|
||||||
|
articles: list[dict] = []
|
||||||
|
for a in result.get("articles") or []:
|
||||||
|
if not isinstance(a, dict):
|
||||||
|
continue
|
||||||
|
title = _norm_str(a, "title")
|
||||||
|
body = _norm_str(a, "body")
|
||||||
|
if not (title or body):
|
||||||
|
continue
|
||||||
|
articles.append({
|
||||||
|
"title": title,
|
||||||
|
"authors": _norm_str(a, "authors"),
|
||||||
|
"summary": _norm_str(a, "summary"),
|
||||||
|
"body": body,
|
||||||
|
"practice_area": _norm_pa(a),
|
||||||
|
"subject_tags": _norm_tags(a),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"cases": cases, "articles": articles}
|
||||||
@@ -1,4 +1,14 @@
|
|||||||
"""Legal document chunker - splits text into sections and chunks for RAG."""
|
"""Legal document chunker - splits text into sections and chunks for RAG.
|
||||||
|
|
||||||
|
The default :func:`chunk_document` emits a single tier of overlapping
|
||||||
|
chunks (legacy single-tier indexing). :func:`chunk_document_hierarchical`
|
||||||
|
emits two tiers — small "child" chunks for retrieval matching, plus
|
||||||
|
larger "parent" chunks that supply broader context to the LLM (parent-
|
||||||
|
doc retrieval, TaskMaster #48). The hierarchical variant lives
|
||||||
|
alongside the legacy one so callers can opt in via
|
||||||
|
``config.PARENT_DOC_RETRIEVAL_ENABLED`` without breaking existing
|
||||||
|
single-tier code paths.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -87,13 +97,40 @@ def _assign_pages(chunks: list[Chunk], text: str, page_offsets: list[int]) -> No
|
|||||||
pos = idx + max(1, len(c.content) // 2)
|
pos = idx + max(1, len(c.content) // 2)
|
||||||
|
|
||||||
|
|
||||||
|
# A section shorter than this (stripped chars) is not a real section — it's
|
||||||
|
# an artifact of a header keyword matched mid-text. Such a fragment is merged
|
||||||
|
# into the preceding section rather than emitted as its own chunk. See #55:
|
||||||
|
# unanchored keywords like "דיון"/"החלטה"/"מסקנה" appearing inside a sentence
|
||||||
|
# used to carve tiny boundary chunks ("דיון). במסגרת ה") that polluted search.
|
||||||
|
MIN_SECTION_CHARS = 60
|
||||||
|
|
||||||
|
# A split chunk shorter than this (stripped chars) must not stand alone — it
|
||||||
|
# rides with adjacent content instead. This is the chunk-level analogue of
|
||||||
|
# MIN_SECTION_CHARS and matches the query-time filter that hides <50-char
|
||||||
|
# chunks. Without it, a section that opens with a short header line ("דיון",
|
||||||
|
# "טענות המשיבים") followed by a paragraph larger than chunk_size flushed the
|
||||||
|
# header as its own tiny chunk (#79, follow-up to #55).
|
||||||
|
MIN_CHUNK_CHARS = 50
|
||||||
|
|
||||||
|
|
||||||
def _split_into_sections(text: str) -> list[tuple[str, str]]:
|
def _split_into_sections(text: str) -> list[tuple[str, str]]:
|
||||||
"""Split text into (section_type, text) pairs based on Hebrew headers."""
|
"""Split text into (section_type, text) pairs based on Hebrew headers.
|
||||||
|
|
||||||
|
Header keywords are matched only at the **start of a line** (after
|
||||||
|
optional whitespace / list numbering like ``5.`` or ``ג.``). A real
|
||||||
|
section header in these decisions sits on its own line; anchoring to
|
||||||
|
the line start prevents common words ("דיון", "החלטה", "מסקנה") that
|
||||||
|
appear mid-sentence from being treated as section boundaries — which
|
||||||
|
previously produced tiny fragment chunks (#55).
|
||||||
|
"""
|
||||||
# Find all section headers and their positions
|
# Find all section headers and their positions
|
||||||
markers: list[tuple[int, str]] = []
|
markers: list[tuple[int, str]] = []
|
||||||
|
|
||||||
for pattern, section_type in SECTION_PATTERNS:
|
for pattern, section_type in SECTION_PATTERNS:
|
||||||
for match in re.finditer(pattern, text):
|
# ^ + MULTILINE: line start only. Optional leading spaces/tabs and an
|
||||||
|
# optional ordinal prefix ("5.", "5)", "ג.") before the keyword.
|
||||||
|
anchored = rf"^[ \t]*(?:\d+[.)]\s*|[א-ת][.)]\s*)?(?:{pattern})"
|
||||||
|
for match in re.finditer(anchored, text, re.MULTILINE):
|
||||||
markers.append((match.start(), section_type))
|
markers.append((match.start(), section_type))
|
||||||
|
|
||||||
if not markers:
|
if not markers:
|
||||||
@@ -110,11 +147,18 @@ def _split_into_sections(text: str) -> list[tuple[str, str]]:
|
|||||||
if intro_text:
|
if intro_text:
|
||||||
sections.append(("intro", intro_text))
|
sections.append(("intro", intro_text))
|
||||||
|
|
||||||
# Each section
|
# Each section. A section whose text is too short to stand alone is
|
||||||
|
# merged into the previous section (keeping the previous type) so a
|
||||||
|
# near-adjacent pair of headers can't produce a fragment chunk.
|
||||||
for i, (pos, section_type) in enumerate(markers):
|
for i, (pos, section_type) in enumerate(markers):
|
||||||
end = markers[i + 1][0] if i + 1 < len(markers) else len(text)
|
end = markers[i + 1][0] if i + 1 < len(markers) else len(text)
|
||||||
section_text = text[pos:end].strip()
|
section_text = text[pos:end].strip()
|
||||||
if section_text:
|
if not section_text:
|
||||||
|
continue
|
||||||
|
if len(section_text) < MIN_SECTION_CHARS and sections:
|
||||||
|
prev_type, prev_text = sections[-1]
|
||||||
|
sections[-1] = (prev_type, f"{prev_text}\n{section_text}")
|
||||||
|
else:
|
||||||
sections.append((section_type, section_text))
|
sections.append((section_type, section_text))
|
||||||
|
|
||||||
return sections
|
return sections
|
||||||
@@ -132,11 +176,20 @@ def _split_section(text: str, chunk_size: int, overlap: int) -> list[str]:
|
|||||||
chunks: list[str] = []
|
chunks: list[str] = []
|
||||||
current: list[str] = []
|
current: list[str] = []
|
||||||
current_tokens = 0
|
current_tokens = 0
|
||||||
|
current_chars = 0
|
||||||
|
|
||||||
for para in paragraphs:
|
for para in paragraphs:
|
||||||
para_tokens = _estimate_tokens(para)
|
para_tokens = _estimate_tokens(para)
|
||||||
|
|
||||||
if current_tokens + para_tokens > chunk_size and current:
|
# Don't flush a buffer that is still below MIN_CHUNK_CHARS — let it
|
||||||
|
# absorb this paragraph even if that overflows chunk_size. A short
|
||||||
|
# header line ("דיון") must ride with the following paragraph rather
|
||||||
|
# than be emitted as a tiny fragment chunk (#79).
|
||||||
|
if (
|
||||||
|
current_tokens + para_tokens > chunk_size
|
||||||
|
and current
|
||||||
|
and current_chars >= MIN_CHUNK_CHARS
|
||||||
|
):
|
||||||
chunks.append("\n".join(current))
|
chunks.append("\n".join(current))
|
||||||
# Keep overlap
|
# Keep overlap
|
||||||
overlap_paras: list[str] = []
|
overlap_paras: list[str] = []
|
||||||
@@ -149,16 +202,173 @@ def _split_section(text: str, chunk_size: int, overlap: int) -> list[str]:
|
|||||||
overlap_tokens += pt
|
overlap_tokens += pt
|
||||||
current = overlap_paras
|
current = overlap_paras
|
||||||
current_tokens = overlap_tokens
|
current_tokens = overlap_tokens
|
||||||
|
current_chars = sum(len(p) for p in current)
|
||||||
|
|
||||||
current.append(para)
|
current.append(para)
|
||||||
current_tokens += para_tokens
|
current_tokens += para_tokens
|
||||||
|
current_chars += len(para)
|
||||||
|
|
||||||
if current:
|
if current:
|
||||||
chunks.append("\n".join(current))
|
chunks.append("\n".join(current))
|
||||||
|
|
||||||
|
# Fold a trailing tiny chunk back into its predecessor — a short trailing
|
||||||
|
# line (e.g. a stray quote fragment) shouldn't stand alone either (#79).
|
||||||
|
if len(chunks) >= 2 and len(chunks[-1].strip()) < MIN_CHUNK_CHARS:
|
||||||
|
tail = chunks.pop()
|
||||||
|
chunks[-1] = f"{chunks[-1]}\n{tail}"
|
||||||
|
|
||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
|
|
||||||
def _estimate_tokens(text: str) -> int:
|
def _estimate_tokens(text: str) -> int:
|
||||||
"""Rough token estimate for Hebrew text (~1.5 chars per token)."""
|
"""Rough token estimate for Hebrew text (~1.5 chars per token)."""
|
||||||
return max(1, len(text) // 2)
|
return max(1, len(text) // 2)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Parent-doc retrieval (TaskMaster #48) ────────────────────────────
|
||||||
|
# Hierarchical chunker — emits a list of (child, parent) pairs:
|
||||||
|
# * each "child" carries the smaller text used for embedding/search
|
||||||
|
# * each "parent" is shared by ~5 consecutive children (1500/300)
|
||||||
|
# The list is FLAT — both parents and children live in the same return
|
||||||
|
# list, distinguished by ``role``. A child's ``parent_local_id`` points
|
||||||
|
# back to its parent's ``local_id``, so the ingest pipeline can resolve
|
||||||
|
# the FK after the parent row is INSERTed and its DB UUID is known.
|
||||||
|
#
|
||||||
|
# Parents are built FIRST (one window of ``parent_size`` tokens per
|
||||||
|
# section, sliding by the parent window — no overlap between parents),
|
||||||
|
# then each parent is sub-divided into overlapping children. This keeps
|
||||||
|
# the parent boundary aligned with semantic sections (so a "discussion"
|
||||||
|
# parent doesn't contain stray "ruling" prose) while still allowing
|
||||||
|
# child overlap for recall.
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HierarchicalChunk:
|
||||||
|
"""One chunk in the two-tier hierarchy.
|
||||||
|
|
||||||
|
Both children and parents share this shape; ``role`` distinguishes
|
||||||
|
them. Children get an embedding at ingest time; parents do not —
|
||||||
|
they exist only to carry context back to the LLM at retrieval time.
|
||||||
|
|
||||||
|
``local_id`` is a stable in-batch identifier (sequential int) used
|
||||||
|
only by the ingest pipeline to wire children to their parent's DB
|
||||||
|
UUID after the parent INSERT returns. It is NOT persisted.
|
||||||
|
"""
|
||||||
|
|
||||||
|
content: str
|
||||||
|
role: str # 'child' | 'parent'
|
||||||
|
section_type: str = "other"
|
||||||
|
page_number: int | None = None
|
||||||
|
chunk_index: int = 0
|
||||||
|
local_id: int = -1
|
||||||
|
parent_local_id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def chunk_document_hierarchical(
|
||||||
|
text: str,
|
||||||
|
child_size: int = config.PARENT_DOC_CHILD_SIZE_TOKENS,
|
||||||
|
parent_size: int = config.PARENT_DOC_PARENT_SIZE_TOKENS,
|
||||||
|
overlap: int = config.PARENT_DOC_CHILD_OVERLAP_TOKENS,
|
||||||
|
page_offsets: list[int] | None = None,
|
||||||
|
) -> list[HierarchicalChunk]:
|
||||||
|
"""Split a document into a two-tier (child, parent) hierarchy.
|
||||||
|
|
||||||
|
Returns a flat list where each element is either a parent or a
|
||||||
|
child. Children carry ``parent_local_id`` pointing back to their
|
||||||
|
parent's ``local_id``. Caller (ingest pipeline) must insert parents
|
||||||
|
first, capture their DB UUIDs by ``local_id``, then insert children
|
||||||
|
with the resolved UUID in ``parent_chunk_id``.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: full document text.
|
||||||
|
child_size: child chunk size in tokens (≈ 300 by default).
|
||||||
|
parent_size: parent chunk size in tokens (≈ 1500 by default).
|
||||||
|
Parents contain ``parent_size // child_size`` children on
|
||||||
|
average.
|
||||||
|
overlap: child-to-child overlap inside a parent (≈ 50 tokens).
|
||||||
|
Parents themselves do not overlap each other.
|
||||||
|
page_offsets: PDF page offsets for tagging chunks with page #.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
* Parents respect section boundaries (header detection from
|
||||||
|
:data:`SECTION_PATTERNS`). A "facts" parent will not include
|
||||||
|
"ruling" text.
|
||||||
|
* Empty text returns an empty list.
|
||||||
|
* Both child and parent rows are tagged with the page of their
|
||||||
|
first character.
|
||||||
|
"""
|
||||||
|
if not text.strip():
|
||||||
|
return []
|
||||||
|
if child_size <= 0 or parent_size <= 0:
|
||||||
|
raise ValueError("child_size and parent_size must be positive")
|
||||||
|
if child_size > parent_size:
|
||||||
|
raise ValueError("child_size must be <= parent_size")
|
||||||
|
|
||||||
|
sections = _split_into_sections(text)
|
||||||
|
out: list[HierarchicalChunk] = []
|
||||||
|
parent_idx = 0 # global parent ordinal (chunk_index for parents)
|
||||||
|
child_idx = 0 # global child ordinal (chunk_index for children)
|
||||||
|
local_id = 0 # sequential id within this document
|
||||||
|
|
||||||
|
for section_type, section_text in sections:
|
||||||
|
# Step 1: split section into parent-sized windows (no overlap).
|
||||||
|
parent_texts = _split_section(section_text, parent_size, overlap=0)
|
||||||
|
for parent_text in parent_texts:
|
||||||
|
parent_local = local_id
|
||||||
|
local_id += 1
|
||||||
|
parent_chunk = HierarchicalChunk(
|
||||||
|
content=parent_text,
|
||||||
|
role="parent",
|
||||||
|
section_type=section_type,
|
||||||
|
chunk_index=parent_idx,
|
||||||
|
local_id=parent_local,
|
||||||
|
parent_local_id=None,
|
||||||
|
)
|
||||||
|
out.append(parent_chunk)
|
||||||
|
parent_idx += 1
|
||||||
|
|
||||||
|
# Step 2: sub-divide this parent into overlapping children.
|
||||||
|
child_texts = _split_section(parent_text, child_size, overlap)
|
||||||
|
for ch_text in child_texts:
|
||||||
|
ch = HierarchicalChunk(
|
||||||
|
content=ch_text,
|
||||||
|
role="child",
|
||||||
|
section_type=section_type,
|
||||||
|
chunk_index=child_idx,
|
||||||
|
local_id=local_id,
|
||||||
|
parent_local_id=parent_local,
|
||||||
|
)
|
||||||
|
out.append(ch)
|
||||||
|
local_id += 1
|
||||||
|
child_idx += 1
|
||||||
|
|
||||||
|
if page_offsets:
|
||||||
|
_assign_pages_hierarchical(out, text, page_offsets)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _assign_pages_hierarchical(
|
||||||
|
chunks: list[HierarchicalChunk],
|
||||||
|
text: str,
|
||||||
|
page_offsets: list[int],
|
||||||
|
) -> None:
|
||||||
|
"""Page-tag both children and parents.
|
||||||
|
|
||||||
|
Same forward-scan strategy as :func:`_assign_pages` but works on
|
||||||
|
the hierarchical list. Parents may span pages; we tag them with
|
||||||
|
the page of their first character (matches how the multimodal
|
||||||
|
retriever joins on page numbers).
|
||||||
|
"""
|
||||||
|
from legal_mcp.services.extractor import page_at_offset
|
||||||
|
pos = 0
|
||||||
|
for c in chunks:
|
||||||
|
idx = text.find(c.content, pos)
|
||||||
|
if idx < 0:
|
||||||
|
idx = text.find(c.content)
|
||||||
|
if idx < 0:
|
||||||
|
continue
|
||||||
|
c.page_number = page_at_offset(idx, page_offsets)
|
||||||
|
# Advance past halfway — children share text with their parent
|
||||||
|
# and with each other (overlap), so a small forward step lets
|
||||||
|
# the next find() still pick up the right occurrence.
|
||||||
|
pos = idx + max(1, len(c.content) // 4)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user