fix(table): add generic merged-cell reconciliation#3731
Conversation
|
✅ DCO Check Passed Thanks @dsolankii, all your commits are properly signed off. 🎉 |
Merge Protections🔴 1 of 2 protections blocking · waiting on 👀 reviews
🔴 Require two reviewer for test updatesWaiting for
This rule is failing.When test data is updated, we require two reviewers
Show 1 satisfied protection🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
|
f6c6070 to
b66f882
Compare
|
This PR adds one regression PDF fixture for #3698:
The fixture is required because the bug reproduces through the full TableFormer PDF conversion path across V1/V2 and I understand this triggers the repository’s test-data protection requiring two approvals. |
geoHeil
left a comment
There was a problem hiding this comment.
Requesting changes. The PR is much broader than the stated fix, has a reproducible lint blocker, and the acceptance gates still allow table metadata/text corruption cases that the PR says it protects against.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Signed-off-by: dsolankii <soldarshu@gmail.com>
b66f882 to
e3b83bb
Compare
|
Thanks @geoHeil for the detailed review. I pushed an update addressing the blocking points. Changes made
Local validationI also tested a separate downloaded manual PDF smoke case outside the pytest fixture: Please let me know if you want any further cleanup or a different split of the modules. |
geoHeil
left a comment
There was a problem hiding this comment.
Re-reviewed current head e3b83bb. The previous findings are addressed: the line-cap hook now passes, the dead candidate pipeline was removed, the PDF regression test is marked ml_pdf_model, and the two acceptance regressions now have focused tests. CI is green too.
Still requesting changes for one new blocker: the current same-shape acceptance guard can reject a legitimate same-row-count overspan repair, so part of the V1 path can stay a no-op while the lower-level fallback test passes.
|
One meta request before the next push: please do a few more self-review iterations before handing this back. This still reads like a broad AI-generated patch: lots of new modules, lots of heuristics, and tests that sometimes prove the helper layer but not the production path.\n\nIdeally run 3-5 review/refinement passes with your assistant or GitHub Copilot review and explicitly ask it to find cases where the new code is wired but ineffective, too broad for #3698, or only tested one layer below the real TableFormer path. Then trim what is not needed for the issue and add top-level regression coverage for the behavior that is supposed to ship. |
|
Thanks @geoHeil — I did several additional self-review passes before this push, specifically looking for cases where the new code was wired but ineffective, too broad for #3698, or tested below the production path. The main issue I found was that same-shape span/topology repairs could be generated but then rejected by the acceptance layer as I updated this narrowly:
I also added top-level
I also removed the confirmed test-only/dead Validation runuv run pytest -q \
tests/test_table_structure_reconciler.py \
tests/test_table_structure_reconciler_top_level.py \
tests/test_table_topology.py \
tests/test_table_structure_acceptance.py \
tests/test_table_structure_candidate_selection.py
uv run pytest -q tests/test_table_structure_3698_regression.py -m ml_pdf_model
uv run pytest -vv tests/test_table_structure_3698_regression.py -m ml_pdf_model
uv run prek run --files $(git diff --cached --name-only --diff-filter=ACMRT)ResultsI also re-ran the real #3698 PDF regression fixture, |
I, dsolankii <soldarshu@gmail.com>, hereby add my Signed-off-by to this commit: 39dbb56 Signed-off-by: dsolankii <soldarshu@gmail.com>
geoHeil
left a comment
There was a problem hiding this comment.
Re-reviewed current head b3b9656. The previous top-level no-op blocker is addressed: the V1 production-flag test now proves the same-row-count overspan repair is selected, and my focused local checks pass (54 passed plus changed-file prek).
Still requesting changes for one remaining safety hole in the new escape hatch: it allows any same-shape text-slot change once span topology changes, so the old text-swap protection is only preserved for identical-span swaps.
|
|
||
| accepted_candidates = [] | ||
| for candidate in structure_candidates: | ||
| allow_same_shape_text_slot_change = ( |
There was a problem hiding this comment.
This escape hatch is still too broad. The new unsafe-swap test only covers identical spans, but this condition disables the text-slot regression guard for any whitelisted candidate whose span topology changes. That means a same-shape candidate can preserve the token multiset, change spans, and still move text to different logical slots while being accepted.
Minimal shape of the failure: baseline has A at (0,0)-(1,1) and B at (0,1)-(1,2) in a 2x2 grid; candidate has B spanning (0,0)-(1,2) and A at (1,0)-(2,1). Both topologies are valid, token counts are preserved, span topology changed, and with allow_same_shape_text_slot_change=True the acceptance report is accepted=True. That is still a text-location corruption.
Please tighten this to allow only the specific slot movements the span repair is supposed to perform, or add a top-level regression where a whitelisted span-changing candidate also swaps/moves unrelated text and is rejected.
Signed-off-by: dsolankii <soldarshu@gmail.com>
|
Thanks @geoHeil — you were right that the same-shape escape hatch was still too broad. I tightened this on the acceptance side so A whitelisted span/topology repair now still has to prove that baseline non-empty full-cell text is uniquely matchable and only changed by a nested span repair. Duplicate-text movement and token-preserving merge/split corruption are rejected conservatively. I also added top-level regressions for:
The existing positives still pass:
I also marked PDFs as binary in Validationuv run prek run --files \
.gitattributes \
docling/models/stages/table_structure/table_structure_acceptance.py \
tests/test_table_structure_reconciler_top_level.py
git diff --check main...HEAD
uv run pytest -q tests/test_table_structure_reconciler_top_level.py
uv run pytest -q \
tests/test_table_structure_reconciler.py \
tests/test_table_structure_reconciler_top_level.py \
tests/test_table_structure_acceptance.py \
tests/test_table_structure_candidate_selection.py \
tests/test_table_topology.py
uv run pytest -q tests/test_table_structure_3698_regression.py -m ml_pdf_model
uv run pytest -vv tests/test_table_structure_3698_regression.py -m ml_pdf_modelResults |
geoHeil
left a comment
There was a problem hiding this comment.
Re-reviewed current head c33338c. CI is green, prek passes locally, and the focused table tests pass locally (57 passed). The previous span-changing text-swap hole is mostly addressed, but there is still one real blocker: the same-shape allowance can accept invented candidate text.
Brutally honest slop read: this is still far too much mechanism for one regression PDF. The latest correctness issue comes from patching guard on top of guard instead of having one crisp invariant: same-shape repair may only preserve existing text ownership or perform a provable span-only expansion/shrink for the same cell text, with no extra candidate text. After this blocker, I would still shrink the arbitrary candidate scorer and the duplicated span-local tuple helpers; they read like generated glue, not a stable table API.
Signed-off-by: dsolankii <soldarshu@gmail.com>
|
Thanks @geoHeil — you were right that the previous text-safety guard was still too permissive. I replaced the global source-token allowance with a local text-ownership check. A candidate can no longer add text merely because that token appears somewhere in the source cells. Candidate-only text now has to be locally supported by source geometry for that candidate cell; otherwise the candidate is rejected and the incumbent is preserved. This keeps reconciliation conservative:
I also cleaned up the patch shape:
I investigated replacing the candidate scorer with deterministic ordering, but that changed the V2 regression result from 10 rows to 9 rows. I restored the scorer and replaced the inline magic values with named constants instead, so this patch does not broaden behavior there. Validation: uv run pytest -q tests/test_table_structure_acceptance.py
# 12 passed
uv run pytest -q tests/test_table_structure_reconciler_top_level.py
# 5 passed
uv run pytest -q \
tests/test_table_structure_reconciler.py \
tests/test_table_structure_reconciler_top_level.py \
tests/test_table_structure_acceptance.py \
tests/test_table_structure_candidate_selection.py \
tests/test_table_topology.py
# 60 passed
uv run pytest -q tests/test_table_structure_3698_regression.py -m ml_pdf_model
# 4 passed
uv run pytest -vv tests/test_table_structure_3698_regression.py -m ml_pdf_model
# 4 passed, covering V1/V2 and do_cell_matching true/false
uv run prek run --files ...
# passed
git diff --check
# passed
make validate
# passed |
Issue resolved by this Pull Request:
Resolves #3698
Description
This PR fixes a TableFormer merged-cell reconstruction issue where merged rows and columns could be mishandled in both TableFormer V1 and TableFormer V2, regardless of whether
do_cell_matchingis enabled or disabled.The implementation is intentionally conservative. It validates candidate table structures before accepting them and preserves the incumbent output when a repair would introduce invalid topology, text loss, unsafe metadata changes, or same-shape text corruption.
What this PR changes
1. Adds topology validation for table structures
Adds reusable topology validation for table cells.
The validation guards against:
Relevant files:
docling/models/stages/table_structure/table_topology.pytests/test_table_topology.py2. Adds conservative table-structure reconciliation
Adds a production-used reconciliation path for merged-cell repair.
The reconciler now handles:
The large reconciler was split into focused modules so the repo max-lines hook passes.
Relevant files:
docling/models/stages/table_structure/table_structure_reconciler.pydocling/models/stages/table_structure/table_structure_columns.pydocling/models/stages/table_structure/table_structure_row_reassignment.pydocling/models/stages/table_structure/table_structure_row_boundary.pydocling/models/stages/table_structure/table_structure_row_spans.pydocling/models/stages/table_structure/table_structure_reconciler_common.py3. Adds protected structure selection and acceptance gates
Adds final acceptance gates before a repaired table can replace the incumbent table.
The acceptance layer rejects candidates that introduce:
This protects against cases such as:
column_header/row_headermetadataA | B -> B | ARelevant files:
docling/models/stages/table_structure/table_structure_acceptance.pydocling/models/stages/table_structure/table_structure_candidate_selection.pytests/test_table_structure_acceptance.pytests/test_table_structure_candidate_selection.py4. Wires reconciliation into TableFormer V1 and V2
The reconciliation path is wired into both TableFormer V1 and TableFormer V2.
Relevant files:
docling/models/stages/table_structure/table_structure_model.pydocling/models/stages/table_structure/table_structure_model_v2.pyWhy this is generic
This PR does not hardcode the reported PDF or its content.
It does not rely on:
The repair logic is based on:
Tests added
Regression fixture
tests/data/pdf/merged_span_detectable_workflow_test.pdfRegression tests for #3698
tests/test_table_structure_3698_regression.pyThis test is marked with:
It covers:
do_cell_matching=Truedo_cell_matching=FalseUnit and behavior tests
tests/test_table_topology.pytests/test_table_structure_reconciler.pytests/test_table_structure_acceptance.pytests/test_table_structure_candidate_selection.pyLocal validation
Manual downloaded PDF smoke test
Notes for reviewers
This PR keeps reconciliation conservative.
If a candidate table structure is unsafe, loses text, introduces invalid topology, drops same-shape header metadata, swaps same-shape cell text, or does not clearly improve over the incumbent, the original model output is preserved.