# 在「自动编号的顶层列表」中新增条款（保留numPr）

2026-06-18 华新慢病运维合同实战。一次返工换来的教训。

## 适用判定（动手前先分清两类合同）

合同的「条款」分两种承载方式，新增条款的手法完全不同：

| 类型 | 特征 | 新增条款手法 |
|------|------|-------------|
| **A. 第X条 文本标题** | 条款标题是 run 里的文字「第七条 …」/「7. …」，段落**无** numPr | `add_clause()`（库会剥 numPr，正确）|
| **B. 自动编号列表项** | 条款本身是自动编号列表项：段落 pPr 带 `<w:numPr>`，编号由 numbering.xml 的 `start`+`lvlText`（如 `一、`/`1.`/`（1）`）自动渲染，run 里**没有**编号文字 | ❌ 不能用 `add_clause()`；按下方「numbered-insert」手法 |

**判定脚本**：对要插入位置附近的条款段落跑 `numbering-diagnose.py`，或直接看锚点段 `pPr/numPr` 是否存在且其 numId 的 lvlText 是序号格式。本案锚点「五、违约责任」段 `pPr` = `pStyle=12 + numPr(numId=1,ilvl=0) + ind`，numId=1→abstractNum start=1 lvlText=`%1、`（japaneseCounting 一、二、三）。

## 为什么 add_clause 在 B 类会坏

`add_clause()`（contract_docx_lib.py 第408-411行）**无条件**剥离新段的 numPr：
```python
numpr = new_ppr.find(qn('numPr'))
if numpr is not None:
    new_ppr.remove(numpr)   # ← B类灾难
```
后果（本案实测）：5 个新增条款全部**丢失自动编号**，且因 pPr 缺 numPr/缩进与列表项不一致，渲染时**堆到了文档最末尾**（签署页之前），既无编号又错位——违反「新增条款插在逻辑对应位置、不堆到最后」+「自动编号保留numPr」两条规则。

`add_clause` 第二个缺陷：它只把**文本** run 包进 w:ins，**没有把段落标记（¶）标记为插入**。B 类里 ¶ 承载着自动编号，¶ 不是 tracked-insert，则接受/拒绝修订时这一项的编号增减不随修订走。

## 正确手法：numbered tracked-insert 段落

克隆锚点段的 pPr（**保留** numPr，让新段成为同一自动编号序列的一员），并把**段落标记本身**也标成 w:ins：

```python
import sys, copy
sys.path.insert(0, '/home/maggie/contract-work')
from contract_docx_lib import ContractEditor, qn
from lxml import etree

ed = ContractEditor(src)

# 1) 定位锚点段（要插在它之后的那条原文条款）
anchor = None
for p in ed.body.findall(qn('p')):
    if '违约责任：按照中华人民共和国民法典' in ed.get_para_text(p):
        anchor = p; break
anchor_ppr = anchor.find(qn('pPr'))
assert anchor_ppr.find(qn('numPr')) is not None, "锚点不是自动编号项，确认是否B类"

def make_numbered_ins_para(text):
    new_p = etree.Element(qn('p'))
    new_ppr = copy.deepcopy(anchor_ppr)          # 含 pStyle + numPr(同numId/ilvl) + ind → 入同一自动编号序列
    # 关键：把段落标记(¶)标成插入，整段(含自动编号)作为 tracked insertion
    rpr_mark = new_ppr.find(qn('rPr'))
    if rpr_mark is None:
        rpr_mark = etree.SubElement(new_ppr, qn('rPr'))
    ins_mark = etree.SubElement(rpr_mark, qn('ins'))
    ins_mark.set(qn('id'), ed._next_id())
    ins_mark.set(qn('author'), 'WB')
    ins_mark.set(qn('date'), ed._revision_date)
    new_p.append(new_ppr)
    # 文本作为 tracked-ins run，用规范化 _body_rpr（完整rFonts四属性+hint=eastAsia+显式sz）
    new_p.append(ed._mk_ins(text, ed._body_rpr))
    return new_p

clauses = [  # 期望的最终正序 六~十
    "保密与数据：……",
    "知识产权与系统交接：……",
    "转包与分包：……",
    "第三方侵权：……",
    "违约赔偿：……",
]

# 2) 全部插在 anchor 之后；倒序 insert 使最终正序
parent = ed.body
anchor_idx = list(parent).index(anchor)
for txt in reversed(clauses):
    parent.insert(anchor_idx + 1, make_numbered_ins_para(txt))

assert ed.validate() == []
ed.save(out)
```

