190 lines
9.1 KiB
Markdown
190 lines
9.1 KiB
Markdown
# 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.
|