@stll/folio-core 0.36.0 → 0.37.1

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.
Files changed (200) hide show
  1. package/README.md +59 -1
  2. package/dist/ai-edits/apply.js +689 -367
  3. package/dist/ai-edits/block-identity.d.ts +4 -1
  4. package/dist/ai-edits/block-identity.js +4 -1
  5. package/dist/ai-edits/blockRange.js +14 -14
  6. package/dist/ai-edits/clean-text.d.ts +40 -1
  7. package/dist/ai-edits/clean-text.js +52 -2
  8. package/dist/ai-edits/headless.d.ts +3 -1
  9. package/dist/ai-edits/headless.js +133 -71
  10. package/dist/ai-edits/index.d.ts +3 -3
  11. package/dist/ai-edits/index.js +2 -2
  12. package/dist/ai-edits/read.js +38 -20
  13. package/dist/ai-edits/snapshot.d.ts +14 -6
  14. package/dist/ai-edits/snapshot.js +99 -19
  15. package/dist/ai-edits/table-cell-mutations.js +1 -1
  16. package/dist/ai-edits/table-row-column-mutations.js +7 -0
  17. package/dist/ai-edits/table-template.d.ts +11 -1
  18. package/dist/ai-edits/table-template.js +49 -17
  19. package/dist/ai-edits/types.d.ts +60 -57
  20. package/dist/ai-suggestions/conflict.js +1 -4
  21. package/dist/ai-suggestions/text-positions.d.ts +8 -0
  22. package/dist/ai-suggestions/text-positions.js +27 -5
  23. package/dist/compare/__fixtures__/body-sequence.js +3 -4
  24. package/dist/compare/column-alignment.d.ts +13 -13
  25. package/dist/compare/column-alignment.js +78 -39
  26. package/dist/compare/compare.d.ts +4 -3
  27. package/dist/compare/compare.js +41 -5
  28. package/dist/compare/content-alignment.d.ts +95 -0
  29. package/dist/compare/content-alignment.js +1450 -0
  30. package/dist/compare/content-types.d.ts +100 -0
  31. package/dist/compare/content-types.js +0 -0
  32. package/dist/compare/content.d.ts +209 -0
  33. package/dist/compare/content.js +885 -0
  34. package/dist/compare/formatting.d.ts +5 -5
  35. package/dist/compare/formatting.js +6 -4
  36. package/dist/compare/plan.d.ts +6 -1
  37. package/dist/compare/plan.js +201 -569
  38. package/dist/compare/scenario.d.ts +2 -2
  39. package/dist/compare/types.d.ts +3 -3
  40. package/dist/compare/verification.d.ts +2 -2
  41. package/dist/compare/verification.js +25 -8
  42. package/dist/compat/eigenpal.d.ts +5 -3
  43. package/dist/compat/eigenpal.js +3 -2
  44. package/dist/controller/headerFooterEditorManager.js +14 -6
  45. package/dist/controller/hiddenEditorManager.d.ts +7 -2
  46. package/dist/controller/hiddenEditorManager.js +15 -6
  47. package/dist/controller/layoutPipeline.js +5 -3
  48. package/dist/controller/noteEditorManager.js +12 -7
  49. package/dist/display-list/build/headerFooterPrimitives.js +1 -0
  50. package/dist/display-list/build/storyPrimitives.d.ts +5 -2
  51. package/dist/display-list/build/storyPrimitives.js +60 -3
  52. package/dist/display-list/build/tablePrimitives.js +30 -0
  53. package/dist/display-list/primitives.d.ts +1 -1
  54. package/dist/document-operations.d.ts +8 -6
  55. package/dist/document-operations.js +121 -26
  56. package/dist/docx/blockContentParser.js +6 -27
  57. package/dist/docx/ensureParaIds.js +9 -4
  58. package/dist/docx/numberingParser.d.ts +3 -1
  59. package/dist/docx/numberingParser.js +4 -1
  60. package/dist/docx/paragraphParser.js +7 -6
  61. package/dist/docx/paragraphTraversal.js +30 -34
  62. package/dist/docx/rezip.d.ts +0 -8
  63. package/dist/docx/rezip.js +41 -16
  64. package/dist/docx/runParser.js +4 -2
  65. package/dist/docx/serializer/paragraphSerializer.js +2 -0
  66. package/dist/docx/serializer/runSerializer.js +5 -3
  67. package/dist/docx/serializer/trackedChangeAttributes.d.ts +3 -1
  68. package/dist/docx/serializer/trackedChangeAttributes.js +11 -1
  69. package/dist/docx/server/build.d.ts +1 -1
  70. package/dist/headless-layout.js +5 -4
  71. package/dist/index.d.ts +5 -3
  72. package/dist/index.js +3 -2
  73. package/dist/internal/headlessRevisionResolution.d.ts +23 -0
  74. package/dist/internal/headlessRevisionResolution.js +176 -0
  75. package/dist/internal/headlessRevisionResolutionGuard.d.ts +12 -0
  76. package/dist/internal/headlessRevisionResolutionGuard.js +28 -0
  77. package/dist/internal/pageBreakRunDescendantIndex.d.ts +9 -0
  78. package/dist/internal/pageBreakRunDescendantIndex.js +27 -0
  79. package/dist/internal/pageBreakRunPartition.d.ts +26 -0
  80. package/dist/internal/pageBreakRunPartition.js +35 -0
  81. package/dist/internal/pageBreakRunSourceDescendantIndex.d.ts +15 -0
  82. package/dist/internal/pageBreakRunSourceDescendantIndex.js +126 -0
  83. package/dist/internal/sectionEndpointResolution.d.ts +32 -0
  84. package/dist/internal/sectionEndpointResolution.js +38 -0
  85. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  86. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +16 -4
  87. package/dist/layout-bridge/convert/headerFooterLayout.js +48 -5
  88. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -0
  89. package/dist/layout-bridge/convert/toFlowBlocks.js +379 -75
  90. package/dist/layout-bridge/dom/clickToPositionDom.d.ts +8 -1
  91. package/dist/layout-bridge/dom/clickToPositionDom.js +114 -40
  92. package/dist/layout-bridge/dom/textStreamDom.d.ts +24 -0
  93. package/dist/layout-bridge/dom/textStreamDom.js +75 -0
  94. package/dist/layout-bridge/headerFooterLayout.js +16 -30
  95. package/dist/layout-engine/index.js +12 -3
  96. package/dist/layout-engine/measure/tableFragmentBorderGeometry.d.ts +17 -0
  97. package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +24 -0
  98. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  99. package/dist/layout-engine/types.d.ts +7 -0
  100. package/dist/layout-painter/imageLayout.d.ts +1 -1
  101. package/dist/layout-painter/renderTable.js +20 -0
  102. package/dist/model.d.ts +1 -1
  103. package/dist/paged-layout/rangeProjection.js +6 -9
  104. package/dist/paged-layout/transactionDirtyRange.d.ts +3 -1
  105. package/dist/paged-layout/transactionDirtyRange.js +23 -1
  106. package/dist/prosemirror/attrs/index.d.ts +7 -3
  107. package/dist/prosemirror/attrs/index.js +113 -7
  108. package/dist/prosemirror/bookmarkBoundaryIntegrity.d.ts +9 -0
  109. package/dist/prosemirror/bookmarkBoundaryIntegrity.js +22 -0
  110. package/dist/prosemirror/commands/comments.d.ts +10 -1
  111. package/dist/prosemirror/commands/comments.js +167 -59
  112. package/dist/prosemirror/commands/formatPainter.d.ts +45 -10
  113. package/dist/prosemirror/commands/formatPainter.js +319 -63
  114. package/dist/prosemirror/commands/index.d.ts +2 -2
  115. package/dist/prosemirror/commands/pageBreak.d.ts +5 -2
  116. package/dist/prosemirror/commands/pageBreak.js +18 -36
  117. package/dist/prosemirror/commands/propertyChangeScope.d.ts +7 -5
  118. package/dist/prosemirror/commands/propertyChangeScope.js +27 -18
  119. package/dist/prosemirror/conversion/fromProseDoc.d.ts +7 -2
  120. package/dist/prosemirror/conversion/fromProseDoc.js +809 -209
  121. package/dist/prosemirror/conversion/runShadingMark.js +2 -2
  122. package/dist/prosemirror/conversion/toProseDoc.d.ts +26 -1
  123. package/dist/prosemirror/conversion/toProseDoc.js +416 -283
  124. package/dist/prosemirror/extensions/StarterKit.js +4 -0
  125. package/dist/prosemirror/extensions/core/ParagraphExtension.js +12 -8
  126. package/dist/prosemirror/extensions/features/ListExtension.js +95 -123
  127. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +1 -1
  128. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +27 -1
  129. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +134 -30
  130. package/dist/prosemirror/extensions/features/pasteCleanup.js +9 -15
  131. package/dist/prosemirror/extensions/marks/BoldExtension.js +4 -3
  132. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.d.ts +4 -7
  133. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.js +5 -11
  134. package/dist/prosemirror/extensions/marks/FontSizeExtension.js +3 -3
  135. package/dist/prosemirror/extensions/marks/ItalicExtension.js +4 -3
  136. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.d.ts +13 -0
  137. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.js +39 -0
  138. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.d.ts +7 -1
  139. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +67 -21
  140. package/dist/prosemirror/extensions/marks/markUtils.d.ts +8 -1
  141. package/dist/prosemirror/extensions/marks/markUtils.js +88 -2
  142. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +44 -3
  143. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  144. package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +15 -4
  145. package/dist/prosemirror/extensions/nodes/PageBreakExtension.d.ts +6 -0
  146. package/dist/prosemirror/extensions/nodes/PageBreakExtension.js +6 -0
  147. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.d.ts +6 -0
  148. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.js +36 -0
  149. package/dist/prosemirror/findReplaceSelection.js +8 -3
  150. package/dist/prosemirror/index.d.ts +3 -3
  151. package/dist/prosemirror/listMarker.d.ts +26 -1
  152. package/dist/prosemirror/listMarker.js +108 -14
  153. package/dist/prosemirror/pageBreakRunProjection.d.ts +28 -0
  154. package/dist/prosemirror/pageBreakRunProjection.js +47 -0
  155. package/dist/prosemirror/paraText.js +6 -0
  156. package/dist/prosemirror/paragraphSpacing.d.ts +29 -0
  157. package/dist/prosemirror/paragraphSpacing.js +125 -0
  158. package/dist/prosemirror/plugins/documentNumbering.d.ts +8 -0
  159. package/dist/prosemirror/plugins/documentNumbering.js +18 -0
  160. package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -2
  161. package/dist/prosemirror/plugins/pmTextScan.js +14 -4
  162. package/dist/prosemirror/plugins/suggestionMode.js +8 -10
  163. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +16 -0
  164. package/dist/prosemirror/rebaseParagraphRunFormatting.js +61 -0
  165. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +64 -0
  166. package/dist/prosemirror/runFormattingInlineCarriers.js +121 -0
  167. package/dist/prosemirror/runFormattingProvenance.d.ts +12 -0
  168. package/dist/prosemirror/runFormattingProvenance.js +48 -0
  169. package/dist/prosemirror/runFormattingReconciliation.d.ts +25 -0
  170. package/dist/prosemirror/runFormattingReconciliation.js +54 -0
  171. package/dist/prosemirror/runPropertyChangeResolution.d.ts +18 -0
  172. package/dist/prosemirror/runPropertyChangeResolution.js +18 -0
  173. package/dist/prosemirror/runStyleFormatting.d.ts +50 -0
  174. package/dist/prosemirror/runStyleFormatting.js +105 -0
  175. package/dist/prosemirror/schema/index.d.ts +3 -3
  176. package/dist/prosemirror/schema/marks.d.ts +82 -9
  177. package/dist/prosemirror/schema/marks.js +73 -1
  178. package/dist/prosemirror/schema/nodes.d.ts +18 -5
  179. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +14 -2
  180. package/dist/prosemirror/styles/resolvedStyleAttrs.js +44 -23
  181. package/dist/prosemirror/styles/styleResolver.d.ts +10 -13
  182. package/dist/prosemirror/styles/styleResolver.js +3 -3
  183. package/dist/prosemirror/styles/styleToggleCascade.d.ts +8 -1
  184. package/dist/prosemirror/styles/styleToggleCascade.js +24 -1
  185. package/dist/prosemirror/tableGridMutation.d.ts +20 -0
  186. package/dist/prosemirror/tableGridMutation.js +30 -0
  187. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +31 -0
  188. package/dist/prosemirror/trackedRunInlineAtoms.js +39 -0
  189. package/dist/prosemirror/utils/visualLineNavigation.js +27 -31
  190. package/dist/prosemirror/validation.js +23 -4
  191. package/dist/render-dom/HeaderFooterSelectionOverlay.js +20 -20
  192. package/dist/render-dom/RenderedDomContext.js +17 -34
  193. package/dist/server.d.ts +4 -4
  194. package/dist/server.js +2 -2
  195. package/dist/style-engine/styleEngine.d.ts +1 -1
  196. package/dist/utils/clipboard.js +1 -1
  197. package/dist/utils/findReplace.js +38 -21
  198. package/dist/version-comparison.d.ts +38 -44
  199. package/dist/version-comparison.js +270 -401
  200. package/package.json +3 -2
