import { NonFlowingPageRelativeAnchorDependencyProof, PageCheckpointDependencyClass, FlowBlock, Layout, LayoutBlockResumeCheckpoint, Measure, Page, HeaderFooterLayout, ColumnLayout } from '../../contracts/src/index.js'; import { FontMeasureContext } from '../../../shared/font-system/src/index.js'; import { LayoutOptions, HeaderFooterConstraints, LayoutExecutionCheckpoint } from '../../layout-engine/src/index.js'; import { computeDirtyRegions } from './diff.js'; import { MeasureCache } from './cache.js'; import { HeaderFooterBatch } from './layoutHeaderFooter.js'; export type HeaderFooterMeasureFn = (block: FlowBlock, constraints: { maxWidth: number; maxHeight: number; }) => Promise; export type HeaderFooterLayoutResult = { kind: 'header' | 'footer'; type: keyof HeaderFooterBatch; layout: HeaderFooterLayout; blocks: FlowBlock[]; measures: Measure[]; /** Effective layout width when table grid widths exceed section content width (SD-1837). */ effectiveWidth?: number; }; /** * SD-3432: the footnote reserve fixed point of a completed layout run, used to * warm-start the next run's convergence loop. The seed is ONLY a starting * vector — every run re-validates it through the full convergence machinery * (pass-1 relayout + plan stability + grow/tighten + widow + trials), so a * stale or wrong seed costs extra passes, never correctness. Captured only * when the run ended on an EXACT fixed point (plan === applied reserves), so * an unchanged document warm-validates in a single relayout. * * Guards carried with the vector (fontSignature / measurement constraints) * exist purely to discard pathological starting vectors after zoom or font * changes; they carry no document identity (no footnote ids, no content * hashes — see the SD-3418 post-mortem for why identity keys are forbidden). */ export type FootnoteReserveSeed = { reserves: number[]; /** Sparse page indexes that held a reserve, ledger, or injected note slice. */ notePageIndexes?: number[]; separatorSpacingBefore: number | undefined; fontSignature: string; measurementWidth: number; measurementHeight: number; /** Exact note measurement width retained with unchanged section geometry. */ footnoteMeasurementWidth?: number; /** Section-column inputs used to derive the retained note measurement width. */ sectionColumnsByIndex?: Map; /** Exact note block objects retained by the host's authoritative note-bundle proof. */ noteBlocksByBlockId?: Map; /** Measures paired with `noteBlocksByBlockId`; object identity is revalidated before reuse. */ noteMeasuresByBlockId?: Map; noteBodyHeightById?: Map; noteFirstLineHeightById?: Map; /** * Bridge-issued proof that every reference was assigned on the completed * layout for one host-issued reference-topology revision. A warm local pass * may reuse it only when the exact topology revision is unchanged and every * reference page is outside the relaid interval. */ footnoteAssignment?: { referenceTopologyRevision: string; referenceCount: number; noteIdCount: number; assignedNoteIdCount: number; referencePageIndexes: number[]; }; }; export type IncrementalLayoutResult = { layout: Layout; /** Pass-owned block plane after derived layout annotations are attached. */ blocks: FlowBlock[]; measures: Measure[]; dirty: ReturnType; headers?: HeaderFooterLayoutResult[]; footers?: HeaderFooterLayoutResult[]; /** * Extra blocks/measures that should be added to the painter's lookup table. * Used for rendering non-body fragments injected into the layout (e.g., footnotes). */ extraBlocks?: FlowBlock[]; extraMeasures?: Measure[]; /** * SD-3432: next-run warm-start seed for the footnote convergence loop. * Null when this run did not end on an exact footnote fixed point (or laid * out no footnotes) — the next run then starts cold. */ footnoteReserveSeed?: FootnoteReserveSeed | null; /** Canonical pre-layout furniture heights that can affect body margins. */ headerFooterGeometryFingerprint: string; /** Immutable input-owned warm seed; consumers must revalidate every key before reuse. */ headerFooterGeometrySeed: HeaderFooterGeometrySeed | null; layoutReuse?: IncrementalLayoutReuseSummary; measureReuse?: { mode: 'full-scan' | 'proved-dirty-only' | 'body-stable'; blocksMeasured: number; measuresAdopted: number; reason: string; }; /** * Per-call bridge-owned timing. This is returned with the canonical layout * result so callers do not need to read the process-global metrics collector. * Substage fields are non-overlapping for the top-level reconciliation: * `measureTotalMs` includes cache lookup and actual measurement details, * and `pageTokenTotalMs` includes token remeasure/relayout details. */ bridgeTiming: IncrementalLayoutBridgeTiming; }; export type IncrementalLayoutBridgeTiming = { totalMs: number; inputPreparationMs: number; measureTotalMs: number; /** Union wall time spent inside caller-owned measure callbacks across body and furniture. */ measureCallbackWallMs: number; measureCacheLookupMs: number; /** Body-measure cache insertion wall time, including content-key composition. */ measureCacheWriteMs: number; /** Previous-measure content-adoption lookup/build wall time. */ measureContentAdoptionMs: number; measureActualMs: number; headerFooterPreLayoutMs: number; headerPreLayoutMs: number; footerPreLayoutMs: number; warmStartPreparationMs: number; /** Wall time inside the initial body `layoutDocument` invocation only. */ layoutDocumentMs: number; /** Initial body-layout wrapper work outside `layoutDocument` (proof, slice, convergence, splice). */ layoutReuseOrchestrationMs: number; /** Initial pagination invocation including bridge reuse orchestration. */ paginationInitialMs: number; /** Body page-token convergence pagination only. */ paginationPageTokenMs: number; /** Footnote convergence pagination only; overlaps `footnoteMs`. */ paginationFootnoteMs: number; /** All initial, page-token, and footnote pagination invocation wall time. */ paginationTotalMs: number; paginationMs: number; pageTokenSetupMs: number; pageTokenTotalMs: number; pageTokenRemeasureMs: number; pageTokenRelayoutMs: number; footnoteMs: number; numberingMs: number; finalHeaderFooterMs: number; layoutExposureMs: number; unattributedMs: number; counters: { blocksRead: number; blocksByKind?: Record; bodyBlocksMeasuredByKind: Record; cacheHits: number; cacheMisses: number; bodyMeasureCacheReads: number; bodyMeasureCacheWrites: number; bodyMeasureCacheKeyComputations: number; measureContentSignatureComputations: number; fontSignaturePresent: number; fontSignatureChanged: number; measuresAdopted: number; pagesPaginated: number | null; pagesSplicedByReuse: number; paginationPasses: number; pageTokenRelayouts: number; headerFooterPreLayoutReuses: number; headerFooterPreLayoutBodyBlocksEnumerated: number; headerFooterPreLayoutSectionsEnumerated: number; footnoteRelayouts: number; footnoteReserveRelayouts: number; footnoteGrowRelayouts: number; footnoteTightenRelayouts: number; footnotePreferredRelayouts: number; footnoteWidowRelayouts: number; footnoteRevertRelayouts: number; footnoteOtherRelayouts: number; footnoteAssignmentReferencesRead: number; footnoteAssignmentReferencesReused: number; footnoteAssignmentPagesIndexed: number; footnoteAssignmentFragmentsIndexed: number; footnoteRevertSnapshotsRestored: number; footnotePreferredDuplicateTargetsSkipped: number; footnoteReservePassReuseAttempts: number; footnoteReservePassReuseHits: number; footnotePreferredTrialsDeferred: number; footnotePreferredUnimprovableTargetsSkipped: number; }; }; type HeaderFooterGeometryPlane = { headerContentHeights?: Partial>; footerContentHeights?: Partial>; headerContentHeightsByRId?: ReadonlyMap; headerContentHeightsBySectionRef?: ReadonlyMap; footerContentHeightsByRId?: ReadonlyMap; footerContentHeightsBySectionRef?: ReadonlyMap; }; export type HeaderFooterGeometrySeed = { readonly version: 2; readonly ownerFingerprint: string; readonly fontSignature: string; readonly pageCountFieldsExact: boolean; readonly constraintsFingerprint: string; readonly sectionMetadataFingerprint: string; readonly sectionMetadataHasChapterNumbering: false; readonly geometryFingerprint: string; readonly requiredHeaderVariants: readonly ('default' | 'first' | 'even' | 'odd')[]; readonly requiredFooterVariants: readonly ('default' | 'first' | 'even' | 'odd')[]; readonly requiredHeaderRelationshipIds: readonly string[]; readonly requiredFooterRelationshipIds: readonly string[]; readonly geometry: HeaderFooterGeometryPlane; }; /** * Structural discriminator for what happened to the document tail (SD-3772 * D5). Consumers branch on THIS, never on the diagnostic `reason` string: * - `none`: no retained tail exists (full recompute). * - `adopted-source-tail`: a convergence page was proved and the source tail * was adopted; `tailAdoption` is non-null and range-valid. * - `relaid-to-document-end`: the bounded slice reached the exact end of the * document and every terminal page was freshly paginated; `tailAdoption` * is null. Every other combination fails closed before publication. */ export type IncrementalLayoutTailDisposition = 'none' | 'adopted-source-tail' | 'relaid-to-document-end'; export type IncrementalLayoutReuseSummary = { mode: 'full' | 'prefix-resume' | 'tail-splice'; /** Diagnostic label only; never drives product behavior (SD-3772 D5). */ reason: string; tailDisposition: IncrementalLayoutTailDisposition; checkpointPageIndex: number | null; /** Last prior page that can be affected by the dirty content/dependencies. */ affectedFrontierPageIndex: number | null; /** Last source-generation page covered by the dirty content. */ sourceAffectedFrontierPageIndex: number | null; convergencePageIndex: number | null; /** Source-generation page adopted at convergence (may differ after page-count shifts). */ sourceConvergencePageIndex: number | null; pagesPaginated: number | null; pagesSplicedByReuse: number; /** * Proof for a retained tail. Pages in this interval remain byte-for-byte * retained; consumers apply the position transforms only when a page enters * the active paint window. */ tailAdoption: IncrementalLayoutTailAdoption | null; }; /** * Exact dirty-measure proof that is useful even when pagination reuse is * independently vetoed. Keeping it separate prevents a global layout * dependency from forcing an O(document) measurement-preparation scan. */ export type IncrementalMeasureReuseProof = Pick; export type LayoutPositionTransform = { atChar: number; delta: number; }; export type IncrementalSectionPageNumberTransform = { /** Section whose retained pages need rebasing before its next boundary. */ sectionIndex: number; /** Difference between target and source section-relative page positions. */ delta: number; }; export type IncrementalDisplayPageNumberTransform = { /** First section whose retained display-page values inherit the local page delta. */ startSectionIndex: number; /** First later section with an explicit numbering restart. */ endSectionIndexExclusive: number; delta: number; }; export type IncrementalLayoutTailAdoption = { startPageIndex: number; endPageIndexExclusive: number; sourcePageStartIndex: number; sourcePageEndIndexExclusive: number; pageIndexDelta: number; sectionPageNumberTransform: IncrementalSectionPageNumberTransform | null; displayPageNumberTransform?: IncrementalDisplayPageNumberTransform | null; /** Exact proof that PAGEREF page/numbering locations are unchanged in the adopted tail. */ pageReferenceLocationsStable: boolean; sourceLayoutEpoch: number | null; positionTransforms: readonly LayoutPositionTransform[]; /** Lazy old->current block-id rekeys for an ordinal-changing structural splice. */ blockIdRewrites?: ReadonlyMap | null; }; type IncrementalPaginationProofBase = { blockIdsUnchanged: true; blockIdsUnique: true; renderInputsUnchanged: true; /** The retained dependency scan proved that no body or furniture PAGE_REF token exists. */ pageReferencesAbsent: boolean; /** The retained dependency scan proved that no live REF/NOTEREF/STYLEREF field exists. */ crossReferencesAbsent?: boolean; pageReferenceDependencyClosure?: { referenceBlockIds: readonly string[]; targetBookmarkIds: readonly string[]; }; }; export type IncrementalPaginationProof = IncrementalPaginationProofBase & ({ profile: 'single-section-local-text'; globalDependenciesAbsent: true; globalDependenciesFencedByDocumentStart?: never; } | { /** Stable dependency-rich documents must replay from a page-zero checkpoint. */ profile: 'document-start-local-text'; globalDependenciesAbsent: false; globalDependenciesFencedByDocumentStart: true; multiColumnSectionsProvedNonBalanceable: boolean; /** Last balanceable section end; convergence is fenced after its retained page. */ balanceableSectionsBoundaryBlockId?: string; vAlignSectionsBoundaryBlockId?: string; vAlignSectionsReplayThroughBoundary?: true; pageRelativeAnchorsScopedBoundary?: { blockId: string; side: 'anchors-before-dirty' | 'anchors-after-dirty'; }; } | { /** Stable dependency-rich documents may replay from an engine-seeded page checkpoint. */ profile: 'page-checkpoint-local-text'; globalDependenciesAbsent: false; globalDependenciesFencedByPageCheckpoint: true; admittedDependencyClasses: readonly PageCheckpointDependencyClass[]; nonFlowingPageRelativeAnchorDependency?: NonFlowingPageRelativeAnchorDependencyProof; localKeepDependencyClosure?: { checkpointPageIndex: number; checkpointBlockId: string | null; predecessorBlockId: string | null; }; /** * SD-3772 D1: true means the host proved (via the shared * `hasGenuinelyUnequalExplicitColumnWidths` predicate) that no * potentially balanceable multi-column section exists anywhere in the * retained document. Balancing is a post-pagination finalizer that a * mid-section checkpoint cannot seed. False is admissible only * together with `balanceableSectionsBoundaryBlockId`, the last * page-bearing block at or before the final balanceable section end. * The bridge requires a document-start replay, includes that section * finalizer, and refuses convergence until a stable page strictly * after the retained boundary. Anything else takes the canonical full * layout. */ multiColumnSectionsProvedNonBalanceable: boolean; balanceableSectionsBoundaryBlockId?: string; /** Last vertically aligned section page; validated before reuse. */ vAlignSectionsBoundaryBlockId?: string; /** Replay page zero through the aligned boundary before convergence. */ vAlignSectionsReplayThroughBoundary?: true; /** * Page-relative body anchors exist WITHOUT the validated non-flowing * inventory proof (`admittedDependencyClasses` must then not contain * `non-flowing-page-relative-body-anchors`). The host proved every * dirty ordinal strictly on `side` of every such anchor and names the * page-extremal anchor block; the bridge itself verifies the page * separation against the retained layout and requires the * document-start checkpoint, so the anchor pages stay either in the * replayed-from-start window (re-laid with cold-run preflight inputs) * or in the start-key-proved adopted tail. Anything else fails * validation and takes the canonical full layout. */ pageRelativeAnchorsScopedBoundary?: { blockId: string; side: 'anchors-before-dirty' | 'anchors-after-dirty'; }; }); export type IncrementalLayoutReuseOptions = { previousLayout: Layout | null; /** Epoch that produced every retained page/index/key in this reuse packet. */ retainedMetadataSourceLayoutEpoch?: number | null; previousPageStartKeys: readonly string[] | null; previousBlockPageIndex: Map | null; maxRelaidPages?: number; requireDocumentStartCheckpoint?: boolean; allowBlockIdChurn?: boolean; pmShift?: LayoutPositionTransform | null; /** Exact dirty identities from the commit envelope when already available. */ dirtyBlockIds?: readonly string[]; /** Exact retained block index, required to align a structural ±1 measure plane. */ previousBlockIndexById?: ReadonlyMap | null; /** Optional retained ordinal index, avoiding a whole-array checkpoint lookup. */ currentBlockIndexById?: ReadonlyMap | null; /** Inverse identity proof for ordinal-scoped ids shifted by a structural edit. */ blockIdRewrites?: { previousToCurrent: ReadonlyMap; currentToPrevious: ReadonlyMap; } | null; /** Retained dependency proof; avoids rescanning every block on a warm edit. */ dependencyProof?: IncrementalPaginationProof | null; /** Exact retained dirty analysis; bypasses computeDirtyRegions on a proved warm edit. */ provedDirtyRegion?: ReturnType | null; /** Cold-observed exact section constraints for each dirty block. */ provedDirtyMeasureConstraints?: ReadonlyMap | null; /** Retained key index used for O(1) convergence candidate lookup. */ previousPageStartKeyIndex?: ReadonlyMap | null; /** * Host-proved note-only refresh. Body ids are unchanged reference anchors; * only the named note projections may differ from the retained note plane. */ provedNoteOnlyRefresh?: { noteIds: readonly string[]; bodyReferenceBlockIds: readonly string[]; }; /** * Host-proved header/footer-only refresh. The bridge still compares the * current pre-layout height fingerprint before retaining body pagination. */ provedHeaderFooterOnlyRefresh?: { bodyProjectionRetainedExact: true; bodyLayoutInputsUnchanged: true; previousGeometryFingerprint: string; }; }; export declare const measureCache: MeasureCache; /** * Reset every module-global cache this bridge holds (measure cache, * header/footer measure cache, header/footer invalidation state). A clean * cold-recompute oracle must start from exactly the state a fresh worker * would have; clearing only the measure cache leaves warm header/footer * state that can legally shift page geometry. */ export declare function clearIncrementalModuleState(): void; type FootnoteGrowConvergenceState = { appliedReserves: readonly number[]; plannedReserves: readonly number[]; pageCount: number; }; type FootnoteGrowConvergenceInput = { maxPasses: number; maxAcceptedPageCount?: number; readState: () => FootnoteGrowConvergenceState; applyReserves: (target: number[], label: string) => Promise; }; declare function growFootnoteReserves({ maxPasses, maxAcceptedPageCount, readState, applyReserves, }: FootnoteGrowConvergenceInput): Promise; export declare const __test_only_growFootnoteReserves: typeof growFootnoteReserves; export interface IncrementalLayoutExecutionControl { signal?: AbortSignal; /** Budget-aware host-task yield; may resolve immediately. */ yieldToHost?: (checkpoint?: LayoutExecutionCheckpoint) => Promise; /** Measurement checkpoints default to 32 blocks; nested layout checkpoints default to 16. */ yieldEveryBlocks?: number; /** Time-aware mounted probe. Null is the allocation-free under-budget path. */ checkpointIfDue?: (checkpoint?: LayoutExecutionCheckpoint) => Promise | null; } export declare function incrementalLayout(previousBlocks: FlowBlock[], _previousLayout: Layout | null, nextBlocks: FlowBlock[], options: LayoutOptions, measureBlock: (block: FlowBlock, constraints: { maxWidth: number; maxHeight: number; }) => Promise, headerFooter?: { headerBlocks?: HeaderFooterBatch; footerBlocks?: HeaderFooterBatch; headerBlocksByRId?: Map; footerBlocksByRId?: Map; constraints: HeaderFooterConstraints; measure?: HeaderFooterMeasureFn; /** * When `false`, header/footer NUMPAGES/SECTIONPAGES fields keep their * source-cached DOCX text (em dash when absent) instead of resolving to * the current — possibly partial — page total. Defaults to exact. */ pageCountFieldsExact?: boolean; /** * @deprecated Compatibility-only. Measurement identity is derived from * content, constraints, font signature, and page-count field mode; render * generation must not invalidate unchanged header/footer measurements. */ cacheGeneration?: number; }, previousMeasures?: Measure[] | null, fontRuntime?: { fontContext?: FontMeasureContext; previousFontSignature?: string; }, warmStart?: { footnoteReserveSeed?: FootnoteReserveSeed | null; /** Host proved the authoritative note projection bundle was retained exactly. */ noteMeasurePlaneRetainedExact?: true; /** Extra note/decorative planes paired with the retained note bundle. */ retainedFootnoteExtras?: { blocks: FlowBlock[]; measures: Measure[]; }; /** Current immutable host owner and the prior bridge-issued geometry seed. */ headerFooterGeometry?: { ownerFingerprint: string; retainedSeed: HeaderFooterGeometrySeed | null; }; }, layoutReuse?: IncrementalLayoutReuseOptions, measureReuseProof?: IncrementalMeasureReuseProof, execution?: IncrementalLayoutExecutionControl): Promise; export declare function __test_only_splicedCheckpointStats(map: ReadonlyMap | undefined): { pieceCount: number; sourceCount: number; maximumTransformTreeDepth: number; } | null; export declare function __test_only_lazyPageTransformStats(pages: Page[]): { segmentCount: number; maximumLedgerCount: number; maximumTreeDepth: number; } | null; /** * Normalizes a margin value, using a fallback for undefined or non-finite values. * Prevents NaN content sizes when margin properties are partially defined. * * @param value - The margin value to normalize (may be undefined) * @param fallback - The default margin value to use if value is invalid * @returns The normalized margin value (guaranteed to be finite) */ export declare const normalizeMargin: (value: number | undefined, fallback: number) => number; /** * Resolves the maximum measurement constraints (width and height) needed for measuring blocks * across all sections in a document. * * This function scans the entire document (including all section breaks) to determine the * widest column configuration and tallest content area that will be encountered during layout. * The result is used for cache invalidation and backward-compatible comparison (see * `canReusePreviousMeasures`). Actual per-block measurement uses `computePerSectionConstraints`. * * Algorithm: * 1. Start with base content width/height from options.pageSize and options.margins * 2. Calculate base column width from options.columns (if multi-column) * 3. Scan all sectionBreak blocks to find maximum column width and content height * 4. For each section: compute content area, calculate column width, track maximum * 5. Return the widest column width and tallest content height found * * Column width calculation: * - Single column: contentWidth (no gap subtraction) * - Multi-column: (contentWidth - totalGap) / columnCount * - Total gap = gap * (columnCount - 1) * * @param options - Layout options containing default page size, margins, and columns * @param blocks - Optional array of flow blocks to scan for section breaks * If not provided, only base constraints from options are used * @returns Object containing: * - measurementWidth: Maximum column width in pixels (guaranteed positive) * - measurementHeight: Maximum content height in pixels (guaranteed positive) * * @throws Error if resolved constraints are non-positive (indicates invalid configuration) * * @example * ```typescript * // Document with two sections: single column and 2-column * const options = { * pageSize: { w: 612, h: 792 }, // Letter size * margins: { top: 72, right: 72, bottom: 72, left: 72 }, * columns: { count: 1, gap: 0 } * }; * const blocks = [ * // ... content blocks ... * { * kind: 'sectionBreak', * columns: { count: 2, gap: 48 }, * // ... other section properties ... * } * ]; * const constraints = resolveMeasurementConstraints(options, blocks); * // Returns: { measurementWidth: 468, measurementHeight: 648 } * // 468px = (612 - 72 - 72) width, single column (wider than 2-column: 234px) * // All blocks measured at 468px will fit in both sections * ``` */ export declare function resolveMeasurementConstraints(options: LayoutOptions, blocks?: FlowBlock[]): { measurementWidth: number; measurementHeight: number; }; export {};