import { Fragment, FlowBlock, ResolvedPage, ResolvedPaintItem } from '../../../contracts/src/index.js'; import { FragmentRenderContext, PositionMapping } from './renderer.js'; import { SdtBoundaryOptions } from './sdt/container.js'; import { BetweenBorderInfo } from './paragraph/borders/index.js'; import { PageStyles } from './styles.js'; export type FragmentDomState = { key: string; signature: string; fragment: Fragment; element: HTMLElement; context: FragmentRenderContext; /** * Interior-pm signature captured from the resolved item at render/rebuild * (painter plan P5). Reuse-with-pm-drift is only provably uniform when the * retained and fresh keys are EQUAL; null (older retained state, missing * stamp) fails closed to a rebuild. */ pmInteriorVersion: string | null; /** * Whether the rendered DOM carries any `data-pm-*` attribute (root or * descendant), recorded once at render/rebuild. A fragment with pm-bearing * DOM but NO fragment-level pm span cannot prove position freshness and * must never be reused across pm drift (anchored drawings/textboxes whose * blocks lack attrs.pmStart). */ hasPmDescendants: boolean; }; export type PageDomState = { element: HTMLElement; fragments: FragmentDomState[]; }; /** Fragment-level render work performed by one `patchPage` call. */ export type PatchPageWork = { fragmentsRendered: number; fragmentsReused: number; }; /** * Painter plan §4.6 (dark observability): work performed by the persistent-page * paint path, accumulated across paints until consumed. Fields the path * cannot attribute yet are reported as 0/null, never invented: * `domNodesCreated` stays null until node counting is instrumented. Since P5, * `pagesPositionRemapped` counts pages whose reused DOM had its pm attributes * uniformly shifted in place (uniform fresh-shift, proven by the interior-pm * signature — no transaction mapping on the window path). * * The per-page index arrays are populated ONLY when the painter is created * with `paintWorkAttribution: true` (the perf harness's repaint oracle needs * WHICH pages). Dark by default: counters are O(1) forever, but unconsumed * arrays would grow per paint on the product path. */ export type PaintWorkSummary = { /** Persistent page roots newly created for a committed scaffold. */ persistentPagesCreated: number; /** Persistent page roots reused by index whose exact geometry changed. */ persistentPagesUpdated: number; /** Persistent page roots removed by a generation commit (never by scroll). */ persistentPagesRemoved: number; /** Pages whose content hydrated into an existing persistent root. */ contentHydrated: number; /** Pages whose painter-owned content descendants were removed, root kept. */ contentDehydrated: number; /** Hydrated pages reconciled through the fragment-keyed patch. */ contentPatched: number; /** Hydrated pages left completely untouched. */ contentUntouched: number; /** Hydrated pages whose pm positions were uniformly shifted in place. */ contentRemapped: number; /** Hydrated pages whose header/footer DOM refreshed without body rebuild. */ contentDecorationsRefreshed: number; /** Hydrated pages demoted from reuse because position drift was not uniform. */ contentPmDemoted: number; fragmentsRendered: number; fragmentsReused: number; domNodesCreated: number | null; /** * Per-page attribution for the counters above (painter plan P5 §4.6): the * repaint oracle's subset gates (`rebuilt ⊆ relevance-changed ∩ window`, * `remapped ⊆ full-changed ∩ window`) need to know WHICH pages, not how * many. Accumulated like the counters, drained on consume. */ createdPersistentPageIndices: number[]; removedPersistentPageIndices: number[]; patchedContentPageIndices: number[]; untouchedContentPageIndices: number[]; decorationRefreshedContentPageIndices: number[]; remappedContentPageIndices: number[]; pmDemotedContentPageIndices: number[]; hydratedContentPageIndices: number[]; dehydratedContentPageIndices: number[]; }; export declare function createEmptyPaintWorkSummary(): PaintWorkSummary; /** * Painter plan P3a: reuse key for one exact page slot on the persistent-page * path. Joins everything the painted page DOM depends on for a fixed page * index: the resolve-stage version stamp per item (the product paint-reuse * mechanism), fragment identity + geometry (stamps do not cover geometry), * and, only when body content contains a dynamic field, its page context. * Header/footer providers have a separate decoration-only refresh path, so * their context must not invalidate otherwise reusable body fragments. * * Returns null when any fragment item carries NO resolve stamp — reuse * cannot be proven safe, so the caller must fall back to a fragment-keyed * patch, never to "untouched". Inside that patch, an unstamped fragment is * itself fail-closed: `patchPage` force-rebuilds any fragment whose resolve * signature is empty (two missing stamps must never compare equal). * * Deliberately EXCLUDED: pm/story position attributes — positions are * coordinates, not content, and a keystroke shifts every downstream one. * Since P5 the window path REMAPS instead: a version-key-matched page whose * fragment pm drifted gets an in-place uniform shift when the interior-pm * signature proves the drift uniform (`planWindowPositionRemap`), and demotes * to a real rebuild otherwise (the shared `planPmReuse` decision inside * `patchPage`). The * pass-level paint-equivalence oracle stays RAW-sha (positions kept): remap * is byte-exact, so reused-then-remapped DOM must equal a fresh dense paint * attribute-for-attribute — that exactness is the proof, never normalize it * away at the pass level (only the §7.7 unit tests use the normalized form). */ export declare function persistentPageVersionKey(page: ResolvedPage, totalPages: number, sectionPageCount: number): string | null; /** * The class state `renderPage`/`patchPage` consume, made explicit. The * DomPainter builds one per call (values like `totalPages`, `layoutEpoch`, * and `currentMapping` change between paints); the deep fragment-rendering * call graph stays on the painter and is reached through the function * members. */ export interface PageContentContext { doc: Document; layoutEpoch: number; totalPages: number; currentMapping: PositionMapping | null; changedBlocks: ReadonlySet; /** Record the smallest newly painted subtree for transaction finalization. */ recordChangedRoot?(root: HTMLElement): void; sdtLabelsRendered: Set; getEffectivePageStyles(): PageStyles | undefined; applySemanticPageOverrides(el: HTMLElement): void; getSectionPageCount(page: ResolvedPage): number; renderFragment(fragment: Fragment, context: FragmentRenderContext, sdtBoundary?: SdtBoundaryOptions, betweenInfo?: BetweenBorderInfo, resolvedItem?: ResolvedPaintItem): HTMLElement; renderDecorationsForPage(pageEl: HTMLElement, page: ResolvedPage, pageIndex: number): void; renderColumnSeparators(pageEl: HTMLElement, page: ResolvedPage, pageWidth: number, pageHeight: number): void; updateStoryPositionAttributes(fragmentEl: HTMLElement, resolvedItem: ResolvedPaintItem | undefined): void; updatePositionAttributes(fragmentEl: HTMLElement, mapping: PositionMapping): void; updateFragmentElement(el: HTMLElement, fragment: Fragment, section?: 'body' | 'header' | 'footer', resolvedItem?: ResolvedPaintItem): void; } /** * The subset of `PageContentContext` needed to build a canonical page shell — * the `.superdoc-page` element with exact geometry, chrome, epoch, and layout * boundary stamps, but no content. Persistent scaffold reconciliation builds * shells through this context and hydrates content independently. */ export type PageShellContext = Pick; /** * The exact page box a shell derives from. `ResolvedPage` satisfies it, and * so does a numbers-only scaffold band — the persistent page surface builds * shells without materializing resolved packets. */ export type PageShellGeometry = { width: number; height: number; }; /** Refresh geometry and document-level presentation without replacing a page. */ export declare function refreshPageShell(ctx: PageShellContext, el: HTMLElement, page: PageShellGeometry): void; /** * Canonical `.superdoc-page` shell: the one place a paginated page element is * created. The persistent scaffold mounts it bare and bounded content * reconciliation hydrates the same element in place. */ export declare function renderPageShell(ctx: PageShellContext, page: PageShellGeometry): HTMLElement; /** * Content half of page painting: resolved fragments, decorations (headers, * footers, behind-doc sections), and column separators, appended to an * existing page shell. Persistent hydration goes through exactly this * function, so a hydrated page is byte-equivalent to one painted * by the content path from scratch — same element, same append order. */ export declare function hydratePageContent(ctx: PageContentContext, el: HTMLElement, page: ResolvedPage, pageIndex: number): FragmentDomState[]; export declare function renderPage(ctx: PageContentContext, page: ResolvedPage, pageIndex: number): PageDomState; /** * Inverse of {@link hydratePageContent} (default persistent page geometry * plan, Unit 1): remove the painter-owned content descendants from an * existing page root, leaving root identity, root attributes, page order, * exact geometry, and the document scroll extent untouched. The tracked * fragment list is removed first (authoritative retained state), then any * painter-owned straggler matched by class/attribute — a fragment orphaned * from the retained list must not survive dehydration. */ export declare function dehydratePageContent(el: HTMLElement, state: PageDomState): void; export declare function patchPage(ctx: PageContentContext, state: PageDomState, page: ResolvedPage, pageIndex: number): PatchPageWork; export declare function pageContextSignature(context: FragmentRenderContext): string; export declare function hasPageContextTokenInBlock(block: FlowBlock | undefined): boolean; export declare function needsRebuildForPageContext(currentContext: FragmentRenderContext, nextContext: FragmentRenderContext, resolvedItem: ResolvedPaintItem | undefined): boolean; export declare function shiftFragmentPositionAttributes(fragmentEl: HTMLElement, deltaPm: number): void; /** The resolve stage's interior-pm signature for this item (painter plan P5), null when unstamped. */ export declare function resolvedPmInteriorVersion(resolvedItem: ResolvedPaintItem | undefined): string | null; /** Fragment-level pm span, or null when either bound is missing. */ export declare function fragmentPmSpan(fragment: Fragment): { start: number; end: number; } | null; /** Whether the rendered fragment DOM carries any pm attribute (root or descendant). */ export declare function elementHasPmAttributes(el: HTMLElement): boolean; /** The `pm::@` interior version, split for uniformity checks. */ export declare function pmInteriorParts(version: string | null): { relative: string; base: number | null; } | null; export type PmReuseDecision = { kind: 'clean'; } | { kind: 'shift'; deltaPm: number; } | { kind: 'table-shift'; repeatHeaderDeltaPm: number; bodyDeltaPm: number; } | { kind: 'rebuild'; }; export declare function applyPmReuseDecision(fragmentEl: HTMLElement, decision: PmReuseDecision): void; /** * Painter plan P5 (review fix): THE single soundness decision for reusing a * stamp-equal fragment across pm drift without a transaction mapping — * consumed by both the window remap planner and `patchPage` so the two paths * can never diverge (a planner "demote" lands on a patch that applies the * SAME rule and rebuilds). * * - `clean`: nothing to do (no pm anywhere, or absolutely identical pm). * - `shift`: drift proven UNIFORM — equal span length, equal relative * interior offsets, and the interior base moved by exactly the fragment * delta. Shifting every pm attribute by `deltaPm` is byte-exact. * - `rebuild`: anything unprovable — one-sided pm, span-length change, * interior redistribution (a moved PM node emits no run), a missing * interior stamp, interior drift under an equal span, or pm-bearing DOM * under a fragment with no fragment-level anchor whose interior moved. */ export declare function planPmReuse(current: Pick, freshFragment: Fragment, freshInteriorVersion: string | null): PmReuseDecision; export declare function resolvedPaintCacheSignature(resolvedItem: ResolvedPaintItem | undefined): string; export declare const fragmentKey: (fragment: Fragment) => string; export declare const hasFragmentGeometryChanged: (previous: Fragment, next: Fragment) => boolean; export declare const isNonBodyStoryBlockId: (blockId: string | undefined) => boolean; /** Order-insensitive equality over two SDT label key sets. */ export declare function sdtLabelSetsEqual(a: ReadonlySet, b: ReadonlySet): boolean; export type WindowPositionRemapEntry = { fragmentState: FragmentDomState; freshItem: ResolvedPaintItem & { fragment: Fragment; }; decision: Exclude; }; export type WindowPositionRemapPlan = { kind: 'none' | 'remap' | 'demote'; drifted: WindowPositionRemapEntry[]; }; /** * Painter plan P5: decide whether a version-key-matched page is reusable * untouched (`none`), needs an in-place uniform position remap (`remap` — * resolve stamps are pm-insensitive, so unchanged content legitimately * drifts), or must be demoted to the fragment-keyed patch (`demote`, where * `pmReuseUnsound` forces a REAL rebuild of the offending fragment). * * Fail-closed: one-sided pm, a span-LENGTH change, an interior-pm signature * mismatch (a PM node inserted/moved inside the block emits no run, so * stamps stay equal while interior offsets move), a missing interior key, or * pm-bearing DOM under a fragment with no fragment-level pm all demote — * only a PROVABLY uniform drift is shifted in place. * * Pairing is lockstep by index (review fix): this only runs under versionKey * EQUALITY, and the key is an order-sensitive join of every fragment's * key+geometry+stamp, so equal keys imply identical ordered fragment * sequences — no per-paint key strings or Maps on the steady-state path. */ export declare function planWindowPositionRemap(state: PageDomState, resolvedPage: ResolvedPage): WindowPositionRemapPlan;