feat(analysis): סיכום-מנהלים מזוקק של טענות הצדדים — מסמך הכנה לדיון (#202, WS3)
מסמך פרוזה נפרד מטיוטת-הביניים (החלטת-יו"ר): תמצית מוקפדת של טענות+תשובות
להכנת היו"ר לדיון. נגזר מ-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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user