fix(style-acq T9): מספור-אוטומטי אמיתי בייצוא DOCX (היה ללא מספור)
באג: ה-exporter הסיר את הקידומת "N." והחיל סגנון "List Paragraph" — שאין לו
numPr בתבנית (אין numbering.xml) → ההחלטות יצאו **ללא מספור** כלל.
- docx_exporter._ensure_decision_numbering: מזריק abstractNum עשרוני (RTL,
lvlJc=right) + num לחלק-המספור פעם אחת; _apply_list_numbering מחבר כל
פסקת-גוף לרשימה הרציפה. מספור Word אמיתי — מתעדכן בעריכה, copy/paste נקי.
אומת מבנית: numId יחיד, decimal, שתי פסקאות→אותו numId, docx נשמר.
- התאמת ANTI_PATTERNS (T7): הוסר manual_paragraph_numbers — "N." בתחילת-שורה
הוא ה-signal הנדרש לייצוא, לא אנטי-דפוס. נשאר inline (1)..(2)/markdown/bullets.
- voice-fingerprint §3.1: תוקן — הכותב כן מקדים "N. " בתחילת-שורה (signal),
הייצוא ממיר ל-auto-numbering. סתירה קודמת ("אל תקליד מספרים") יושבה.
⚠️ אימות-מבנה עבר; אימות ויזואלי ב-Word מומלץ על ייצוא ראשון. G11.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -184,9 +184,9 @@
|
|||||||
### 3.1 ❌ אסור: רשימת-מיני ממוספרת בתוך פסקת-אנליזה (פיצול טיעון ל-`(1)...(2)...`)
|
### 3.1 ❌ אסור: רשימת-מיני ממוספרת בתוך פסקת-אנליזה (פיצול טיעון ל-`(1)...(2)...`)
|
||||||
**ב-0/33** מהחלטות הסופיות יש `(1) ... (2) ... (3) ...` המפצל טיעון בתוך פסקת אנליזה אחת. טענות וניתוח נכתבים כ**נרטיב רציף** עם ביטויי-מעבר ("עוד נטען", "באשר ל-", "יתרה מכך"), לא כרשימת-מיני.
|
**ב-0/33** מהחלטות הסופיות יש `(1) ... (2) ... (3) ...` המפצל טיעון בתוך פסקת אנליזה אחת. טענות וניתוח נכתבים כ**נרטיב רציף** עם ביטויי-מעבר ("עוד נטען", "באשר ל-", "יתרה מכך"), לא כרשימת-מיני.
|
||||||
|
|
||||||
✅ **ההחלטה כן ממוספרת — תמיד.** פסקאות ההחלטה ממוספרות סדרתית (1, 2, 3 ... עד הסוף), כמקובל בפסיקה. **המספור מוחל אוטומטית בשלב ייצוא ה-DOCX (Word auto-numbering)** ולכן אינו מופיע בחילוץ-טקסט גולמי — אל תסיק מהיעדרו בטקסט שאין מספור.
|
✅ **ההחלטה כן ממוספרת — תמיד.** פסקאות ההחלטה ממוספרות סדרתית (1, 2, 3 ... עד הסוף), כמקובל בפסיקה.
|
||||||
⚠️ **הכותב לא יקליד מספרים כטקסט ידני** ("12. ", "13. ") בתוך התוכן — הם נוצרים אוטומטית בייצוא, ומספרים ידניים שוברים את ה-copy/paste וכופלים מספור. כתוב את הפסקה ללא מספר מוביל.
|
✅ **הכותב מקדים כל פסקת-החלטה ב-"N. " בתחילת שורה** (1., 2., 3. ... סדרתי). זהו ה-signal שמנוע-הייצוא מזהה (`docx_exporter._NUM_PREFIX_RE`): הוא **מסיר את הקידומת הידנית וממיר אותה למספור-אוטומטי אמיתי של Word** (`_ensure_decision_numbering` — רשימה עשרונית רציפה, RTL). כך ה-DOCX מתמספר מעצמו (מתעדכן בעריכה, copy/paste נקי ללא ספרות תועות).
|
||||||
**ב-3/3 טיוטות AI** הופיעו מספרים ידניים בטקסט — שהוסרו/הומרו לאוטומטיים בעריכה. (תיקון 2026-06-06: ההנחה הקודמת ש"ההחלטות החדשות ללא מספור" הייתה ארטיפקט-חילוץ.)
|
⚠️ **המספר חייב להיות בתחילת השורה בלבד** — מספר *באמצע* פסקה הוא רשימת-מיני אסורה (§3.1 לעיל). (תיקון 2026-06-06: ההנחה ש"ההחלטות החדשות ללא מספור" הייתה ארטיפקט-חילוץ; וההנחה ש"הכותב לא יקליד מספרים" שגויה — הקידומת בתחילת-שורה היא ה-signal לייצוא, שמומר ל-auto-numbering.)
|
||||||
|
|
||||||
### 3.2 ⚠️ מותנה: כותרת משנה בלב בלוק י
|
### 3.2 ⚠️ מותנה: כותרת משנה בלב בלוק י
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,84 @@ def _suppress_paragraph_numbering(paragraph) -> None:
|
|||||||
pPr.append(numPr)
|
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:
|
def _clear_body(doc) -> None:
|
||||||
"""Remove all paragraphs in the document body while keeping sectPr.
|
"""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("[]📷 "))
|
_add_image_placeholder(doc, stripped.strip("[]📷 "))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Numbered body paragraph ("1. text") → List Paragraph with auto-num.
|
# Numbered body paragraph ("1. text") → real Word auto-numbering (T9).
|
||||||
# The literal prefix is dropped; Word renders "1. 2. 3. ..." via numId.
|
# 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)
|
num_match = _NUM_PREFIX_RE.match(stripped)
|
||||||
if num_match:
|
if num_match:
|
||||||
body_text = num_match.group(2).strip()
|
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
|
continue
|
||||||
|
|
||||||
_add_styled_paragraph(doc, stripped, style="Normal")
|
_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) ──
|
# ── Anti-patterns (what Dafna avoids) — detectable signals for style-distance (T7) ──
|
||||||
# Derived from daphna-voice-fingerprint.md §3 (corrected 2026-06-06: sequential
|
# Derived from daphna-voice-fingerprint.md §3 (corrected 2026-06-06). NOTE: a leading
|
||||||
# paragraph numbering is REQUIRED — applied as Word auto-numbering at export — so the
|
# "N." per paragraph is NOT an anti-pattern — it is the REQUIRED signal the DOCX
|
||||||
# anti-pattern is MANUAL numbers typed as text, not numbering itself).
|
# 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] = [
|
ANTI_PATTERNS: list[dict] = [
|
||||||
{"name": "manual_paragraph_numbers",
|
|
||||||
"regex": r"(?m)^\s*\d{1,3}\.\s",
|
|
||||||
"note": "מספרים ידניים כטקסט בראש פסקה — אמורים להיות auto-numbering בייצוא"},
|
|
||||||
{"name": "inline_numbered_fragments",
|
{"name": "inline_numbered_fragments",
|
||||||
"regex": r"\([0-9]\)[^\n]{0,200}\([0-9]\)",
|
"regex": r"\([0-9]\)[^\n]{0,200}\([0-9]\)",
|
||||||
"note": "פיצול טיעון לרשימת-מיני (1)...(2) בתוך פסקת-אנליזה"},
|
"note": "פיצול טיעון לרשימת-מיני (1)...(2) בתוך פסקת-אנליזה"},
|
||||||
|
|||||||
Reference in New Issue
Block a user