Skip to content

feat: tracked paragraph splits on newline (ISSUES.md #58+61) - #72

Merged
pablospe merged 5 commits into
mainfrom
task/6d54ccbc-newline---tracked-paragraph-sp
Jul 24, 2026
Merged

pablospe merged 5 commits into
mainfrom
task/6d54ccbc-newline---tracked-paragraph-sp

Conversation

@pablospe

@pablospe pablospe commented Jul 24, 2026 •

Copy link
Copy Markdown
Owner

Summary

A \n in any content text is now a tracked paragraph split at that point: the first paragraph's mark is flagged as an inserted revision and the tail moves into a new following paragraph as unchanged content. Accepting keeps the split; rejecting rejoins. Works in replace (replace_with), insert_after/insert_before (text), rewrite_paragraph (new_text), and inside batch_edit ops. Multiple \ns make multiple splits.

One \n-containing operation is one revision group / one changeset covering the delete, the per-paragraph insertions, and each inserted paragraph mark — reject_group/reject_changeset revert the whole split atomically.

Details

  • Split primitives + newline dispatch from replace/insert/rewrite. Reopen reconstruction keys on the durable author+date+mark signal so a reopened split stays one inferred group across both paragraphs (survives Word stripping our extension attributes).
  • Document.split_paragraph(ref, *, before) — explicit sugar for a pure split.
  • EditResult.refs — every resulting paragraph ref in document order, resolved by element identity so a split that shifts later indexes still reports live positions (batch/rewrite share one revision-element index; no-split edits stay DOM-walk-free).
  • Control-char rejection — all other C0/DEL controls (tab, CR, NUL, …) are rejected at every text input with a teaching error; only \n is special.
  • Safe refusals — refuses to split a paragraph carrying a w:sectPr, or at a boundary inside an existing revision. The latter is checked up front so a refused single-op split leaves no partial mutation (single edits have no DOM rollback).

Verification

  • Full suite: 1331 passed, 2 skipped.
  • ruff check clean; ty check exit 0.
  • New tests/test_paragraph_split.py plus split-reconstruction and control-char coverage.

Summary by CodeRabbit

  • New Features
    • Added tracked paragraph splitting when edit content includes \n (including insert/replace/rewrite and batch operations).
    • Introduced split_paragraph and extended EditResult with refs to describe all resulting paragraphs.
    • Added paragraph-split tracking/count support for revision groups.
  • Bug Fixes
    • Improved split reconstruction across save/reopen and strengthened accept/reject recovery behavior.
  • Documentation
    • Updated API and skill docs to cover split_paragraph, EditResult.refs, and newline/control-character rules.
  • Tests
    • Expanded suites for control-character rejection, split edge cases, and reconstruction/atomicity.

A `\n` in any content text (replace's replace_with, insert_after/before's
text, rewrite_paragraph's new_text, or a batch_edit op) now means a tracked
paragraph split at that point: the first paragraph's mark is flagged as an
inserted revision and the tail moves into a new following paragraph as
unchanged content. Accepting keeps the split; rejecting rejoins. One
`\n`-containing operation is one revision group / one changeset covering the
delete, the per-paragraph insertions, and each inserted mark.

- Split primitives + newline dispatch from replace/insert/rewrite; reopen
  reconstruction keys on the durable author+date+mark signal so a reopened
  split stays one inferred group across both paragraphs.
- Document.split_paragraph(ref, *, before) convenience sugar.
- EditResult.refs: every resulting paragraph ref in document order, resolved
  by element identity so a split that shifts later indexes still reports live
  positions (batch/rewrite share one revision-element index).
- Reject all other C0/DEL control characters (tab, CR, NUL, ...) at every
  text input with a teaching error; only `\n` is special (a split).
- Refuse to split a paragraph carrying a w:sectPr, or at a boundary inside an
  existing revision — the latter checked up front so a refused single-op split
  leaves no partial mutation (single edits have no rollback).

Full suite: 1331 passed, 2 skipped.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Newline-containing edits now create tracked paragraph splits, expose all resulting paragraph refs, support batch operations and rejection rejoining, and reject unsupported control characters across editing and comment APIs. Documentation and regression tests cover the new behavior.

Changes

Tracked paragraph splitting

Layer / File(s) Summary
Control-character validation
docx_editor/xml_editor.py, docx_editor/track_changes.py, docx_editor/comments.py, tests/test_error_quality.py, skills/docx/SKILL.md
Unsupported C0 and DEL characters are rejected across edit and comment inputs; newline remains valid only for content edits.
Tracked split and rejoin engine
docx_editor/track_changes.py, tests/test_paragraph_split.py, tests/test_revision_groups.py
Newline-driven replacements, insertions, and rewrites create tracked paragraph marks, reconstruct split groups after reopening, and rejoin paragraphs when rejected.
Result references and public API
docx_editor/document.py, docx_editor/track_changes.py, docs/api.md, skills/docx/SKILL.md, tests/test_paragraph_split.py
EditResult.refs, Document.split_paragraph, and split-aware batch result resolution report resulting paragraphs in document order.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Document
  participant RevisionManager
  participant WordXML
  Document->>RevisionManager: submit newline-containing edit
  RevisionManager->>WordXML: create tracked paragraph split
  RevisionManager-->>Document: return group and split metadata
  Document->>RevisionManager: resolve resulting refs
  RevisionManager-->>Document: return paragraph refs
Loading

Possibly related PRs

Poem

I’m a rabbit who split paragraphs neat,
With tracked little marks from each hop of my feet.
Refs now point where the new lines grow,
Bad tabs and controls get told “no.”
Reject, rejoin—back home we go!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: tracked paragraph splits triggered by newlines.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/6d54ccbc-newline---tracked-paragraph-sp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.1%. Comparing base (f826d14) to head (69962d3).

Additional details and impacted files
@@           Coverage Diff           @@
##            main     #72     +/-   ##
=======================================
+ Coverage   95.9%   96.1%   +0.2%     
=======================================
  Files         11      11             
  Lines       4081    4355    +274     
  Branches     809     859     +50     
=======================================
+ Hits        3915    4187    +272     
- Misses        77      78      +1     
- Partials      89      90      +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/api.md`:
- Around line 398-404: Correct the `split_paragraph` heading anchor referenced
by the Newlines paragraph so `#split_paragraphref-before-occurrencenone` matches
the heading’s generated slug. Update the heading text or explicit anchor as
needed, preserving the documented `split_paragraph` reference.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 13ab818c-5580-4cdf-94ce-21dde3ec0353

📥 Commits

Reviewing files that changed from the base of the PR and between f826d14 and 9dbbd9d.

📒 Files selected for processing (9)
  • docs/api.md
  • docx_editor/comments.py
  • docx_editor/document.py
  • docx_editor/track_changes.py
  • docx_editor/xml_editor.py
  • skills/docx/SKILL.md
  • tests/test_error_quality.py
  • tests/test_paragraph_split.py
  • tests/test_revision_groups.py

Comment thread docs/api.md
Comment on lines +398 to +404
> **Newlines split paragraphs.** A `\n` in any content text (`replace`'s
> `replace_with`, `insert_after`/`insert_before`'s `text`, `rewrite_paragraph`'s
> `new_text`, or a `batch_edit` op) is a **tracked paragraph split**: the first
> paragraph's mark is flagged inserted and the tail moves into a new paragraph.
> Accepting keeps the split; rejecting rejoins. All other C0 control characters
> (`\t`, `\r`, …) are rejected with a teaching `ValueError`. See
> [`split_paragraph`](#split_paragraphref-before-occurrencenone) and [`EditResult`](#editresult).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Broken in-page link fragment. The split_paragraph heading slugifies with a double hyphen between ref and before (the , *, sequence collapses to two spaces → --), so #split_paragraphref-before-occurrencenone does not resolve to any heading.

📝 Proposed fix
-> [`split_paragraph`](`#split_paragraphref-before-occurrencenone`) and [`EditResult`](`#editresult`).
+> [`split_paragraph`](`#split_paragraphref--before-occurrencenone`) and [`EditResult`](`#editresult`).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
> **Newlines split paragraphs.** A `\n` in any content text (`replace`'s
> `replace_with`, `insert_after`/`insert_before`'s `text`, `rewrite_paragraph`'s
> `new_text`, or a `batch_edit` op) is a **tracked paragraph split**: the first
> paragraph's mark is flagged inserted and the tail moves into a new paragraph.
> Accepting keeps the split; rejecting rejoins. All other C0 control characters
> (`\t`, `\r`, …) are rejected with a teaching `ValueError`. See
> [`split_paragraph`](#split_paragraphref-before-occurrencenone) and [`EditResult`](#editresult).
> **Newlines split paragraphs.** A `\n` in any content text (`replace`'s
> `replace_with`, `insert_after`/`insert_before`'s `text`, `rewrite_paragraph`'s
> `new_text`, or a `batch_edit` op) is a **tracked paragraph split**: the first
> paragraph's mark is flagged inserted and the tail moves into a new paragraph.
> Accepting keeps the split; rejecting rejoins. All other C0 control characters
> (`\t`, `\r`, …) are rejected with a teaching `ValueError`. See
> [`split_paragraph`](`#split_paragraphref--before-occurrencenone`) and [`EditResult`](`#editresult`).
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 404-404: Link fragments should be valid

(MD051, link-fragments)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/api.md` around lines 398 - 404, Correct the `split_paragraph` heading
anchor referenced by the Newlines paragraph so
`#split_paragraphref-before-occurrencenone` matches the heading’s generated
slug. Update the heading text or explicit anchor as needed, preserving the
documented `split_paragraph` reference.

Source: Linters/SAST tools

pablospe added 2 commits July 24, 2026 14:58
Address issues found in multi-reviewer review of the split feature:

- reject_revision of a non-terminal split mark no longer orphans the
  downstream break: _rejoin_paragraph migrates the following paragraph's
  pending mark onto the surviving paragraph, so the later split stays tracked
  and rejectable (group/changeset reject was already safe via reverse-id order).
- Refuse re-splitting a paragraph that already carries a pending inserted mark,
  up front and atomically — a second mark would be invalid OOXML (CT_ParaRPr
  allows one w:ins) and mis-attribute the break.
- rewrite_paragraph refuses (atomically, before mutating) a rewrite whose
  newlines fall into separate diff hunks — that would split one paragraph in
  several places. A single hunk with several newlines still threads fine.
- Split-inserted segments inherit the surrounding run's rPr instead of dropping
  to document default.
- Extract _paragraph_mark_rPr (schema-order pPr/rPr) shared by mark insertion
  and the rejoin migration; simplify reconstruction run_key to (author, date);
  add missing return annotations; quote 'reply' in a control-char error.

Regression tests for each. Full suite: 1336 passed, 2 skipped.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docx_editor/track_changes.py (1)

3009-3028: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Split segments after the first lose formatting when the split lands at the paragraph's end.

_insert_ins_at_paragraph_start derives the inserted segment's rPr only from paragraph.getElementsByTagName("w:r"), falling back to "" when the (new) paragraph has no runs yet. In _apply_paragraph_splits, every intermediate tail paragraph created by _split_paragraph_at_position is genuinely empty until its own segment is inserted whenever the split point is at or beyond the end of the current paragraph's text — which is exactly what happens for a multi-\n insert_after/insert_before/replace at the end of a paragraph (e.g. appending "A\nB\nC" after the last word).

Segment 1 ("A") correctly inherits formatting because _rewrite_insert_at's "insert at end" branch pulls rPr from the original last run. But segments 2+ ("B", "C", ...) are inserted into a paragraph with zero runs at that point, so they silently drop to "" (document-default) formatting instead of the surrounding bold/italic/etc. — contradicting this PR's stated goal that split segments inherit surrounding run properties. test_split_inserted_segment_inherits_run_format only exercises a single split with a non-empty tail, so this gap is untested.

Suggest threading a fallback rPr (captured once from the pre-split boundary run) through _apply_paragraph_splits into _insert_ins_at_paragraph_start, used only when the target paragraph has no runs of its own yet.

🎨 Sketch of the fix
-    def _apply_paragraph_splits(self, p1, split_pos: int, segments: list[str]) -> int:
+    def _apply_paragraph_splits(self, p1, split_pos: int, segments: list[str], fallback_rpr: str = "") -> int:
         member_id = -1
         current_p = p1
         pos = split_pos
         for segment in segments:
             new_p, mark_id = self._split_paragraph_at_position(current_p, pos)
             member_id = mark_id
             if segment:
-                self._insert_ins_at_paragraph_start(new_p, segment)
+                self._insert_ins_at_paragraph_start(new_p, segment, fallback_rpr)
             current_p = new_p
             pos = len(segment)
         return member_id

-    def _insert_ins_at_paragraph_start(self, paragraph, text: str) -> None:
+    def _insert_ins_at_paragraph_start(self, paragraph, text: str, fallback_rpr: str = "") -> None:
         runs = paragraph.getElementsByTagName("w:r")
-        rPr_xml = get_rPr_xml(runs[0]) if runs else ""
+        rPr_xml = get_rPr_xml(runs[0]) if runs else fallback_rpr

Callers (_split_replace, _split_insert, _rewrite_insert_at) would capture the boundary run's rPr once (e.g. via self._get_run_info on the last visible run before the split point) and pass it through.

Also applies to: 3141-3161

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docx_editor/track_changes.py` around lines 3009 - 3028, Thread a captured
boundary-run rPr fallback through the paragraph-splitting flow so empty
intermediate paragraphs retain surrounding formatting. Update callers such as
_split_replace, _split_insert, and _rewrite_insert_at to capture the pre-split
boundary rPr once, pass it into _apply_paragraph_splits, and have
_apply_paragraph_splits forward it to _insert_ins_at_paragraph_start; use the
fallback only when the target paragraph has no runs, preserving each paragraph’s
own formatting when present.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@docx_editor/track_changes.py`:
- Around line 3009-3028: Thread a captured boundary-run rPr fallback through the
paragraph-splitting flow so empty intermediate paragraphs retain surrounding
formatting. Update callers such as _split_replace, _split_insert, and
_rewrite_insert_at to capture the pre-split boundary rPr once, pass it into
_apply_paragraph_splits, and have _apply_paragraph_splits forward it to
_insert_ins_at_paragraph_start; use the fallback only when the target paragraph
has no runs, preserving each paragraph’s own formatting when present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3ed269d8-acfa-474c-86eb-650f8d47f00b

📥 Commits

Reviewing files that changed from the base of the PR and between 9dbbd9d and 1888792.

📒 Files selected for processing (4)
  • docx_editor/comments.py
  • docx_editor/track_changes.py
  • tests/test_paragraph_split.py
  • tests/test_revision_groups.py

pablospe added 2 commits July 24, 2026 16:00
Bring patch coverage back over target after the hardening round:

- Tests for the previously-untested split paths: splitting a paragraph with
  rich properties (pStyle/rPr/pPrChange/mark) so the tail's pPr copy is
  exercised and cleaned; a pStyle paragraph with no rPr; rewrite append that
  diffs into an insert hunk (empty and non-empty first segment); replace with a
  leading newline (empty first segment); reply_to_comment control-char
  rejection; and the foreign-authored-mark reconstruction branch.
- Mark genuinely-unreachable defensive branches with `# pragma: no cover`
  (rejoin with no following paragraph, split-continuation prev_para guard,
  _resulting_refs fallbacks); replace the parentNode ty-guard with an assert.
- ruff format normalization.

Full suite: 1343 passed, 2 skipped; every changed source line covered.
A multi-newline split at the end of a paragraph (e.g. appending "A\nB\nC" past
the last word) opens empty tail paragraphs, so a segment inserted there had no
adjacent run to copy formatting from and dropped to the document default —
only the first segment kept the surrounding format. Thread the boundary run's
rPr through _apply_paragraph_splits so every segment inherits it, propagating
across a run of empty tails. Regression tests for end-split formatting and the
consecutive-newline (empty middle paragraph) edge.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
docx_editor/document.py (1)

397-419: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve target identity for no-op items in splitting batches.

When a batch splits P1 and a no-op targets original P2, group_id is None takes this stale-index fallback and returns the new P2 (the split tail), not the original paragraph now at P3. This affects both batch_edit() and batch_rewrite(). Preserve each operation’s pre-mutation paragraph element and resolve its live index when no group exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docx_editor/document.py` around lines 397 - 419, Update the no-group path in
the operation handling used by batch_edit() and batch_rewrite() to preserve each
operation’s original paragraph element before mutations and resolve that
element’s current index afterward, rather than relying on the stale old_ref
index. Keep the existing grouped-split handling unchanged, and use the preserved
element identity to return the original no-op target even when earlier splits
shift paragraph positions.
docx_editor/track_changes.py (2)

675-678: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep paragraph-mark IDs consistent with rollback and resolution.

_paragraph_mark_ids is mutable registry state, but _registry_snapshot() excludes it and _restore_registry() does not restore it, so failed edit/rewrite batches restore the old paragraph-mark snapshot instead of rolling it back. Resolved paragraph-mark insertions are also never discarded, so accepted or rejected IDs can still be counted by split_count(). Add this set to the snapshot and remove IDs when paragraph-mark insertions are accepted or rejected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docx_editor/track_changes.py` around lines 675 - 678, Update the registry
snapshot and restore logic to include _paragraph_mark_ids so failed edit or
rewrite batches roll back this mutable state. In the paragraph-mark insertion
resolution paths, remove each ID from _paragraph_mark_ids when the insertion is
accepted or rejected, ensuring split_count() only counts unresolved
paragraph-mark insertions.

3678-3689: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Resolve the owner paragraph before removing the mark.

removeChild(mark_ins) detaches the mark from its parentNode, so _ancestor_paragraph(mark_ins) returns None and the rejoin returns without merging the following paragraph. Capture p1 before removing the node.

Proposed fix
+        p1 = _ancestor_paragraph(mark_ins)
+        if p1 is None:
+            return
         rPr = mark_ins.parentNode
         rPr.removeChild(mark_ins)
         ...
-        p1 = _ancestor_paragraph(mark_ins)
-        if p1 is None:
-            return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docx_editor/track_changes.py` around lines 3678 - 3689, Move the
_ancestor_paragraph(mark_ins) call in the mark-ins removal flow so p1 is
resolved before rPr.removeChild(mark_ins) detaches the node. Preserve the
existing wrapper cleanup and subsequent paragraph-rejoin behavior, using the
captured p1 for the later guard and merge logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docx_editor/track_changes.py`:
- Around line 3022-3032: Update the split-segment loop around
_split_paragraph_at_position so fallback_rPr retains the last non-empty run
formatting when the current paragraph has no runs. Only replace fallback_rPr
when boundary_runs contains a run, allowing later non-empty segments such as “C”
to reuse formatting across empty paragraphs while preserving the existing
insertion flow.

---

Outside diff comments:
In `@docx_editor/document.py`:
- Around line 397-419: Update the no-group path in the operation handling used
by batch_edit() and batch_rewrite() to preserve each operation’s original
paragraph element before mutations and resolve that element’s current index
afterward, rather than relying on the stale old_ref index. Keep the existing
grouped-split handling unchanged, and use the preserved element identity to
return the original no-op target even when earlier splits shift paragraph
positions.

In `@docx_editor/track_changes.py`:
- Around line 675-678: Update the registry snapshot and restore logic to include
_paragraph_mark_ids so failed edit or rewrite batches roll back this mutable
state. In the paragraph-mark insertion resolution paths, remove each ID from
_paragraph_mark_ids when the insertion is accepted or rejected, ensuring
split_count() only counts unresolved paragraph-mark insertions.
- Around line 3678-3689: Move the _ancestor_paragraph(mark_ins) call in the
mark-ins removal flow so p1 is resolved before rPr.removeChild(mark_ins)
detaches the node. Preserve the existing wrapper cleanup and subsequent
paragraph-rejoin behavior, using the captured p1 for the later guard and merge
logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ab1c0577-7f6a-4acb-9fc2-1ee2cb52a69b

📥 Commits

Reviewing files that changed from the base of the PR and between 1888792 and 69962d3.

📒 Files selected for processing (5)
  • docx_editor/document.py
  • docx_editor/track_changes.py
  • tests/test_error_quality.py
  • tests/test_paragraph_split.py
  • tests/test_revision_groups.py

Comment on lines +3022 to +3032
# Formatting to fall back on when the new paragraph's tail is empty
# (a split at the end of the current paragraph): its trailing run
# carries the boundary formatting. This propagates through a run of
# empty tails (e.g. appending "A\nB\nC" past the last word), so every
# segment keeps the surrounding format instead of only the first.
boundary_runs = current_p.getElementsByTagName("w:r")
fallback_rPr = get_rPr_xml(boundary_runs[-1]) if boundary_runs else ""
new_p, mark_id = self._split_paragraph_at_position(current_p, pos)
member_id = mark_id
if segment:
self._insert_ins_at_paragraph_start(new_p, segment, fallback_rPr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve fallback formatting across empty split segments.

For formatted A\n\nC inserts at paragraph end, the empty middle paragraph has no runs, so the next iteration resets fallback_rPr to "" and C loses formatting. Retain the last non-empty fallback.

Proposed fix
+        fallback_rPr = ""
         for segment in segments:
             boundary_runs = current_p.getElementsByTagName("w:r")
-            fallback_rPr = get_rPr_xml(boundary_runs[-1]) if boundary_runs else ""
+            if boundary_runs:
+                fallback_rPr = get_rPr_xml(boundary_runs[-1])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Formatting to fall back on when the new paragraph's tail is empty
# (a split at the end of the current paragraph): its trailing run
# carries the boundary formatting. This propagates through a run of
# empty tails (e.g. appending "A\nB\nC" past the last word), so every
# segment keeps the surrounding format instead of only the first.
boundary_runs = current_p.getElementsByTagName("w:r")
fallback_rPr = get_rPr_xml(boundary_runs[-1]) if boundary_runs else ""
new_p, mark_id = self._split_paragraph_at_position(current_p, pos)
member_id = mark_id
if segment:
self._insert_ins_at_paragraph_start(new_p, segment, fallback_rPr)
fallback_rPr = ""
for segment in segments:
# Formatting to fall back on when the new paragraph's tail is empty
# (a split at the end of the current paragraph): its trailing run
# carries the boundary formatting. This propagates through a run of
# empty tails (e.g. appending "A\nB\nC" past the last word), so every
# segment keeps the surrounding format instead of only the first.
boundary_runs = current_p.getElementsByTagName("w:r")
if boundary_runs:
fallback_rPr = get_rPr_xml(boundary_runs[-1])
new_p, mark_id = self._split_paragraph_at_position(current_p, pos)
member_id = mark_id
if segment:
self._insert_ins_at_paragraph_start(new_p, segment, fallback_rPr)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docx_editor/track_changes.py` around lines 3022 - 3032, Update the
split-segment loop around _split_paragraph_at_position so fallback_rPr retains
the last non-empty run formatting when the current paragraph has no runs. Only
replace fallback_rPr when boundary_runs contains a run, allowing later non-empty
segments such as “C” to reuse formatting across empty paragraphs while
preserving the existing insertion flow.

@pablospe
pablospe merged commit 7159279 into main Jul 24, 2026
10 checks passed
pablospe added a commit that referenced this pull request Aug 30, 2026
…OADMAP.md #72)

DOCX_SESSION_START_TIMEOUT becomes DOCX_EDITOR_SESSION_START_TIMEOUT,
matching the repo's one existing env var before the name ships. The budget
now rejects inf (an unbounded wait defeats the diagnostic), CI sets 60
instead of 120 and bounds the job at 45 minutes so a runner where every
kernel start times out fails in bounded time. The three fake-kernel tests
that only checked the error text now also prove the stand-in died, the
stop-session kill spy filters on the kernel's pid, and the second-phase
budget is computed at its use so the report cannot total more than the
budget.
@pablospe
pablospe deleted the task/6d54ccbc-newline---tracked-paragraph-sp branch August 30, 2026 19:28
pablospe added a commit that referenced this pull request Aug 30, 2026
* fix: make the session tests robust to CI load (ROADMAP.md #72)

start_session reads DOCX_SESSION_START_TIMEOUT for its budget (explicit
argument wins, 30 s default unchanged) and a start that runs out of budget
now reports which phase timed out, whether the kernel was still alive, the
load average, the connection-file summary and the kernel's stderr. CI sets
the variable to 120.

The session tests replace their six timing sleeps with a handshake on the
kernel's execute_input broadcast, replace the wall-clock stop assertion
with a behavioural one (no signal sent, kernel gone), and add fake-kernel
regression tests for the diagnosed failure shape: connection file present,
control channel never answering.

* fix: prefix the start-budget env var, reject inf, bound the CI job (ROADMAP.md #72)

DOCX_SESSION_START_TIMEOUT becomes DOCX_EDITOR_SESSION_START_TIMEOUT,
matching the repo's one existing env var before the name ships. The budget
now rejects inf (an unbounded wait defeats the diagnostic), CI sets 60
instead of 120 and bounds the job at 45 minutes so a runner where every
kernel start times out fails in bounded time. The three fake-kernel tests
that only checked the error text now also prove the stand-in died, the
stop-session kill spy filters on the kernel's pid, and the second-phase
budget is computed at its use so the report cannot total more than the
budget.
pablospe added a commit that referenced this pull request Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant