# Split-Run Numbering in docx XML ## Problem Contract numbering like `(5)` is often split across multiple `` runs in the XML: ```xml 5 )委托方 ``` A naive `tracked_replace("(5)", "(6)")` searching for the complete string in a single `` will **silently fail** — no match, no error, no renumbering. ## Solution: Multi-run concatenation + split ### Algorithm ```python def tracked_replace_split_number(p, old_num, new_num): """Handle (old_num) spread across multiple runs.""" target = f'({old_num})' new_target = f'({new_num})' # 1. Collect all plain runs (not inside w:ins or w:del) plain_runs = [(index, run, text) for each child of p] # 2. Slide a window: concatenate adjacent run texts until target is found for start in range(len(plain_runs)): concat = "" for end in range(start, start+4): # max 4 runs for a number concat += plain_runs[end].text if target in concat: # Found! Extract before/after text around the number runs_to_wrap = plain_runs[start:end+1] # ...proceed to replace # 3. Remove original runs, insert: # - [before_run if text before number] # - DEL element with delText=target # - INS element with t=new_target # - [after_run if text after number, e.g. "委托方"] # 4. Set rsid attributes: rsidDel on DEL runs, rsidR on INS runs ``` ### Critical: Process order **Always renumber from bottom to top** (last paragraph first) to avoid index shifting: ```python # CORRECT renumber = [(P113, '10', '11'), (P112, '9', '10'), (P111, '7', '8')] # WRONG - P112 was already renumbered when we get to it renumber = [(P111, '7', '8'), (P112, '9', '10'), (P113, '10', '11')] ``` ### Edge cases encountered (2026-06-08) - `(` + `10)` (two runs, not three) — the closing `)` merged with the digit - `(` + `5` + `)委托方` — closing `)` merged with following text, must split run to preserve "委托方" - Copy `w:rPr` from original runs to all new DEL/INS runs to preserve font/size ## Lesson This was the root cause of a terminal review failure where 3 new clauses were inserted without numbering, and subsequent numbering was not renumbered. The `tracked_replace` function matched nothing because it expected `(5)` as a single text node.