# 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

```python
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)
```python
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
```python
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
```python
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:
```xml
<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.
