Skip to content

[1/3] refactor(painter): consume only ResolvedLayout (SD-2836)#3116

Merged
luccas-harbour merged 13 commits intomainfrom
tadeu/sd-2836-1-consume-resolved-layout
May 5, 2026
Merged

[1/3] refactor(painter): consume only ResolvedLayout (SD-2836)#3116
luccas-harbour merged 13 commits intomainfrom
tadeu/sd-2836-1-consume-resolved-layout

Conversation

@tupizz
Copy link
Copy Markdown
Contributor

@tupizz tupizz commented May 4, 2026

Stack

# PR Branch Status
1 this PR tadeu/sd-2836-1-consume-resolved-layout open
2 #3117 tadeu/sd-2836-2-collapse-legacy-api open
3 #3118 tadeu/sd-2836-3-lock-boundary open

Review in order. PR2 and PR3 depend on this one; merge this first.

What changed

First step of the SD-2836 "dumb painter" refactor. Migrates DomPainter's internal pipeline so every render path consumes ResolvedLayout while keeping the legacy public API working as a compatibility shim.

Painter (packages/layout-engine/painters/dom)

  • paint() body and the six top-level rendering methods now operate on ResolvedLayout / ResolvedPage / ResolvedPaintItem.
  • Page-level helpers (decoration sections, virtualization, snapshot, virtualization pins) migrated to ResolvedPage.
  • currentLayout is typed ResolvedLayout | null.
  • Decoration provider callback signature migrated Page -> ResolvedPage (coupled migration; pulled forward into PR1 because painter internals call the provider).

Contracts (packages/layout-engine/contracts)

  • New run-helpers.ts hosting expandRunsForInlineNewlines and sliceRunsForLine (shared between painter and resolved-layout consumers).
  • resolved-layout.ts: fragment: Fragment back-pointer added to ResolvedFragmentItem, ResolvedTableItem, ResolvedImageItem, ResolvedDrawingItem. ResolvedPage gains optional columns and columnRegions.

Super-editor consumer

  • HeaderFooterSessionManager.rebuildRegions(resolvedLayout), updateDecorationProviders(layout, resolvedLayout), #stripFootnoteReserveFromBottomMargin(margins, page: ResolvedPage | null), #computeExpectedSectionType(kind, page: ResolvedPage, ...) migrated to ResolvedPage.
  • PresentationEditor.#updateDecorationProviders(layout, resolvedLayout) plumbed through.

Docs

  • packages/layout-engine/AGENTS.md "DomPainter is Dumb" section updated.

Why this split

This PR is large but focused: every change here is "consume ResolvedLayout instead of Layout/Fragment/PaintInput" inside the painter and its direct callers. The legacy public surface stays callable, so consumers don't break mid-stack. PR2 then collapses that surface; PR3 locks the boundary.

Verification

  • All package tests pass: painter-dom, pm-adapter, layout-bridge, layout-engine, super-editor.
  • HEAD-vs-origin/main corpus snapshot diff: 0 changed / 407 unchanged (rendering-neutral).

Acceptance criteria

  • DomPainter renders only from ResolvedLayout internally
  • Fragment back-pointer available on resolved items for legacy paths
  • Decoration providers receive ResolvedPage
  • No regression in corpus snapshot diff

Test plan

  • CI green
  • Reviewer confirms no painter method still reads from Layout directly
  • PR2 collapses the legacy surface on top
  • PR3 locks the boundary

@tupizz tupizz requested a review from a team as a code owner May 4, 2026 16:49
@linear
Copy link
Copy Markdown

linear Bot commented May 4, 2026

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 33e0471db3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/layout-engine/painters/dom/src/renderer.ts
@tupizz tupizz force-pushed the tadeu/sd-2836-1-consume-resolved-layout branch from 33e0471 to c5d0b09 Compare May 4, 2026 17:10
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@tupizz tupizz self-assigned this May 4, 2026
@luccas-harbour luccas-harbour self-requested a review May 4, 2026 19:28
Copy link
Copy Markdown
Contributor

@luccas-harbour luccas-harbour left a comment

Choose a reason for hiding this comment

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

hey @tupizz! I left a few inline comments

also here are some test coverage gaps found by AI:

  1. packages/super-editor/src/editors/v1/core/presentation-editor/tests/HeaderFooterSessionManager.test.ts** (file-level — coverage gap)

rebuildRegions is now the load-bearing entry from ResolvedLayout, but the existing tests in this file set headerRegions manually and bypass it. would be worth a focused test calling rebuildRegions with a synthetic ResolvedLayout that exercises footnoteReserved > 0, per-page height variation, and sectionIndex > 0 so the resolved-side migration has direct coverage.

  1. packages/layout-engine/layout-resolved/src/resolveLayout.test.ts** (file-level — coverage gap)

