Merge pull request 'fix(style-acq T9): מספור-אוטומטי אמיתי בייצוא DOCX' (#88) from worktree-style-acquisition-mvp into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m28s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m28s
This commit was merged in pull request #88.
This commit is contained in:
@@ -184,9 +184,9 @@
|
||||
### 3.1 ❌ אסור: רשימת-מיני ממוספרת בתוך פסקת-אנליזה (פיצול טיעון ל-`(1)...(2)...`)
|
||||
**ב-0/33** מהחלטות הסופיות יש `(1) ... (2) ... (3) ...` המפצל טיעון בתוך פסקת אנליזה אחת. טענות וניתוח נכתבים כ**נרטיב רציף** עם ביטויי-מעבר ("עוד נטען", "באשר ל-", "יתרה מכך"), לא כרשימת-מיני.
|
||||
|
||||
✅ **ההחלטה כן ממוספרת — תמיד.** פסקאות ההחלטה ממוספרות סדרתית (1, 2, 3 ... עד הסוף), כמקובל בפסיקה. **המספור מוחל אוטומטית בשלב ייצוא ה-DOCX (Word auto-numbering)** ולכן אינו מופיע בחילוץ-טקסט גולמי — אל תסיק מהיעדרו בטקסט שאין מספור.
|
||||
⚠️ **הכותב לא יקליד מספרים כטקסט ידני** ("12. ", "13. ") בתוך התוכן — הם נוצרים אוטומטית בייצוא, ומספרים ידניים שוברים את ה-copy/paste וכופלים מספור. כתוב את הפסקה ללא מספר מוביל.
|
||||
**ב-3/3 טיוטות AI** הופיעו מספרים ידניים בטקסט — שהוסרו/הומרו לאוטומטיים בעריכה. (תיקון 2026-06-06: ההנחה הקודמת ש"ההחלטות החדשות ללא מספור" הייתה ארטיפקט-חילוץ.)
|
||||
✅ **ההחלטה כן ממוספרת — תמיד.** פסקאות ההחלטה ממוספרות סדרתית (1, 2, 3 ... עד הסוף), כמקובל בפסיקה.
|
||||
✅ **הכותב מקדים כל פסקת-החלטה ב-"N. " בתחילת שורה** (1., 2., 3. ... סדרתי). זהו ה-signal שמנוע-הייצוא מזהה (`docx_exporter._NUM_PREFIX_RE`): הוא **מסיר את הקידומת הידנית וממיר אותה למספור-אוטומטי אמיתי של Word** (`_ensure_decision_numbering` — רשימה עשרונית רציפה, RTL). כך ה-DOCX מתמספר מעצמו (מתעדכן בעריכה, copy/paste נקי ללא ספרות תועות).
|
||||
⚠️ **המספר חייב להיות בתחילת השורה בלבד** — מספר *באמצע* פסקה הוא רשימת-מיני אסורה (§3.1 לעיל). (תיקון 2026-06-06: ההנחה ש"ההחלטות החדשות ללא מספור" הייתה ארטיפקט-חילוץ; וההנחה ש"הכותב לא יקליד מספרים" שגויה — הקידומת בתחילת-שורה היא ה-signal לייצוא, שמומר ל-auto-numbering.)
|
||||
|
||||
### 3.2 ⚠️ מותנה: כותרת משנה בלב בלוק י
|
||||
|
||||
|
||||
@@ -112,6 +112,84 @@ def _suppress_paragraph_numbering(paragraph) -> None:
|
||||
pPr.append(numPr)
|
||||
|
||||
|
||||
def _ensure_decision_numbering(doc) -> int:
|
||||
"""T9 — define a single continuous decimal list (RTL) and return its numId.
|
||||
|
||||
Dafna's decisions are ALWAYS sequentially numbered (1. 2. 3. ...). The template
|
||||
ships no numbering definition, so previously the body paragraphs were stripped of
|
||||
their manual "N." prefix and styled "List Paragraph" — which carries NO numPr,
|
||||
yielding UNNUMBERED output. Here we inject one decimal abstractNum + num into the
|
||||
numbering part once per document; body paragraphs then reference it (real Word
|
||||
auto-numbering → renumbers automatically, copy-pastes cleanly).
|
||||
"""
|
||||
cached = getattr(doc, "_decision_num_id", None)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
numbering = doc.part.numbering_part.element # <w:numbering>
|
||||
|
||||
def _next_id(tag: str, attr: str) -> int:
|
||||
ids = [int(el.get(qn(attr))) for el in numbering.findall(qn(tag))
|
||||
if el.get(qn(attr)) and el.get(qn(attr)).isdigit()]
|
||||
return (max(ids) + 1) if ids else 1
|
||||
|
||||
abstract_id = _next_id("w:abstractNum", "w:abstractNumId")
|
||||
num_id = _next_id("w:num", "w:numId")
|
||||
|
||||
abstract = OxmlElement("w:abstractNum")
|
||||
abstract.set(qn("w:abstractNumId"), str(abstract_id))
|
||||
mlt = OxmlElement("w:multiLevelType")
|
||||
mlt.set(qn("w:val"), "singleLevel")
|
||||
abstract.append(mlt)
|
||||
lvl = OxmlElement("w:lvl")
|
||||
lvl.set(qn("w:ilvl"), "0")
|
||||
for tag, val in (("w:start", "1"), ("w:numFmt", "decimal"),
|
||||
("w:lvlText", "%1."), ("w:lvlJc", "right")):
|
||||
el = OxmlElement(tag)
|
||||
el.set(qn("w:val"), val)
|
||||
lvl.append(el)
|
||||
lvl_ppr = OxmlElement("w:pPr")
|
||||
ind = OxmlElement("w:ind")
|
||||
ind.set(qn("w:start"), "720")
|
||||
ind.set(qn("w:hanging"), "360")
|
||||
lvl_ppr.append(ind)
|
||||
lvl.append(lvl_ppr)
|
||||
abstract.append(lvl)
|
||||
|
||||
num = OxmlElement("w:num")
|
||||
num.set(qn("w:numId"), str(num_id))
|
||||
anum_ref = OxmlElement("w:abstractNumId")
|
||||
anum_ref.set(qn("w:val"), str(abstract_id))
|
||||
num.append(anum_ref)
|
||||
|
||||
# abstractNum elements must precede num elements in <w:numbering>.
|
||||
last_abstract = numbering.findall(qn("w:abstractNum"))
|
||||
if last_abstract:
|
||||
last_abstract[-1].addnext(abstract)
|
||||
else:
|
||||
numbering.insert(0, abstract)
|
||||
numbering.append(num)
|
||||
|
||||
doc._decision_num_id = num_id
|
||||
return num_id
|
||||
|
||||
|
||||
def _apply_list_numbering(paragraph, num_id: int) -> None:
|
||||
"""Attach paragraph to the continuous decision list (real auto-numbering)."""
|
||||
pPr = paragraph._p.get_or_add_pPr()
|
||||
existing = pPr.find(qn("w:numPr"))
|
||||
if existing is not None:
|
||||
pPr.remove(existing)
|
||||
numPr = OxmlElement("w:numPr")
|
||||
ilvl = OxmlElement("w:ilvl")
|
||||
ilvl.set(qn("w:val"), "0")
|
||||
nid = OxmlElement("w:numId")
|
||||
nid.set(qn("w:val"), str(num_id))
|
||||
numPr.append(ilvl)
|
||||
numPr.append(nid)
|
||||
pPr.append(numPr)
|
||||
|
||||
|
||||
def _clear_body(doc) -> None:
|
||||
"""Remove all paragraphs in the document body while keeping sectPr.
|
||||
|
||||
@@ -485,12 +563,15 @@ def _write_block_to_docx(doc, block_id: str, title: str, content: str) -> None:
|
||||
_add_image_placeholder(doc, stripped.strip("[]📷 "))
|
||||
continue
|
||||
|
||||
# Numbered body paragraph ("1. text") → List Paragraph with auto-num.
|
||||
# The literal prefix is dropped; Word renders "1. 2. 3. ..." via numId.
|
||||
# Numbered body paragraph ("1. text") → real Word auto-numbering (T9).
|
||||
# The literal prefix is dropped and a numPr referencing the document's
|
||||
# continuous decimal list is attached, so Word renders "1. 2. 3. ..."
|
||||
# itself (renumbers on edit, copy-pastes without stray digits).
|
||||
num_match = _NUM_PREFIX_RE.match(stripped)
|
||||
if num_match:
|
||||
body_text = num_match.group(2).strip()
|
||||
_add_styled_paragraph(doc, body_text, style="List Paragraph")
|
||||
para = _add_styled_paragraph(doc, body_text, style="List Paragraph")
|
||||
_apply_list_numbering(para, _ensure_decision_numbering(doc))
|
||||
continue
|
||||
|
||||
_add_styled_paragraph(doc, stripped, style="Normal")
|
||||
|
||||
@@ -43,13 +43,11 @@ GOLDEN_RATIOS: dict[str, dict[str, tuple[int, int]]] = {
|
||||
}
|
||||
|
||||
# ── Anti-patterns (what Dafna avoids) — detectable signals for style-distance (T7) ──
|
||||
# Derived from daphna-voice-fingerprint.md §3 (corrected 2026-06-06: sequential
|
||||
# paragraph numbering is REQUIRED — applied as Word auto-numbering at export — so the
|
||||
# anti-pattern is MANUAL numbers typed as text, not numbering itself).
|
||||
# Derived from daphna-voice-fingerprint.md §3 (corrected 2026-06-06). NOTE: a leading
|
||||
# "N." per paragraph is NOT an anti-pattern — it is the REQUIRED signal the DOCX
|
||||
# exporter converts to real Word auto-numbering (docx_exporter._ensure_decision_numbering).
|
||||
# The real anti-patterns are mid-paragraph mini-lists, markdown, and bullets.
|
||||
ANTI_PATTERNS: list[dict] = [
|
||||
{"name": "manual_paragraph_numbers",
|
||||
"regex": r"(?m)^\s*\d{1,3}\.\s",
|
||||
"note": "מספרים ידניים כטקסט בראש פסקה — אמורים להיות auto-numbering בייצוא"},
|
||||
{"name": "inline_numbered_fragments",
|
||||
"regex": r"\([0-9]\)[^\n]{0,200}\([0-9]\)",
|
||||
"note": "פיצול טיעון לרשימת-מיני (1)...(2) בתוך פסקת-אנליזה"},
|
||||
|
||||
Reference in New Issue
Block a user