Files
hermes-skills/skills/legal/contract-editor/references/same-template-revision-transfer.md
T

9.1 KiB

Same-Template Revision Transfer (同模板修订参照)

When Doro says "参照X合同的修订进行修订" — apply the same WB revisions from a reference contract to another contract using the same template.

⚠️ Doro 强制验证纪律(2026-07-08 明确要求)

Doro 明确要求做同模板修订参照时必须走完以下步骤,缺一不可:

  1. 先确认模板一致性:逐段对比两份合同原文(去掉修订后的文本),确认段落数一致、差异仅限业务内容(项目名称、单价等),其余结构完全相同。打印差异段数/总段数(如"9/62段有差异")。
  2. 参照修订:提取参照合同的WB修订→适配目标合同业务语境→应用
  3. 格式/字体/编号全检:所有INS的rPr必须与前后邻居run一致(逐个检查sz/rFonts/bold)。遵守workflow规则(author=WB、精准到字、不整段del+ins)
  4. 全文阅读审查合理性:渲染accept后全文,逐段通读确认修订逻辑合理、不破坏上下文语义
  5. 交付前检查:python-docx可打开、无异常字符、修订数与参照合同一致、他人修订保持不动

不能跳步直接做修订然后上传。 Doro原话:"你先确认:两个合同是不是模板一样,内容一样;如果一样,参照修改;你修改的格式、字体、编号等,都要遵守workflow的规则;全文阅读,修订是否合理。最后检查交付。"

Workflow

Step 1: Extract revisions from reference contract

import zipfile
from lxml import etree

ns = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'

def extract_wb_revisions(filepath):
    """Extract all WB-authored INS and DEL from a contract."""
    with zipfile.ZipFile(filepath, 'r') as z:
        content = z.read('word/document.xml')
    tree = etree.fromstring(content)
    
    revisions = []
    for ins in tree.iter(f'{{{ns}}}ins'):
        if ins.get(f'{{{ns}}}author') != 'WB':
            continue
        texts = [t.text for t in ins.iter(f'{{{ns}}}t') if t.text]
        # Get parent paragraph for context
        parent_p = ins
        while parent_p is not None and parent_p.tag != f'{{{ns}}}p':
            parent_p = parent_p.getparent()
        p_texts = [t.text for t in parent_p.iter(f'{{{ns}}}t') if t.text] if parent_p is not None else []
        revisions.append({
            'type': 'INS', 'text': ''.join(texts),
            'para_context': ''.join(p_texts)[:120]
        })
    
    for d in tree.iter(f'{{{ns}}}del'):
        if d.get(f'{{{ns}}}author') != 'WB':
            continue
        texts = [t.text for t in d.iter(f'{{{ns}}}delText') if t.text]
        parent_p = d
        while parent_p is not None and parent_p.tag != f'{{{ns}}}p':
            parent_p = parent_p.getparent()
        p_texts = [t.text for t in parent_p.iter(f'{{{ns}}}t') if t.text] if parent_p is not None else []
        revisions.append({
            'type': 'DEL', 'text': ''.join(texts),
            'para_context': ''.join(p_texts)[:120]
        })
    return revisions

Step 2: Identify modification patterns

Group INS/DEL pairs by paragraph context to understand what was changed:

  • Simple text replacement: DEL "协议" + INS "合同" in same paragraph
  • Text insertion: INS without corresponding DEL (e.g., data ownership sentence)
  • Prefix insertion: INS "上海市" before existing text

Step 3: Context adaptation

When the template is shared but service content differs, adapt context-specific terms:

  • "体检服务" → "口腔检查服务"
  • "学生个人信息、健康检查结果" → "个人信息、检查结果"
  • Keep legal boilerplate identical (e.g., "归甲方所有", "合同期满")

Step 4: Apply to target contract (zipfile+lxml)

Use three operations:

A. Tracked replace (DEL old + INS new)

def do_tracked_replace(para, find_text, replace_text):
    """Find text in paragraph runs, create DEL + INS."""
    # Build character map from runs (skip ins/del elements)
    char_map = []
    for elem in para:
        if elem.tag == f'{{{ns}}}r':
            t = elem.find(f'{{{ns}}}t')
            if t is not None and t.text:
                for ci in range(len(t.text)):
                    char_map.append((elem, t, ci))
    
    full_text = ''.join(cm[1].text[cm[2]] for cm in char_map)
    pos = full_text.find(find_text)
    if pos == -1:
        return False
    
    # Verify single-run containment, then split into before/DEL/INS/after
    # ... (see session code for full implementation)