one gap: the page-level columns/columnRegions fields forwarded at resolveLayout.ts:309-310 aren't asserted anywhere and they feed renderColumnSeparators (user-visible). a one-liner asserting they make it onto ResolvedPage would close that.

Comment thread packages/layout-engine/painters/dom/src/renderer.ts Outdated
Copy link
Copy Markdown
Contributor Author

@tupizz tupizz left a comment

Choose a reason for hiding this comment

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

Both coverage gaps addressed in commit bc048e9:

1. rebuildRegions direct tests — added a new describe('rebuildRegions — ResolvedLayout entry') block in HeaderFooterSessionManager.test.ts with three focused tests:

  • shrinks footer height by footnoteReserved and shifts its offset upward (page 1: untouched 36/720; page 2 with footnoteReserved=24: 12/744)
  • honors per-page height variation (3 pages of 792 / 1000 / 1400 → footer offsets 720 / 928 / 1328)
  • propagates sectionIndex from ResolvedPage onto built header/footer regions

2. columns / columnRegions forwarding — added a one-shot test in resolveLayout.test.ts asserting both fields make it onto ResolvedPage from a multi-region page input.

All 22 tests in HeaderFooterSessionManager.test.ts and 113 in resolveLayout.test.ts pass.

Copy link
Copy Markdown
Contributor

@luccas-harbour luccas-harbour left a comment

Choose a reason for hiding this comment

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

LGTM

@luccas-harbour luccas-harbour enabled auto-merge May 5, 2026 14:36
@luccas-harbour luccas-harbour added this pull request to the merge queue May 5, 2026
@caio-pizzol caio-pizzol removed this pull request from the merge queue due to the queue being cleared May 5, 2026
tupizz added 13 commits May 5, 2026 15:01
Move the inline-newline run expander from @superdoc/pm-adapter into
@superdoc/contracts. The function is a pure transformation on Run[]
shapes already defined here; relocating it lets painter-dom consume the
helper without depending on pm-adapter (per the SD-2836 acceptance
criterion: no painter-dom imports from pm-adapter or layout-bridge).

pm-adapter chains its public re-export through contracts, keeping the
import path stable until painter-dom is migrated to consume directly.
Move the line-aware run slicer from @superdoc/layout-bridge into
@superdoc/contracts, alongside expandRunsForInlineNewlines. The function
is a pure transformation on Run/Line shapes already defined here;
relocating it lets painter-dom consume the helper without depending on
layout-bridge (per the SD-2836 acceptance criterion).

layout-bridge chains its public re-export through contracts, keeping
the import path stable until painter-dom is migrated to consume directly.

Also restore a TrackedChangeMeta import in pm-adapter's paragraph.test.ts
that the prior commit dropped; the type is still referenced outside the
migrated describe block.
Switch the painter's two cross-package run-helper imports
(expandRunsForInlineNewlines, sliceRunsForLine) to source from
@superdoc/contracts directly, then drop @superdoc/pm-adapter and
@superdoc/layout-bridge from painter-dom's runtime dependencies.

This closes the painter-dom -> pm-adapter / layout-bridge import edge
called out in the SD-2836 acceptance criteria. Architecture-boundary
guards added in [3/3] of this stack will prevent reintroduction.
Add a `fragment: Fragment` field to ResolvedFragmentItem,
ResolvedTableItem, ResolvedImageItem, and ResolvedDrawingItem, and
populate it from the corresponding resolve* helper. The reference is
shared, not copied: items now carry the same Fragment object that lives
on the source page.

This is the precondition for stopping painter iteration over
`page.fragments`. The next commit drops getResolvedFragmentItem and
switches the painter to iterate ResolvedPage.items, reading the source
fragment via `item.fragment` instead of indexing back into the legacy
fragments array.

Includes focused tests in resolveLayout.test.ts asserting back-pointer
identity for each kind.
…ms (SD-2836)

Replace the three `page.fragments.forEach((fragment, index) => ...)`
loops in renderPage / patchPage / initPage with iterations over
`resolvedItems` (the resolved page's items), reading the source
Fragment via the back-pointer added in the previous commit.

Removes:
- getResolvedFragmentItem (parallel-array index lookup into resolved items)
- direct iteration of `page.fragments` from the painter render path

Updates affected hand-rolled resolved-layout tests to populate the new
required `fragment` back-pointer; the painter now treats items without
a fragment as not-yet-renderable rather than indexing back into the
legacy fragments array.
Switch paint()'s top-level reads from input.sourceLayout to
input.resolvedLayout for: layoutEpoch, page count, and the mounted-page
indices.

The local `layout = input.sourceLayout` binding stays for one more
commit because the four legacy-Layout helpers (beginPaintSnapshot,
fullRender, patchLayout, renderHorizontal, renderBookMode,
renderVirtualized) still take a Layout argument. The next commit
migrates those signatures and removes the binding entirely.
…836)

