refactor(export): שם קובץ טיוטת-הביניים → טיוטה-טענות_הצדדים_{N} (הנחיית חיים)
במקום "טיוטת-ביניים-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) <noreply@anthropic.com>
This commit is contained in:
@@ -719,13 +719,13 @@ ls data/cases/$CASE_NUMBER/documents/research/analysis-and-research.md
|
|||||||
```
|
```
|
||||||
mcp__legal-ai__export_interim_draft(case_number="...")
|
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`):
|
5. **דווח לחיים** (כולל מייל דרך `scripts/notify.py`):
|
||||||
```
|
```
|
||||||
## טיוטת ביניים מוכנה — ערר {case_number}
|
## טיוטת ביניים מוכנה — ערר {case_number}
|
||||||
|
|
||||||
📄 **קובץ:** `data/cases/{case_number}/exports/טיוטת-ביניים-v{N}.docx`
|
📄 **קובץ:** `data/cases/{case_number}/exports/טיוטה-טענות_הצדדים_{N}.docx`
|
||||||
|
|
||||||
### מה כלול
|
### מה כלול
|
||||||
| בלוק | כותרת | מילים |
|
| בלוק | כותרת | מילים |
|
||||||
|
|||||||
@@ -399,8 +399,15 @@ _INTERIM_BLOCK_ORDER = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _draft_filename_prefix(mode: str) -> str:
|
def _draft_naming(mode: str) -> tuple[str, str]:
|
||||||
return "טיוטת-ביניים" if mode == "interim" else "טיוטה"
|
"""(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(
|
async def export_decision(
|
||||||
@@ -485,16 +492,19 @@ async def export_decision(
|
|||||||
if not output_path:
|
if not output_path:
|
||||||
export_dir = config.find_case_dir(case["case_number"]) / "exports"
|
export_dir = config.find_case_dir(case["case_number"]) / "exports"
|
||||||
export_dir.mkdir(parents=True, exist_ok=True)
|
export_dir.mkdir(parents=True, exist_ok=True)
|
||||||
prefix = _draft_filename_prefix(mode)
|
prefix, sep = _draft_naming(mode)
|
||||||
existing = sorted(export_dir.glob(f"{prefix}-v*.docx"))
|
existing = sorted(export_dir.glob(f"{prefix}{sep}*.docx"))
|
||||||
next_ver = 1
|
next_ver = 1
|
||||||
for p in existing:
|
for p in existing:
|
||||||
try:
|
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)
|
next_ver = max(next_ver, ver + 1)
|
||||||
except (IndexError, ValueError):
|
except (IndexError, ValueError):
|
||||||
pass
|
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
|
# Persist through the storage layer (INV-STG1). Under the filesystem
|
||||||
# backend the bytes land at output_path exactly as before; a caller-
|
# backend the bytes land at output_path exactly as before; a caller-
|
||||||
|
|||||||
@@ -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:
|
async def export_interim_draft(case_number: str, output_path: str = "") -> str:
|
||||||
"""ייצוא טיוטת ביניים ל-DOCX — אותו עיצוב של טיוטה רגילה (David, RTL,
|
"""ייצוא טיוטת ביניים ל-DOCX — אותו עיצוב של טיוטה רגילה (David, RTL,
|
||||||
bookmarks), אבל בסדר חדש: רקע → תכניות+היתרים → טענות → הליכים, ללא
|
bookmarks), אבל בסדר חדש: רקע → תכניות+היתרים → טענות → הליכים, ללא
|
||||||
דיון/סיכום/חתימות. שם הקובץ: טיוטת-ביניים-v{N}.docx.
|
דיון/סיכום/חתימות. שם הקובץ: טיוטה-טענות_הצדדים_{N}.docx.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
case_number: מספר תיק הערר
|
case_number: מספר תיק הערר
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ function formatDate(epoch: number): string {
|
|||||||
/**
|
/**
|
||||||
* Pick the newest export file whose name starts with `prefix`, preferring the
|
* 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
|
* 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(
|
function pickLatestVersioned(
|
||||||
files: ExportFile[] | undefined,
|
files: ExportFile[] | undefined,
|
||||||
@@ -100,7 +100,10 @@ function pickLatestVersioned(
|
|||||||
const matches = (files ?? []).filter((f) => f.filename.startsWith(prefix));
|
const matches = (files ?? []).filter((f) => f.filename.startsWith(prefix));
|
||||||
if (!matches.length) return null;
|
if (!matches.length) return null;
|
||||||
const withVer = matches.map((f) => {
|
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 };
|
return { ...f, version: m ? parseInt(m[1], 10) : null };
|
||||||
});
|
});
|
||||||
withVer.sort((a, b) => {
|
withVer.sort((a, b) => {
|
||||||
@@ -172,8 +175,8 @@ export function DraftsPanel({
|
|||||||
|
|
||||||
// ── "הפקת מסמכים" derived state (#214) ──
|
// ── "הפקת מסמכים" derived state (#214) ──
|
||||||
// Latest interim ("party-claims") partial draft in the exports table — the poll
|
// Latest interim ("party-claims") partial draft in the exports table — the poll
|
||||||
// target for button 2. The CEO writes it as טיוטת-ביניים-{case}-vN.docx.
|
// target for button 2. The CEO writes it as טיוטה-טענות_הצדדים_{N}.docx.
|
||||||
const latestInterim = pickLatestVersioned(exports, "טיוטת-ביניים-");
|
const latestInterim = pickLatestVersioned(exports, "טיוטה-טענות_הצדדים_");
|
||||||
const summaryReady = Boolean(summary?.markdown);
|
const summaryReady = Boolean(summary?.markdown);
|
||||||
|
|
||||||
function handleGenerateSummary() {
|
function handleGenerateSummary() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* Generation runs on the host (claude_session → claude -p), NOT in the container,
|
* 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:
|
* so the trigger endpoints return 202-accepted and the UI POLLS for completion:
|
||||||
* • summary → GET /api/cases/{n}/research/party-claims-summary (200 = ready)
|
* • 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).
|
* (polled via useExports — see exports.ts).
|
||||||
*
|
*
|
||||||
* The full-decision export ("הפק טיוטת החלטה מלאה") is NOT here — it reuses the
|
* 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`,
|
`/api/cases/${caseNumber}/generate/interim-draft`,
|
||||||
{ method: "POST" },
|
{ 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.
|
// useExports); re-arm the status chip so it shows the new run immediately.
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
qc.invalidateQueries({
|
qc.invalidateQueries({
|
||||||
|
|||||||
@@ -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
|
# 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.
|
# platform Port → POST /api/agents/{id}/wakeup), NEVER a direct DB insert.
|
||||||
# Polling for completion uses the existing read endpoints (research/party-claims-
|
# 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:
|
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):
|
async def api_generate_interim_draft(case_number: str):
|
||||||
"""Fire-and-accept: wake the CEO to generate the partial "party-claims" draft
|
"""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
|
(שלב 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")
|
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":
|
if action == "party_claims_summary":
|
||||||
from legal_mcp.services import party_claims_summary as pcs
|
from legal_mcp.services import party_claims_summary as pcs
|
||||||
return pcs.summary_file_path(case_number).exists()
|
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"
|
export_dir = config.find_case_dir(case_number) / "exports"
|
||||||
if not export_dir.exists():
|
if not export_dir.exists():
|
||||||
return False
|
return False
|
||||||
return any(
|
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()
|
for f in export_dir.iterdir()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user