feat(calibration): style-clean recommend_effort (anti-primary) + confidence + --rerank (#213)
Make the effort-calibration recommendation style-clean (style acquisition is
the goal — write like chair Dafna Tamir):
- recommend_effort ranks by anti_pattern_total (PRIMARY, the clean style-rule
violation count) → golden_ratio_deviation_pp (SECONDARY, structural-style,
None=worst) → distance (faint final tiebreak). change_percent is EXCLUDED from
the ranking (it mixes style with content-completeness per 07-learning §0.7 —
noise for STYLE) and kept in the report as context only.
- Add a confidence flag ("clear"/"weak"): a pick whose anti-pattern lead over the
runner-up is within noise (margin < epsilon = max(0.5, 0.20·spread)) is flagged
⚠️ weak, so an over-claimed pick (the old confident block-zayin "low") is never
shown as confident. Rendered as a confidence column in the report.
- Add --rerank <report.json>: re-apply the new key to a saved calibration JSON
offline (NO LLM, NO DB), print + write *-reranked.md. Re-ranks today's 8-final
run without regenerating.
- recommend_effort stays a single shared pure function reused by
scripts/calibrate_block_yod.py (G2); both --self-test suites pass.
- Spec note added (07-learning §0.7) + SCRIPTS.md updated.
Invariants: G8 (style-clean empirical ranking), G2 (single shared recommend_effort,
reused by the block-yod harness — no parallel metric path), INV-LRN5 (style-only
signal; change% reported-not-ranked).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,8 @@
|
|||||||
|
|
||||||
**הקריאה:** טבלת `style_distance_history` (append-only) + `GET /api/learning/style-distance-history`. ירידה ב-`anti_pattern_total`/`change_percent` ככל ש-N גדל = **הוכחה מתגלגלת שהלמידה מכלילה** (INV-LRN4 — זהו משטח-המגמה של "ניגוד-האמת"). **אזהרת-פרשנות:** `change_percent` מערבב סגנון עם שלמות-תוכן (לפעמים היו"ר מכפילה אורך כי חסרה מהות, לפעמים חותכת) → `anti_pattern_total` הוא הסיגנל הנקי-יותר לסגנון. שימוש-חוזר בשירות `style_distance` ובבריכת `appeal_type_rules` — אין מסלול-מדד מקביל (G2).
|
**הקריאה:** טבלת `style_distance_history` (append-only) + `GET /api/learning/style-distance-history`. ירידה ב-`anti_pattern_total`/`change_percent` ככל ש-N גדל = **הוכחה מתגלגלת שהלמידה מכלילה** (INV-LRN4 — זהו משטח-המגמה של "ניגוד-האמת"). **אזהרת-פרשנות:** `change_percent` מערבב סגנון עם שלמות-תוכן (לפעמים היו"ר מכפילה אורך כי חסרה מהות, לפעמים חותכת) → `anti_pattern_total` הוא הסיגנל הנקי-יותר לסגנון. שימוש-חוזר בשירות `style_distance` ובבריכת `appeal_type_rules` — אין מסלול-מדד מקביל (G2).
|
||||||
|
|
||||||
|
> **המלצת-הכיול style-ranked (#213):** `recommend_effort` ב-`scripts/calibrate_effort.py` מדרג efforts לפי `anti_pattern_total` **ראשי** (סיגנל-סגנון נקי) → סטיית-יחסי-זהב → `distance` (tiebreak קלוש). `change_percent` **מדווח-לא-מדורג** מאותה אזהרת-פרשנות לעיל (מערבב סגנון עם שלמות-תוכן). ההמלצה נושאת דגל-`confidence` ("clear"/"weak") כך שבחירה שההובלה-ב-anti שלה בתוך-הרעש (epsilon=max(0.5, 0.20·spread)) לא מוצגת כוודאית. אין מסלול-מדד מקביל — שימוש-חוזר ב-`style_distance` (G2).
|
||||||
|
|
||||||
> **חלון נקי חד-פעמי:** תיק שכבר `lessons_folded` פספס את חלון ה-held-out שלו — אין backfill. הטבלה מתמלאת קדימה מהסופי הבא.
|
> **חלון נקי חד-פעמי:** תיק שכבר `lessons_folded` פספס את חלון ה-held-out שלו — אין backfill. הטבלה מתמלאת קדימה מהסופי הבא.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
| `calibrate_halacha_dedup.py` | python | **#82.1** — כיול ספי ה-dedup הלקסיקלי (#82.3) מול gold-set הניקוי. קורא `halacha-cleanup-manifest-*.csv` (זוגות duplicate↔survivor מתויגי-אדם), טוען טקסט-survivor מה-DB, ו-sweep של (jaccard_min × levenshtein_min) עם P/R/F1, מסמן את נקודת-העבודה המוגדרת. אימת ש-(0.55, 0.70) → **precision 1.0** (אפס false-merge), recall 0.30 — מתאים לאיתות-משני שחוסם auto-approve. `--manifest <path>`. רץ עם venv של mcp-server | חד-פעמי — כיול (בוצע 2026-06-06) |
|
| `calibrate_halacha_dedup.py` | python | **#82.1** — כיול ספי ה-dedup הלקסיקלי (#82.3) מול gold-set הניקוי. קורא `halacha-cleanup-manifest-*.csv` (זוגות duplicate↔survivor מתויגי-אדם), טוען טקסט-survivor מה-DB, ו-sweep של (jaccard_min × levenshtein_min) עם P/R/F1, מסמן את נקודת-העבודה המוגדרת. אימת ש-(0.55, 0.70) → **precision 1.0** (אפס false-merge), recall 0.30 — מתאים לאיתות-משני שחוסם auto-approve. `--manifest <path>`. רץ עם venv של mcp-server | חד-פעמי — כיול (בוצע 2026-06-06) |
|
||||||
| `ab_halacha_opus48.py` | python | **A/B לא-הרסני לחילוץ הלכות (Claude)** — מריץ מחדש חילוץ הלכות על פסק-דין בודד דרך מודל/effort נבחרים (`AB_MODEL`/`AB_EFFORT`, ברירת-מחדל `claude-opus-4-8`/`xhigh`) ומשווה לסטטיסטיקות ההלכות הקיימות ב-DB **בלי למחוק/לכתוב כלום**. משכפל את `halacha_extractor.extract()` (אותם פרומפטים, בחירת-צ'אנקים, אימות-ציטוט) ומחליף רק את קריאת ה-LLM ב-`claude -p --model --effort`. מפיק `data/ab_halacha_<case>_<effort>.json`. הרצה: `DOTENV_PATH=/home/chaim/.env DATA_DIR=.../data .venv/bin/python scripts/ab_halacha_opus48.py <case_law_id>`. **ממצא 2026-05-31 (שטיין 1128-08-20):** Opus 4.8@xhigh חילץ 51 מול 124 בייצור (100% quote-verified מול 96%) אך ביטחון מכויל-נמוך יותר (חציון 0.75 מול 0.82) — ולכן **לא** מקטין את תור-האישור-הידני תחת sweep אוטו-אישור conf≥0.78 (26 מול 24). שיפור איכות, לא צמצום-תור. | ידני (החלטת מודל-חילוץ) |
|
| `ab_halacha_opus48.py` | python | **A/B לא-הרסני לחילוץ הלכות (Claude)** — מריץ מחדש חילוץ הלכות על פסק-דין בודד דרך מודל/effort נבחרים (`AB_MODEL`/`AB_EFFORT`, ברירת-מחדל `claude-opus-4-8`/`xhigh`) ומשווה לסטטיסטיקות ההלכות הקיימות ב-DB **בלי למחוק/לכתוב כלום**. משכפל את `halacha_extractor.extract()` (אותם פרומפטים, בחירת-צ'אנקים, אימות-ציטוט) ומחליף רק את קריאת ה-LLM ב-`claude -p --model --effort`. מפיק `data/ab_halacha_<case>_<effort>.json`. הרצה: `DOTENV_PATH=/home/chaim/.env DATA_DIR=.../data .venv/bin/python scripts/ab_halacha_opus48.py <case_law_id>`. **ממצא 2026-05-31 (שטיין 1128-08-20):** Opus 4.8@xhigh חילץ 51 מול 124 בייצור (100% quote-verified מול 96%) אך ביטחון מכויל-נמוך יותר (חציון 0.75 מול 0.82) — ולכן **לא** מקטין את תור-האישור-הידני תחת sweep אוטו-אישור conf≥0.78 (26 מול 24). שיפור איכות, לא צמצום-תור. | ידני (החלטת מודל-חילוץ) |
|
||||||
| `ab_halacha_codex.py` | python | **A/B לא-הרסני לחילוץ הלכות (Codex/gpt-5.5)** — עמית ל-`ab_halacha_opus48` אך מחליף את `claude -p` ב-`codex exec --model gpt-5.5` (אימות ChatGPT, ללא OPENAI_API_KEY). אותם פרומפטים ואותו הסקת quote-verification. הפלט האחרון של הסוכן (`-o FILE`) נפענח כ-JSON. `AB_MODEL` (default `gpt-5.5`), `AB_REASONING` low/medium/high/xhigh (default `medium`), `AB_CONCURRENCY` (default 1), `CODEX_BIN`. מפיק `data/ab_halacha_codex_<case>_<model>_<reasoning>.json`. הרצה: `DOTENV_PATH=/home/chaim/.env DATA_DIR=.../data mcp-server/.venv/bin/python scripts/ab_halacha_codex.py <case_law_id>`. **ממצא 2026-06-17 (8181-21 האוניברסיטה העברית):** gpt-5.5@medium חילץ 27 מול 28 של Opus (quote-verified 100%/100%), ביטחון חציון 0.86 מול 0.78 — אך **0 פריטים מתחת ל-0.7** (לעומת 9/28 של Opus = 32%), דבר המצביע על over-confidence. holding↑ (12 מול 7), procedural↓ (4 מול 7). **מסקנה: ריאלי כ-fallback חירום; לא מוכן לייצור ללא כיול-ביטחון.** | ידני (בנצ'מרק מודל codex) |
|
| `ab_halacha_codex.py` | python | **A/B לא-הרסני לחילוץ הלכות (Codex/gpt-5.5)** — עמית ל-`ab_halacha_opus48` אך מחליף את `claude -p` ב-`codex exec --model gpt-5.5` (אימות ChatGPT, ללא OPENAI_API_KEY). אותם פרומפטים ואותו הסקת quote-verification. הפלט האחרון של הסוכן (`-o FILE`) נפענח כ-JSON. `AB_MODEL` (default `gpt-5.5`), `AB_REASONING` low/medium/high/xhigh (default `medium`), `AB_CONCURRENCY` (default 1), `CODEX_BIN`. מפיק `data/ab_halacha_codex_<case>_<model>_<reasoning>.json`. הרצה: `DOTENV_PATH=/home/chaim/.env DATA_DIR=.../data mcp-server/.venv/bin/python scripts/ab_halacha_codex.py <case_law_id>`. **ממצא 2026-06-17 (8181-21 האוניברסיטה העברית):** gpt-5.5@medium חילץ 27 מול 28 של Opus (quote-verified 100%/100%), ביטחון חציון 0.86 מול 0.78 — אך **0 פריטים מתחת ל-0.7** (לעומת 9/28 של Opus = 32%), דבר המצביע על over-confidence. holding↑ (12 מול 7), procedural↓ (4 מול 7). **מסקנה: ריאלי כ-fallback חירום; לא מוכן לייצור ללא כיול-ביטחון.** | ידני (בנצ'מרק מודל codex) |
|
||||||
| `calibrate_effort.py` | python | **#208 (WS5/Q1, INV-G8 eval-harness) — כיול model×effort של הכותב מול הסופיים.** A/B per-(תיק,בלוק,effort) על `draft_final_pairs` בעלי `final_text`: מייצר מחדש כל בלוק דרך מסלול-הייצור (`block_writer.write_block(effort_override=…)` → `claude_session.query` → `claude -p`, Opus 4.8 נעוץ, **מקומי-בלבד**) ומודד מול ה**סקשן** המתאים בסופי דרך `services/style_distance.block_distance_to_final` (מקור-מדידה יחיד, G2): `change_percent` (compute_diff_stats) · `anti_pattern_total` (`lessons.ANTI_PATTERNS`, הסיגנל הנקי-לסגנון) · `golden_ratio_deviation_pp` · `distance` מרוכב. **ממליץ** per-בלוק על ה-effort בעל ה-distance-הממוצע-הנמוך (לצד ברירת-המחדל מ-#204). מפיק `data/eval/effort-calibration-<ts>.{json,md}` ב**כתיבה אינקרמנטלית** — הדוח נכתב מחדש אחרי כל בלוק שמסתיים, כך שקריסה באמצע הגריד לא מאבדת בלוקים שכבר יוצרו (INV-G8). כל תא-(תיק,בלוק,effort) שנכשל (rate-limit חולף, או חריגה) נרשם כ-warning ומדולג — לא מפיל את הריצה. **`block-yod` (דיון) הוצא מברירות-המחדל** (`CALIBRATABLE_BLOCKS`) — דורש כיוון-מאושר (brainstorm→approve_direction) שתיקי-הכיול חסרים ולכן `write_block` זורק; נשאר ב-`VALID_BLOCKS` לכפייה ידנית (`--blocks block-yod`) על-אחריות-המשתמש, כשהשער-per-תא שומר אותו לא-פטאלי. ⚠️ **גודל-מדגם מודפס בראש הדוח — עדות-כיוון, לא רגרסיה** (מעט סופיים-עלויים). `--self-test` (offline, אפס DB/CLI — מוכיח את לוגיקת-המדידה) · `--dry-run` (תכנון-גריד) · `--efforts`/`--blocks`/`--case`/`--repeats`. **חובה מקומי** (claude CLI; לא בקונטיינר/worktree-ללא-CLI). הרצה: `POSTGRES_PASSWORD=… mcp-server/.venv/bin/python scripts/calibrate_effort.py`. | ידני — לכיול ברירות-effort של הכותב |
|
| `calibrate_effort.py` | python | **#208 (WS5/Q1, INV-G8 eval-harness) — כיול model×effort של הכותב מול הסופיים.** A/B per-(תיק,בלוק,effort) על `draft_final_pairs` בעלי `final_text`: מייצר מחדש כל בלוק דרך מסלול-הייצור (`block_writer.write_block(effort_override=…)` → `claude_session.query` → `claude -p`, Opus 4.8 נעוץ, **מקומי-בלבד**) ומודד מול ה**סקשן** המתאים בסופי דרך `services/style_distance.block_distance_to_final` (מקור-מדידה יחיד, G2): `change_percent` (compute_diff_stats) · `anti_pattern_total` (`lessons.ANTI_PATTERNS`, הסיגנל הנקי-לסגנון) · `golden_ratio_deviation_pp` · `distance` מרוכב. **ממליץ** per-בלוק (style-clean, #213) לפי `anti_pattern_total` **ראשי** (סיגנל-הסגנון הנקי) → סטיית-יחסי-זהב → `distance` (tiebreak קלוש); **`change_percent` מדווח-לא-מדורג** (מערבב סגנון עם שלמות-תוכן, 07-learning §0.7). ההמלצה נושאת דגל-`confidence` ("clear"/"weak" — הובלת-anti < epsilon=max(0.5, 0.20·spread) ⇒ ⚠️weak בתוך-הרעש, לא מוצג כוודאי) לצד ברירת-המחדל מ-#204. מפיק `data/eval/effort-calibration-<ts>.{json,md}` ב**כתיבה אינקרמנטלית** — הדוח נכתב מחדש אחרי כל בלוק שמסתיים, כך שקריסה באמצע הגריד לא מאבדת בלוקים שכבר יוצרו (INV-G8). כל תא-(תיק,בלוק,effort) שנכשל (rate-limit חולף, או חריגה) נרשם כ-warning ומדולג — לא מפיל את הריצה. **`block-yod` (דיון) הוצא מברירות-המחדל** (`CALIBRATABLE_BLOCKS`) — דורש כיוון-מאושר (brainstorm→approve_direction) שתיקי-הכיול חסרים ולכן `write_block` זורק; נשאר ב-`VALID_BLOCKS` לכפייה ידנית (`--blocks block-yod`) על-אחריות-המשתמש, כשהשער-per-תא שומר אותו לא-פטאלי. ⚠️ **גודל-מדגם מודפס בראש הדוח — עדות-כיוון, לא רגרסיה** (מעט סופיים-עלויים). `--self-test` (offline, אפס DB/CLI — מוכיח מדידה + דירוג-סגנון + confidence + rerank) · **`--rerank <report.json>`** (offline, אפס LLM/DB — מדרג-מחדש דוח-כיול שמור תחת המפתח החדש, מדפיס טבלה + כותב `*-reranked.md`) · `--dry-run` (תכנון-גריד) · `--efforts`/`--blocks`/`--case`/`--repeats`. **חובה מקומי** (claude CLI; לא בקונטיינר/worktree-ללא-CLI). הרצה: `POSTGRES_PASSWORD=… mcp-server/.venv/bin/python scripts/calibrate_effort.py`. | ידני — לכיול ברירות-effort של הכותב |
|
||||||
| `calibrate_block_yod.py` | python | **#212 (WS5/Q1, INV-G8 eval-harness) — כיול effort ל-בלוק י' (דיון והכרעה) בלבד.** ה-harness ש-#208 לא יכל לכייל: `write_block(block-yod)` זורק "ללא כיוון מאושר" כי בלוק-הדיון דורש `decisions.direction_doc.approved` שתיקי-הכיול חסרים. **התוספת על #208 — שכבת אספקת-כיוון read-only:** לפני ייצור בלוק-הדיון, ה-harness מזרים `direction_doc` שמיש דרך **overlay על קריאת-ה-DB** (monkeypatch של `db.get_decision_by_case`/`db.get_decision` לתהליך-הזה-בלבד) — **אפס כתיבה ל-DB**, מצב-תיק-הייצור לא משתנה (השער reverted ב-finally). **אסטרטגיית-אספקה per-תיק:** (1) **stored** (מועדף, נאמן) — כיוון-מאושר שמור (`approved`+`selected_direction`, או סכמת-8126 עם `direction_id`/`structure`); (2) **reconstructed** (נסיגה, עדות-כיוון-בלבד) — `direction_doc` מינימלי שנגזר מ-`decision.outcome` + סקשן-הדיון של הסופי **באותו תיק** (INV-LRN5: outcome+מצביע-לסופי-עצמי, אפס מהות חוצת-תיקים, מתויג `reconstructed`); (3) **skip** אם אין דיון/כיוון. מודד דרך אותו `style_distance.block_distance_to_final` (מקור-מדידה יחיד, G2) ומשתמש-חוזר ב-`_score_cell`/`recommend_effort` מ-`calibrate_effort` (#208). **ממצא-קורפוס (2026-06-30):** מ-8 סופיים, 7 ניתנים-לכיול; רק 8126-03-25 הוא stored+scorable (8174-12-24 שמור-אך-חסר-סקשן-דיון) → `--allow-reconstructed false` נותן תא-יחיד; ברירת-המחדל (reconstructed פעיל) נותנת 7 → הנסיגה היא מה שמפעיל את ה-harness היום. כתיבה אינקרמנטלית per-תיק + שער-per-תא לא-פטאלי (INV-G8). ⚠️ **גודל-מדגם בראש הדוח — עדות-כיוון, לא רגרסיה.** `--self-test` (offline, אפס DB/CLI — מוכיח מדידה + לוגיקת-אספקת-כיוון + overlay) · `--dry-run` (תכנון-גריד + מצב-אספקה per-תיק, צריך DB) · `--efforts`/`--case`/`--repeats`/`--allow-reconstructed`. ברירת-מחדל נוכחית: `xhigh` (`block_writer.py:72`, 'by feel'). **חובה מקומי** (claude CLI; לא בקונטיינר/worktree-ללא-CLI). הרצת-כיול-חי (לבצע ע"י היו"ר): `POSTGRES_PASSWORD=… POSTGRES_HOST=127.0.0.1 POSTGRES_PORT=5433 mcp-server/.venv/bin/python scripts/calibrate_block_yod.py`. | ידני — לכיול ה-effort של בלוק-הדיון (היו"ר מריץ; long, צורך-מכסה) |
|
| `calibrate_block_yod.py` | python | **#212 (WS5/Q1, INV-G8 eval-harness) — כיול effort ל-בלוק י' (דיון והכרעה) בלבד.** ה-harness ש-#208 לא יכל לכייל: `write_block(block-yod)` זורק "ללא כיוון מאושר" כי בלוק-הדיון דורש `decisions.direction_doc.approved` שתיקי-הכיול חסרים. **התוספת על #208 — שכבת אספקת-כיוון read-only:** לפני ייצור בלוק-הדיון, ה-harness מזרים `direction_doc` שמיש דרך **overlay על קריאת-ה-DB** (monkeypatch של `db.get_decision_by_case`/`db.get_decision` לתהליך-הזה-בלבד) — **אפס כתיבה ל-DB**, מצב-תיק-הייצור לא משתנה (השער reverted ב-finally). **אסטרטגיית-אספקה per-תיק:** (1) **stored** (מועדף, נאמן) — כיוון-מאושר שמור (`approved`+`selected_direction`, או סכמת-8126 עם `direction_id`/`structure`); (2) **reconstructed** (נסיגה, עדות-כיוון-בלבד) — `direction_doc` מינימלי שנגזר מ-`decision.outcome` + סקשן-הדיון של הסופי **באותו תיק** (INV-LRN5: outcome+מצביע-לסופי-עצמי, אפס מהות חוצת-תיקים, מתויג `reconstructed`); (3) **skip** אם אין דיון/כיוון. מודד דרך אותו `style_distance.block_distance_to_final` (מקור-מדידה יחיד, G2) ומשתמש-חוזר ב-`_score_cell`/`recommend_effort` מ-`calibrate_effort` (#208). **ממצא-קורפוס (2026-06-30):** מ-8 סופיים, 7 ניתנים-לכיול; רק 8126-03-25 הוא stored+scorable (8174-12-24 שמור-אך-חסר-סקשן-דיון) → `--allow-reconstructed false` נותן תא-יחיד; ברירת-המחדל (reconstructed פעיל) נותנת 7 → הנסיגה היא מה שמפעיל את ה-harness היום. כתיבה אינקרמנטלית per-תיק + שער-per-תא לא-פטאלי (INV-G8). ⚠️ **גודל-מדגם בראש הדוח — עדות-כיוון, לא רגרסיה.** `--self-test` (offline, אפס DB/CLI — מוכיח מדידה + לוגיקת-אספקת-כיוון + overlay) · `--dry-run` (תכנון-גריד + מצב-אספקה per-תיק, צריך DB) · `--efforts`/`--case`/`--repeats`/`--allow-reconstructed`. ברירת-מחדל נוכחית: `xhigh` (`block_writer.py:72`, 'by feel'). **חובה מקומי** (claude CLI; לא בקונטיינר/worktree-ללא-CLI). הרצת-כיול-חי (לבצע ע"י היו"ר): `POSTGRES_PASSWORD=… POSTGRES_HOST=127.0.0.1 POSTGRES_PORT=5433 mcp-server/.venv/bin/python scripts/calibrate_block_yod.py`. | ידני — לכיול ה-effort של בלוק-הדיון (היו"ר מריץ; long, צורך-מכסה) |
|
||||||
| `monitor_halacha_quality.py` | python | מנטר איכות חילוץ הלכות. בודק drift של `avg(confidence)` בין baseline היסטורי לחלון אחרון. מחזיר JSON מטריקות + alert ב-stderr אם drift > threshold (ברירת מחדל 5%). 2 סדרות: trusted (approved+published) ו-all_extracted. תומך `--window N` / `--threshold X` / `--min-sample N` / `--silent` / `--exit-on-alert`. רץ ב-container או מקומית עם `mcp-server/.venv` (אין תלות ב-LLM, רק SQL). **תזמון מומלץ**: `0 8 * * 1` (יום ראשון 08:00, שבועי) | `0 8 * * 1` (לתזמן) |
|
| `monitor_halacha_quality.py` | python | מנטר איכות חילוץ הלכות. בודק drift של `avg(confidence)` בין baseline היסטורי לחלון אחרון. מחזיר JSON מטריקות + alert ב-stderr אם drift > threshold (ברירת מחדל 5%). 2 סדרות: trusted (approved+published) ו-all_extracted. תומך `--window N` / `--threshold X` / `--min-sample N` / `--silent` / `--exit-on-alert`. רץ ב-container או מקומית עם `mcp-server/.venv` (אין תלות ב-LLM, רק SQL). **תזמון מומלץ**: `0 8 * * 1` (יום ראשון 08:00, שבועי) | `0 8 * * 1` (לתזמן) |
|
||||||
| `audit_training_corpus.py` | python | audit של `style_corpus` — לכל החלטה: שדות מטא-דאטה מאוכלסים (`summary`/`outcome`/`key_principles`/`appeal_subtype`/`subject_categories`), קישור ל-`documents` (FK + chunks + embeddings). מפיק `data/audit/corpus-YYYY-MM-DD.json` + summary בקונסול. דרוש `POSTGRES_URL` או POSTGRES_*. אין תלויות חיצוניות מלבד asyncpg. **רץ מהמכונה המקומית** (לא קונטיינר) — חיבור ישיר ל-Postgres :5433 | ידני / קדם-עבודה לפני enrichment של מטא-דאטה |
|
| `audit_training_corpus.py` | python | audit של `style_corpus` — לכל החלטה: שדות מטא-דאטה מאוכלסים (`summary`/`outcome`/`key_principles`/`appeal_subtype`/`subject_categories`), קישור ל-`documents` (FK + chunks + embeddings). מפיק `data/audit/corpus-YYYY-MM-DD.json` + summary בקונסול. דרוש `POSTGRES_URL` או POSTGRES_*. אין תלויות חיצוניות מלבד asyncpg. **רץ מהמכונה המקומית** (לא קונטיינר) — חיבור ישיר ל-Postgres :5433 | ידני / קדם-עבודה לפני enrichment של מטא-דאטה |
|
||||||
|
|||||||
@@ -19,9 +19,15 @@ WHAT IT MEASURES (per (case, block, effort) cell — the A/B grid):
|
|||||||
• distance — normalized composite (lower = closer to Dafna)
|
• distance — normalized composite (lower = closer to Dafna)
|
||||||
No parallel metric path: it reuses style_distance + learning_loop (G2).
|
No parallel metric path: it reuses style_distance + learning_loop (G2).
|
||||||
|
|
||||||
RECOMMENDATION: for each block, the effort with the lowest MEAN composite distance
|
RECOMMENDATION (#213 — STYLE-CLEAN, anti-primary): for each block, the effort is
|
||||||
across cases (ties → fewer anti-patterns → lower change_percent). Reported next to
|
ranked by `anti_pattern_total` FIRST (the clean Dafna-style-rule-violation count),
|
||||||
the #204 current default so a regression/improvement is visible.
|
then `golden_ratio_deviation_pp` (structural-style), and `distance` only as a faint
|
||||||
|
final tiebreak. `change_percent` is REPORTED-NOT-RANKED: per 07-learning §0.7 it
|
||||||
|
mixes style with content-completeness (a case's missing facts the model can't know)
|
||||||
|
→ noise for STYLE. The pick carries a `confidence` flag ("clear"/"weak"): a pick
|
||||||
|
whose anti-pattern lead over the runner-up is within noise is flagged "weak" so an
|
||||||
|
over-claimed pick (the old block-zayin "low") is never shown as confident. Reported
|
||||||
|
next to the #204 current default so a regression/improvement is visible.
|
||||||
|
|
||||||
⚠️ SAMPLE-SIZE CAVEAT (honored, not hidden): very few cases have an uploaded final
|
⚠️ SAMPLE-SIZE CAVEAT (honored, not hidden): very few cases have an uploaded final
|
||||||
(draft_final_pairs.final_text non-empty). The report prints n_finals PROMINENTLY and
|
(draft_final_pairs.final_text non-empty). The report prints n_finals PROMINENTLY and
|
||||||
@@ -94,20 +100,66 @@ VALID_EFFORTS = {"low", "medium", "high", "xhigh", "max"}
|
|||||||
|
|
||||||
|
|
||||||
# ── pure helpers (offline-testable) ──────────────────────────────────────────
|
# ── pure helpers (offline-testable) ──────────────────────────────────────────
|
||||||
def recommend_effort(cells: list[dict]) -> dict | None:
|
# Style-clean ranking epsilon (#213). Confidence in a pick = how far the best
|
||||||
"""Pick the best effort for ONE block from its scored cells.
|
# effort's anti_pattern_total beats the 2nd-best. We call a pick "clear" only when
|
||||||
|
# that margin clears a floor that is itself the LARGER of:
|
||||||
|
# • an absolute floor (≥0.5 avg anti-patterns — a half-violation per case), and
|
||||||
|
# • a fraction of the spread across efforts (best can't claim a lead that is mere
|
||||||
|
# noise on a block whose efforts barely differ).
|
||||||
|
# Below that → "weak" (within noise; nominal pick still reported, but flagged so a
|
||||||
|
# within-noise pick is never shown as confident). Documented in the docstring.
|
||||||
|
CONFIDENCE_ABS_FLOOR = 0.5 # avg anti-patterns (a half-violation per case)
|
||||||
|
CONFIDENCE_SPREAD_FRACTION = 0.20 # of the across-effort anti spread
|
||||||
|
|
||||||
cells: [{"effort","distance","anti_pattern_total","change_percent","n"}].
|
|
||||||
Lowest mean composite distance wins; ties broken by fewer anti-patterns,
|
def _anti_confidence(rows: list[dict]) -> tuple[str, float, float]:
|
||||||
then lower change_percent. Pure → unit-tested in --self-test.
|
"""Confidence that the anti-primary pick is real, not within-noise.
|
||||||
|
|
||||||
|
Returns (confidence, margin, epsilon) where confidence ∈ {"clear","weak"}.
|
||||||
|
margin = (2nd-best anti) − (best anti); epsilon = the bar margin must clear.
|
||||||
|
A single effort (no rival) is trivially "clear". Pure → unit-tested.
|
||||||
|
"""
|
||||||
|
if len(rows) < 2:
|
||||||
|
return "clear", float("inf"), 0.0
|
||||||
|
antis = sorted(r["anti_pattern_total"] for r in rows)
|
||||||
|
best, second = antis[0], antis[1]
|
||||||
|
margin = second - best
|
||||||
|
spread = antis[-1] - antis[0]
|
||||||
|
epsilon = max(CONFIDENCE_ABS_FLOOR, CONFIDENCE_SPREAD_FRACTION * spread)
|
||||||
|
return ("clear" if margin >= epsilon else "weak"), round(margin, 4), round(epsilon, 4)
|
||||||
|
|
||||||
|
|
||||||
|
def recommend_effort(cells: list[dict]) -> dict | None:
|
||||||
|
"""Pick the STYLE-CLEANEST effort for ONE block from its scored cells (#213).
|
||||||
|
|
||||||
|
cells: [{"effort","distance","anti_pattern_total","change_percent",
|
||||||
|
"golden_ratio_deviation_pp","n"}].
|
||||||
|
|
||||||
|
Ranking key (style-clean, G8 / 07-learning §0.7) — change_percent is EXCLUDED
|
||||||
|
from the ranking (it mixes style with content-completeness; reported only):
|
||||||
|
1. PRIMARY anti_pattern_total (↑ fewer Dafna-style violations = better)
|
||||||
|
2. SECONDARY golden_ratio_deviation_pp (↑ structural-style; None → +inf worst)
|
||||||
|
3. TIEBREAK distance (↑ faint final tiebreak only)
|
||||||
|
|
||||||
|
Returns the winning row dict (so callers reading ["effort"] keep working) with
|
||||||
|
two added keys: ``confidence`` ∈ {"clear","weak"} and ``confidence_margin`` —
|
||||||
|
so a within-noise pick is never presented as confident. Pure → unit-tested.
|
||||||
"""
|
"""
|
||||||
if not cells:
|
if not cells:
|
||||||
return None
|
return None
|
||||||
ranked = sorted(
|
ranked = sorted(
|
||||||
cells,
|
cells,
|
||||||
key=lambda c: (c["distance"], c["anti_pattern_total"], c["change_percent"]),
|
key=lambda c: (
|
||||||
|
c["anti_pattern_total"],
|
||||||
|
c["golden_ratio_deviation_pp"] if c.get("golden_ratio_deviation_pp") is not None else float("inf"),
|
||||||
|
c["distance"],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return ranked[0]
|
best = dict(ranked[0])
|
||||||
|
conf, margin, _eps = _anti_confidence(cells)
|
||||||
|
best["confidence"] = conf
|
||||||
|
best["confidence_margin"] = margin
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
def aggregate_cell(per_run: list[dict]) -> dict:
|
def aggregate_cell(per_run: list[dict]) -> dict:
|
||||||
@@ -133,6 +185,88 @@ def _current_default(block_id: str) -> str | None:
|
|||||||
return cfg.get("effort", DEFAULT_EFFORT)
|
return cfg.get("effort", DEFAULT_EFFORT)
|
||||||
|
|
||||||
|
|
||||||
|
# ── re-rank a saved report (offline — NO LLM, NO DB) ─────────────────────────
|
||||||
|
def rerank_saved(saved: dict) -> dict:
|
||||||
|
"""Re-apply the CURRENT (#213) recommend_effort to a saved calibration report.
|
||||||
|
|
||||||
|
Reads the per-(block,effort) rows that the run already persisted
|
||||||
|
(distance/anti/change%/ratioΔ/n) and recomputes recommended + confidence — no
|
||||||
|
regeneration, no model, no DB. Lets a past run be re-ranked under the new
|
||||||
|
style-clean key. Returns {"block_id": {old, new, confidence, confidence_margin,
|
||||||
|
current_default, efforts}}. Pure → unit-tested in --self-test.
|
||||||
|
"""
|
||||||
|
out: dict[str, dict] = {}
|
||||||
|
for block_id, bd in (saved.get("by_block") or {}).items():
|
||||||
|
rows = bd.get("efforts") or []
|
||||||
|
rec = recommend_effort(rows)
|
||||||
|
out[block_id] = {
|
||||||
|
"section": bd.get("section"),
|
||||||
|
"current_default": bd.get("current_default"),
|
||||||
|
"old_recommended": bd.get("recommended"),
|
||||||
|
"new_recommended": rec["effort"] if rec else None,
|
||||||
|
"confidence": rec["confidence"] if rec else None,
|
||||||
|
"confidence_margin": rec.get("confidence_margin") if rec else None,
|
||||||
|
"efforts": rows,
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _render_rerank_md(saved: dict, reranked: dict, src: Path) -> str:
|
||||||
|
g = saved.get("grid", {})
|
||||||
|
finals = g.get("finals", [])
|
||||||
|
lines = [
|
||||||
|
f"# #213 — re-rank של {src.name} (style-clean, anti-primary · OFFLINE)\n",
|
||||||
|
f"> מקור: `{src.name}` · {g.get('n_finals','?')} סופיים "
|
||||||
|
f"({', '.join(finals) or '—'}) · ללא LLM/DB — דירוג-מחדש בלבד.\n",
|
||||||
|
"## old pick → new pick (+confidence)\n",
|
||||||
|
"| block | section | current | old pick | **new pick** | confidence | n |",
|
||||||
|
"|---|---|---|---|---|---|---|",
|
||||||
|
]
|
||||||
|
for b, rd in reranked.items():
|
||||||
|
old = rd.get("old_recommended") or "—"
|
||||||
|
new = rd.get("new_recommended") or "—"
|
||||||
|
mark = "" if new == old else " ⬅︎"
|
||||||
|
conf = rd.get("confidence")
|
||||||
|
conf_cell = {"clear": "clear", "weak": "⚠️ weak (within-noise)"}.get(conf, "—")
|
||||||
|
n_b = rd["efforts"][0]["n"] if rd.get("efforts") else 0
|
||||||
|
lines.append(
|
||||||
|
f"| {b} | {rd.get('section') or '—'} | {rd.get('current_default') or '—'} | "
|
||||||
|
f"{old} | **{new}**{mark} | {conf_cell} | {n_b} |")
|
||||||
|
lines.append("")
|
||||||
|
for b, rd in reranked.items():
|
||||||
|
if not rd.get("efforts"):
|
||||||
|
continue
|
||||||
|
lines += [f"### {b} ({rd.get('section') or '—'})\n",
|
||||||
|
"| effort | distance | anti_total | change% (reported) | ratioΔpp | n |",
|
||||||
|
"|---|---|---|---|---|---|"]
|
||||||
|
for r in rd["efforts"]:
|
||||||
|
star = " ⭐" if r["effort"] == rd.get("new_recommended") else ""
|
||||||
|
ratio = r.get("golden_ratio_deviation_pp")
|
||||||
|
lines.append(
|
||||||
|
f"| {r['effort']}{star} | {r['distance']:.4f} | {r['anti_pattern_total']} | "
|
||||||
|
f"{r['change_percent']} | {ratio if ratio is not None else '—'} | {r['n']} |")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("> דירוג **style-clean** (#213): anti_total ראשי → ratioΔ → distance (tiebreak); "
|
||||||
|
"**change% מדווח-לא-מדורג** (07-learning §0.7); "
|
||||||
|
"confidence=⚠️weak ⇒ הובלת-anti < epsilon = max(0.5, 0.20·spread).\n")
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _do_rerank(path: Path) -> int:
|
||||||
|
"""--rerank entry: load a saved report, re-rank offline, print + write *-reranked.md."""
|
||||||
|
if not path.exists():
|
||||||
|
print(f"rerank: file not found: {path}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
saved = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
reranked = rerank_saved(saved)
|
||||||
|
md = _render_rerank_md(saved, reranked, path)
|
||||||
|
print(md)
|
||||||
|
out = path.with_name(path.stem + "-reranked.md")
|
||||||
|
out.write_text(md, encoding="utf-8")
|
||||||
|
print(f" reranked report written: {out}", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
# ── self-test (no DB, no model) ──────────────────────────────────────────────
|
# ── self-test (no DB, no model) ──────────────────────────────────────────────
|
||||||
def _self_test() -> int:
|
def _self_test() -> int:
|
||||||
ok = True
|
ok = True
|
||||||
@@ -180,16 +314,66 @@ def _self_test() -> int:
|
|||||||
{"background", "claims", "discussion", "summary"}))
|
{"background", "claims", "discussion", "summary"}))
|
||||||
chk("split found ≥1 section", len(secs) >= 1)
|
chk("split found ≥1 section", len(secs) >= 1)
|
||||||
|
|
||||||
# recommend_effort: lowest distance wins; tie → fewer anti-patterns.
|
# recommend_effort (#213, style-clean): anti_pattern_total is PRIMARY — the
|
||||||
|
# effort with the FEWEST style violations wins, even if its distance is higher.
|
||||||
rec = recommend_effort([
|
rec = recommend_effort([
|
||||||
{"effort": "low", "distance": 0.40, "anti_pattern_total": 5, "change_percent": 40, "n": 1},
|
{"effort": "low", "distance": 0.20, "anti_pattern_total": 5, "change_percent": 22,
|
||||||
{"effort": "high", "distance": 0.20, "anti_pattern_total": 2, "change_percent": 25, "n": 1},
|
"golden_ratio_deviation_pp": 3.0, "n": 1},
|
||||||
{"effort": "xhigh", "distance": 0.20, "anti_pattern_total": 1, "change_percent": 22, "n": 1},
|
{"effort": "high", "distance": 0.40, "anti_pattern_total": 2, "change_percent": 40,
|
||||||
|
"golden_ratio_deviation_pp": 5.0, "n": 1},
|
||||||
|
{"effort": "xhigh", "distance": 0.50, "anti_pattern_total": 1, "change_percent": 50,
|
||||||
|
"golden_ratio_deviation_pp": 9.0, "n": 1},
|
||||||
])
|
])
|
||||||
chk("recommend picks lowest distance", rec["distance"] == 0.20)
|
chk("recommend: anti-primary (not distance)", rec["effort"] == "xhigh")
|
||||||
chk("recommend tie → fewer anti-patterns", rec["effort"] == "xhigh")
|
chk("recommend: change_percent NOT ranked", rec["distance"] == 0.50) # worst distance still won
|
||||||
chk("recommend empty ⇒ None", recommend_effort([]) is None)
|
chk("recommend empty ⇒ None", recommend_effort([]) is None)
|
||||||
|
|
||||||
|
# SECONDARY = golden_ratio_deviation_pp when anti ties; distance only as last tiebreak.
|
||||||
|
rec_tie = recommend_effort([
|
||||||
|
{"effort": "low", "distance": 0.10, "anti_pattern_total": 3, "change_percent": 10,
|
||||||
|
"golden_ratio_deviation_pp": 8.0, "n": 1},
|
||||||
|
{"effort": "high", "distance": 0.90, "anti_pattern_total": 3, "change_percent": 90,
|
||||||
|
"golden_ratio_deviation_pp": 2.0, "n": 1},
|
||||||
|
])
|
||||||
|
chk("recommend: ratio breaks anti-tie", rec_tie["effort"] == "high")
|
||||||
|
rec_ratio_tie = recommend_effort([
|
||||||
|
{"effort": "low", "distance": 0.30, "anti_pattern_total": 3, "change_percent": 10,
|
||||||
|
"golden_ratio_deviation_pp": 5.0, "n": 1},
|
||||||
|
{"effort": "high", "distance": 0.10, "anti_pattern_total": 3, "change_percent": 90,
|
||||||
|
"golden_ratio_deviation_pp": 5.0, "n": 1},
|
||||||
|
])
|
||||||
|
chk("recommend: distance breaks final tie", rec_ratio_tie["effort"] == "high")
|
||||||
|
# None ratio is treated as worst (never beats a real deviation on the secondary key).
|
||||||
|
rec_none = recommend_effort([
|
||||||
|
{"effort": "low", "distance": 0.10, "anti_pattern_total": 2, "change_percent": 10,
|
||||||
|
"golden_ratio_deviation_pp": None, "n": 1},
|
||||||
|
{"effort": "high", "distance": 0.90, "anti_pattern_total": 2, "change_percent": 90,
|
||||||
|
"golden_ratio_deviation_pp": 4.0, "n": 1},
|
||||||
|
])
|
||||||
|
chk("recommend: None ratio is worst", rec_none["effort"] == "high")
|
||||||
|
|
||||||
|
# confidence flag (#213): a clear anti lead ⇒ "clear"; within-noise ⇒ "weak".
|
||||||
|
rec_clear = recommend_effort([
|
||||||
|
{"effort": "low", "distance": 0.5, "anti_pattern_total": 1.0, "change_percent": 50,
|
||||||
|
"golden_ratio_deviation_pp": 5.0, "n": 5},
|
||||||
|
{"effort": "high", "distance": 0.5, "anti_pattern_total": 4.0, "change_percent": 50,
|
||||||
|
"golden_ratio_deviation_pp": 5.0, "n": 5},
|
||||||
|
])
|
||||||
|
chk("confidence: big anti lead ⇒ clear", rec_clear["confidence"] == "clear")
|
||||||
|
rec_weak = recommend_effort([
|
||||||
|
{"effort": "low", "distance": 0.4, "anti_pattern_total": 3.6, "change_percent": 93,
|
||||||
|
"golden_ratio_deviation_pp": 6.7, "n": 5},
|
||||||
|
{"effort": "xhigh", "distance": 0.5, "anti_pattern_total": 3.6, "change_percent": 102,
|
||||||
|
"golden_ratio_deviation_pp": 6.6, "n": 5},
|
||||||
|
{"effort": "high", "distance": 0.53, "anti_pattern_total": 3.8, "change_percent": 104,
|
||||||
|
"golden_ratio_deviation_pp": 5.7, "n": 5},
|
||||||
|
])
|
||||||
|
chk("confidence: anti tie ⇒ weak", rec_weak["confidence"] == "weak")
|
||||||
|
rec_single = recommend_effort([
|
||||||
|
{"effort": "low", "distance": 0.4, "anti_pattern_total": 2.0, "change_percent": 50,
|
||||||
|
"golden_ratio_deviation_pp": 5.0, "n": 5}])
|
||||||
|
chk("confidence: single effort ⇒ clear", rec_single["confidence"] == "clear")
|
||||||
|
|
||||||
# aggregate_cell: means + ratio drops Nones, keeps n.
|
# aggregate_cell: means + ratio drops Nones, keeps n.
|
||||||
agg = aggregate_cell([
|
agg = aggregate_cell([
|
||||||
{"distance": 0.2, "anti_pattern_total": 2, "change_percent": 20, "golden_ratio_deviation_pp": 3.0},
|
{"distance": 0.2, "anti_pattern_total": 2, "change_percent": 20, "golden_ratio_deviation_pp": 3.0},
|
||||||
@@ -199,6 +383,23 @@ def _self_test() -> int:
|
|||||||
chk("aggregate n counted", agg["n"] == 2)
|
chk("aggregate n counted", agg["n"] == 2)
|
||||||
chk("aggregate ratio skips None", agg["golden_ratio_deviation_pp"] == 3.0)
|
chk("aggregate ratio skips None", agg["golden_ratio_deviation_pp"] == 3.0)
|
||||||
|
|
||||||
|
# rerank_saved: re-applies the new key to a saved report's stored rows, no I/O.
|
||||||
|
# Mimics the real 8-final block-vav rows: low has fewest anti by a clear margin.
|
||||||
|
saved = {"grid": {"n_finals": 7, "finals": ["x"]}, "by_block": {
|
||||||
|
"block-vav": {"section": "background", "current_default": "medium",
|
||||||
|
"recommended": "low", "efforts": [
|
||||||
|
{"effort": "low", "distance": 0.4454, "anti_pattern_total": 1.57, "change_percent": 87.39, "golden_ratio_deviation_pp": 6.33, "n": 7},
|
||||||
|
{"effort": "medium", "distance": 0.4749, "anti_pattern_total": 2.29, "change_percent": 87.76, "golden_ratio_deviation_pp": 6.37, "n": 7},
|
||||||
|
{"effort": "high", "distance": 0.5381, "anti_pattern_total": 4.14, "change_percent": 90.69, "golden_ratio_deviation_pp": 5.99, "n": 7},
|
||||||
|
{"effort": "xhigh", "distance": 0.5413, "anti_pattern_total": 4.57, "change_percent": 89.53, "golden_ratio_deviation_pp": 6.41, "n": 7},
|
||||||
|
]},
|
||||||
|
}}
|
||||||
|
rr = rerank_saved(saved)
|
||||||
|
chk("rerank: block-vav stays low", rr["block-vav"]["new_recommended"] == "low")
|
||||||
|
chk("rerank: block-vav clear (anti lead)", rr["block-vav"]["confidence"] == "clear")
|
||||||
|
chk("rerank: keeps old pick for diff", rr["block-vav"]["old_recommended"] == "low")
|
||||||
|
chk("rerank: renders markdown", "new pick" in _render_rerank_md(saved, rr, Path("x.json")))
|
||||||
|
|
||||||
print("ALL PASS" if ok else "*** FAILURES ***")
|
print("ALL PASS" if ok else "*** FAILURES ***")
|
||||||
return 0 if ok else 1
|
return 0 if ok else 1
|
||||||
|
|
||||||
@@ -329,6 +530,8 @@ async def _run(args, ts: str) -> dict:
|
|||||||
"section": section,
|
"section": section,
|
||||||
"current_default": _current_default(block_id),
|
"current_default": _current_default(block_id),
|
||||||
"recommended": rec["effort"] if rec else None,
|
"recommended": rec["effort"] if rec else None,
|
||||||
|
"confidence": rec["confidence"] if rec else None,
|
||||||
|
"confidence_margin": rec.get("confidence_margin") if rec else None,
|
||||||
"efforts": effort_rows,
|
"efforts": effort_rows,
|
||||||
"per_case": per_case,
|
"per_case": per_case,
|
||||||
}
|
}
|
||||||
@@ -386,13 +589,18 @@ def _write_report(result: dict, ts: str) -> tuple[Path, Path]:
|
|||||||
mp.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
mp.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
return jp, mp
|
return jp, mp
|
||||||
|
|
||||||
lines += ["## המלצה per-בלוק (distance נמוך = קרוב יותר לדפנה)\n",
|
lines += ["## המלצה per-בלוק (style-clean: anti_pattern_total ראשי — #213)\n",
|
||||||
"| block | section | current | **recommended** | n |", "|---|---|---|---|---|"]
|
"| block | section | current | **recommended** | confidence | n |",
|
||||||
|
"|---|---|---|---|---|---|"]
|
||||||
for b, bd in result["by_block"].items():
|
for b, bd in result["by_block"].items():
|
||||||
rec = bd.get("recommended") or "—"
|
rec = bd.get("recommended") or "—"
|
||||||
mark = "" if rec == bd.get("current_default") else " ⬅︎"
|
mark = "" if rec == bd.get("current_default") else " ⬅︎"
|
||||||
|
conf = bd.get("confidence")
|
||||||
|
conf_cell = {"clear": "clear", "weak": "⚠️ weak (within-noise)"}.get(conf, "—")
|
||||||
n_b = bd["efforts"][0]["n"] if bd.get("efforts") else 0
|
n_b = bd["efforts"][0]["n"] if bd.get("efforts") else 0
|
||||||
lines.append(f"| {b} | {bd.get('section','')} | {bd.get('current_default') or '—'} | **{rec}**{mark} | {n_b} |")
|
lines.append(
|
||||||
|
f"| {b} | {bd.get('section','')} | {bd.get('current_default') or '—'} | "
|
||||||
|
f"**{rec}**{mark} | {conf_cell} | {n_b} |")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
for b, bd in result["by_block"].items():
|
for b, bd in result["by_block"].items():
|
||||||
lines += [f"### {b} ({bd.get('section','')})\n",
|
lines += [f"### {b} ({bd.get('section','')})\n",
|
||||||
@@ -405,8 +613,10 @@ def _write_report(result: dict, ts: str) -> tuple[Path, Path]:
|
|||||||
f"| {r['effort']}{star} | {r['distance']:.4f} | {r['anti_pattern_total']} | "
|
f"| {r['effort']}{star} | {r['distance']:.4f} | {r['anti_pattern_total']} | "
|
||||||
f"{r['change_percent']} | {ratio if ratio is not None else '—'} | {r['n']} |")
|
f"{r['change_percent']} | {ratio if ratio is not None else '—'} | {r['n']} |")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("> change% מערבב סגנון עם שלמות-תוכן (07-learning §0.7); "
|
lines.append("> דירוג-ההמלצה **style-clean** (#213): anti_total ראשי → ratioΔ → distance (tiebreak). "
|
||||||
"anti_total הוא הסיגנל הנקי-יותר לסגנון.\n")
|
"**change% מדווח-לא-מדורג** — מערבב סגנון עם שלמות-תוכן (07-learning §0.7), "
|
||||||
|
"anti_total הוא הסיגנל הנקי-לסגנון. confidence=⚠️weak ⇒ הבחירה בתוך-הרעש "
|
||||||
|
"(ההובלה ב-anti קטנה מ-epsilon = max(0.5, 0.20·spread)).\n")
|
||||||
mp.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
mp.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
return jp, mp
|
return jp, mp
|
||||||
|
|
||||||
@@ -414,6 +624,8 @@ def _write_report(result: dict, ts: str) -> tuple[Path, Path]:
|
|||||||
async def main() -> int:
|
async def main() -> int:
|
||||||
ap = argparse.ArgumentParser(description="#208 model/effort calibration harness")
|
ap = argparse.ArgumentParser(description="#208 model/effort calibration harness")
|
||||||
ap.add_argument("--self-test", action="store_true", help="offline measurement-logic proof (no DB/CLI)")
|
ap.add_argument("--self-test", action="store_true", help="offline measurement-logic proof (no DB/CLI)")
|
||||||
|
ap.add_argument("--rerank", metavar="REPORT.json", default=None,
|
||||||
|
help="re-rank a saved calibration JSON under the current style-clean key (no LLM/DB)")
|
||||||
ap.add_argument("--dry-run", action="store_true", help="plan the A/B grid over existing finals, no model calls")
|
ap.add_argument("--dry-run", action="store_true", help="plan the A/B grid over existing finals, no model calls")
|
||||||
ap.add_argument("--efforts", default=",".join(DEFAULT_EFFORTS),
|
ap.add_argument("--efforts", default=",".join(DEFAULT_EFFORTS),
|
||||||
help=f"comma effort grid (default {','.join(DEFAULT_EFFORTS)})")
|
help=f"comma effort grid (default {','.join(DEFAULT_EFFORTS)})")
|
||||||
@@ -428,6 +640,9 @@ async def main() -> int:
|
|||||||
if args.self_test:
|
if args.self_test:
|
||||||
return _self_test()
|
return _self_test()
|
||||||
|
|
||||||
|
if args.rerank:
|
||||||
|
return _do_rerank(Path(args.rerank))
|
||||||
|
|
||||||
args.efforts = [e.strip() for e in args.efforts.split(",") if e.strip()]
|
args.efforts = [e.strip() for e in args.efforts.split(",") if e.strip()]
|
||||||
bad = [e for e in args.efforts if e not in VALID_EFFORTS]
|
bad = [e for e in args.efforts if e not in VALID_EFFORTS]
|
||||||
if bad:
|
if bad:
|
||||||
@@ -452,7 +667,8 @@ async def main() -> int:
|
|||||||
else:
|
else:
|
||||||
for b, bd in result["by_block"].items():
|
for b, bd in result["by_block"].items():
|
||||||
print(f" {b:16} current={bd.get('current_default') or '—':6} "
|
print(f" {b:16} current={bd.get('current_default') or '—':6} "
|
||||||
f"→ recommended={bd.get('recommended') or '—'}")
|
f"→ recommended={bd.get('recommended') or '—':6} "
|
||||||
|
f"[{bd.get('confidence') or '—'}]")
|
||||||
print(f" report: {mp}")
|
print(f" report: {mp}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user