computeSdtBoundaries and computeBetweenBorderFlags previously took both
the raw `fragments` array and the parallel resolvedItems array. They
now take only resolvedItems and read each fragment via the back-pointer
added in [PR1#4]. Updates all four call sites in renderer.ts plus the
between-borders test fixture.

This eliminates the painter's lookup into `page.fragments` from the
helper-call layer, leaving only the deeper-method Layout dependency
(beginPaintSnapshot, fullRender, patchLayout, renderHorizontal,
renderBookMode, renderVirtualized) to migrate next.
Switch DomPainter's internal state and helpers from raw Layout/Page to
ResolvedLayout/ResolvedPage:

* The six top-level legacy-Layout methods (beginPaintSnapshot,
  fullRender, patchLayout, renderHorizontal, renderBookMode,
  renderVirtualized) take ResolvedLayout. paint() drops the
  `const layout = input.sourceLayout` binding and calls every method
  with input.resolvedLayout.
* The page-level helpers (renderPage, createPageState, patchPage,
  renderDecorationsForPage, renderDecorationSection,
  getDecorationAnchorPageOriginY, renderPageRuler,
  renderColumnSeparators) take ResolvedPage and read width/height/
  margins/numberText/etc. directly. Drops every redundant
  `resolvedPage?: ResolvedPage | null` parameter.
* `currentLayout: Layout | null` -> `ResolvedLayout | null`. Strips
  the `(page.size ?? layout.pageSize)` cascades inside virtualization
  + page-iteration code; uses ResolvedPage.width/.height directly.
* Lifts `columns` and `columnRegions` onto ResolvedPage so the
  column-separator renderer can read them without falling back to
  raw Page.

Couples PageDecorationProvider (planned PR2 work) since the painter
now passes ResolvedPage to the third callback argument:

* `PageDecorationProvider`'s `page?` parameter is `ResolvedPage`.
* `HeaderFooterSessionManager.rebuildRegions` takes ResolvedLayout.
  `updateDecorationProviders(layout, resolvedLayout)` plumbed through
  PresentationEditor.
* The provider closure body and internal helpers
  (`#stripFootnoteReserveFromBottomMargin`, `#computeExpectedSectionType`)
  now operate on ResolvedPage; `page?.size?.h` -> `page?.height`.
* `buildLegacyPaintInput` always calls `resolveLayout` so the legacy
  paint(Layout) path produces ResolvedPages even when no body
  blocks/measures are supplied (preserves page-level metadata).

Skips a "decoration item synthesis" describe block that exercises
`paint(Layout)` + `setData` + `setResolvedLayout`. Those legacy entry
points get deleted in the next commit; the block is being preserved as
.skip so the deletion is visible in diff.
Drops the now-unused @superdoc/layout-bridge and @superdoc/pm-adapter
entries from pnpm-lock.yaml so CI's --frozen-lockfile install matches
package.json.
…ock (SD-2836)

rebuildRegions now iterates resolvedLayout.pages (was layout.pages). The PresentationEditor test mocked resolveLayout to return empty pages, which caused the header/footer region tests to populate zero regions and bookmark navigation to fail. The mock now synthesizes a minimal ResolvedPage per source Layout page so region tests stay green.
- Drop redundant pageSize parameter from createPageState/patchPage; read
  page.width/height directly from ResolvedPage.
- Migrate createDecorationProvider to ResolvedLayout; updateDecorationProviders
  no longer needs the legacy Layout parameter.
- Add direct rebuildRegions(resolvedLayout) tests covering footnoteReserved>0,
  per-page height variation, and sectionIndex>0.
- Assert columns and columnRegions forward through to ResolvedPage in
  resolveLayout tests.
* refactor(painter): collapse legacy API surface (SD-2836)

Make ResolvedLayout the only painter input contract:

* DomPainterInput collapses to `{ resolvedLayout: ResolvedLayout }`.
  sourceLayout, blocks/measures, headerBlocks/Measures,
  footerBlocks/Measures all removed.
* DomPainterOptions: drop blocks/measures.
* DomPainterHandle: drop setData, setResolvedLayout. paint takes only
  DomPainterInput. Drops the `paint(Layout)` overload across painter,
  PresentationPainterAdapter, and (transitively) PresentationEditor's
  paintInput.
* createDomPainter shrinks to a thin pass-through. Removes
  buildLegacyPaintInput, normalizeDomPainterInput, isDomPainterInput,
  wrapProvider, resolveDecorationItems, normalizeOptionalBlockMeasurePair,
  assertRequiredBlockMeasurePair, createEmptyResolvedLayout,
  LegacyDomPainterState, OptionalBlockMeasurePair.
* PageDecorationPayload.items becomes required (the synthesis path
  is gone).

Tests:
* New `_test-utils.ts` exposes a legacy-shaped `createTestPainter`
  that test files import in place of `createDomPainter`. The helper
  builds a real DomPainterInput internally and synthesizes decoration
  items so existing test bodies don't have to be rewritten.
* All 17 painter test files migrated to the helper.
* Two source-anchor tests still failing under investigation;
  remaining work is bounded and tracked.

* fix(painter): chain user onPaintSnapshot in test utility (SD-2836)

createTestPainter was overwriting the user-supplied onPaintSnapshot callback with its own, so tests that relied on the callback (source-anchor tests) saw an undefined snapshot. Chain the user callback after the internal one.

* test(pm-adapter): migrate integration tests to ResolvedLayout (SD-2836)

The three integration tests in pm-adapter were calling painter.paint(layout, mount) with raw Layout. They now resolveLayout() first and call painter.paint({ resolvedLayout }, mount). All 1794 pm-adapter tests pass.

* test(layout-bridge): migrate perf benchmark to ResolvedLayout (SD-2836)

The layout-bridge incremental-pipeline performance benchmark called painter.paint(layout, mount) and painter.setData(...). Both are removed by the API collapse. Migrate to resolveLayout() + DomPainterInput so the benchmark continues to exercise the painter under the new contract.

* chore(deps): declare @superdoc/layout-resolved as devDep where tests use it (SD-2836)

PR1 added test imports of @superdoc/layout-resolved in
pm-adapter/src/integration.test.ts and layout-bridge/test/benchmarks/index.ts
without declaring it as a devDependency. Both packages now resolve the
import locally via pnpm.

* fix(super-editor): honor PageDecorationPayload.items contract (SD-2836)

When resolveAlignedDecorationItems cannot align items 1:1 with fragments
(returns undefined), HeaderFooterSessionManager now bails out with null
instead of emitting a payload with items: undefined, which would violate
the now-required PageDecorationPayload.items contract from PR2.
normalizeDecorationItems narrowed to ResolvedPaintItem[] -> ResolvedPaintItem[].

Also refresh painter-dom README: drop blocks/measures/setData/paint(layout)
examples; document the ResolvedLayout-only paint() and the items-aligned-
with-fragments invariant on PageDecorationPayload.

* [3/3] test(painter): lock ResolvedLayout-only boundary (SD-2836) (#3118)

* test(painter): lock ResolvedLayout-only boundary (SD-2836)

* chore(painter): drop unused imports and skipped legacy tests (SD-2836)

renderer.ts: drop unused Layout, Page, Measure, FlowBlock, ParagraphBorder type imports left over from the migration. index.test.ts: delete the skipped 'decoration item synthesis' describe block (it was protecting the synthesis path that has been removed).

* chore(deps): regenerate lockfile after dropping layout-resolved runtime dep (SD-2836)

Moves @superdoc/layout-resolved to devDependencies in the lockfile to
match package.json, so CI's --frozen-lockfile install matches. Boundary
tests still need it under devDependencies.
After rebasing PR1's painter migration onto main, the new column-separator
content-presence gate added by SD-2452 (a5ff6ed) reads page.fragments
directly. On the new architecture the painter receives a ResolvedPage whose
fragments live as page.items[]. Switch the in-region scan to iterate items
(every ResolvedPaintItem carries x/y).

Also extend the test-only createTestPainter shim to auto-synthesize minimal
ParagraphBlock/ParagraphMeasure entries for any layout fragment whose blockId
isn't covered by the supplied blocks. Tests that only exercise wrapper-level
rendering (column separators, page chrome) can keep using fragAt(...) without
boilerplate matching blocks.
@tupizz tupizz force-pushed the tadeu/sd-2836-1-consume-resolved-layout branch from b22c3a1 to 7ee93e9 Compare May 5, 2026 18:07
@luccas-harbour luccas-harbour merged commit c8d63f4 into main May 5, 2026
68 checks passed
@luccas-harbour luccas-harbour deleted the tadeu/sd-2836-1-consume-resolved-layout branch May 5, 2026 18:49
@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in @superdoc-dev/react v1.2.0-next.99

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in vscode-ext v2.3.0-next.101

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in @superdoc-dev/mcp v0.3.0-next.57

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in superdoc v1.30.0-next.57

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in superdoc-cli v0.8.0-next.74

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in superdoc-sdk v1.8.0-next.57

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants