feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# 在「自动编号的顶层列表」中新增条款(保留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 之后。**
|
||||
Reference in New Issue
Block a user