superdoc 2.11.0 → 2.11.1-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,7 +19,7 @@ var COLLABORATION_UPGRADE_ENGINE_MINIMUM_NODE_MAJOR = 20;
19
19
  var PRIVATE_ENGINE_INFO = (0, _superdoc_docx_engine_collaboration_upgrade_engine.getCollaborationUpgradeEngineInfo)();
20
20
  var ENGINE_INFO = Object.freeze({
21
21
  ...PRIVATE_ENGINE_INFO,
22
- superdocVersion: "2.11.0",
22
+ superdocVersion: "2.11.1-next.2",
23
23
  roomSchemaVersion: Object.freeze({ ...PRIVATE_ENGINE_INFO.roomSchemaVersion }),
24
24
  supportedBundleVersions: SUPPORTED_COLLABORATION_UPGRADE_BUNDLE_VERSIONS,
25
25
  supportedV1ReaderContractVersions: SUPPORTED_V1_READER_CONTRACT_VERSIONS
@@ -18,7 +18,7 @@ var COLLABORATION_UPGRADE_ENGINE_MINIMUM_NODE_MAJOR = 20;
18
18
  var PRIVATE_ENGINE_INFO = getCollaborationUpgradeEngineInfo$1();
19
19
  var ENGINE_INFO = Object.freeze({
20
20
  ...PRIVATE_ENGINE_INFO,
21
- superdocVersion: "2.11.0",
21
+ superdocVersion: "2.11.1-next.2",
22
22
  roomSchemaVersion: Object.freeze({ ...PRIVATE_ENGINE_INFO.roomSchemaVersion }),
23
23
  supportedBundleVersions: SUPPORTED_COLLABORATION_UPGRADE_BUNDLE_VERSIONS,
24
24
  supportedV1ReaderContractVersions: SUPPORTED_V1_READER_CONTRACT_VERSIONS
@@ -2762,9 +2762,13 @@ export type HeaderFooterLayout = {
2762
2762
  export type LayoutBlockResumeCheckpoint = {
2763
2763
  blockId: BlockId;
2764
2764
  pageIndex: number;
2765
+ /** Page before keep/fit decisions; editing may pull a deferred paragraph back here. */
2766
+ preflightPageIndex?: number;
2765
2767
  prefixFragmentCount: number;
2766
2768
  cursorY: number;
2767
2769
  maxCursorY: number;
2770
+ /** Exact accepted body footprint for coupled paragraph/note pagination. */
2771
+ committedBodyBottom?: number;
2768
2772
  columnIndex: number;
2769
2773
  trailingSpacing: number;
2770
2774
  lastParagraphStyleId?: string;
@@ -0,0 +1,59 @@
1
+ import { Page } from '../../contracts/src/index.js';
2
+ import { FootnoteSlice } from './footnote-content.js';
3
+ import { FootnotePageLedgerDraft, FootnotePagePlanInput, PendingFootnote } from './footnote-page-planner.js';
4
+ export type CoupledFootnotePaginationOptions = Omit<FootnotePagePlanInput, 'pageIndex' | 'columnCount' | 'availableHeight' | 'idsByColumn' | 'pendingByColumn'> & {
5
+ pageSize: {
6
+ w: number;
7
+ h: number;
8
+ };
9
+ /** The bridge must validate exact boundary provenance before supplying a start, never a height-only seed. */
10
+ start?: {
11
+ pageIndex: number;
12
+ pendingByColumn: FootnotePagePlanInput['pendingByColumn'];
13
+ isPreviouslyAnchored(id: string): boolean;
14
+ };
15
+ };
16
+ export type CoupledFootnotePageCompletion = {
17
+ page: Page;
18
+ pageIndex: number;
19
+ bodyBottom: number;
20
+ physicalBottom: number;
21
+ anchors: readonly {
22
+ refId: string;
23
+ }[];
24
+ };
25
+ export type CoupledFootnotePageFlow = {
26
+ incomingDemand(pageIndex: number): {
27
+ height: number;
28
+ refs: number;
29
+ };
30
+ completePage(completion: CoupledFootnotePageCompletion): void;
31
+ hasPendingContinuation(): boolean;
32
+ };
33
+ export type CoupledFootnotePagination = {
34
+ flow: CoupledFootnotePageFlow;
35
+ plan: {
36
+ slicesByPage: Map<number, FootnoteSlice[]>;
37
+ reserves: number[];
38
+ hasContinuationByColumn: Map<string, boolean>;
39
+ separatorSpacingBefore: number;
40
+ ledgersByPage: Map<number, FootnotePageLedgerDraft>;
41
+ incomingByPage: Map<number, Map<number, PendingFootnote[]>>;
42
+ outgoingByPage: Map<number, Map<number, PendingFootnote[]>>;
43
+ };
44
+ };
45
+ export type CoupledFootnotePaginationFailure = 'unsupported-page' | 'page-sequence' | 'missing-anchor' | 'duplicate-claim' | 'ordered-minimum' | 'physical-overflow' | 'range-conservation' | 'reference-conservation' | 'no-progress';
46
+ export declare class CoupledFootnotePaginationError extends Error {
47
+ readonly code: CoupledFootnotePaginationFailure;
48
+ readonly pageIndex: number;
49
+ readonly footnoteId?: string | undefined;
50
+ readonly name = "CoupledFootnotePaginationError";
51
+ constructor(code: CoupledFootnotePaginationFailure, pageIndex: number, message: string, footnoteId?: string | undefined);
52
+ }
53
+ /** Call only at real document EOF; expected IDs exclude references anchored before this pass. */
54
+ export declare const validateCoupledFootnoteReferenceConservation: (plan: Pick<CoupledFootnotePagination["plan"], "ledgersByPage">, expectedAnchorIds: Iterable<string>) => void;
55
+ /**
56
+ * Owns forward page admission from a fresh start or a bridge-validated boundary.
57
+ * Prepared inventories and prior anchor ownership remain lookup-only.
58
+ */
59
+ export declare const createCoupledFootnotePagination: (options: CoupledFootnotePaginationOptions) => CoupledFootnotePagination;
@@ -0,0 +1,37 @@
1
+ import { FlowBlock, Measure } from '../../contracts/src/index.js';
2
+ import { LayoutOptions } from '../../layout-engine/src/index.js';
3
+ export type CoupledFootnoteReference = {
4
+ id: string;
5
+ pos: number;
6
+ blockId?: string;
7
+ runOrdinal?: number | null;
8
+ };
9
+ export type CoupledFootnoteProfileRejection = 'no-footnote-references' | 'partial-or-semantic-flow' | 'multiple-sections' | 'invalid-page-geometry' | 'unsupported-columns' | 'unsupported-vertical-alignment' | 'unsupported-section-boundary' | 'unsupported-body-block' | 'unsupported-paragraph-geometry' | 'body-page-token' | 'measure-mismatch' | 'duplicate-block-id' | 'duplicate-reference-id' | 'missing-native-owner' | 'unresolved-reference-line' | 'unordered-native-references' | 'retained-profile-unproved' | 'reference-topology-changed';
10
+ export type CoupledFootnoteParagraphUpdate = {
11
+ block: FlowBlock;
12
+ measure: Measure;
13
+ refs: readonly CoupledFootnoteReference[];
14
+ previousRefs: readonly CoupledFootnoteReference[];
15
+ /** Immediate CURRENT neighbors; null only at a true story boundary. */
16
+ predecessor: FlowBlock | null;
17
+ successor: FlowBlock | null;
18
+ };
19
+ export type CoupledFootnoteLocalProfileProof = {
20
+ baselineProfileExact: true;
21
+ referenceTopologyExact: true;
22
+ };
23
+ /**
24
+ * Revalidates only changed paragraphs beneath a bridge-owned retained global
25
+ * profile and exact current note topology. The bridge must prove those owners
26
+ * and supply all affected paragraphs, including referenced neighbors whose
27
+ * skip context changed. These flags do not mint a global/profile certificate.
28
+ */
29
+ export declare const getCoupledFootnoteParagraphUpdatesUnsupportedReason: (updates: readonly CoupledFootnoteParagraphUpdate[], proof: CoupledFootnoteLocalProfileProof | null | undefined) => CoupledFootnoteProfileRejection | null;
30
+ export declare const supportsCoupledFootnoteParagraphUpdates: (updates: readonly CoupledFootnoteParagraphUpdate[], proof: CoupledFootnoteLocalProfileProof | null | undefined) => boolean;
31
+ /**
32
+ * Admission for fresh, single-section body/note flow. This does not certify
33
+ * note-content inventories or retained pages; their owners validate those.
34
+ * Rejection codes carry no document text and are suitable for diagnostics.
35
+ */
36
+ export declare const getCoupledFootnotePaginationUnsupportedReason: (blocks: FlowBlock[], measures: readonly Measure[], options: LayoutOptions, refs: readonly CoupledFootnoteReference[]) => CoupledFootnoteProfileRejection | null;
37
+ export declare const supportsCoupledFootnotePagination: (blocks: FlowBlock[], measures: readonly Measure[], options: LayoutOptions, refs: readonly CoupledFootnoteReference[]) => boolean;
@@ -0,0 +1,39 @@
1
+ import { FlowBlock, Measure, ParagraphBlock } from '../../contracts/src/index.js';
2
+ export type FootnoteRange = {
3
+ kind: 'paragraph';
4
+ blockId: string;
5
+ fromLine: number;
6
+ toLine: number;
7
+ totalLines: number;
8
+ height: number;
9
+ spacingAfter: number;
10
+ } | {
11
+ kind: 'list-item';
12
+ blockId: string;
13
+ itemId: string;
14
+ fromLine: number;
15
+ toLine: number;
16
+ totalLines: number;
17
+ height: number;
18
+ spacingAfter: number;
19
+ } | {
20
+ kind: 'table' | 'image' | 'drawing';
21
+ blockId: string;
22
+ height: number;
23
+ };
24
+ export type FootnoteSlice = {
25
+ id: string;
26
+ pageIndex: number;
27
+ columnIndex: number;
28
+ isContinuation: boolean;
29
+ ranges: FootnoteRange[];
30
+ totalHeight: number;
31
+ };
32
+ export declare const getParagraphSpacingAfter: (block: ParagraphBlock) => number;
33
+ export declare const resolveSeparatorSpacingBefore: (rangesByFootnoteId: ReadonlyMap<string, readonly FootnoteRange[]>, measuresById: ReadonlyMap<string, Measure>, explicitValue: number | undefined, fallbackValue: number) => number;
34
+ export declare const getRangeRenderHeight: (range: FootnoteRange) => number;
35
+ export declare const buildFootnoteRanges: (blocks: readonly FlowBlock[], measuresById: ReadonlyMap<string, Measure>) => FootnoteRange[];
36
+ export declare const fitFootnoteContent: (id: string, inputRanges: readonly FootnoteRange[], availableHeight: number, pageIndex: number, columnIndex: number, isContinuation: boolean, measuresById: ReadonlyMap<string, Measure>, forceFirstRange: boolean) => {
37
+ slice: FootnoteSlice;
38
+ remainingRanges: FootnoteRange[];
39
+ };
@@ -0,0 +1,38 @@
1
+ import { Page } from '../../contracts/src/index.js';
2
+ import { CoupledFootnotePaginationOptions } from './coupled-footnote-pagination.js';
3
+ import { FootnoteRange } from './footnote-content.js';
4
+ export type FootnoteCertificatePreparedOptions = Readonly<Omit<CoupledFootnotePaginationOptions, 'start'>>;
5
+ type PendingEntry = Readonly<{
6
+ id: string;
7
+ ranges: readonly Readonly<FootnoteRange>[];
8
+ }>;
9
+ export type FootnotePendingQueue = ReadonlyMap<number, readonly PendingEntry[]>;
10
+ declare const ownerBrand: unique symbol;
11
+ declare const certificateBrand: unique symbol;
12
+ export type FootnoteCertificateOwner = Readonly<{
13
+ prepared: FootnoteCertificatePreparedOptions;
14
+ [ownerBrand]: true;
15
+ }>;
16
+ export type FootnotePageCertificateFacts = Readonly<{
17
+ /** Issuance/source index; transporting a certificate does not rebase it. */
18
+ pageIndex: number;
19
+ incomingByColumn: FootnotePendingQueue;
20
+ outgoingByColumn: FootnotePendingQueue;
21
+ }>;
22
+ export type FootnotePageCertificate = FootnotePageCertificateFacts & Readonly<{
23
+ owner: FootnoteCertificateOwner;
24
+ [certificateBrand]: true;
25
+ }>;
26
+ /** Prepared inputs must remain unchanged for the owner's lifetime; no inventory is copied or scanned here. */
27
+ export declare const createFootnoteCertificateOwner: (prepared: FootnoteCertificatePreparedOptions) => FootnoteCertificateOwner;
28
+ export declare const issueFootnotePageCertificate: (page: Page, owner: FootnoteCertificateOwner, facts: FootnotePageCertificateFacts) => FootnotePageCertificate;
29
+ /** Omitting expectedOwner discovers retained inputs only; tail admission must supply its validated owner. */
30
+ export declare const readFootnotePageCertificate: (page: Page | null | undefined, expectedOwner?: FootnoteCertificateOwner) => FootnotePageCertificate | null;
31
+ /**
32
+ * Restores note-continuation provenance lost by string-key page cloning.
33
+ * The caller must separately prove body/rekey equivalence; this does not certify body reuse.
34
+ */
35
+ export declare const transferFootnotePageCertificate: (source: Page, target: Page, pageIndexDelta: number) => FootnotePageCertificate | null;
36
+ /** Queue order and every range field are part of the boundary; equal height is not equivalence. */
37
+ export declare const pendingFootnoteQueuesEqual: (left: FootnotePendingQueue, right: FootnotePendingQueue) => boolean;
38
+ export {};
@@ -0,0 +1,49 @@
1
+ import { FootnotePageLedger, Measure } from '../../contracts/src/index.js';
2
+ import { FootnoteRange, FootnoteSlice } from './footnote-content.js';
3
+ export type PendingFootnote = {
4
+ id: string;
5
+ ranges: FootnoteRange[];
6
+ };
7
+ type IncomingFootnote = {
8
+ readonly id: string;
9
+ readonly ranges: readonly FootnoteRange[];
10
+ };
11
+ export type FootnotePageLedgerDraft = Omit<FootnotePageLedger, 'pageIndex' | 'appliedBodyReservePx' | 'deadReservePx'>;
12
+ export type FootnotePagePlanInput = {
13
+ pageIndex: number;
14
+ columnCount: number;
15
+ /** Physical capacity for this page's note band, including separator, padding, and gaps. */
16
+ availableHeight: number;
17
+ idsByColumn: ReadonlyMap<number, readonly string[]>;
18
+ rangesByFootnoteId: ReadonlyMap<string, readonly FootnoteRange[]>;
19
+ measuresById: ReadonlyMap<string, Measure>;
20
+ fullHeightById: ReadonlyMap<string, number>;
21
+ firstLineHeightById: ReadonlyMap<string, number>;
22
+ pendingByColumn: ReadonlyMap<number, readonly IncomingFootnote[]>;
23
+ separatorSpacingBefore: number;
24
+ dividerHeight: number;
25
+ continuationDividerHeight: number;
26
+ topPadding: number;
27
+ gap: number;
28
+ };
29
+ export type FootnotePagePlan = {
30
+ slices: FootnoteSlice[];
31
+ pendingByColumn: Map<number, PendingFootnote[]>;
32
+ ledger: FootnotePageLedgerDraft;
33
+ /** Rounded reserve capped at the supplied capacity; not a proof that every slice fits. */
34
+ reserve: number;
35
+ /** Unrounded, uncapped height of the tallest placed column. */
36
+ actualReserve: number;
37
+ hasContinuationByColumn: Map<number, boolean>;
38
+ capped: boolean;
39
+ overflowHeightPx: number;
40
+ overflowHeightByColumn: Map<number, number>;
41
+ /** Outgoing ranges or physical overflow still require a controller decision. */
42
+ hasUnresolvedContent: boolean;
43
+ };
44
+ /**
45
+ * Places one page from exact incoming ranges. Global inventories are lookup-only;
46
+ * body placement and any future-page capacity decision belong to the caller.
47
+ */
48
+ export declare const planFootnotePage: (input: FootnotePagePlanInput) => FootnotePagePlan;
49
+ export {};
@@ -4,6 +4,7 @@ import { LayoutOptions, HeaderFooterConstraints, LayoutExecutionCheckpoint } fro
4
4
  import { computeDirtyRegions } from './diff.js';
5
5
  import { MeasureCache } from './cache.js';
6
6
  import { HeaderFooterBatch } from './layoutHeaderFooter.js';
7
+ import { FootnoteCertificateOwner, FootnoteCertificatePreparedOptions } from './footnote-page-certificate.js';
7
8
  export type HeaderFooterMeasureFn = (block: FlowBlock, constraints: {
8
9
  maxWidth: number;
9
10
  maxHeight: number;
@@ -32,6 +33,10 @@ export type HeaderFooterLayoutResult = {
32
33
  * hashes — see the SD-3418 post-mortem for why identity keys are forbidden).
33
34
  */
34
35
  export type FootnoteReserveSeed = {
36
+ /** Identifies canonical forward output; not, by itself, a reuse certificate. */
37
+ paginationPolicy?: 'coupled-v1';
38
+ /** Opaque bridge-issued note/profile state; numeric reserve vectors never authorize page reuse. */
39
+ coupled?: CoupledFootnoteRetainedState;
35
40
  reserves: number[];
36
41
  /** Sparse page indexes that held a reserve, ledger, or injected note slice. */
37
42
  notePageIndexes?: number[];
@@ -63,6 +68,23 @@ export type FootnoteReserveSeed = {
63
68
  referencePageIndexes: number[];
64
69
  };
65
70
  };
71
+ type CoupledFootnoteRetainedState = {
72
+ readonly owner: FootnoteCertificateOwner;
73
+ /** Current note inventory; may extend the certificate owner's page input during exact Undo. */
74
+ readonly prepared: FootnoteCertificatePreparedOptions;
75
+ readonly headerFooterGeometryFingerprint: string;
76
+ readonly reserves: readonly number[];
77
+ readonly notePageIndexes: readonly number[];
78
+ readonly refs: readonly FootnoteReference[];
79
+ readonly referencePlaneIdentity: FootnotesLayoutInput['refs'];
80
+ readonly referenceTopologyRevision: string | undefined;
81
+ readonly refIndexesByBlock: ReadonlyMap<string, readonly number[]>;
82
+ readonly refIndexById: ReadonlyMap<string, number>;
83
+ readonly blocksById: Map<string, FlowBlock>;
84
+ readonly extraBlocks: FlowBlock[];
85
+ readonly extraMeasures: Measure[];
86
+ readonly extraIndexById: ReadonlyMap<string, number>;
87
+ };
66
88
  export type IncrementalLayoutResult = {
67
89
  layout: Layout;
68
90
  /** Pass-owned block plane after derived layout annotations are attached. */
@@ -104,6 +126,8 @@ export type IncrementalLayoutResult = {
104
126
  bridgeTiming: IncrementalLayoutBridgeTiming;
105
127
  };
106
128
  export type IncrementalLayoutBridgeTiming = {
129
+ /** Content-free reason a document used the conservative note fallback. */
130
+ footnoteCoupledRejection?: string;
107
131
  totalMs: number;
108
132
  inputPreparationMs: number;
109
133
  measureTotalMs: number;
@@ -169,6 +193,9 @@ export type IncrementalLayoutBridgeTiming = {
169
193
  footnoteWidowRelayouts: number;
170
194
  footnoteRevertRelayouts: number;
171
195
  footnoteOtherRelayouts: number;
196
+ footnoteCoupledRelayouts?: number;
197
+ footnoteCoupledPages?: number;
198
+ footnoteCoupledFallbacks?: number;
172
199
  footnoteAssignmentReferencesRead: number;
173
200
  footnoteAssignmentReferencesReused: number;
174
201
  footnoteAssignmentPagesIndexed: number;
@@ -397,6 +424,11 @@ export type IncrementalLayoutReuseOptions = {
397
424
  noteIds: readonly string[];
398
425
  bodyReferenceBlockIds: readonly string[];
399
426
  };
427
+ /** History-only inverse of a compiler-proved footnote removal. */
428
+ provedNoteReferenceRestoration?: {
429
+ sourceTxId: string;
430
+ restoredNoteIds: readonly string[];
431
+ };
400
432
  /**
401
433
  * Host-proved header/footer-only refresh. The bridge still compares the
402
434
  * current pre-layout height fingerprint before retaining body pagination.
@@ -416,6 +448,45 @@ export declare const measureCache: MeasureCache<Measure>;
416
448
  * state that can legally shift page geometry.
417
449
  */
418
450
  export declare function clearIncrementalModuleState(): void;
451
+ type FootnoteReference = {
452
+ id: string;
453
+ /**
454
+ * Legacy v1 PM-position anchor. Resolved via fragment `pmStart` / `pmEnd`
455
+ * range matching. Required for v1 producers; v2 producers may set this
456
+ * to a synthetic value (e.g. block ordinal) and supply a `blockId`
457
+ * anchor for resolution.
458
+ */
459
+ pos: number;
460
+ /**
461
+ * v2 source anchor identifying the rendered body
462
+ * reference marker. When set, `assignFootnotesToColumns` resolves
463
+ * the reference's page/column by matching against
464
+ * `layout.pages[].fragments[].blockId` instead of falling back to
465
+ * positional fragment lookup. Editor-neutral by design.
466
+ */
467
+ blockId?: string;
468
+ /**
469
+ * Optional paragraph-run anchor used by v2 refs.
470
+ *
471
+ * A long paragraph can span multiple page fragments. When `blockId` alone
472
+ * is used, the bridge can only resolve the FIRST fragment carrying that
473
+ * block, which places later-line footnotes too early and cascades reserve
474
+ * drift across the document. When `runOrdinal` is present and a paragraph
475
+ * measure is available, the bridge resolves the fragment whose line range
476
+ * actually contains the referenced run.
477
+ */
478
+ runOrdinal?: number | null;
479
+ };
480
+ type FootnotesLayoutInput = {
481
+ refs: FootnoteReference[];
482
+ blocksById: Map<string, FlowBlock[]>;
483
+ /** Host-issued identity for the body reference topology. */
484
+ referenceTopologyRevision?: string;
485
+ gap?: number;
486
+ topPadding?: number;
487
+ dividerHeight?: number;
488
+ separatorSpacingBefore?: number;
489
+ };
419
490
  type FootnoteGrowConvergenceState = {
420
491
  appliedReserves: readonly number[];
421
492
  plannedReserves: readonly number[];
@@ -472,12 +543,33 @@ export declare function incrementalLayout(previousBlocks: FlowBlock[], _previous
472
543
  blocks: FlowBlock[];
473
544
  measures: Measure[];
474
545
  };
546
+ /** Bridge-issued pre-removal owner selected by the exact history source tx. */
547
+ historyFootnoteRestoration?: {
548
+ sourceTxId: string;
549
+ restoredNoteIds: readonly string[];
550
+ sourceSeed: FootnoteReserveSeed;
551
+ };
475
552
  /** Current immutable host owner and the prior bridge-issued geometry seed. */
476
553
  headerFooterGeometry?: {
477
554
  ownerFingerprint: string;
478
555
  retainedSeed: HeaderFooterGeometrySeed | null;
479
556
  };
480
557
  }, layoutReuse?: IncrementalLayoutReuseOptions, measureReuseProof?: IncrementalMeasureReuseProof, execution?: IncrementalLayoutExecutionControl): Promise<IncrementalLayoutResult>;
558
+ declare function shouldAttemptPreparedCoupledConvergenceRetry(input: {
559
+ preparedCoupled: boolean;
560
+ provedPrefixToDocumentEnd: boolean;
561
+ sameInvocationReserveRelayout: boolean;
562
+ paginationPrefix: boolean;
563
+ boundedRenderDiagnosticRetry: boolean;
564
+ retryAttempted: boolean;
565
+ terminalAttempted: boolean;
566
+ reachesSourceTail: boolean;
567
+ currentPageHorizon: number;
568
+ sourceAffectedFrontierPageIndex: number;
569
+ checkpointPageIndex: number;
570
+ previousPageCount: number;
571
+ }): boolean;
572
+ export declare const __test_only_shouldAttemptPreparedCoupledConvergenceRetry: typeof shouldAttemptPreparedCoupledConvergenceRetry;
481
573
  export declare function __test_only_splicedCheckpointStats(map: ReadonlyMap<string, LayoutBlockResumeCheckpoint> | undefined): {
482
574
  pieceCount: number;
483
575
  sourceCount: number;
@@ -1,4 +1,4 @@
1
- export type LayoutExecutionPhase = 'measure:block' | 'layout-document:prepare' | 'layout-document:preflight-section' | 'layout-document:preflight-footnote' | 'layout-document:preflight-anchor' | 'layout-document:preflight-keep-next' | 'layout-document:block' | 'layout-document:table-fragment' | 'layout-document:finalize-page' | 'layout-document:finalize-section' | 'page-token:prepare' | 'page-token:page' | 'numbering-context:chapter' | 'numbering-context:page' | 'footnote:phase' | 'header-footer:variant' | 'header-footer:block' | 'header-footer:page';
1
+ export type LayoutExecutionPhase = 'measure:block' | 'layout-document:prepare' | 'layout-document:preflight-section' | 'layout-document:preflight-footnote' | 'layout-document:preflight-anchor' | 'layout-document:preflight-keep-next' | 'layout-document:block' | 'layout-document:table-fragment' | 'layout-document:footnote-continuation' | 'layout-document:finalize-page' | 'layout-document:finalize-section' | 'page-token:prepare' | 'page-token:page' | 'numbering-context:chapter' | 'numbering-context:page' | 'footnote:phase' | 'header-footer:variant' | 'header-footer:block' | 'header-footer:page';
2
2
  export type LayoutExecutionCheckpoint = {
3
3
  phase: LayoutExecutionPhase;
4
4
  index?: number;
@@ -2,10 +2,13 @@ import { FlowBlock } from '../../contracts/src/index.js';
2
2
  import { LayoutWorkCheckpoint } from './execution.js';
3
3
  import { FootnoteAnchorRef } from './layout-paragraph.js';
4
4
  export type FootnoteAnchorIndexInput = {
5
+ /** Use proved native paragraph/run ownership in the coupled page path. */
6
+ nativeRunOwnership?: boolean;
5
7
  refs?: Array<{
6
8
  id: string;
7
9
  pos: number;
8
10
  blockId?: string;
11
+ runOrdinal?: number | null;
9
12
  }>;
10
13
  bodyHeightById?: Map<string, number>;
11
14
  firstLineHeightById?: Map<string, number>;
@@ -0,0 +1,28 @@
1
+ import { Page } from '../../contracts/src/index.js';
2
+ import { PageState } from './paginator.js';
3
+ import { FootnoteAnchorRef } from './layout-paragraph.js';
4
+ /** Internal body/notes handshake. The bridge owns exact continuation ranges. */
5
+ export type FootnotePageFlow = {
6
+ /** False for a bounded block horizon: its EOF is not the document's EOF. */
7
+ completeDocument?: boolean;
8
+ incomingDemand(pageIndex: number): {
9
+ height: number;
10
+ refs: number;
11
+ };
12
+ completePage(input: {
13
+ page: Page;
14
+ pageIndex: number;
15
+ bodyBottom: number;
16
+ physicalBottom: number;
17
+ anchors: readonly FootnoteAnchorRef[];
18
+ }): void;
19
+ hasPendingContinuation(): boolean;
20
+ };
21
+ /**
22
+ * Both kept groups and individual lines use this budget. A numeric reserve
23
+ * from an earlier pagination has no authority over this page's exact queue.
24
+ */
25
+ export declare function coupledFootnoteBodyBottom(state: PageState, anchoredHeight: number, anchoredRefs: number, incoming: {
26
+ height: number;
27
+ refs: number;
28
+ }, overhead: (refs: number) => number, minimumBodyHeight?: number): number;
@@ -1,6 +1,9 @@
1
1
  import { ColumnLayout, FlowBlock, HeaderFooterLayout, Layout, Measure, Page, ParagraphBlock, ParagraphMeasure, SectionMetadata, FlowMode, DocumentBackground, ParagraphLineRegion } from '../../contracts/src/index.js';
2
+ import { FootnotePageFlow } from './footnote-page-flow.js';
2
3
  import { PageState, ConstraintBoundary } from './paginator.js';
3
4
  import { LayoutExecutionControl } from './execution.js';
5
+ export type { FootnotePageFlow } from './footnote-page-flow.js';
6
+ export { findLineIndexForRunOrdinal } from './layout-utils.js';
4
7
  type PageSize = {
5
8
  w: number;
6
9
  h: number;
@@ -19,6 +22,8 @@ type Margins = {
19
22
  */
20
23
  export declare const SEMANTIC_PAGE_HEIGHT_PX = 1000000;
21
24
  export type LayoutOptions = {
25
+ /** @internal Exact forward body/note pagination; never supplied by document authors. */
26
+ footnotePageFlow?: FootnotePageFlow;
22
27
  pageSize?: PageSize;
23
28
  margins?: Margins;
24
29
  documentBackground?: DocumentBackground;
@@ -53,6 +58,8 @@ export type LayoutOptions = {
53
58
  refs?: Array<{
54
59
  id: string;
55
60
  pos: number;
61
+ blockId?: string;
62
+ runOrdinal?: number | null;
56
63
  }>;
57
64
  /**
58
65
  * SD-3049: total measured body height per footnote id (sum of measured
@@ -204,6 +211,7 @@ export type LayoutOptions = {
204
211
  prefixFragments: readonly Page['fragments'][number][];
205
212
  cursorY: number;
206
213
  maxCursorY: number;
214
+ committedBodyBottom?: number;
207
215
  columnIndex: number;
208
216
  trailingSpacing: number;
209
217
  lastParagraphStyleId?: string;
@@ -1,5 +1,6 @@
1
1
  import { FloatingObjectManager } from './floating-objects.js';
2
2
  import { PageState } from './paginator.js';
3
+ import { FootnotePageFlow } from './footnote-page-flow.js';
3
4
  import { PageMargins, ParagraphBlock, ParagraphMeasure, ImageBlock, ImageMeasure, DrawingBlock, DrawingMeasure, ParagraphLineRegion } from '../../contracts/src/index.js';
4
5
  import { AnchoredTable } from './anchors.js';
5
6
  /**
@@ -9,11 +10,14 @@ import { AnchoredTable } from './anchors.js';
9
10
  */
10
11
  export type FootnoteAnchorRef = {
11
12
  pmPos: number;
13
+ /** Exact run in this paragraph; absent for legacy or containing-table anchors. */
14
+ runOrdinal?: number;
12
15
  refId: string;
13
16
  fullHeight: number;
14
17
  firstLineHeight: number;
15
18
  };
16
19
  export type ParagraphLayoutContext = {
20
+ incomingFootnoteDemand?: FootnotePageFlow['incomingDemand'];
17
21
  block: ParagraphBlock;
18
22
  measure: ParagraphMeasure;
19
23
  columnWidth: number;
@@ -66,6 +66,8 @@ export declare function sliceLines(lines: ParagraphMeasure['lines'], startIndex:
66
66
  height: number;
67
67
  };
68
68
  export type { LinePmRange };
69
+ /** A marker belongs to the first measured line containing its run. */
70
+ export declare const findLineIndexForRunOrdinal: (lines: readonly Line[] | undefined, runOrdinal: number) => number | null;
69
71
  export declare const computeFragmentPmRange: (block: ParagraphBlock, lines: ParagraphMeasure["lines"], fromLine: number, toLine: number) => LinePmRange;
70
72
  export declare const computeLinePmRange: (block: ParagraphBlock, line: Line) => LinePmRange;
71
73
  /**
@@ -20,6 +20,8 @@ export type PageState = {
20
20
  * Used when starting a mid-page region so the new section begins below
21
21
  * all column content, not just the current column's cursor. */
22
22
  maxCursorY: number;
23
+ /** Committed paragraph footprint, excluding speculative before/after spacing. */
24
+ committedBodyBottom?: number;
23
25
  /**
24
26
  * SD-3049: Page-level footnote reserve already baked into `contentBottom`
25
27
  * via `getActiveBottomMargin`. The block-aware break decision compares
@@ -78,6 +80,7 @@ export type PaginatorOptions = {
78
80
  prefixFragments: readonly Page['fragments'][number][];
79
81
  cursorY: number;
80
82
  maxCursorY: number;
83
+ committedBodyBottom?: number;
81
84
  columnIndex: number;
82
85
  trailingSpacing: number;
83
86
  lastParagraphStyleId?: string;
@@ -108,6 +111,8 @@ export type PaginatorOptions = {
108
111
  * created. Defaults to 0 when not provided.
109
112
  */
110
113
  getFootnoteReserveForPage?: (pageIndex: number) => number;
114
+ /** Coupled note-only continuation pages are real content before note injection. */
115
+ keepNoteOnlyPages?: boolean;
111
116
  };
112
117
  export declare class PaginationEarlyStop extends Error {
113
118
  constructor();
package/dist/superdoc.cjs CHANGED
@@ -316,7 +316,7 @@ var shuffleArray = (array) => {
316
316
  var DEFAULT_ENDPOINT = "https://ingest.superdoc.dev/v1/collect";
317
317
  function getSuperdocVersion() {
318
318
  try {
319
- return "2.11.0";
319
+ return "2.11.1-next.2";
320
320
  } catch {
321
321
  return "unknown";
322
322
  }
@@ -45188,7 +45188,7 @@ var SuperDoc = class extends require_eventemitter3.import_eventemitter3.default
45188
45188
  this.config.colors = shuffleArray(this.config.colors);
45189
45189
  this.userColorMap = /* @__PURE__ */ new Map();
45190
45190
  this.colorIndex = 0;
45191
- this.version = "2.11.0";
45191
+ this.version = "2.11.1-next.2";
45192
45192
  this.#log("🦋 [superdoc] Using SuperDoc version:", this.version);
45193
45193
  this.superdocId = config.superdocId || require_uuid.v4();
45194
45194
  this.colors = this.config.colors ?? [];
@@ -315,7 +315,7 @@ var shuffleArray = (array) => {
315
315
  var DEFAULT_ENDPOINT = "https://ingest.superdoc.dev/v1/collect";
316
316
  function getSuperdocVersion() {
317
317
  try {
318
- return "2.11.0";
318
+ return "2.11.1-next.2";
319
319
  } catch {
320
320
  return "unknown";
321
321
  }
@@ -45119,7 +45119,7 @@ var SuperDoc = class extends import_eventemitter3.default {
45119
45119
  this.config.colors = shuffleArray(this.config.colors);
45120
45120
  this.userColorMap = /* @__PURE__ */ new Map();
45121
45121
  this.colorIndex = 0;
45122
- this.version = "2.11.0";
45122
+ this.version = "2.11.1-next.2";
45123
45123
  this.#log("🦋 [superdoc] Using SuperDoc version:", this.version);
45124
45124
  this.superdocId = config.superdocId || v4();
45125
45125
  this.colors = this.config.colors ?? [];