# 在「用户已自行修订过」的合同上叠加我方修订

实战来源：金信大厦5层东部租赁合同（2026-06-25）。Maggie 本人已用修订模式改了 6 处（author="maggie jia"），要求小Maggie 在此基础上**再补几处**（模版比对后补不可抗力对等、装修残值公式、抵押救济），**保留她的全部修订一字不动**。

## 何时用本配方
- 收到的 docx **已带 track changes**（settings.xml 有 `<w:trackRevisions/>`，文中有 author≠WB/小Maggie 的 w:ins/w:del）。
- 任务是**在用户既有修订之上追加几处**，不是重审、不是从干净稿做。
- **不重跑 workflow，也不用 ContractEditor 库**——库的字符级 diff 引擎会把用户既有 w:ins/w:del 卷进来重算，破坏其修订。一律 zipfile+lxml 直接追加节点。

## 五步配方

### 1. 新修订 id 从 `maxid+1000` 起，防撞 + 便于事后过滤
```python
maxid = 0
for el in root.iter():
    if el.tag in (Wq+"ins", Wq+"del"):
        v = el.get(Wq+"id")
        if v and v.isdigit(): maxid = max(maxid, int(v))
nextid = [maxid + 1000]            # 1000 间隔：本次新增 id 全 >1000，过滤/核验时一眼区分
def newid(): nextid[0]+=1; return str(nextid[0])
```
为什么 +1000 不是 +1：核验「我的修订」与「用户的修订」时，`int(id)>1000` 直接切分两批，不必记具体数字。

### 2. 作者：沿用文档既有修订线，不强行套 WB
金信大厦案文档既有修订 author="maggie jia"，本次追加**沿用同一 author**（保持修订线一致、Maggie 看就是「她那条线的延续」）。
> 注意与「author 铁律=WB」的边界：WB 是 Doro 体系合同审查的署名；当**文档已有用户自己的修订线**、任务是「在她的修订上接着改」时，沿用她的 author 让修订归并到同一作者更自然。归属按文档既有线定，不是无脑套 WB。拿不准就问。

### 3. rPr：克隆「用户已渲染正确的 INS」当样板，预防中文字体回退坑
不要自己造 rPr。找一个用户已有的、**中文显示正常的** w:ins run，读它的 rPr 当模板：
```python
# 金信大厦案模板：<w:rFonts w:ascii="Times New Roman" w:eastAsiaTheme="minorEastAsia"
#                  w:hAnsi="Times New Roman" w:cs="Times New Roman" w:hint="eastAsia"/>
#                 <w:sz w:val="21"/><w:szCs w:val="21"/>
def make_rpr():
    rpr = etree.Element(Wq+"rPr")
    rf = etree.SubElement(rpr, Wq+"rFonts")
    rf.set(Wq+"ascii","Times New Roman"); rf.set(Wq+"eastAsiaTheme","minorEastAsia")
    rf.set(Wq+"hAnsi","Times New Roman"); rf.set(Wq+"cs","Times New Roman"); rf.set(Wq+"hint","eastAsia")
    sz = etree.SubElement(rpr, Wq+"sz"); sz.set(Wq+"val","21")
    etree.SubElement(rpr, Wq+"szCs").set(Wq+"val","21")
    return rpr
```
`eastAsiaTheme="minorEastAsia"+hint="eastAsia"` 让中文走主题回退（金信大厦回退到宋体），西文 Times New Roman——这是这类合同 INS 中文正常显示的关键，详见 SKILL.md「INS 中文字体」节。

### 4. 三种插入机制（按改动类型选）
- **纯追加**（句末补一句救济/公式）：定位段落最后一个 normal run / 最后一个 ins，`last.addnext(make_ins(text, rpr))`。
- **删一段换对等表述**（单向条款改双向）：split 原 run → 原 run 文本保留前半、`addnext(make_del(后半, 原rpr))` → 再 `del.addnext(make_ins(对等表述, rpr))`。
- **替换数值/词**（6→12 个月、percent→百分之）：字符级定位，DEL 旧 + INS 新。

`make_del` 用 `<w:del><w:r><w:delText>`、克隆原 run 的 rPr 加 `rsidDel`；`make_ins` 用 `<w:ins><w:r><w:t xml:space="preserve">`。

### 5. 只换 document.xml（settings 已开 trackRevisions 就不动它）
```python
with zipfile.ZipFile(SRC) as zin, zipfile.ZipFile(tmp,"w",zipfile.ZIP_DEFLATED) as zout:
    for it in zin.namelist():
        zout.writestr(it, new_doc if it=="word/document.xml" else zin.read(it))
```

## 四查验证（缺一不可）
1. **python-docx 能打开**（`Document(out)`）——XML 合法。
2. **接受所有修订后文本正确**——抽出「保留 ins 内容、丢弃 del 内容」的纯文本，逐处核我改的几段语句通顺、内容对。
3. **本次新增 INS（id>1000）含中文 run 字体非 Times New Roman**——`eastAsiaTheme=="minorEastAsia" or (ea and ea!="Times New Roman")` 应全真。
4. **🔴 用户原有修订逐 id 比对一字未动**——把源文件与产物里 `id<=maxid` 的所有 ins/del 提成 `(id, tag, author, 文本)` 排序比对，必须**完全相等**。这是本配方的核心安全验证：证明我只追加、没碰用户的任何一处。

## 收口（与库路径相同）
`scripts/accept-revisions-preview.py` 生成干净版 → OnlyOffice 渲染 → vision 视觉验收。
- **vision 报「页底某句截断」先分清 PDF 分页 vs 真丢数据**：金信大厦案 vision 报抵押救济句在 P7 底部截断，实为该句跨页接到 P8 开头——①数据层读该 INS 完整内容在；②P7+P8 拍平后 grep 完整句存在 → 确认是 PDF 分页跨页，OnlyOffice 滚动查看正常，**不返工**。判据同 contract-portfolio-analysis Pitfall：数据层完整+跨页搜得到=分页现象。
- vision 对字体/‰%/小符号的误判同样适用——回数据层核，别据像素返工（见 SKILL.md「交付前视觉验收的两个已知误判」）。
