From 638dbf9b9fcdaf94a49478b209873e2dacc80580 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 28 Jun 2026 22:10:39 +0000 Subject: [PATCH] feat(learning): feed corpus-measured section ratios to the writer (P2 #7, structural half) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../src/legal_mcp/services/block_writer.py | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/mcp-server/src/legal_mcp/services/block_writer.py b/mcp-server/src/legal_mcp/services/block_writer.py index 38caccb..31edd77 100644 --- a/mcp-server/src/legal_mcp/services/block_writer.py +++ b/mcp-server/src/legal_mcp/services/block_writer.py @@ -297,6 +297,32 @@ STRUCTURE_GUIDANCE = { "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( case_id: UUID, @@ -352,12 +378,8 @@ async def write_block( post_hearing_context = await _build_post_hearing_context(case_id) outcome = canonical_outcome((decision or {}).get("outcome", "rejection")) - 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() + structure_guidance = await _structure_guidance_with_corpus( + outcome, case.get("practice_area", "")) # Content checklist — tells block-yod WHAT topics to cover 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) outcome = canonical_outcome((decision or {}).get("outcome", "rejection")) - 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() + structure_guidance = await _structure_guidance_with_corpus( + outcome, case.get("practice_area", "")) # Content checklist + methodology for block-yod content_checklist = ""