# Standalone Char-Level Tracked Changes + Comments (non-workflow) When modifying contracts **outside** the Doro/邱律师 workflow (e.g. Maggie directly asks to revise a client's agreement), the full `ContractEditor` library + review-rules machinery is overkill. Use this lightweight pattern instead. ## When to use - Maggie sends a contract and says "帮我改一下" / "修改这份协议" - No workflow, no reviewer, no deliverer — just direct revision - Still must produce Word-native tracked changes (del/ins) + comments ## Core technique: `difflib.SequenceMatcher` char-level diff ```python import difflib from docx.oxml.ns import qn from docx.oxml import OxmlElement def char_level_replace(para, new_text, author="WB", date="2026-07-07T10:00:00Z"): """Replace paragraph text with char-level tracked changes. Unchanged chars → normal w:r (preserved). Deleted chars → w:del + w:delText. Inserted chars → w:ins + w:t. """ p = para._element old_text = para.text if old_text == new_text: return # Remove existing runs (preserve pPr) for child in list(p): tag = child.tag.split('}')[-1] if '}' in child.tag else child.tag if tag in ('r', 'ins', 'del', 'hyperlink'): p.remove(child) sm = difflib.SequenceMatcher(None, old_text, new_text) for op, i1, i2, j1, j2 in sm.get_opcodes(): if op == 'equal': p.append(make_run(old_text[i1:i2])) elif op == 'delete': p.append(make_del_run(old_text[i1:i2], author, date)) elif op == 'insert': p.append(make_ins_run(new_text[j1:j2], author, date)) elif op == 'replace': p.append(make_del_run(old_text[i1:i2], author, date)) p.append(make_ins_run(new_text[j1:j2], author, date)) ``` ## Comments injection (bypassing python-docx limitations) python-docx has no native comment support. Inject manually: 1. Add `commentRangeStart` + `commentRangeEnd` + `commentReference` run to target paragraph 2. Build `word/comments.xml` as a plain string (proper namespace, no lxml serialization quirks) 3. Inject into the docx ZIP: update `[Content_Types].xml` + `word/_rels/document.xml.rels` ### Critical: comments.xml namespace **Wrong** (causes "reuse of xmlns" error): ```python comments_xml = etree.Element(qn('w:comments')) comments_xml.set(qn('xmlns:w'), WNS) # ❌ double declaration ``` **Right** (build as plain string): ```python def build_comments_xml(comments_list): lines = [''] lines.append('') for cid, text in comments_list: safe = text.replace("&", "&").replace("<", "<").replace(">", ">") lines.append(f' ') lines.append(f' {safe}') lines.append(f' ') lines.append('') return "\n".join(lines) ``` ## Pitfalls learned (2026-07-07 退休返聘案) 1. **lxml etree serialization breaks Word**: `etree.tostring()` produces `xmlns:ns0=...` prefix notation that Word/OnlyOffice cannot parse. Always build comments.xml as a plain string. 2. **Entire-paragraph del+ins is unacceptable**: Maggie and Doro both require char-level precision. "原文相同的部分保留,不一样的用修订" — this is non-negotiable. 3. **New paragraphs (fully inserted)**: Use `pPr/rPr/ins` mark to flag the ¶ itself as inserted, plus `w:ins` wrapping the text run. Both are needed for Word to show the full paragraph as tracked insertion. 4. **Verify files open correctly**: After save, always `Document(path)` to confirm no XML parse errors. ## Template (full working script structure) See `/tmp/modify_v3_charlevel.py` from the 2026-07-07 session — processes two contracts (full-time + part-time) with char-level diff + comments injection. Pattern: `process_contract(input, output, is_fulltime=bool)`.