From 9e7ef4fcedf3b35de12c1a65c469c4c9f2e569b7 Mon Sep 17 00:00:00 2001 From: Chaim Date: Mon, 6 Jul 2026 12:31:30 +0000 Subject: [PATCH] =?UTF-8?q?refactor(export):=20=D7=A9=D7=9D=20=D7=A7=D7=95?= =?UTF-8?q?=D7=91=D7=A5=20=D7=98=D7=99=D7=95=D7=98=D7=AA-=D7=94=D7=91?= =?UTF-8?q?=D7=99=D7=A0=D7=99=D7=99=D7=9D=20=E2=86=92=20=D7=98=D7=99=D7=95?= =?UTF-8?q?=D7=98=D7=94-=D7=98=D7=A2=D7=A0=D7=95=D7=AA=5F=D7=94=D7=A6?= =?UTF-8?q?=D7=93=D7=93=D7=99=D7=9D=5F{N}=20(=D7=94=D7=A0=D7=97=D7=99?= =?UTF-8?q?=D7=99=D7=AA=20=D7=97=D7=99=D7=99=D7=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit במקום "טיוטת-ביניים-v{N}.docx" — שם מדבר יותר: "טיוטה-טענות_הצדדים_{N}.docx". הפורמט הסופי ("טיוטה-v{N}") לא משתנה. - docx_exporter._draft_naming: (prefix, sep) פר-מצב; interim=("טיוטה-טענות_הצדדים","_"), final=("טיוטה","-v"). glob/parse משתמשים ב-rsplit(sep,1) — נכון גם כשה-prefix עצמו מכיל "_". - app.py _generation_output_ready + frontend pickLatestVersioned: matcher-prefix מעודכן; regex-הגרסה עוגן-לסוף /[_v](\d+)$/ (ה-"_" שבתוך הprefix לא מבלבל). - docstrings + legal-ceo.md + הערות מעודכנים לשם החדש. types.ts (auto-gen) יתעדכן ב-api:types. שינוי-נתון (שם-קובץ), לא layout — בגדר חריג-שער-העיצוב. tsc+eslint+py_compile ירוקים. הערה: ייצור-השם רץ ב-MCP המקומי (host) → דורש reload; ה-matchers בקונטיינר (Coolify). Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/agents/legal-ceo.md | 4 ++-- .../src/legal_mcp/services/docx_exporter.py | 22 ++++++++++++++----- mcp-server/src/legal_mcp/tools/drafting.py | 2 +- web-ui/src/components/cases/drafts-panel.tsx | 11 ++++++---- web-ui/src/lib/api/generate.ts | 4 ++-- web/app.py | 8 +++---- 6 files changed, 32 insertions(+), 19 deletions(-) diff --git a/.claude/agents/legal-ceo.md b/.claude/agents/legal-ceo.md index 7e0e875..5514e7b 100644 --- a/.claude/agents/legal-ceo.md +++ b/.claude/agents/legal-ceo.md @@ -719,13 +719,13 @@ ls data/cases/$CASE_NUMBER/documents/research/analysis-and-research.md ``` mcp__legal-ai__export_interim_draft(case_number="...") ``` - מייצר `data/cases/{case_number}/exports/טיוטת-ביניים-v{N}.docx`, מעדכן `active_draft_path`. + מייצר `data/cases/{case_number}/exports/טיוטה-טענות_הצדדים_{N}.docx`, מעדכן `active_draft_path`. 5. **דווח לחיים** (כולל מייל דרך `scripts/notify.py`): ``` ## טיוטת ביניים מוכנה — ערר {case_number} - 📄 **קובץ:** `data/cases/{case_number}/exports/טיוטת-ביניים-v{N}.docx` + 📄 **קובץ:** `data/cases/{case_number}/exports/טיוטה-טענות_הצדדים_{N}.docx` ### מה כלול | בלוק | כותרת | מילים | diff --git a/mcp-server/src/legal_mcp/services/docx_exporter.py b/mcp-server/src/legal_mcp/services/docx_exporter.py index 4ad2602..1fa6b0e 100644 --- a/mcp-server/src/legal_mcp/services/docx_exporter.py +++ b/mcp-server/src/legal_mcp/services/docx_exporter.py @@ -399,8 +399,15 @@ _INTERIM_BLOCK_ORDER = [ ] -def _draft_filename_prefix(mode: str) -> str: - return "טיוטת-ביניים" if mode == "interim" else "טיוטה" +def _draft_naming(mode: str) -> tuple[str, str]: + """(filename prefix, version separator) per export mode. + + interim → ``טיוטה-טענות_הצדדים_{N}.docx`` (chair-requested naming); + final → ``טיוטה-v{N}.docx``. + """ + if mode == "interim": + return "טיוטה-טענות_הצדדים", "_" + return "טיוטה", "-v" async def export_decision( @@ -485,16 +492,19 @@ async def export_decision( if not output_path: export_dir = config.find_case_dir(case["case_number"]) / "exports" export_dir.mkdir(parents=True, exist_ok=True) - prefix = _draft_filename_prefix(mode) - existing = sorted(export_dir.glob(f"{prefix}-v*.docx")) + prefix, sep = _draft_naming(mode) + existing = sorted(export_dir.glob(f"{prefix}{sep}*.docx")) next_ver = 1 for p in existing: try: - ver = int(p.stem.split("-v")[1]) + # Version is the trailing integer after the separator. Using + # rsplit keeps this correct even when the prefix itself contains + # the separator char (e.g. "טיוטה-טענות_הצדדים" with sep="_"). + ver = int(p.stem.rsplit(sep, 1)[1]) next_ver = max(next_ver, ver + 1) except (IndexError, ValueError): pass - output_path = str(export_dir / f"{prefix}-v{next_ver}.docx") + output_path = str(export_dir / f"{prefix}{sep}{next_ver}.docx") # Persist through the storage layer (INV-STG1). Under the filesystem # backend the bytes land at output_path exactly as before; a caller- diff --git a/mcp-server/src/legal_mcp/tools/drafting.py b/mcp-server/src/legal_mcp/tools/drafting.py index 98b5dd2..7e2b21b 100644 --- a/mcp-server/src/legal_mcp/tools/drafting.py +++ b/mcp-server/src/legal_mcp/tools/drafting.py @@ -716,7 +716,7 @@ async def write_interim_draft(case_number: str, instructions: str = "") -> str: async def export_interim_draft(case_number: str, output_path: str = "") -> str: """ייצוא טיוטת ביניים ל-DOCX — אותו עיצוב של טיוטה רגילה (David, RTL, bookmarks), אבל בסדר חדש: רקע → תכניות+היתרים → טענות → הליכים, ללא - דיון/סיכום/חתימות. שם הקובץ: טיוטת-ביניים-v{N}.docx. + דיון/סיכום/חתימות. שם הקובץ: טיוטה-טענות_הצדדים_{N}.docx. Args: case_number: מספר תיק הערר diff --git a/web-ui/src/components/cases/drafts-panel.tsx b/web-ui/src/components/cases/drafts-panel.tsx index 5cb2c0c..846372d 100644 --- a/web-ui/src/components/cases/drafts-panel.tsx +++ b/web-ui/src/components/cases/drafts-panel.tsx @@ -91,7 +91,7 @@ function formatDate(epoch: number): string { /** * Pick the newest export file whose name starts with `prefix`, preferring the * highest v-number and falling back to the latest created_at. Used by the - * "הפקת מסמכים" card to surface the most recent טיוטת-ביניים-* file (#214). + * "הפקת מסמכים" card to surface the most recent טיוטה-טענות_הצדדים_* file (#214). */ function pickLatestVersioned( files: ExportFile[] | undefined, @@ -100,7 +100,10 @@ function pickLatestVersioned( const matches = (files ?? []).filter((f) => f.filename.startsWith(prefix)); if (!matches.length) return null; const withVer = matches.map((f) => { - const m = f.filename.match(/v(\d+)/); + // Version is the trailing integer, after either "-v" (final) or "_" + // (interim: טיוטה-טענות_הצדדים_{N}). Anchor to the end so the "_" inside + // the interim prefix isn't mistaken for the version separator. + const m = f.filename.match(/[_v](\d+)(?:\.\w+)?$/); return { ...f, version: m ? parseInt(m[1], 10) : null }; }); withVer.sort((a, b) => { @@ -172,8 +175,8 @@ export function DraftsPanel({ // ── "הפקת מסמכים" derived state (#214) ── // Latest interim ("party-claims") partial draft in the exports table — the poll - // target for button 2. The CEO writes it as טיוטת-ביניים-{case}-vN.docx. - const latestInterim = pickLatestVersioned(exports, "טיוטת-ביניים-"); + // target for button 2. The CEO writes it as טיוטה-טענות_הצדדים_{N}.docx. + const latestInterim = pickLatestVersioned(exports, "טיוטה-טענות_הצדדים_"); const summaryReady = Boolean(summary?.markdown); function handleGenerateSummary() { diff --git a/web-ui/src/lib/api/generate.ts b/web-ui/src/lib/api/generate.ts index 4a651f4..1133455 100644 --- a/web-ui/src/lib/api/generate.ts +++ b/web-ui/src/lib/api/generate.ts @@ -9,7 +9,7 @@ * Generation runs on the host (claude_session → claude -p), NOT in the container, * so the trigger endpoints return 202-accepted and the UI POLLS for completion: * • summary → GET /api/cases/{n}/research/party-claims-summary (200 = ready) - * • interim draft → the exports list grows a "טיוטת-ביניים-{n}-vN.docx" file + * • interim draft → the exports list grows a "טיוטה-טענות_הצדדים_{N}.docx" file * (polled via useExports — see exports.ts). * * The full-decision export ("הפק טיוטת החלטה מלאה") is NOT here — it reuses the @@ -137,7 +137,7 @@ export function useGenerateInterimDraft(caseNumber: string) { `/api/cases/${caseNumber}/generate/interim-draft`, { method: "POST" }, ), - // The result lands in the exports list (טיוטת-ביניים-*.docx, polled by + // The result lands in the exports list (טיוטה-טענות_הצדדים_*.docx, polled by // useExports); re-arm the status chip so it shows the new run immediately. onSuccess: () => { qc.invalidateQueries({ diff --git a/web/app.py b/web/app.py index a98a95d..c8db7aa 100644 --- a/web/app.py +++ b/web/app.py @@ -3213,7 +3213,7 @@ async def api_party_claims_summary(case_number: str): # Wakeup goes through the Paperclip API helper (pc_wake_ceo_for_action → the # platform Port → POST /api/agents/{id}/wakeup), NEVER a direct DB insert. # Polling for completion uses the existing read endpoints (research/party-claims- -# summary for the summary; the exports list for the טיוטת-ביניים-*.docx file). +# summary for the summary; the exports list for the טיוטה-טענות_הצדדים_*.docx file). async def _wake_ceo_action(case_number: str, action: str) -> dict: @@ -3246,7 +3246,7 @@ async def api_generate_party_claims_summary(case_number: str): async def api_generate_interim_draft(case_number: str): """Fire-and-accept: wake the CEO to generate the partial "party-claims" draft (שלב H → write_interim_draft + export_interim_draft). Runs host-side; poll the - exports list for the new טיוטת-ביניים-{case}-vN.docx.""" + exports list for the new טיוטה-טענות_הצדדים_{N}.docx.""" return await _wake_ceo_action(case_number, "interim_draft") @@ -3261,12 +3261,12 @@ def _generation_output_ready(case_number: str, action: str) -> bool: if action == "party_claims_summary": from legal_mcp.services import party_claims_summary as pcs return pcs.summary_file_path(case_number).exists() - # interim_draft → a "טיוטת-ביניים-{case}-*.docx" file in the case exports dir. + # interim_draft → a "טיוטה-טענות_הצדדים_*.docx" file in the case exports dir. export_dir = config.find_case_dir(case_number) / "exports" if not export_dir.exists(): return False return any( - f.is_file() and f.suffix.lower() == ".docx" and f.name.startswith("טיוטת-ביניים-") + f.is_file() and f.suffix.lower() == ".docx" and f.name.startswith("טיוטה-טענות_הצדדים_") for f in export_dir.iterdir() )