Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions extradoc/src/extradoc/mock/table_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,29 @@ def handle_insert_table(

# Step 5: Insert table into content array
inject_content.insert(inject_idx, table_elem)
# A table is never the last element of a body segment, so the API emits a
# trailing carrier paragraph only when nothing (or another table) follows.
# When the step-1 split already left a following paragraph (the common mid-
# document insert), that paragraph IS the carrier; emitting another would
# over-count the footprint by one newline. See
# ``reconcile_v3.lower._batch_insert_size_from_reqs``.
if segment_id is None:
inject_content.insert(
inject_idx + 1,
_build_table_carrier_paragraph(
paragraph_style=_resolve_preceding_paragraph_style(
inject_idx,
inject_content,
),
text_style=inherited_text_style,
),
following = (
inject_content[inject_idx + 1]
if inject_idx + 1 < len(inject_content)
else None
)
if following is None or following.get("table") is not None:
inject_content.insert(
inject_idx + 1,
_build_table_carrier_paragraph(
paragraph_style=_resolve_preceding_paragraph_style(
inject_idx,
inject_content,
),
text_style=inherited_text_style,
),
)

# No index shifting — reindex handles it
return {}
Expand Down
31 changes: 21 additions & 10 deletions extradoc/src/extradoc/reconcile_v3/lower.py
Original file line number Diff line number Diff line change
Expand Up @@ -1686,15 +1686,25 @@ def _final_doc_size_from_reqs(reqs: list[Request]) -> int:

def _batch_insert_size_from_reqs(reqs: list[Request]) -> int:
"""Compute the net UTF-16 size added to the story segment by this element's
requests, measured as the sum of each insert request's own contribution.

Differs from ``_final_doc_size_from_reqs`` in how ``insertTable`` is
counted: here we count only the table skeleton as the API delta
(``1 + rows * (1 + cols * 2)``), matching the request-level accounting
used by the matched-element post_insert_shift. We do NOT add the
pre-paragraph \\n or trailing carrier paragraph here — those are either
absorbed into the existing paragraph structure or emitted as separate
``insertText`` requests already counted above.
requests — i.e. how far each insert shifts the content that follows it.

This value feeds ``post_insert_shift`` for matched-element updates, so it
MUST equal the true API footprint of the requests. An ``insertTable`` shifts
subsequent content by the table span PLUS exactly one ``\\n`` — never two:

* table span → ``2 + rows * (1 + cols * 2)``
(table opener 1 + terminal 1 + rows*(row opener 1 + cols*cell 2))
* one ``\\n`` → 1
giving ``3 + rows*(1 + cols*2)``, which matches the per-request accounting
in ``_final_doc_size_from_reqs`` (``2 + rows*(1+cols*2) + 1``).

The single ``\\n`` is the pre-split newline when the table is inserted mid-
document (the following existing paragraph acts as the table's carrier), or
the trailing carrier newline when it is inserted at end-of-segment — never
both. The earlier ``4 + rows*(1+cols*2)`` over-counted the mid-document case
by 1, sliding every later matched-update op one char off. The offline mock
hid this because ``mock/table_ops`` also emitted both newlines; only a real
SUGGEST push + accepted-text verify exposes the disagreement.
"""
total = 0
for req in reqs:
Expand All @@ -1704,7 +1714,8 @@ def _batch_insert_size_from_reqs(reqs: list[Request]) -> int:
it = req.insert_table
rows = it.rows or 0
cols = it.columns or 0
total += 1 + rows * (1 + cols * 2)
# table span (2 + rows*(1+cols*2)) + exactly one \n
total += 3 + rows * (1 + cols * 2)
elif req.insert_page_break is not None:
total += 2
elif req.insert_section_break is not None:
Expand Down
10 changes: 8 additions & 2 deletions extradoc/tests/reconcile_v3/test_story_content_update_bug.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,14 @@ def _simulate_requests_and_find_stale_deletes(
idx = ins.location.index
rows = ins.rows or 0
cols = ins.columns or 0
# Table skeleton: 1 + rows * (1 + cols * 2) UTF-16 units.
delta = 1 + rows * (1 + cols * 2)
# Full body-segment insertTable footprint — how far it shifts
# subsequent content — matching the real API (controlled probe)
# and ``_batch_insert_size_from_reqs``: table span
# (2 + rows*(1+cols*2)) + exactly ONE \n. The API adds a single
# newline (the pre-split \n mid-doc, or the trailing carrier at
# end-of-segment) — never both, so the shift is span+1, not
# span+2.
delta = 3 + rows * (1 + cols * 2)
if idx > len(origins):
violations.append(
f"req[{req_idx}] insertTable: index={idx} > "
Expand Down