Compare commits

...

4 Commits

Author SHA1 Message Date
08419e4434 Merge pull request 'fix(writer): כלל-הסגנון בסוף הפרומפט — חותך אנטי-דפוסים ב-72–93%' (#421) from worktree-anti-pattern-directive-position into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 2m34s
G12 Leak-Guard / leak-guard (push) Successful in 4s
Lint — undefined names / undefined-names (push) Successful in 12s
2026-07-28 11:53:57 +00:00
2ebaa82f85 Merge remote-tracking branch 'origin/main' into worktree-anti-pattern-directive-position
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
2026-07-28 11:53:23 +00:00
79d9ea55d8 Merge pull request 'feat(eval): ממד model×prompt ל-harness הכיול (#208) — A/B מודל-ייצור מול הסופיים' (#420) from worktree-opus5-model-calibration into main
Some checks failed
Build & Deploy / build-and-deploy (push) Has been cancelled
G12 Leak-Guard / leak-guard (push) Has been cancelled
Lint — undefined names / undefined-names (push) Has been cancelled
2026-07-28 11:52:57 +00:00
42ea1a7c58 fix(writer): כלל-הסגנון בסוף הפרומפט — הוא היה שם, במקום שבו הוא לא תופס
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 35s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
הרשימה הקנונית של lessons.ANTI_PATTERNS כבר הוזרקה לכותב, אבל בתו ~46,781
מתוך 46,950 של style_context — שהוא עצמו מקטע אחד מתוך ~12 בפרומפט. הטיוטות
המשיכו לפלוט בדיוק את מה שהיא אוסרת.

A/B מדוד מול הסופיים החתומים (9 תיקים, 60 ייצורים, 2026-07-28) הראה שאותו
כלל, בסוף הפרומפט, חותך anti_pattern_total ב-72–93%:

  block-vav   opus-4-8 1.75→0.12 · opus-5 2.25→0.62
  block-zayin opus-4-8 4.57→0.43 · opus-5 4.43→0.43

וב-distance: −12%/−29% (4-8), −9%/−27% (5). זה שיפור גדול פי-3 מכל הבדל
שנמדד בין המודלים עצמם.

- `lessons.anti_pattern_directive()` — רינדור שני של אותה רשימה קנונית
  (מקור אחד, שתי תצוגות — לא שני כללים).
- מתווסף **אחרון** בשני מסלולי-הכתיבה: `write_block` (בתהליך) ו-
  `get_block_context` (סוכן legal-writer). אילו הוחל רק באחד, שני הכותבים
  היו נפרדים בסגנון (G2).
- **תיקון בליעה-שקטה (§6):** הרשימה הקנונית רונדרה בתוך לולאת-ה-overrides,
  כך שכשל-DB בקטגוריה מוקדמת (golden_ratios) הפיל את הלולאה והשמיט את
  אינווריאנטי-הסגנון כליל — עם אזהרה גנרית בלבד. עכשיו היא מרונדרת ללא
  תנאי, לפני כל קריאת-DB; הערות-היו"ר מתווספות מעליה.

invariants: G11 (תוכן משפטי — סגנון דפנה) · G2 (מקור-אמת יחיד לכלל, ושני
מסלולי-הכתיבה מיושרים) · §6 (אין בליעה שקטה).

בדיקות: 473 passed (3 חדשות — רינדור מלא, שני המסלולים, שרידות לכשל-DB).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:14:47 +00:00
3 changed files with 108 additions and 4 deletions

View File

@@ -23,8 +23,10 @@ from pathlib import Path
from legal_mcp import config
from legal_mcp.services import db, embeddings, claude_session, audit, storage
from legal_mcp.services.lessons import (
ANTI_PATTERNS as _ANTI_PATTERNS,
OUTCOME_LABELS_HE,
PRACTICE_AREA_OVERRIDES,
anti_pattern_directive,
canonical_outcome,
get_content_checklist,
get_methodology_summary,
@@ -475,6 +477,12 @@ async def write_block(
if instructions:
prompt += f"\n\n## הנחיות נוספות:\n{instructions}"
# LAST in the prompt, deliberately (see lessons.anti_pattern_directive): the
# same canonical rule already appears inside style_context, but ~47K chars
# deep, where it measurably fails to bind. Restating it here is the only
# change the A/B isolated as effective — so nothing may be appended after it.
prompt += "\n\n" + anti_pattern_directive()
# Block י requires approved direction
if block_id == "block-yod":
dir_doc = (decision or {}).get("direction_doc") or {}
@@ -1119,6 +1127,16 @@ async def _build_style_context(practice_area: str = "") -> str:
# ── למידה מצטברת (T15) — עריכות היו"ר ב-/methodology + לקחי /training ──
# גובר על ברירות-המחדל לעיל. כך כל מה שלמדנו עד היום מגיע לכותב.
learned: list[str] = []
# The canonical anti-patterns are rendered UNCONDITIONALLY, before any DB
# call. They used to be produced inside the overrides loop below — so a
# failure on an EARLIER category (e.g. golden_ratios) aborted the loop and
# dropped the style invariants from the prompt silently, with only a generic
# "overrides not loaded" warning to show for it (§6). A chair-override
# outage must not be able to un-teach Dafna's structural style.
learned.append("\n**אנטי-דפוסים (להימנע) — כתוב נרטיב משפטי רציף; הימנע מ:**")
for ap in _ANTI_PATTERNS:
learned.append(f"- {ap['note']}")
try:
for cat, label in (
("golden_ratios", "יחסי-זהב (אחוזי-סעיפים)"),
@@ -1137,10 +1155,8 @@ async def _build_style_context(practice_area: str = "") -> str:
# corrects them, and drafts keep emitting them (the gap that left
# 8137 with 28 hits). Chair additions layer on top; they never
# remove the canonical ones.
from legal_mcp.services.lessons import ANTI_PATTERNS as _ANTI
learned.append(f"\n**{label} — כתוב נרטיב משפטי רציף; הימנע מ:**")
for ap in _ANTI:
learned.append(f"- {ap['note']}")
# The canonical list is already rendered above, outside this try —
# here we only layer the chair's ADDITIONS on top of it.
for k, v in (ov or {}).items():
learned.append(f"- (יו\"ר) {k}: {json.dumps(v, ensure_ascii=False)}")
continue
@@ -1276,6 +1292,12 @@ async def get_block_context(case_id: UUID, block_id: str, instructions: str = ""
if instructions:
formatted_prompt += f"\n\n## הנחיות נוספות:\n{instructions}"
# Same closing directive, same position, same canonical source as write_block.
# This is the EXTERNAL-writer path (legal-writer agent) — if the rule were
# applied only in write_block, agent-written blocks would keep emitting the
# anti-patterns and the two writers would drift apart (G2).
formatted_prompt += "\n\n" + anti_pattern_directive()
# Block י requires approved direction
if block_id == "block-yod":
dir_doc = (decision or {}).get("direction_doc") or {}

View File

@@ -59,6 +59,28 @@ ANTI_PATTERNS: list[dict] = [
"note": "רשימות תבליטים באנליזה — דפנה כותבת נרטיב רציף"},
]
def anti_pattern_directive() -> str:
"""The closing style directive, rendered from ANTI_PATTERNS (the same list
style_distance scores against — one source, two renderings, not two rules).
WHY THIS EXISTS SEPARATELY FROM the style-context rendering: the rule was
already reaching the writer, buried ~47K chars deep inside style_context,
and drafts kept emitting the very patterns it forbids. A measured A/B over
the signed finals (9 cases, 60 generations, 2026-07-28) showed that the SAME
rule restated at the END of the assembled prompt cuts anti-pattern hits by
7293% on both blocks and both models:
block-vav opus-4-8 1.75 → 0.12 | opus-5 2.25 → 0.62
block-zayin opus-4-8 4.57 → 0.43 | opus-5 4.43 → 0.43
So this is a POSITION fix, not a new instruction. Keep it last in the prompt.
"""
lines = ["## כלל-סגנון מחייב (גובר על כל דוגמה בהקשר שלמעלה)",
"כתוב נרטיב משפטי רציף בלבד — פסקאות שלמות. אסור:"]
lines += [f"- {ap['note']}" for ap in ANTI_PATTERNS]
return "\n".join(lines)
# ── Paragraph length guidance (word counts) ────────────────────────
PARAGRAPH_LENGTHS = {

View File

@@ -0,0 +1,60 @@
"""The style invariants must actually REACH the writer.
Both tests here cover defects found by the 2026-07-28 model×prompt A/B over the
signed finals: the canonical anti-patterns were present in the prompt but buried
~47K chars into style_context (where they measurably failed to bind), and they
were rendered inside a try/except that an unrelated DB failure could abort.
"""
import pytest
from legal_mcp.services import block_writer
from legal_mcp.services.lessons import ANTI_PATTERNS, anti_pattern_directive
def test_directive_renders_every_canonical_anti_pattern():
"""One source, two renderings — the directive may not drift from the list
style_distance scores against."""
text = anti_pattern_directive()
for ap in ANTI_PATTERNS:
assert ap["note"] in text, f"missing anti-pattern in directive: {ap['name']}"
def test_both_writer_paths_append_the_directive_last():
"""write_block (in-process) and get_block_context (legal-writer agent) must
both close with the directive — otherwise the two writers drift (G2)."""
import inspect
src = inspect.getsource(block_writer)
for fn in ("async def write_block(", "async def get_block_context("):
start = src.index(fn)
# bound the search to this function: up to the next top-level def
rest = src[start + len(fn):]
nxt = rest.find("\nasync def ")
body = rest[: nxt if nxt != -1 else len(rest)]
assert "anti_pattern_directive()" in body, f"{fn} does not append the style directive"
@pytest.mark.asyncio
async def test_style_context_keeps_anti_patterns_when_overrides_fail(monkeypatch):
"""A chair-override outage must not silently un-teach the structural style.
Regression: the canonical list used to be emitted inside the overrides loop,
so a throw on an EARLIER category (golden_ratios) dropped it entirely.
"""
async def _boom(*a, **k):
raise RuntimeError("methodology table unavailable")
async def _empty(*a, **k):
return []
# Every DB accessor this function touches is stubbed — the test must not open
# a real connection (a live pool here leaks across the shared event loop and
# breaks unrelated tests later in the run).
monkeypatch.setattr(block_writer.db, "get_style_patterns", _empty)
monkeypatch.setattr(block_writer.db, "get_methodology_overrides", _boom)
monkeypatch.setattr(block_writer.db, "get_recent_decision_lessons", _empty)
ctx = await block_writer._build_style_context("היטל השבחה")
assert "נרטיב משפטי רציף" in ctx
for ap in ANTI_PATTERNS:
assert ap["note"] in ctx, f"anti-pattern dropped on override failure: {ap['name']}"