Skip to content

Commit d5b2dc1

Browse files
committed
docs(painter): state painter-never-measures as hard invariant (SD-2957)
Update painter-dom README and layout-engine AGENTS.md (CLAUDE.md is a symlink) to document the two hard invariants enforced by the architecture boundary tests: no paint-time DOM measurement (Guard E) and resolve stage as the unique source of truth (no resolvedItem?.X ?? fragment.X coalescing). Replaces aspirational 'painter is dumb' language with what's actually ratchet-protected.
1 parent f233e6b commit d5b2dc1

2 files changed

Lines changed: 42 additions & 11 deletions

File tree

packages/layout-engine/AGENTS.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,29 @@ ProseMirror Doc → pm-adapter → FlowBlock[] → layout-engine → Layout[]
2222

2323
## Key Insight: DomPainter is "Dumb"
2424

25-
DomPainter receives a single paint-ready input — `ResolvedLayout` — with
26-
positioned fragments, pre-resolved styles, and `fragment` back-pointers on
27-
every `ResolvedPaintItem` — and renders the result to DOM. It does NOT do
28-
layout logic, measurement, or PM-adapter conversion (that's upstream in
29-
`layout-engine/` / `layout-resolved/` / `pm-adapter/`).
30-
31-
The painter has zero runtime imports from `@superdoc/pm-adapter`,
32-
`@superdoc/layout-bridge`, or `@superdoc/layout-resolved`. Architecture
33-
boundary tests in `tests/src/architecture-boundaries.test.ts` (Guard D)
34-
enforce this.
25+
DomPainter receives a single paint-ready input — `ResolvedLayout` — and
26+
renders the result to DOM. It does NOT do layout logic, measurement, or
27+
PM-adapter conversion (that's upstream in `layout-engine/` /
28+
`layout-resolved/` / `pm-adapter/`).
29+
30+
This is enforced as two hard invariants, not aspirational language:
31+
32+
1. **No upstream package imports.** The painter has zero runtime imports
33+
from `@superdoc/pm-adapter`, `@superdoc/layout-bridge`, or
34+
`@superdoc/layout-resolved`. Guard D in
35+
`tests/src/architecture-boundaries.test.ts` enforces this (SD-2836).
36+
2. **No paint-time DOM measurement.** The painter never reads
37+
`clientHeight`, `offsetWidth`, or `getBoundingClientRect` off rendered
38+
content. Every size and offset comes pre-computed from the resolved
39+
layout. If a required field is missing, the painter throws — it does
40+
not rescue incomplete upstream data by measuring. Scroll/viewport
41+
plumbing and interactive ruler drag handlers are the only exempt
42+
consumers. Guard E enforces this (SD-2957).
43+
44+
The painter also does not coalesce resolved-item fields with the legacy
45+
`fragment` back-pointer (no `resolvedItem?.X ?? fragment.X` patterns); the
46+
resolve stage is the unique source of truth for every field the painter
47+
reads.
3548

3649
## Common Tasks
3750

packages/layout-engine/painters/dom/README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ painter.setProviders(newHeader, newFooter); // optional helper for provider chan
3636
Notes:
3737
- `paint()` takes only `{ resolvedLayout }` — no raw `Layout`, `blocks`, or `measures`.
3838
- Header/footer providers must return a `PageDecorationPayload` whose `items` are
39-
aligned 1:1 with `fragments` (same length, same order).
39+
aligned 1:1 with `fragments` (same length, same order). `offset` is required.
4040
- Virtualization is opt-in and only supported in vertical mode (windowed pages with spacers).
4141
- Renderer is read-only: no editing/input handling is included here.
42+
43+
## Hard invariants
44+
45+
- **The painter never measures the DOM at paint time.** Every size, offset,
46+
and dimension consumed during rendering must come pre-computed on the
47+
`ResolvedLayout` it receives. If a required field is missing, the painter
48+
throws — it does not paper over incomplete upstream data with `clientHeight`,
49+
`offsetWidth`, or element cloning. Scroll/viewport plumbing and interactive
50+
ruler handles are the only allowed DOM-measurement consumers; both are
51+
delineated in `painters/dom/src/ruler/` and the scroll mapping in
52+
`renderer.ts`. Enforced by Guard E in `tests/src/architecture-boundaries.test.ts`
53+
(SD-2957).
54+
- **The resolve stage is the unique source of truth for every field the
55+
painter reads.** The painter does not coalesce resolved-item fields with
56+
the legacy `fragment` back-pointer; if `resolvedItem?.X` is absent, that's
57+
a producer-completeness issue to fix in `layout-resolved`, not at paint
58+
time. Enforced by absence — any future regression to a `?? fragment.X`
59+
fallback fails review.

0 commit comments

Comments
 (0)