From 9a19db1ec15c704a87354b414c2058cc930d8895 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 28 Jun 2026 22:26:04 +0000 Subject: [PATCH] feat(learning): voice-fingerprint corpus-refresh proposal generator (#161, prose half) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The voice-fingerprint (docs/daphna-voice-fingerprint.md) is the PRIMARY abstract style channel the writer consumes (07-learning §0.2 channel A), but it was hand-authored and never regenerated from the growing corpus — the "voice" the writer adapts stayed frozen while finals accumulated. PR #345 unfroze the structural half (corpus-measured ratios → writer); this is the prose half. scripts/regenerate_voice_fingerprint.py reads the corpus (style_corpus stats + a bounded block-level style_exemplars sample + measured section ratios) and asks Opus (local) to PROPOSE refinements/additions to the fingerprint — STYLE / METHOD / VOICE / LEXICON only, grounded in corpus evidence, never case substance (INV-LRN5), preserving curated insights rather than replacing them. CHAIR-GATED, HARD (INV-LRN1): the prose profile is voice-knowledge, not a low-risk style rule, so it does NOT auto-flow. The script writes a PROPOSAL to data/curator-proposals/ (the existing curator-proposal surface) and NEVER overwrites the live fingerprint; the chair reviews and hand-commits accepted parts — the same manual gate the fingerprint already uses. Runs on the host (claude CLI). Reuses claude_session + measure_corpus_ratios + the curator-proposals dir (G2 — no parallel synthesis or gate). SCRIPTS.md + spec §0.2 updated. Invariants: INV-LRN1 (hard chair gate for voice-knowledge prose), INV-LRN5 (style-only synthesis), G2 (reuses existing synthesis inputs + proposal surface). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/spec/07-learning.md | 2 +- scripts/SCRIPTS.md | 1 + scripts/regenerate_voice_fingerprint.py | 113 ++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100755 scripts/regenerate_voice_fingerprint.py diff --git a/docs/spec/07-learning.md b/docs/spec/07-learning.md index 0865259..f0103d1 100644 --- a/docs/spec/07-learning.md +++ b/docs/spec/07-learning.md @@ -35,7 +35,7 @@ היעד הוא **Text Style Transfer** מבוסס **פרופיל-סגנון מופשט** — להכליל את סגנון/שיטת דפנה ולהתאים לתיק הספציפי. fine-tuning של משקולות **לא רלוונטי**: המודל (Opus) סגור, והקורפוס (~48 החלטות, יו"ר חדשה) קטן מדי — מצב שבו הספרות מראה שפרופיל-מופשט + דוגמאות מנצח (≈+15% מעל RAG-בלבד). **מדיניות-העתקה לפי סוג-תוכן:** קבוע/נוסחאי (פתיחים דוקטרינליים, תבניות-סיום) → מותר להעתיק; ניתוח/טענות ספציפיים → להכליל ולהתאים; מהות (הלכה/עובדה מתיק אחר) → אסור (INV-LRN5). ### 0.2 שלושת ערוצי-ההזנה לכותב -1. **A — פרופיל-מופשט (ראשי):** voice-fingerprint + author-features כמותיים, מוזרק לכתיבה. +1. **A — פרופיל-מופשט (ראשי):** voice-fingerprint + author-features כמותיים, מוזרק לכתיבה. **חידוש מהקורפוס:** החלק הכמותי (יחסי-מבנה) מתרענן אוטומטית-ודטרמיניסטית מהקורפוס הגדל (`measure_corpus_ratios`→כותב); הפרוזה המופשטת מסונתזת מחדש ע"י `scripts/regenerate_voice_fingerprint.py` (Opus) כ**הצעה מגודרת-יו"ר** (INV-LRN1 קשיח — לא auto, היו"ר מקבעת ידנית; STYLE בלבד INV-LRN5). 2. **B — דוגמאות + תבניות (תומך):** פסקאות-בלוק אמיתיות + Copy-Paste Templates + contrastive. 3. **C — deep-read (נקודתי):** voice-XXXX.md — worked example לתיק-מופת. diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index 363dfa9..ca17225 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -101,6 +101,7 @@ | `notify.py` | python | שליחת מייל התראה מסוכנים via SMTP (Gmail) | נקרא ע"י סוכנים | | `bidi_table.py` | python | יצירת טבלאות box-drawing עם תמיכה ב-BiDi (עברית+אנגלית) | ספריית עזר | | `convert_decision_template.py` | python | המרת `data/training/טיוטת החלטה.dotx` → `skills/docx/decision_template.docx` לטעינה ב-python-docx | להריץ כשמתעדכנת התבנית | +| `regenerate_voice_fingerprint.py` | python | מסנתז מהקורפוס (style_corpus + דוגמאות + יחסים) **הצעה** לעדכון פרופיל-הקול (Opus מקומי). שער-יו"ר קשיח (INV-LRN1): כותב ל-`data/curator-proposals/`, לעולם לא דורס את `docs/daphna-voice-fingerprint.md` — היו"ר סוקרת ומקבעת ידנית. החצי-הפרוזה של #161 (החצי-המבני=ratios ב-PR#345). רץ על המארח (claude CLI). STYLE/METHOD בלבד (INV-LRN5) | ידני / לתזמן תקופתית | | `deploy-track-changes.sh` | bash | סנכרון skills CMP↔CMPA + בדיקות + הנחיות deploy לארכיטקטורת Track Changes | ידני | ## חד פעמי diff --git a/scripts/regenerate_voice_fingerprint.py b/scripts/regenerate_voice_fingerprint.py new file mode 100755 index 0000000..f812983 --- /dev/null +++ b/scripts/regenerate_voice_fingerprint.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Propose a corpus-refreshed update to Dafna's voice-fingerprint (TaskMaster #161). + +The voice-fingerprint (docs/daphna-voice-fingerprint.md) is the PRIMARY abstract +style channel the writer consumes (07-learning §0.2, channel A). It was hand-authored +and never regenerated from the growing corpus — so the "voice" the writer adapts has +been frozen while the corpus grew. This script reads the corpus (style_corpus stats + +block-level style_exemplars sample + measured section ratios) and asks Opus (local) +to PROPOSE refinements/additions to the fingerprint — STYLE / METHOD / VOICE / LEXICON +ONLY, never case substance (INV-LRN5). + +CHAIR-GATED (INV-LRN1, HARD gate — the prose profile is voice-knowledge, not a +low-risk style rule, so it does NOT auto-flow): the proposal is written to +data/curator-proposals/ and NEVER overwrites the live fingerprint. The chair reviews +it (in /training, with the other curator proposals) and hand-commits the parts she +accepts to docs/daphna-voice-fingerprint.md — the same manual gate the fingerprint +already uses. This is the prose half of the voice-profile refresh; the structural +half (corpus-measured ratios → writer) shipped in PR #345. + +Runs on the HOST (claude_session needs the local claude CLI). Usage: + DOTENV_PATH=/home/chaim/.env DATA_DIR=/home/chaim/legal-ai/data \\ + /home/chaim/legal-ai/mcp-server/.venv/bin/python scripts/regenerate_voice_fingerprint.py +""" +from __future__ import annotations + +import asyncio +import logging +from datetime import datetime, timezone + +from legal_mcp import config +from legal_mcp.services import claude_session, db +from legal_mcp.services.style_distance import measure_corpus_ratios + +logging.basicConfig(level=logging.INFO, format="%(message)s") +log = logging.getLogger("voice_fingerprint_refresh") + +FINGERPRINT = config.DATA_DIR.parent / "docs" / "daphna-voice-fingerprint.md" +PROPOSALS = config.DATA_DIR / "curator-proposals" +_PER_SECTION = 5 # exemplars per section in the evidence pack +_MAX_PARA_CHARS = 700 # cap each exemplar so the prompt stays bounded + +_SYSTEM = """אתה מזקק את טביעת-אצבע-הקול של עו"ד דפנה תמיר — פרופיל-סגנון מופשט (איך היא כותבת: קול, שיטה, מבנה, לקסיקון, אנטי-דפוסים), לא תוכן. +חוקים מחייבים: +- **סגנון/שיטה בלבד (INV-LRN5):** אסור לכלול מהות משפטית ספציפית — הלכה, עובדה, תקדים, או ניסוח מתיק קונקרטי. רק הכללות-סגנון. +- **לחדד ולהרחיב, לא לזרוק:** שמר את התובנות האצורות בפרופיל הנוכחי; הצע תוספות/חידודים מעוגנים בראיות-הקורפוס שלהלן. +- **לעגן:** כל קביעה חדשה נשענת על דפוס שחוזר בראיות. אם אין עיגון — אל תמציא ואל תוסיף. +- שמר על מבנה-המסמך והסעיפים הקיימים. +הפלט: גרסת-fingerprint מוצעת מלאה (Markdown), מוכנה לסקירת-יו"ר — בלי הקדמות או הסברים מסביב.""" + + +async def _gather_evidence() -> tuple[str, str]: + pool = await db.get_pool() + async with pool.acquire() as conn: + n_corpus = await conn.fetchval("SELECT count(*) FROM style_corpus WHERE coalesce(full_text,'') <> ''") + n_ex = await conn.fetchval("SELECT count(*) FROM style_exemplars") + rows = await conn.fetch( + """SELECT section, decision_number, outcome, paragraph_text FROM ( + SELECT section, decision_number, outcome, paragraph_text, + row_number() OVER (PARTITION BY section + ORDER BY decision_number DESC, word_count DESC) AS rn + FROM style_exemplars + ) t WHERE rn <= $1 ORDER BY section, rn""", + _PER_SECTION, + ) + ratios = await measure_corpus_ratios() + parts = [f"קורפוס: {n_corpus} החלטות, {n_ex} דוגמאות-בלוק.", "", "יחסי-מבנה מדודים מהקורפוס (אחוז-מהסך, לפי תוצאה):"] + for outcome, entry in (ratios or {}).items(): + secs = ", ".join(f"{s} {round(p)}%" for s, p in (entry.get("sections") or {}).items()) + parts.append(f" - {outcome} (n={entry.get('n', 0)}): {secs}") + parts += ["", "דוגמאות-סגנון מייצגות (פסקאות אמיתיות של דפנה — מקור ללמידת-קול, לא להעתקת-מהות):"] + for r in rows: + para = (r["paragraph_text"] or "").strip()[:_MAX_PARA_CHARS] + parts.append(f"\n[{r['section']} · {r['decision_number']} · {r['outcome'] or '—'}]\n{para}") + return "\n".join(parts), f"{n_corpus} החלטות / {n_ex} דוגמאות" + + +async def main() -> int: + if not FINGERPRINT.exists(): + log.error("fingerprint not found: %s", FINGERPRINT) + return 1 + current = FINGERPRINT.read_text(encoding="utf-8") + evidence, summary = await _gather_evidence() + + user = ( + f"## הפרופיל הנוכחי (לחדד ולהרחיב, לא להחליף):\n{current}\n\n" + f"## ראיות-קורפוס:\n{evidence}\n\n" + "## המשימה:\nהצע גרסת-fingerprint מעודכנת המשלבת חידודים/תוספות-סגנון מעוגנים בראיות לעיל, " + "תוך שימור כל התובנות האצורות. סגנון/שיטה בלבד — בלי מהות." + ) + log.info("synthesizing voice-fingerprint proposal from %s (Opus, local)…", summary) + proposed = await claude_session.query( + user, timeout=claude_session.LONG_TIMEOUT, system=_SYSTEM, + model="claude-opus-4-8", tools="", + ) + if not proposed or not proposed.strip(): + log.error("empty proposal from model — aborting (nothing written)") + return 1 + + PROPOSALS.mkdir(parents=True, exist_ok=True) + ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + out = PROPOSALS / f"voice-fingerprint-{ts}.md" + header = ( + f"\n" + "\n\n" + ) + out.write_text(header + proposed, encoding="utf-8") + log.info("✓ proposal written (chair-gated, NOT applied): %s", out) + log.info(" review in /training → commit accepted parts to %s", FINGERPRINT) + return 0 + + +if __name__ == "__main__": + raise SystemExit(asyncio.run(main()))