B. Insert before text

def do_tracked_insert_before(para, anchor_text, insert_text):
    """Insert INS element right before anchor_text."""
    for elem in para:
        if elem.tag == f'{{{ns}}}r':
            t = elem.find(f'{{{ns}}}t')
            if t is not None and t.text and anchor_text in t.text:
                # Split run, insert INS before anchor portion
                ...

C. Insert after text

def do_tracked_insert_after(para, anchor_text, insert_text):
    """Insert INS element right after anchor_text."""
    for elem in para:
        if elem.tag == f'{{{ns}}}r':
            t = elem.find(f'{{{ns}}}t')
            if t is not None and t.text and anchor_text in t.text:
                # Split run, insert INS after anchor portion
                ...

Data Attribution Rule (2026-07-08 Doro clarification)

When writing data ownership/attribution clauses across same-template contracts:

LOCKED (identical across all contracts): 归属表述 = "归甲方或相关权利方所有"

  • NOT "归甲方所有" (excludes data subjects' rights under PIPL)
  • The phrase "甲方或相关权利方" covers both: data甲方 owns (aggregated stats, service outputs) AND personal info that belongs to data subjects

NOT LOCKED (varies by contract): The descriptive content before the attribution phrase

  • 体检合同: "乙方在提供体检服务过程中获取和产生的全部数据(包括但不限于学生个人信息、健康检查结果等)"
  • 口腔检查合同: "乙方在提供口腔检查服务过程中获取和产生的全部数据(包括但不限于个人信息、检查结果等)"
  • Other contracts: adapt to the specific service/data context

Doro原话: "我只需要涉及到数据权利的归属时,把归属谁改成'归甲方或相关权利方所有',其他的内容不同合同会不同,所以你不能写死。"

Rule source: review-rules.md §4 保密/数据 (updated 2026-07-08)

Pitfalls

1. <w:proofErr> splits runs

Text like "青浦区练塘镇" may be split into multiple runs separated by <w:proofErr> elements:

<w:r><w:t>青浦区练塘</w:t></w:r>
<w:proofErr w:type="gramStart"/>
<w:r><w:t>镇社区卫生服务中心</w:t></w:r>

Fix: Search at individual run level (for elem in para: if elem.tag == w:r), not at full paragraph text level. Insert INS before the run containing the anchor, not at a text position within full paragraph text.

2. "达成如下协议" — not all "协议" should be replaced

In the reference contract, "达成如下协议:" was NOT changed (it's a formulaic expression meaning "reached the following agreement"). Only contextual uses of "协议" meaning "this agreement/contract" were changed to "合同"/"本合同".

Rule: Compare reference contract's accepted text to determine which instances were changed and which were left alone.

3. INS rPr must clone from target run (not reference)

The target contract's runs may have different formatting than the reference. Always clone rPr from the target paragraph's existing run, not from the reference contract.

4. Order of operations matters

Do replacements BEFORE insertions. Insertions change paragraph structure and character positions, which can break subsequent text searches.

Recommended order:

  1. All do_tracked_replace calls (these only split existing runs)
  2. All do_tracked_insert_after / do_tracked_insert_before calls (these add new elements)

5. Verify with accept-all view

After applying all revisions, verify by building accepted text (skip DEL, include INS) for key paragraphs and comparing against the reference contract's accepted text.

2026-07-08 实证:练塘口腔检查合同

Reference: 【修】2026年学生体检外包合同--练塘(1).docx Target: 2026年口腔检查外包合同--练塘.docx

Modifications applied:

# Type Content Adaptation
1 INS before "青浦区" "上海市" None (identical)
2a INS after "保密义务。" Data ownership sentence "体检服务"→"口腔检查服务", "学生个人信息、健康检查结果"→"个人信息、检查结果"
2b DEL "协议" + INS "合同" "协议期满"→"合同期满" None
2c DEL "服务协议" + INS "本合同" "服务协议解除"→"本合同解除" None
2d DEL "本协议" + INS "本合同" "本协议的履行"→"本合同的履行" None
3 DEL "本协议" + INS "本合同" "本协议一式"→"本合同一式" None

proofErr pitfall encountered: "青浦区练塘" split by <w:proofErr> — had to insert at run level rather than text-position level.