feat(analysis): סיכום-מנהלים מזוקק של טענות הצדדים — מסמך הכנה לדיון (#202, WS3)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

מסמך פרוזה נפרד מטיוטת-הביניים (החלטת-יו"ר): תמצית מוקפדת של טענות+תשובות
להכנת היו"ר לדיון. נגזר מ-legal_arguments/claims הקיימים — אותו מקור-אמת,
ללא חילוץ-מחדש (G2). מתוחם לטענות-התיק בלבד (INV-AH).

- כלי-MCP חדש summarize_party_claims (drafting.py + server.py); ייצור עובר
  claude_session נעוץ Opus 4.8 + effort=high (זיקוק/סינתזה); local-only.
- שירות party_claims_summary.py: בחירת-מקור arguments→claims, פרומפט מעוגן-מקור,
  שמירה ל-data/cases/{n}/documents/research/ (git + S3).
- ייצוא DOCX בסגנון-תבנית דפנה (build_party_claims_summary_docx) + endpoints
  read/download/export-docx ב-web/app.py (מגישים בלבד — אין claude CLI בקונטיינר).
- _next_version הוכלל ל-prefix-scoped (תאימות-לאחור) למניעת התנגשות-גרסאות.
- ספ 04-analysis-writing.md §1.3 + 8 בדיקות אופליין (מקור/נעיצת-מודל/markdown→docx).

Invariants: G2 (מקור-אמת יחיד, בלי מסלול מקביל) · X9 TOOL1/TOOL4 (envelope +
extract↔serve) · INV-AH (עיגון-מקור, abstention על טענה לא-ברורה) ·
claude_session local-only · G9 (provenance — נשמר durably + git/S3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 11:22:20 +00:00
parent 61e6be4b90
commit aa2548ff5d
7 changed files with 605 additions and 3 deletions

View File

@@ -386,9 +386,9 @@ def _group_precedents(
return case_level, by_section
def _next_version(export_dir: Path) -> int:
"""Return the next version number for ניתוח-משפטי-v{N}.docx."""
existing = sorted(export_dir.glob("ניתוח-משפטי-v*.docx"))
def _next_version(export_dir: Path, prefix: str = "ניתוח-משפטי") -> int:
"""Return the next version number for {prefix}-v{N}.docx in export_dir."""
existing = sorted(export_dir.glob(f"{prefix}-v*.docx"))
next_ver = 1
for p in existing:
try:
@@ -511,3 +511,77 @@ async def build_analysis_docx(case_number: str) -> Path:
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)
return out_path
# ── Generic markdown → DOCX (party-claims summary, #202) ───────────
# ATX-heading markers → template heading style. The party-claims summary is a
# plain markdown document (not the structured analysis-and-research.md), so it
# is rendered line-by-line through the same template machinery.
_ATX_HEADING_RE = re.compile(r"^(#{1,4})\s+(.+)$")
_HR_RE = re.compile(r"^\s*[-*_]{3,}\s*$")
def _render_markdown_body(doc: DocumentT, markdown: str) -> None:
"""Render a markdown string into the doc using the template styles.
Handles ATX headings (# .. ####), horizontal rules (skipped), and delegates
every other line to _emit_content_line (bullets, numbered, bold-labels, plain).
"""
for raw in markdown.splitlines():
line = raw.rstrip()
if not line.strip():
continue
if _HR_RE.match(line):
continue
m = _ATX_HEADING_RE.match(line.strip())
if m:
level = len(m.group(1))
# # → Heading 1 (doc title), ## → Heading 1, ### / #### → Heading 2.
style = "Heading 1" if level <= 2 else "Heading 2"
_add_paragraph(doc, m.group(2).strip(), style)
continue
_emit_content_line(doc, line)
async def build_party_claims_summary_docx(case_number: str) -> Path:
"""Build a DOCX of the party-claims executive summary using the template
styles, saved versioned under the case's exports folder.
Reads the saved party-claims-summary.md (produced by the summarize_party_claims
MCP tool — generation is local-only). Raises FileNotFoundError if the summary
or the template is missing.
"""
from legal_mcp.services import party_claims_summary
if not TEMPLATE_PATH.exists():
raise FileNotFoundError(
f"Template not found at {TEMPLATE_PATH}. "
"Run: python scripts/convert_decision_template.py"
)
summary_path = party_claims_summary.summary_file_path(case_number)
if not summary_path.exists():
raise FileNotFoundError(
f"Party-claims summary not found for case {case_number}. "
"Run summarize_party_claims first."
)
markdown = summary_path.read_text(encoding="utf-8")
doc = Document(str(TEMPLATE_PATH))
_clear_body(doc)
_render_markdown_body(doc, markdown)
export_dir = config.find_case_dir(case_number) / "exports"
export_dir.mkdir(parents=True, exist_ok=True)
_PREFIX = "סיכום-מנהלים-טענות"
out_path = export_dir / f"{_PREFIX}-v{_next_version(export_dir, _PREFIX)}.docx"
buf = io.BytesIO()
doc.save(buf)
await storage.put_bytes(
out_path.relative_to(config.DATA_DIR).as_posix(), buf.getvalue(),
bucket=storage.Bucket.DOCUMENTS,
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)
return out_path