@@ -1,8 +1,9 @@
1
+ import { folioAIBlockIdStability } from "./ai-edits/block-identity.js";
1
2
  import { FolioDocxReviewer } from "./ai-edits/headless.js";
2
- import { createWordDiffSession } from "./ai-edits/word-diff.js";
3
+ import { alignFolioContentBlocks, alignFolioContentStructure, createFolioContentAlignmentWorkSession, exceedsFolioContentLcsBudget } from "./compare/content-alignment.js";
4
+ import { compareAlignedFolioContent, createContentComparisonWorkSession } from "./compare/content.js";
3
5
  import { pairFolioDocumentStories } from "./document-stories.js";
4
6
  import { FOLIO_DOCUMENT_METADATA_PROPERTIES, FOLIO_DOCUMENT_PRIVACY_TRANSFORMS, PRIVATE_METADATA_PROPERTIES_BY_TRANSFORM, isFolioDocumentPrivacyTransform } from "./docx/metadataPrivacy.js";
5
- import { getFolioParaIdFromBlockId } from "./types/block-id.js";
6
7
  import { TaggedError, panic } from "better-result";
7
8
  //#region src/version-comparison.ts
8
9
  /**
@@ -19,25 +20,17 @@ import { TaggedError, panic } from "better-result";
19
20
  *
20
21
  * ## Alignment
21
22
  *
22
- * Blocks are paired across the two snapshots in three passes, each only
23
- * considering blocks the previous pass left unpaired:
23
+ * Compatible body and table/container segments are aligned by the neutral
24
+ * comparison core. Within each compatible segment, blocks are paired in
25
+ * three passes, each only considering blocks the previous pass left unpaired:
24
26
  *
25
- * 1. **Stable-id pairing.** Blocks whose ids are equal AND not a `seq-NNNN`
26
- * positional fallback ({@link getFolioParaIdFromBlockId} returns non-null)
27
- * are paired directly. This covers both id shapes a snapshot can carry:
28
- * - A real Word `w14:paraId`: stable identity, independent of text — an
29
- * equal-id pair with different text is a genuine edit (`modified`).
30
- * - `FolioDocxReviewer`'s deterministic fallback id (assigned when the
31
- * source paragraph has no `w14:paraId`), which hashes the paragraph's
32
- * TEXT plus its document ordinal. It is structurally indistinguishable
33
- * from a real paraId ({@link getFolioParaIdFromBlockId} can't tell them
34
- * apart), but pairing on equality is still safe: two equal deterministic
35
- * ids necessarily came from identical text at an identical ordinal, so
36
- * the pair is always text-equal (`unchanged`) — never a false
37
- * `modified`. What it can't do is FIND a paragraph whose ordinal shifted
38
- * (an insertion/deletion earlier in the document) even though its text
39
- * is unchanged: that pair has two different fallback ids and falls
40
- * through to pass 2.
27
+ * 1. **Stable-id pairing.** Blocks whose ids are equal and whose snapshot
28
+ * provenance marks those ids stable are paired directly. A source
29
+ * `w14:paraId` is stable identity independent of text, so an equal-id pair
30
+ * with different text is a genuine edit (`modified`). Deterministically
31
+ * synthesized ids and `seq-NNNN` fallbacks are positional instead: the
32
+ * adapter preserves that provenance so an ordinal shift falls through to
33
+ * exact-text alignment rather than becoming a false identity match.
41
34
  * 2. **Exact-text pairing.** An order-preserving LCS over remaining blocks,
42
35
  * matched by exact text equality. This is what recovers same-text blocks
43
36
  * that pass 1 missed because a fallback id shifted with the ordinal. Its
@@ -61,13 +54,10 @@ import { TaggedError, panic } from "better-result";
61
54
  *
62
55
  * Relocated content would otherwise report as an unrelated `deleted` +
63
56
  * `added` pair (both order-preserving passes drop crossing matches by
64
- * design). A post-pass re-classifies such pairs: an `added` and a `deleted`
65
- * block with identical text and at least {@link MOVE_MINIMUM_WORD_COUNT}
66
- * words become `movedFrom` / `movedTo` entries sharing a `moveGroupId`. The
67
- * word-count floor keeps boilerplate one-liners ("Confidential", empty
68
- * headings) from pairing as spurious moves. Blocks a positional zip already
69
- * mis-paired as `modified` are out of this pass's reach — a known limitation
70
- * of the gap fallback, not of the move pass.
57
+ * design). The neutral comparison core re-classifies eligible exact and
58
+ * closely edited pairs as `movedFrom` / `movedTo` entries sharing a
59
+ * `moveGroupId`. Candidate counts and similarity work share bounded budgets
60
+ * across every story in the package comparison.
71
61
  *
72
62
  * ## Format-only changes
73
63
  *
@@ -89,319 +79,58 @@ const isFolioVersionComparisonScope = (value) => FOLIO_VERSION_COMPARISON_SCOPES
89
79
  var InvalidFolioVersionComparisonOptionsError = class extends TaggedError("InvalidFolioVersionComparisonOptionsError") {};
90
80
  const FOLIO_VERSION_COMPARISON_PRIVACY_TRANSFORMS = FOLIO_DOCUMENT_PRIVACY_TRANSFORMS;
91
81
  const isFolioVersionComparisonPrivacyTransform = (value) => isFolioDocumentPrivacyTransform(value);
92
- /** Run-level formatting properties compared for `formatChanged` detection. */
93
- const FORMAT_PROPERTIES = [
94
- "bold",
95
- "italic",
96
- "underline",
97
- "strike",
98
- "fontFamily",
99
- "fontSizePt",
100
- "color"
101
- ];
102
- const isStableBlockId = (id) => getFolioParaIdFromBlockId(id) !== null;
103
- /**
104
- * Longest increasing subsequence by `revisedIndex`, assuming `pairs` is
105
- * already sorted by `baseIndex` ascending. Drops any pair that would make
106
- * the alignment walk backward in the revised document — the guard against
107
- * both id collisions (pass 1) and any crossing match (pass 1 + 2 combined).
108
- */
109
- const longestIncreasingByRevisedIndex = (pairs) => {
110
- if (pairs.length === 0) return [];
111
- const lengths = new Int32Array(pairs.length).fill(1);
112
- const predecessors = new Int32Array(pairs.length).fill(-1);
113
- let bestEnd = 0;
114
- for (let i = 0; i < pairs.length; i++) {
115
- for (let j = 0; j < i; j++) {
116
- const current = pairs[j];
117
- const candidate = pairs[i];
118
- if (!current || !candidate) continue;
119
- if (current.revisedIndex < candidate.revisedIndex && (lengths[j] ?? 0) + 1 > (lengths[i] ?? 0)) {
120
- lengths[i] = (lengths[j] ?? 0) + 1;
121
- predecessors[i] = j;
122
- }
123
- }
124
- if ((lengths[i] ?? 0) > (lengths[bestEnd] ?? 0)) bestEnd = i;
125
- }
126
- const ordered = [];
127
- for (let cursor = bestEnd; cursor !== -1; cursor = predecessors[cursor] ?? -1) {
128
- const pair = pairs[cursor];
129
- if (pair) ordered.push(pair);
130
- }
131
- return ordered.toReversed();
82
+ /** Inline formatting properties compared for `formatChanged` detection. */
83
+ const INLINE_FORMAT_PROPERTIES = {
84
+ bold: true,
85
+ italic: true,
86
+ underline: true,
87
+ strike: true,
88
+ fontFamily: true,
89
+ fontSizePt: true,
90
+ color: true
132
91
  };
133
- /** Pass 1: pair blocks with equal, non-`seq-NNNN` ids. See the module doc comment. */
134
- const pairByStableId = (base, revised) => {
135
- const revisedIndexById = /* @__PURE__ */ new Map();
136
- revised.forEach((block, revisedIndex) => {
137
- if (isStableBlockId(block.id)) revisedIndexById.set(block.id, revisedIndex);
138
- });
139
- const candidates = [];
140
- base.forEach((block, baseIndex) => {
141
- if (!isStableBlockId(block.id)) return;
142
- const revisedIndex = revisedIndexById.get(block.id);
143
- if (revisedIndex !== void 0) candidates.push({
144
- baseIndex,
145
- revisedIndex
146
- });
147
- });
148
- return longestIncreasingByRevisedIndex(candidates);
92
+ const PARAGRAPH_FORMAT_PROPERTIES = {
93
+ styleId: true,
94
+ listLevel: true,
95
+ alignment: true,
96
+ spacing: true
149
97
  };
150
- /**
151
- * Cell budget for pass 2's O(m·n) exact-text LCS table (`dp` below allocates
152
- * `(m + 1) * (n + 1)` numbers). A document with no `w14:paraId`s — or an
153
- * adversarial one crafted to defeat pass 1 — can leave thousands of blocks
154
- * unpaired on both sides; without a cap, `pairByExactText` would allocate a
155
- * quadratic-sized table for it. Past this budget, {@link exceedsLcsBudget}
156
- * makes pass 2 back off entirely so alignment falls through to pass 3's
157
- * linear positional zip instead — pairing is less precise for these
158
- * degenerate inputs, but memory use stays bounded.
159
- */
160
- const MAX_LCS_CELLS = 4e6;
161
- /** True when an `unpairedBaseCount * unpairedRevisedCount` LCS table would exceed {@link MAX_LCS_CELLS}. */
162
- const exceedsLcsBudget = (unpairedBaseCount, unpairedRevisedCount) => unpairedBaseCount * unpairedRevisedCount > MAX_LCS_CELLS;
163
- const createLcsBudget = () => ({ remainingCells: MAX_LCS_CELLS });
164
- /** Pass 2: order-preserving LCS by exact text equality over the blocks pass 1 left unpaired. */
165
- const pairByExactText = (base, revised, lcsBudget) => {
166
- const m = base.length;
167
- const n = revised.length;
168
- if (m === 0 || n === 0) return [];
169
- if (exceedsLcsBudget(m, n) || lcsBudget.remainingCells <= 0) return [];
170
- lcsBudget.remainingCells -= m * n;
171
- const baseTexts = base.map(({ block }) => block.text);
172
- const revisedTexts = revised.map(({ block }) => block.text);
173
- const stride = n + 1;
174
- const dp = new Int32Array((m + 1) * stride);
175
- for (let i = m - 1; i >= 0; i--) {
176
- const rowOffset = i * stride;
177
- const nextRowOffset = (i + 1) * stride;
178
- const baseText = baseTexts[i];
179
- for (let j = n - 1; j >= 0; j--) {
180
- const revisedText = revisedTexts[j];
181
- dp[rowOffset + j] = baseText === revisedText ? (dp[nextRowOffset + j + 1] ?? 0) + 1 : Math.max(dp[nextRowOffset + j] ?? 0, dp[rowOffset + j + 1] ?? 0);
182
- }
183
- }
184
- const pairs = [];
185
- let i = 0;
186
- let j = 0;
187
- while (i < m && j < n) {
188
- const baseEntry = base[i];
189
- const revisedEntry = revised[j];
190
- if (!baseEntry || !revisedEntry) break;
191
- if (baseTexts[i] === revisedTexts[j]) {
192
- pairs.push({
193
- baseIndex: baseEntry.index,
194
- revisedIndex: revisedEntry.index
195
- });
196
- i++;
197
- j++;
198
- continue;
199
- }
200
- if ((dp[(i + 1) * stride + j] ?? 0) >= (dp[i * stride + j + 1] ?? 0)) i++;
201
- else j++;
202
- }
203
- return pairs;
98
+ const exceedsLcsBudget = exceedsFolioContentLcsBudget;
99
+ const createLcsBudget = () => {
100
+ return { remainingCells: createFolioContentAlignmentWorkSession().remainingLcsCells };
204
101
  };
205
102
  /**
206
- * Run the three-pass alignment (see the module doc comment) over two block
207
- * snapshots and flatten it into an ordered event stream. Shared by
208
- * {@link compareDocxVersions} and the redline generator so both interpret
209
- * one document walk instead of re-deriving it.
103
+ * Compatibility adapter for the DOCX snapshot comparison surface.
210
104
  *
211
- * `lcsBudget` defaults to a fresh, single-call budget so a caller comparing
212
- * one block pair in isolation (the redline generator) behaves exactly as
213
- * before. {@link compareDocxVersions} passes one budget object shared across
214
- * every story pair instead, so pass 2's cell allowance is aggregate across
215
- * the whole comparison rather than reset per story.
105
+ * Older snapshots encode id stability in the id shape, so the adapter resolves
106
+ * that policy while the representation-neutral core can treat caller ids as
107
+ * stable by default.
216
108
  */
217
109
  const alignFolioBlocks = (baseBlocks, revisedBlocks, lcsBudget = createLcsBudget()) => {
218
- const stableIdAnchors = pairByStableId(baseBlocks, revisedBlocks);
219
- const usedBaseIndexes = new Set(stableIdAnchors.map((anchor) => anchor.baseIndex));
220
- const usedRevisedIndexes = new Set(stableIdAnchors.map((anchor) => anchor.revisedIndex));
221
- const baseRemaining = [];
222
- baseBlocks.forEach((block, blockIndex) => {
223
- if (!usedBaseIndexes.has(blockIndex)) baseRemaining.push({
224
- block,
225
- index: blockIndex
226
- });
110
+ const workSession = createFolioContentAlignmentWorkSession({ lcsCells: lcsBudget.remainingCells });
111
+ const events = alignFolioContentBlocks(baseBlocks, revisedBlocks, {
112
+ workSession,
113
+ stableIdMismatch: "pair",
114
+ idStability: folioAIBlockIdStability
227
115
  });
228
- const revisedRemaining = [];
229
- revisedBlocks.forEach((block, blockIndex) => {
230
- if (!usedRevisedIndexes.has(blockIndex)) revisedRemaining.push({
231
- block,
232
- index: blockIndex
233
- });
234
- });
235
- const exactTextAnchors = pairByExactText(baseRemaining, revisedRemaining, lcsBudget);
236
- const anchors = longestIncreasingByRevisedIndex([...stableIdAnchors, ...exactTextAnchors].toSorted((a, b) => a.baseIndex - b.baseIndex));
237
- const events = [];
238
- /** Pass 3: positionally zip the leftover blocks in one gap between anchors. */
239
- const emitGap = (baseFrom, baseTo, revisedFrom, revisedTo) => {
240
- const pairedCount = Math.min(baseTo - baseFrom, revisedTo - revisedFrom);
241
- for (let k = 0; k < pairedCount; k++) {
242
- const baseBlock = baseBlocks[baseFrom + k];
243
- const revisedBlock = revisedBlocks[revisedFrom + k];
244
- if (baseBlock && revisedBlock) events.push({
245
- type: "pair",
246
- baseBlock,
247
- revisedBlock
248
- });
249
- }
250
- for (let k = baseFrom + pairedCount; k < baseTo; k++) {
251
- const block = baseBlocks[k];
252
- if (block) events.push({
253
- type: "baseOnly",
254
- block
255
- });
256
- }
257
- for (let k = revisedFrom + pairedCount; k < revisedTo; k++) {
258
- const block = revisedBlocks[k];
259
- if (block) events.push({
260
- type: "revisedOnly",
261
- block
262
- });
263
- }
264
- };
265
- let baseCursor = 0;
266
- let revisedCursor = 0;
267
- for (const anchor of anchors) {
268
- emitGap(baseCursor, anchor.baseIndex, revisedCursor, anchor.revisedIndex);
269
- const baseBlock = baseBlocks[anchor.baseIndex];
270
- const revisedBlock = revisedBlocks[anchor.revisedIndex];
271
- if (baseBlock && revisedBlock) events.push({
272
- type: "pair",
273
- baseBlock,
274
- revisedBlock
275
- });
276
- baseCursor = anchor.baseIndex + 1;
277
- revisedCursor = anchor.revisedIndex + 1;
278
- }
279
- emitGap(baseCursor, baseBlocks.length, revisedCursor, revisedBlocks.length);
116
+ lcsBudget.remainingCells = workSession.remainingLcsCells;
280
117
  return events;
281
118
  };
282
- const previewRunsText = (runs) => runs.map((run) => run.text).join("");
283
- /**
284
- * Character-aligned formatting diff of two text-equal blocks. Walks both
285
- * blocks' preview runs in parallel and collects every property whose value
286
- * differs anywhere in the overlap. A side with `previewRuns === undefined`
287
- * (the snapshot omits them when every run is unstyled) counts as one
288
- * unstyled run spanning the whole text. When both sides carry runs but
289
- * their concatenated texts disagree (non-text inline content can make the
290
- * preview text drift from the block text), positions can't be aligned, so
291
- * detection backs off and reports no change.
292
- */
293
- const diffPreviewRunFormatting = (base, revised) => {
294
- if (base.previewRuns === void 0 && revised.previewRuns === void 0) return [];
295
- const baseText = base.previewRuns === void 0 ? null : previewRunsText(base.previewRuns);
296
- const revisedText = revised.previewRuns === void 0 ? null : previewRunsText(revised.previewRuns);
297
- if (baseText !== null && revisedText !== null && baseText !== revisedText) return [];
298
- const text = baseText ?? revisedText;
299
- if (text === null || text.length === 0) return [];
300
- const baseRuns = base.previewRuns ?? [{ text }];
301
- const revisedRuns = revised.previewRuns ?? [{ text }];
302
- const changed = /* @__PURE__ */ new Set();
303
- let baseRunIndex = 0;
304
- let revisedRunIndex = 0;
305
- let baseOffset = 0;
306
- let revisedOffset = 0;
307
- while (baseRunIndex < baseRuns.length && revisedRunIndex < revisedRuns.length) {
308
- const baseRun = baseRuns[baseRunIndex];
309
- const revisedRun = revisedRuns[revisedRunIndex];
310
- if (!baseRun || !revisedRun) break;
311
- const step = Math.min(baseRun.text.length - baseOffset, revisedRun.text.length - revisedOffset);
312
- if (step > 0) {
313
- for (const property of FORMAT_PROPERTIES) if (baseRun[property] !== revisedRun[property]) changed.add(property);
314
- }
315
- baseOffset += step;
316
- revisedOffset += step;
317
- if (baseOffset >= baseRun.text.length) {
318
- baseRunIndex++;
319
- baseOffset = 0;
320
- }
321
- if (revisedOffset >= revisedRun.text.length) {
322
- revisedRunIndex++;
323
- revisedOffset = 0;
324
- }
325
- }
326
- return FORMAT_PROPERTIES.filter((property) => changed.has(property));
327
- };
328
- /**
329
- * Floor for move detection: an added/deleted text must hold at least this
330
- * many whitespace-separated words before an identical pair re-classifies as
331
- * a move. Short boilerplate ("Confidential", a bare heading word) recurs
332
- * throughout real documents and would otherwise pair as spurious moves.
333
- */
334
- const MOVE_MINIMUM_WORD_COUNT = 3;
335
- const meetsMoveWordCount = (text) => {
336
- const words = text.matchAll(/\S+/gu);
337
- let count = 0;
338
- while (!words.next().done) if (++count >= MOVE_MINIMUM_WORD_COUNT) return true;
339
- return false;
119
+ const legacySegments = (segments) => segments.map(({ type, text }) => ({
120
+ type,
121
+ text
122
+ }));
123
+ const ownStringKeys = (value) => Object.keys(value);
124
+ const legacyFormattingProperties = (formatting) => {
125
+ const properties = [];
126
+ for (const property of ownStringKeys(PARAGRAPH_FORMAT_PROPERTIES)) if (formatting.paragraph?.[property] !== void 0) properties.push(property);
127
+ for (const property of ownStringKeys(INLINE_FORMAT_PROPERTIES)) if (formatting.ranges.some(({ formatting: range }) => range[property] !== void 0)) properties.push(property);
128
+ return properties;
340
129
  };
341
- /**
342
- * Cap on how many same-text `deleted` candidates {@link detectMoves} queues
343
- * per distinct text. Duplicated boilerplate at or above the word floor
344
- * (e.g. a repeated long clause) could otherwise grow one text's candidate
345
- * list without bound; past this cap, further same-text deletions are simply
346
- * left as `deleted` (never matched to a move) instead of queued.
347
- */
348
- const MAX_MOVE_CANDIDATES_PER_TEXT = 1e4;
349
- const dequeueDeletedIndex = (queue) => {
350
- if (!queue || queue.head >= queue.items.length) return;
351
- const index = queue.items[queue.head];
352
- queue.head += 1;
353
- return index;
354
- };
355
- /**
356
- * Re-classify `deleted` + `added` pairs with identical text as
357
- * `movedFrom` / `movedTo` entries sharing a `moveGroupId`, in place, so each
358
- * side keeps its slot in the revised-side document order. Matching is FIFO
359
- * per text, so duplicated boilerplate above the word floor pairs
360
- * first-to-first rather than fanning out.
361
- */
362
- const detectMoves = (changes, counts, firstMoveGroupId) => {
363
- const deletedIndexesByText = /* @__PURE__ */ new Map();
364
- changes.forEach((change, index) => {
365
- if (change.type !== "deleted" || !meetsMoveWordCount(change.text)) return;
366
- const queue = deletedIndexesByText.get(change.text);
367
- if (!queue) {
368
- deletedIndexesByText.set(change.text, {
369
- items: [index],
370
- head: 0
371
- });
372
- return;
373
- }
374
- if (queue.items.length < MAX_MOVE_CANDIDATES_PER_TEXT) queue.items.push(index);
375
- });
376
- if (deletedIndexesByText.size === 0) return;
377
- let moveGroupId = firstMoveGroupId - 1;
378
- changes.forEach((change, index) => {
379
- if (change.type !== "added") return;
380
- const deletedIndex = dequeueDeletedIndex(deletedIndexesByText.get(change.text));
381
- if (deletedIndex === void 0) return;
382
- const deleted = changes[deletedIndex];
383
- if (!deleted || deleted.type !== "deleted") return;
384
- moveGroupId++;
385
- changes[deletedIndex] = {
386
- type: "movedFrom",
387
- blockId: deleted.blockId,
388
- kind: deleted.kind,
389
- text: deleted.text,
390
- moveGroupId,
391
- baseHandle: deleted.baseHandle
392
- };
393
- changes[index] = {
394
- type: "movedTo",
395
- blockId: change.blockId,
396
- kind: change.kind,
397
- text: change.text,
398
- moveGroupId,
399
- revisedHandle: change.revisedHandle
400
- };
401
- counts.deleted--;
402
- counts.added--;
403
- counts.moved++;
404
- });
130
+ const legacyVersionChangeProperties = ({ blockProperties, formatting }) => {
131
+ const properties = [...blockProperties];
132
+ if (formatting) properties.push(...legacyFormattingProperties(formatting));
133
+ return properties;
405
134
  };
406
135
  const createSummaryCounts = () => ({
407
136
  added: 0,
@@ -421,85 +150,227 @@ const addSummaryCounts = (target, source) => {
421
150
  target.metadataChanged += source.metadataChanged;
422
151
  target.unchanged += source.unchanged;
423
152
  };
424
- const compareStoryBlocks = ({ baseStory, revisedStory, baseBlocks, revisedBlocks, firstMoveGroupId, includeText, includeFormatting, lcsBudget, diffText }) => {
153
+ /**
154
+ * A disabled text scope reclassifies text-only comparison units as unchanged;
155
+ * it never removes them from the summary. Moves count once at their target,
156
+ * while a split or merge retains its legacy two-unit accounting.
157
+ */
158
+ const excludedTextEventUnitCount = (event) => {
159
+ switch (event.type) {
160
+ case "unchanged":
161
+ case "formatting":
162
+ case "modified": return null;
163
+ case "movedFrom": return 0;
164
+ case "deleted":
165
+ case "inserted":
166
+ case "movedTo": return 1;
167
+ case "split": return event.revisedBlocks.length;
168
+ case "merge": return event.baseBlocks.length;
169
+ default: return panic("Unhandled scoped neutral comparison event", { event });
170
+ }
171
+ };
172
+ const compareStoryBlocks = ({ baseStory, revisedStory, baseBlocks, revisedBlocks, firstMoveGroupId, includeText, includeFormatting, workSession }) => {
173
+ const compared = compareAlignedFolioContent({
174
+ baseBlocks,
175
+ revisedBlocks,
176
+ steps: alignFolioContentStructure({
177
+ baseBlocks,
178
+ revisedBlocks,
179
+ workSession: workSession.alignment,
180
+ stableIdMismatch: "pair",
181
+ idStability: folioAIBlockIdStability
182
+ }),
183
+ workSession,
184
+ idStability: folioAIBlockIdStability,
185
+ maxChanges: Number.MAX_SAFE_INTEGER
186
+ });
187
+ if (compared.isErr()) return panic("A version comparison exceeded an unreachable internal result limit", { cause: compared.error });
425
188
  const changes = [];
426
189
  const counts = createSummaryCounts();
427
- for (const event of alignFolioBlocks(baseBlocks, revisedBlocks, lcsBudget)) {
428
- if (event.type === "pair") {
429
- if (!baseStory || !revisedStory) panic("A paired comparison event requires both story handles");
430
- const { baseBlock, revisedBlock } = event;
431
- const baseHandle = {
432
- story: baseStory,
433
- blockId: baseBlock.id
434
- };
435
- const revisedHandle = {
436
- story: revisedStory,
437
- blockId: revisedBlock.id
438
- };
439
- if (baseBlock.text !== revisedBlock.text) {
440
- if (!includeText) {
441
- counts.unchanged++;
442
- continue;
190
+ const baseBlockById = /* @__PURE__ */ new Map();
191
+ if (!includeText && includeFormatting) for (const block of baseBlocks) baseBlockById.set(block.id, block);
192
+ const baseHandle = (block) => {
193
+ if (!baseStory) return panic("A neutral comparison event requires a base story handle");
194
+ return {
195
+ story: baseStory,
196
+ blockId: block.id
197
+ };
198
+ };
199
+ const revisedHandle = (block) => {
200
+ if (!revisedStory) return panic("A neutral comparison event requires a revised story handle");
201
+ return {
202
+ story: revisedStory,
203
+ blockId: block.id
204
+ };
205
+ };
206
+ const addModified = ({ baseBlock, revisedBlock, segments, blockProperties, formatting }) => {
207
+ const changedProperties = legacyVersionChangeProperties({
208
+ blockProperties,
209
+ formatting: includeFormatting ? formatting : void 0
210
+ });
211
+ counts.modified++;
212
+ changes.push({
213
+ type: "modified",
214
+ blockId: revisedBlock.id,
215
+ kind: revisedBlock.kind,
216
+ segments: legacySegments(segments),
217
+ ...changedProperties.length > 0 && { changedProperties },
218
+ baseHandle: baseHandle(baseBlock),
219
+ revisedHandle: revisedHandle(revisedBlock)
220
+ });
221
+ };
222
+ const addFormattingOrUnchanged = ({ baseBlock, revisedBlock, formatting }) => {
223
+ const changedProperties = includeFormatting && formatting ? legacyFormattingProperties(formatting) : [];
224
+ if (changedProperties.length === 0) {
225
+ counts.unchanged++;
226
+ return;
227
+ }
228
+ counts.formatChanged++;
229
+ changes.push({
230
+ type: "formatChanged",
231
+ blockId: revisedBlock.id,
232
+ kind: revisedBlock.kind,
233
+ text: revisedBlock.text,
234
+ changedProperties,
235
+ baseHandle: baseHandle(baseBlock),
236
+ revisedHandle: revisedHandle(revisedBlock)
237
+ });
238
+ };
239
+ const addDeleted = (block) => {
240
+ counts.deleted++;
241
+ changes.push({
242
+ type: "deleted",
243
+ blockId: block.id,
244
+ kind: block.kind,
245
+ text: block.text,
246
+ baseHandle: baseHandle(block)
247
+ });
248
+ };
249
+ const addInserted = (block) => {
250
+ counts.added++;
251
+ changes.push({
252
+ type: "added",
253
+ blockId: block.id,
254
+ kind: block.kind,
255
+ text: block.text,
256
+ revisedHandle: revisedHandle(block)
257
+ });
258
+ };
259
+ for (const event of compared.value.events) {
260
+ if (!includeText && includeFormatting && event.type === "movedTo" && event.formatting) {
261
+ const baseBlock = baseBlockById.get(event.baseBlockId);
262
+ if (!baseBlock) return panic("A moved comparison event requires its base block", { blockId: event.baseBlockId });
263
+ addFormattingOrUnchanged({
264
+ baseBlock,
265
+ revisedBlock: event.revisedBlocks[0],
266
+ formatting: event.formatting
267
+ });
268
+ continue;
269
+ }
270
+ if (!includeText) {
271
+ const excludedUnitCount = excludedTextEventUnitCount(event);
272
+ if (excludedUnitCount !== null) {
273
+ counts.unchanged += excludedUnitCount;
274
+ continue;
275
+ }
276
+ }
277
+ switch (event.type) {
278
+ case "unchanged":
279
+ counts.unchanged++;
280
+ break;
281
+ case "formatting":
282
+ addFormattingOrUnchanged({
283
+ baseBlock: event.baseBlocks[0],
284
+ revisedBlock: event.revisedBlocks[0],
285
+ formatting: event.formatting
286
+ });
287
+ break;
288
+ case "modified": {
289
+ const baseBlock = event.baseBlocks[0];
290
+ const revisedBlock = event.revisedBlocks[0];
291
+ if (includeText && (baseBlock.text !== revisedBlock.text || event.changedProperties.length)) {
292
+ addModified({
293
+ baseBlock,
294
+ revisedBlock,
295
+ segments: event.segments,
296
+ blockProperties: event.changedProperties,
297
+ formatting: event.formatting
298
+ });
299
+ break;
443
300
  }
444
- counts.modified++;
301
+ addFormattingOrUnchanged({
302
+ baseBlock,
303
+ revisedBlock,
304
+ formatting: event.formatting
305
+ });
306
+ break;
307
+ }
308
+ case "deleted":
309
+ addDeleted(event.baseBlocks[0]);
310
+ break;
311
+ case "inserted":
312
+ addInserted(event.revisedBlocks[0]);
313
+ break;
314
+ case "movedFrom": {
315
+ const block = event.baseBlocks[0];
445
316
  changes.push({
446
- type: "modified",
447
- blockId: revisedBlock.id,
448
- kind: revisedBlock.kind,
449
- segments: diffText(baseBlock.text, revisedBlock.text),
450
- baseHandle,
451
- revisedHandle
317
+ type: "movedFrom",
318
+ blockId: block.id,
319
+ kind: block.kind,
320
+ text: block.text,
321
+ moveGroupId: firstMoveGroupId + event.moveId - 1,
322
+ baseHandle: baseHandle(block)
452
323
  });
453
- continue;
324
+ break;
454
325
  }
455
- const changedProperties = includeFormatting ? diffPreviewRunFormatting(baseBlock, revisedBlock) : [];
456
- if (changedProperties.length > 0) {
457
- counts.formatChanged++;
326
+ case "movedTo": {
327
+ const block = event.revisedBlocks[0];
328
+ const changedProperties = legacyVersionChangeProperties({
329
+ blockProperties: event.changedProperties ?? [],
330
+ formatting: includeFormatting ? event.formatting : void 0
331
+ });
458
332
  changes.push({
459
- type: "formatChanged",
460
- blockId: revisedBlock.id,
461
- kind: revisedBlock.kind,
462
- text: revisedBlock.text,
463
- changedProperties,
464
- baseHandle,
465
- revisedHandle
333
+ type: "movedTo",
334
+ blockId: block.id,
335
+ kind: block.kind,
336
+ text: block.text,
337
+ moveGroupId: firstMoveGroupId + event.moveId - 1,
338
+ ...event.segments && { segments: legacySegments(event.segments) },
339
+ ...changedProperties.length > 0 && { changedProperties },
340
+ revisedHandle: revisedHandle(block)
466
341
  });
467
- continue;
342
+ counts.moved++;
343
+ break;
468
344
  }
469
- counts.unchanged++;
470
- continue;
471
- }
472
- if (event.type === "baseOnly") {
473
- if (!includeText) continue;
474
- if (!baseStory) panic("A base-only comparison event requires a base story handle");
475
- counts.deleted++;
476
- changes.push({
477
- type: "deleted",
478
- blockId: event.block.id,
479
- kind: event.block.kind,
480
- text: event.block.text,
481
- baseHandle: {
482
- story: baseStory,
483
- blockId: event.block.id
484
- }
485
- });
486
- continue;
487
- }
488
- if (!includeText) continue;
489
- if (!revisedStory) panic("A revised-only comparison event requires a revised story handle");
490
- counts.added++;
491
- changes.push({
492
- type: "added",
493
- blockId: event.block.id,
494
- kind: event.block.kind,
495
- text: event.block.text,
496
- revisedHandle: {
497
- story: revisedStory,
498
- blockId: event.block.id
345
+ case "split": {
346
+ const baseBlock = event.baseBlocks[0];
347
+ const [firstRevised, secondRevised] = event.revisedBlocks;
348
+ addModified({
349
+ baseBlock,
350
+ revisedBlock: firstRevised,
351
+ segments: workSession.diffText(baseBlock.text, firstRevised.text),
352
+ blockProperties: [],
353
+ formatting: void 0
354
+ });
355
+ addInserted(secondRevised);
356
+ break;
499
357
  }
500
- });
358
+ case "merge": {
359
+ const [firstBase, secondBase] = event.baseBlocks;
360
+ const revisedBlock = event.revisedBlocks[0];
361
+ addModified({
362
+ baseBlock: firstBase,
363
+ revisedBlock,
364
+ segments: workSession.diffText(firstBase.text, revisedBlock.text),
365
+ blockProperties: [],
366
+ formatting: void 0
367
+ });
368
+ addDeleted(secondBase);
369
+ break;
370
+ }
371
+ default: panic("Unhandled neutral content comparison event", { event });
372
+ }
501
373
  }
502
- detectMoves(changes, counts, firstMoveGroupId);
503
374
  return {
504
375
  baseStory,
505
376
  revisedStory,
@@ -581,8 +452,7 @@ const compareDocxVersions = async (base, revised, options = {}) => {
581
452
  const baseStories = baseReviewer.listStories().map(({ handle }) => handle);
582
453
  const revisedStories = revisedReviewer.listStories().map(({ handle }) => handle);
583
454
  let nextMoveGroupId = 1;
584
- const lcsBudget = createLcsBudget();
585
- const wordDiffSession = createWordDiffSession();
455
+ const comparisonWorkSession = createContentComparisonWorkSession();
586
456
  for (const pair of pairFolioDocumentStories(baseStories, revisedStories)) {
587
457
  const baseBlocks = pair.baseStory ? baseReviewer.readReviewedStory({
588
458
  story: pair.baseStory,
@@ -599,8 +469,7 @@ const compareDocxVersions = async (base, revised, options = {}) => {
599
469
  firstMoveGroupId: nextMoveGroupId,
600
470
  includeText: scopes.has("text"),
601
471
  includeFormatting: scopes.has("formatting"),
602
- lcsBudget,
603
- diffText: wordDiffSession.diff
472
+ workSession: comparisonWorkSession
604
473
  });
605
474
  stories.push(storyDiff);
606
475
  for (const change of storyDiff.changes) changes.push(change);