Operand

do no harem.

gram: docs

> ./openspec/changes/engine-spine-tier2/specs/engine-layout-run/spec.md

## ADDED Requirements
### Requirement: Shared layout pass engine.run(state)
The engine SHALL expose `run(state)` implementing the 6-step layout pass: PM doc → flow blocks → measure → header/footer resolve → margin extension → `layoutDocument` (with two-pass footnote stabilization when footnotes exist) → `renderPages`. The pass SHALL be lifted from React's superset implementation, with column/per-block-width support, scroll-anchor restore, the `painter:painted` signal, and the full `renderPages` option set (including `resolvedCommentIds`, `pageBorders`, header/footer distances). Adapter-specific outputs SHALL be delivered through `EngineHost` hooks (`onLayout`, `onPainted`, `onAnchorPositions`, `onScrollRestore`, `onTotalPages`), each optional so an adapter opts into only what it renders.
#### Scenario: Identical layout for the same state
- **WHEN** `run(state)` executes for a given EditorState in either adapter
- **THEN** it produces the same `Layout`, blocks, and measures the prior adapter pipeline produced, and paints the same pages DOM
#### Scenario: Vue reaches React layout parity
- **WHEN** the Vue adapter adopts `engine.run`
- **THEN** Vue gains column layout, scroll-anchor restore, the `painter:painted` event, and the previously-missing render options — with no regression to the existing Vue layout output for documents that use none of those features
#### Scenario: Optional output hooks
- **WHEN** an adapter leaves `onAnchorPositions` / `onScrollRestore` undefined
- **THEN** `run` skips those outputs without error (the adapter simply doesn't render them)
#### Scenario: Footnote two-pass preserved
- **WHEN** the document contains footnotes
- **THEN** `run` performs the initial layout, builds the footnote content map, and stabilizes — matching the prior convergence behavior
### Requirement: Lifecycle timing-equivalence
The engine SHALL emit its outputs via host hooks without dictating _when_ the adapter acts on them. The React host SHALL act on `onScrollRestore`, `onLayout`, and `onPainted` at the same lifecycle point as before the lift (post-commit `useLayoutEffect`, the `painter:painted` event), not synchronously inside `run`. Scroll-anchor restore and selection-overlay positioning SHALL be observably unchanged across a relayout.
#### Scenario: Scroll anchor survives relayout unchanged
- **WHEN** the user has scrolled and a relayout is triggered (e.g. an edit above the viewport)
- **THEN** the scroll position is restored to the same anchor it would have been before the engine lift
#### Scenario: Selection overlay timing preserved
- **WHEN** a transaction triggers a relayout
- **THEN** the selection overlay / caret repaints at the correct position with no visible jump, matching pre-lift timing