要点：
- **倒序插入**：每条都插在 `anchor_idx+1`，倒序遍历 → 最终正序。
- **同一 numId/ilvl**：克隆锚点 pPr 即自动继承，新条款自动续编（本案锚点是五 → 新条款渲染为六、七、八、九、十，后续原文自动顺延为十一、十二…，**无需手动改任何原文编号**）。
- **¶ 标插入** + **文本 run 标插入**，两者都要，缺一不可。
- 文本 run 用 `ed._body_rpr`（库已规整：四属性 rFonts + hint=eastAsia + 显式 sz），不要手搓 rPr。

## 交付前验证（B 类专项）

1. **接受所有修订后**渲染（删 w:del + 删带 `pPr/rPr/del` 的整段 + 解包 w:ins）→ 确认新条款编号与锚点连续、原文顺延正确、无错位到末尾。
2. **字体核对走「同段原文」标准**：本案原文正文 run = `<w:rFonts hint="eastAsia"/><w:szCs val="21"/>`（**无**显式 eastAsia 名，继承 docDefaults 宋体）。新 INS run 与之等效即合格——`ea=None hint=eastAsia` 是**正确**的，`wb-ins-font-verify.py` 若按绝对属性报 `ea=None` 是假阳性（见 contract-reviewer 的 2026-06-17 培训合同条）。唯一差异是 INS 多了显式 `<w:sz val="21">`（w:ins 必需），渲染一致。
3. **LibreOffice 渲染假象**：用 `libreoffice→pdftotext` 自查时，被顺延的自动编号项会显示 `十二、[七、]` 这种**方括号叠加**（recomputed 新号 + cached 旧号），这是 LibreOffice markup 渲染产物，**不是错误**，XML 里没有字面方括号。判真实编号一律以「接受所有修订后」或 OnlyOffice 渲染为准（OnlyOffice 是 Maggie/Doro 实际所用引擎）。

## 锚点选择铁律：插在 body text 之后，不是 heading 之后

**这是一个极易犯的错误**（2026-06-26 朱家角环保袋合同实证）：

当Reviewer要求"在违约责任条款之后、争议解决条款之前新增XX条款"时，合同结构通常是：
```
P74: 八．违约责任        ← heading（numId=1）
P75: 若乙方未按本合同... ← body text（无 numPr）
P76: 九．合同金额        ← 下一个 heading（numId=1）
```

**错误做法**：锚点 = P74（heading），插入后 → 新条款夹在 heading 和它的 body text 之间，结构错乱。

**正确做法**：锚点 = P75（body text），插入后 → 新条款在 body text 之后、下一个 heading 之前，结构正确。

**判据**：`numbering-diagnose.py` 确认锚点段的 `numPr` 状态——heading 有 numPr，body text 无 numPr。新条款应克隆**下一个 heading**（如 P76 合同金额）的 pPr（含 numPr），插入在**前一个 body text**（如 P75）之后。

## 一句话

锚点是自动编号列表项（pPr 有 numPr）→ 别用 add_clause，克隆锚点 pPr（留 numPr）+ ¶ 标 w:ins + 文本标 w:ins，倒序插入，新条款自动续编、原文自动顺延。**插在 body text 之后，不是 heading 之后。**
