feat(learning): feed corpus-measured section ratios to the writer (P2 #7, structural half)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

Golden ratios were measured from the corpus (style_distance.measure_corpus_ratios)
but never reached the writer — it used only the hardcoded STRUCTURE_GUIDANCE text.
So the structural target was frozen, and drafts kept deviating (e.g. discussion
block 10% vs a 40-47% target) with no corrective signal.

_structure_guidance_with_corpus(outcome, practice_area) now appends the
corpus-measured section ratios for the block's outcome to the structure guidance,
at both writer entry points (write_block + get_block_context) via one shared helper
(G2). Because measure_corpus_ratios reads style_corpus — which grows with every
enrolled final (see exemplar-growth, #343) — the writer's structural target now
reflects Dafna's ACTUAL current distribution and improves as the corpus grows.
Deterministic (no LLM); best-effort.

This is the STRUCTURAL half of the voice-profile-refresh (#7). The remaining half —
LLM-regenerating the abstract voice-fingerprint PROSE from the corpus (chair-gated)
— is a distinct larger initiative, deliberately deferred rather than rushed.

Invariants: G1 (the writer gets the live measured target, not a frozen constant),
G2 (one structure-guidance builder shared by both entry points; reuses the existing
measure_corpus_ratios — no parallel measurement), INV-LRN4 (the corpus the loop
grows now feeds the writer's structural target).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 22:10:39 +00:00
parent b0a6c2fe01
commit 638dbf9b9f

View File

@@ -297,6 +297,32 @@ STRUCTURE_GUIDANCE = {
"partial_acceptance": "קבלה חלקית — מיפוי מתחים: מה מתקבל ולמה, מה נדחה ולמה, איזון.", "partial_acceptance": "קבלה חלקית — מיפוי מתחים: מה מתקבל ולמה, מה נדחה ולמה, איזון.",
} }
_RATIO_SECTION_HE = {"background": "רקע", "claims": "טענות", "discussion": "דיון", "summary": "סיכום"}
async def _structure_guidance_with_corpus(outcome: str, practice_area: str) -> str:
"""Outcome structure guidance + the CORPUS-MEASURED section ratios (T10). The
measured ratios come from style_distance.measure_corpus_ratios over style_corpus,
which grows with every enrolled final — so the writer's structural target reflects
Dafna's ACTUAL current distribution, not a frozen constant (this is the structural
half of the voice-profile that learns from each decision). Deterministic (no LLM);
best-effort. Shared by both writer entry points (G2)."""
sg = STRUCTURE_GUIDANCE.get(outcome, "")
if practice_area == "betterment_levy":
sg = (sg + " | היטל השבחה: "
+ " ".join(PRACTICE_AREA_OVERRIDES["betterment_levy"]["discussion_rules"])).strip()
try:
from legal_mcp.services.style_distance import measure_corpus_ratios
entry = (await measure_corpus_ratios() or {}).get(outcome) or {}
secs = entry.get("sections") or {}
if secs:
parts = [f"{_RATIO_SECTION_HE.get(s, s)} ~{round(p)}%" for s, p in secs.items()]
sg = (sg + f" | מבנה-יעד מדוד מהקורפוס ({entry.get('n', 0)} החלטות {outcome}): "
+ " · ".join(parts)).strip()
except Exception as e:
logger.warning("corpus ratios not loaded for structure guidance: %s", e)
return sg
async def write_block( async def write_block(
case_id: UUID, case_id: UUID,
@@ -352,12 +378,8 @@ async def write_block(
post_hearing_context = await _build_post_hearing_context(case_id) post_hearing_context = await _build_post_hearing_context(case_id)
outcome = canonical_outcome((decision or {}).get("outcome", "rejection")) outcome = canonical_outcome((decision or {}).get("outcome", "rejection"))
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "") structure_guidance = await _structure_guidance_with_corpus(
if case.get("practice_area") == "betterment_levy": outcome, case.get("practice_area", ""))
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 = ""
@@ -1094,12 +1116,8 @@ async def get_block_context(case_id: UUID, block_id: str, instructions: str = ""
post_hearing_context = await _build_post_hearing_context(case_id) post_hearing_context = await _build_post_hearing_context(case_id)
outcome = canonical_outcome((decision or {}).get("outcome", "rejection")) outcome = canonical_outcome((decision or {}).get("outcome", "rejection"))
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "") structure_guidance = await _structure_guidance_with_corpus(
if case.get("practice_area") == "betterment_levy": outcome, case.get("practice_area", ""))
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 = ""