@stll/folio-core 0.35.1 → 0.37.0

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 (236) hide show
  1. package/README.md +59 -1
  2. package/dist/ai-edits/apply.js +878 -377
  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 +141 -73
  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 +105 -20
  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 +62 -61
  20. package/dist/ai-edits/word-diff.d.ts +18 -3
  21. package/dist/ai-edits/word-diff.js +556 -67
  22. package/dist/ai-suggestions/conflict.js +1 -4
  23. package/dist/ai-suggestions/text-positions.d.ts +8 -0
  24. package/dist/ai-suggestions/text-positions.js +27 -5
  25. package/dist/compare/__fixtures__/body-sequence.js +3 -4
  26. package/dist/compare/column-alignment.d.ts +13 -13
  27. package/dist/compare/column-alignment.js +78 -39
  28. package/dist/compare/compare.d.ts +4 -3
  29. package/dist/compare/compare.js +48 -21
  30. package/dist/compare/content-alignment.d.ts +95 -0
  31. package/dist/compare/content-alignment.js +1450 -0
  32. package/dist/compare/content-types.d.ts +100 -0
  33. package/dist/compare/content-types.js +0 -0
  34. package/dist/compare/content.d.ts +209 -0
  35. package/dist/compare/content.js +885 -0
  36. package/dist/compare/formatting.d.ts +5 -5
  37. package/dist/compare/formatting.js +6 -4
  38. package/dist/compare/plan.d.ts +6 -1
  39. package/dist/compare/plan.js +171 -566
  40. package/dist/compare/scenario.d.ts +2 -2
  41. package/dist/compare/types.d.ts +3 -3
  42. package/dist/compare/verification.d.ts +8 -9
  43. package/dist/compare/verification.js +72 -50
  44. package/dist/compat/eigenpal.d.ts +5 -3
  45. package/dist/compat/eigenpal.js +3 -2
  46. package/dist/controller/headerFooterEditorManager.js +18 -10
  47. package/dist/controller/hiddenEditorApi.js +1 -1
  48. package/dist/controller/hiddenEditorManager.d.ts +7 -2
  49. package/dist/controller/hiddenEditorManager.js +15 -6
  50. package/dist/controller/layoutPipeline.js +5 -3
  51. package/dist/controller/noteEditorManager.js +12 -7
  52. package/dist/display-list/build/headerFooterPrimitives.js +1 -0
  53. package/dist/display-list/build/storyPrimitives.d.ts +5 -2
  54. package/dist/display-list/build/storyPrimitives.js +60 -3
  55. package/dist/display-list/build/tablePrimitives.js +30 -0
  56. package/dist/display-list/primitives.d.ts +1 -1
  57. package/dist/document-operations.d.ts +8 -4
  58. package/dist/document-operations.js +104 -12
  59. package/dist/docx/blockContentParser.js +6 -27
  60. package/dist/docx/commentParser.js +2 -4
  61. package/dist/docx/commentRangeIntegrity.js +2 -4
  62. package/dist/docx/documentClone.d.ts +2 -0
  63. package/dist/docx/documentClone.js +2 -0
  64. package/dist/docx/ensureParaIds.js +9 -4
  65. package/dist/docx/headerFooterParser.js +2 -4
  66. package/dist/docx/normalizeBaseDirection.js +5 -7
  67. package/dist/docx/numberingParser.d.ts +3 -1
  68. package/dist/docx/numberingParser.js +4 -1
  69. package/dist/docx/paragraphParser.js +34 -35
  70. package/dist/docx/paragraphPropertySource.d.ts +49 -0
  71. package/dist/docx/paragraphPropertySource.js +113 -0
  72. package/dist/docx/paragraphTraversal.js +30 -34
  73. package/dist/docx/rezip.d.ts +0 -8
  74. package/dist/docx/rezip.js +41 -16
  75. package/dist/docx/runConsolidator.js +2 -4
  76. package/dist/docx/runParser.js +6 -17
  77. package/dist/docx/sectionParser.js +1 -15
  78. package/dist/docx/serializer/paragraphSerializer.js +289 -30
  79. package/dist/docx/serializer/runSerializer.js +6 -12
  80. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -10
  81. package/dist/docx/serializer/tableSerializer.js +4 -23
  82. package/dist/docx/serializer/trackedChangeAttributes.d.ts +11 -0
  83. package/dist/docx/serializer/trackedChangeAttributes.js +31 -0
  84. package/dist/docx/server/build.d.ts +1 -1
  85. package/dist/docx/server/createBilingualDocument.js +8 -10
  86. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +22 -13
  87. package/dist/docx/server/validateDocxConformance.js +17 -13
  88. package/dist/docx/tableParser.js +1 -37
  89. package/dist/docx/trackedChangeInfo.d.ts +11 -0
  90. package/dist/docx/trackedChangeInfo.js +38 -0
  91. package/dist/docx/verbatimCapture.d.ts +20 -9
  92. package/dist/docx/verbatimCapture.js +208 -8
  93. package/dist/docx/xmlParser.d.ts +3 -1
  94. package/dist/docx/xmlParser.js +6 -1
  95. package/dist/docx/xmlSafety.d.ts +1 -1
  96. package/dist/docx/xmlSafety.js +157 -0
  97. package/dist/headless-layout.js +5 -4
  98. package/dist/index.d.ts +5 -3
  99. package/dist/index.js +3 -2
  100. package/dist/internal/headlessRevisionResolution.d.ts +23 -0
  101. package/dist/internal/headlessRevisionResolution.js +155 -0
  102. package/dist/internal/headlessRevisionResolutionGuard.d.ts +12 -0
  103. package/dist/internal/headlessRevisionResolutionGuard.js +28 -0
  104. package/dist/internal/pageBreakRunDescendantIndex.d.ts +9 -0
  105. package/dist/internal/pageBreakRunDescendantIndex.js +27 -0
  106. package/dist/internal/pageBreakRunPartition.d.ts +26 -0
  107. package/dist/internal/pageBreakRunPartition.js +35 -0
  108. package/dist/internal/pageBreakRunSourceDescendantIndex.d.ts +15 -0
  109. package/dist/internal/pageBreakRunSourceDescendantIndex.js +126 -0
  110. package/dist/internal/sectionEndpointResolution.d.ts +32 -0
  111. package/dist/internal/sectionEndpointResolution.js +38 -0
  112. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  113. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +16 -4
  114. package/dist/layout-bridge/convert/headerFooterLayout.js +52 -9
  115. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -0
  116. package/dist/layout-bridge/convert/toFlowBlocks.js +391 -82
  117. package/dist/layout-bridge/dom/clickToPositionDom.d.ts +8 -1
  118. package/dist/layout-bridge/dom/clickToPositionDom.js +114 -40
  119. package/dist/layout-bridge/dom/textStreamDom.d.ts +24 -0
  120. package/dist/layout-bridge/dom/textStreamDom.js +75 -0
  121. package/dist/layout-bridge/headerFooterLayout.js +16 -30
  122. package/dist/layout-engine/index.js +12 -3
  123. package/dist/layout-engine/measure/tableFragmentBorderGeometry.d.ts +17 -0
  124. package/dist/layout-engine/measure/tableFragmentBorderGeometry.js +24 -0
  125. package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
  126. package/dist/layout-engine/types.d.ts +7 -0
  127. package/dist/layout-painter/imageLayout.d.ts +1 -1
  128. package/dist/layout-painter/renderTable.js +20 -0
  129. package/dist/markdown/renderBlock.js +7 -10
  130. package/dist/model.d.ts +1 -1
  131. package/dist/paged-layout/rangeProjection.js +6 -9
  132. package/dist/paged-layout/transactionDirtyRange.d.ts +3 -1
  133. package/dist/paged-layout/transactionDirtyRange.js +23 -1
  134. package/dist/prosemirror/attrs/index.d.ts +7 -3
  135. package/dist/prosemirror/attrs/index.js +128 -7
  136. package/dist/prosemirror/bookmarkBoundaryIntegrity.d.ts +9 -0
  137. package/dist/prosemirror/bookmarkBoundaryIntegrity.js +22 -0
  138. package/dist/prosemirror/commands/comments.d.ts +10 -1
  139. package/dist/prosemirror/commands/comments.js +551 -87
  140. package/dist/prosemirror/commands/formatPainter.d.ts +45 -10
  141. package/dist/prosemirror/commands/formatPainter.js +319 -63
  142. package/dist/prosemirror/commands/index.d.ts +2 -2
  143. package/dist/prosemirror/commands/pageBreak.d.ts +5 -2
  144. package/dist/prosemirror/commands/pageBreak.js +18 -36
  145. package/dist/prosemirror/commands/propertyChangeScope.d.ts +27 -3
  146. package/dist/prosemirror/commands/propertyChangeScope.js +85 -14
  147. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -5
  148. package/dist/prosemirror/conversion/fromProseDoc.js +935 -223
  149. package/dist/prosemirror/conversion/runShadingMark.js +2 -2
  150. package/dist/prosemirror/conversion/toProseDoc.d.ts +26 -1
  151. package/dist/prosemirror/conversion/toProseDoc.js +439 -291
  152. package/dist/prosemirror/extensions/StarterKit.js +4 -0
  153. package/dist/prosemirror/extensions/core/ParagraphExtension.js +54 -6
  154. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -1
  155. package/dist/prosemirror/extensions/features/ListExtension.js +95 -123
  156. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +19 -6
  157. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +27 -1
  158. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +134 -30
  159. package/dist/prosemirror/extensions/features/pasteCleanup.js +9 -15
  160. package/dist/prosemirror/extensions/marks/BoldExtension.js +4 -3
  161. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.d.ts +4 -7
  162. package/dist/prosemirror/extensions/marks/CharacterStyleExtension.js +5 -11
  163. package/dist/prosemirror/extensions/marks/FontSizeExtension.js +3 -3
  164. package/dist/prosemirror/extensions/marks/ItalicExtension.js +4 -3
  165. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.d.ts +13 -0
  166. package/dist/prosemirror/extensions/marks/PageBreakRunOwnerExtension.js +39 -0
  167. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.d.ts +7 -1
  168. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +67 -21
  169. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +2 -0
  170. package/dist/prosemirror/extensions/marks/markUtils.d.ts +8 -1
  171. package/dist/prosemirror/extensions/marks/markUtils.js +88 -2
  172. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +44 -3
  173. package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
  174. package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +15 -4
  175. package/dist/prosemirror/extensions/nodes/PageBreakExtension.d.ts +6 -0
  176. package/dist/prosemirror/extensions/nodes/PageBreakExtension.js +6 -0
  177. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.d.ts +6 -0
  178. package/dist/prosemirror/extensions/nodes/PageBreakRunExtension.js +36 -0
  179. package/dist/prosemirror/findReplaceSelection.js +8 -3
  180. package/dist/prosemirror/index.d.ts +3 -3
  181. package/dist/prosemirror/listMarker.d.ts +26 -1
  182. package/dist/prosemirror/listMarker.js +108 -14
  183. package/dist/prosemirror/pageBreakRunProjection.d.ts +28 -0
  184. package/dist/prosemirror/pageBreakRunProjection.js +47 -0
  185. package/dist/prosemirror/paraText.js +6 -0
  186. package/dist/prosemirror/paragraphAlignment.d.ts +12 -0
  187. package/dist/prosemirror/paragraphAlignment.js +13 -0
  188. package/dist/prosemirror/paragraphSpacing.d.ts +29 -0
  189. package/dist/prosemirror/paragraphSpacing.js +125 -0
  190. package/dist/prosemirror/plugins/documentNumbering.d.ts +8 -0
  191. package/dist/prosemirror/plugins/documentNumbering.js +18 -0
  192. package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -2
  193. package/dist/prosemirror/plugins/pmTextScan.js +14 -4
  194. package/dist/prosemirror/plugins/suggestionMode.js +9 -11
  195. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +16 -0
  196. package/dist/prosemirror/rebaseParagraphRunFormatting.js +61 -0
  197. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +64 -0
  198. package/dist/prosemirror/runFormattingInlineCarriers.js +121 -0
  199. package/dist/prosemirror/runFormattingProvenance.d.ts +12 -0
  200. package/dist/prosemirror/runFormattingProvenance.js +48 -0
  201. package/dist/prosemirror/runFormattingReconciliation.d.ts +25 -0
  202. package/dist/prosemirror/runFormattingReconciliation.js +54 -0
  203. package/dist/prosemirror/runPropertyChangeResolution.d.ts +18 -0
  204. package/dist/prosemirror/runPropertyChangeResolution.js +18 -0
  205. package/dist/prosemirror/runStyleFormatting.d.ts +50 -0
  206. package/dist/prosemirror/runStyleFormatting.js +105 -0
  207. package/dist/prosemirror/schema/index.d.ts +3 -3
  208. package/dist/prosemirror/schema/marks.d.ts +84 -9
  209. package/dist/prosemirror/schema/marks.js +73 -1
  210. package/dist/prosemirror/schema/nodes.d.ts +30 -6
  211. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +14 -2
  212. package/dist/prosemirror/styles/resolvedStyleAttrs.js +45 -23
  213. package/dist/prosemirror/styles/styleResolver.d.ts +10 -13
  214. package/dist/prosemirror/styles/styleResolver.js +3 -3
  215. package/dist/prosemirror/styles/styleToggleCascade.d.ts +8 -1
  216. package/dist/prosemirror/styles/styleToggleCascade.js +24 -1
  217. package/dist/prosemirror/tableGridMutation.d.ts +20 -0
  218. package/dist/prosemirror/tableGridMutation.js +30 -0
  219. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +31 -0
  220. package/dist/prosemirror/trackedRunInlineAtoms.js +39 -0
  221. package/dist/prosemirror/utils/visualLineNavigation.js +27 -31
  222. package/dist/prosemirror/validation.js +23 -4
  223. package/dist/redline.js +4 -1
  224. package/dist/render-dom/HeaderFooterSelectionOverlay.js +20 -20
  225. package/dist/render-dom/RenderedDomContext.js +17 -34
  226. package/dist/server.d.ts +4 -4
  227. package/dist/server.js +2 -2
  228. package/dist/style-engine/styleEngine.d.ts +1 -1
  229. package/dist/utils/clipboard.js +1 -1
  230. package/dist/utils/findReplace.js +38 -21
  231. package/dist/utils/mergeDocumentContent.js +3 -3
  232. package/dist/utils/replaceText.js +2 -1
  233. package/dist/version-comparison.d.ts +38 -44
  234. package/dist/version-comparison.js +270 -399
  235. package/dist/watermark/index.js +2 -4
  236. package/package.json +8 -2
@@ -1,4 +1,7 @@
1
+ import { FolioAIBlock } from "./types.js";
1
2
  //#region src/ai-edits/block-identity.d.ts
3
+ /** Resolve explicit snapshot provenance, falling back to the legacy id-shape convention. */
4
+ declare const folioAIBlockIdStability: ({ id, idStability }: FolioAIBlock) => "stable" | "positional";
2
5
  declare const stripBlockIdentityAttrs: (attrs: Record<string, unknown>) => Record<string, unknown>;
3
6
  //#endregion
4
- export { stripBlockIdentityAttrs };
7
+ export { folioAIBlockIdStability, stripBlockIdentityAttrs };
@@ -1,9 +1,12 @@
1
+ import { getFolioParaIdFromBlockId } from "../types/block-id.js";
1
2
  //#region src/ai-edits/block-identity.ts
2
3
  const IDENTITY_BLOCK_ATTRS = /* @__PURE__ */ new Set(["paraId", "textId"]);
4
+ /** Resolve explicit snapshot provenance, falling back to the legacy id-shape convention. */
5
+ const folioAIBlockIdStability = ({ id, idStability }) => idStability ?? (getFolioParaIdFromBlockId(id) === null ? "positional" : "stable");
3
6
  const stripBlockIdentityAttrs = (attrs) => {
4
7
  const next = {};
5
8
  for (const [key, value] of Object.entries(attrs)) next[key] = IDENTITY_BLOCK_ATTRS.has(key) ? null : value;
6
9
  return next;
7
10
  };
8
11
  //#endregion
9
- export { stripBlockIdentityAttrs };
12
+ export { folioAIBlockIdStability, stripBlockIdentityAttrs };
@@ -1,6 +1,6 @@
1
1
  import { findParagraphByParaId } from "../prosemirror/utils/findParagraphByParaId.js";
2
2
  import { getFolioParaIdFromBlockId, getSequentialFolioBlockIdIndex } from "../types/block-id.js";
3
- import { buildCleanBlockText } from "./clean-text.js";
3
+ import { buildCleanBlockText, resolveCleanTextRange } from "./clean-text.js";
4
4
  import { createFolioAIEditSnapshot, hashFolioAIBlockText, isFolioAIContentBlock } from "./snapshot.js";
5
5
  //#region src/ai-edits/blockRange.ts
6
6
  const resolveFolioAIBlockRange = ({ blockId, doc, snapshot }) => {
@@ -28,14 +28,14 @@ const resolveFolioAITextRange = ({ range, doc, snapshot }) => {
28
28
  const blockNode = doc.nodeAt(blockRange.from);
29
29
  if (!blockNode?.isTextblock) return null;
30
30
  const cleanBlock = buildCleanBlockText(blockNode, blockRange.from);
31
- const from = cleanBlock.offsets[range.startOffset];
32
- const to = cleanBlock.offsets[range.endOffset];
33
- if (from === void 0 || to === void 0) return null;
34
- if (hashFolioAIBlockText(cleanBlock.text.slice(range.startOffset, range.endOffset)) !== range.selectedTextHash) return null;
35
- return clampRangeToDocSize(doc.content.size, {
36
- from,
37
- to
31
+ const resolved = resolveCleanTextRange({
32
+ cleanBlock,
33
+ startOffset: range.startOffset,
34
+ endOffset: range.endOffset
38
35
  });
36
+ if (resolved === null) return null;
37
+ if (hashFolioAIBlockText(cleanBlock.text.slice(range.startOffset, range.endOffset)) !== range.selectedTextHash) return null;
38
+ return clampRangeToDocSize(doc.content.size, resolved);
39
39
  };
40
40
  /**
41
41
  * Resolve a free-text passage inside a block to a `{ from, to }` PM range.
@@ -66,13 +66,13 @@ const resolvePassageRange = ({ blockId, text, doc, snapshot }) => {
66
66
  const cleanBlock = buildCleanBlockText(blockNode, blockRange.from);
67
67
  const match = matchPassage(cleanBlock.text, needleSource);
68
68
  if (match === null) return null;
69
- const from = cleanBlock.offsets[match.start];
70
- const to = cleanBlock.offsets[match.end];
71
- if (from === void 0 || to === void 0) return null;
72
- return clampRangeToDocSize(doc.content.size, {
73
- from,
74
- to
69
+ const resolved = resolveCleanTextRange({
70
+ cleanBlock,
71
+ startOffset: match.start,
72
+ endOffset: match.end
75
73
  });
74
+ if (resolved === null) return null;
75
+ return clampRangeToDocSize(doc.content.size, resolved);
76
76
  };
77
77
  /**
78
78
  * Turn a caller-supplied passage into a regex source that tolerates whitespace
@@ -1,3 +1,4 @@
1
+ import { PageBreakRunAttrs } from "../prosemirror/schema/nodes.js";
1
2
  import { Node } from "prosemirror-model";
2
3
  //#region src/ai-edits/clean-text.d.ts
3
4
  /**
@@ -21,7 +22,45 @@ import { Node } from "prosemirror-model";
21
22
  type CleanBlockText = {
22
23
  text: string;
23
24
  offsets: number[];
25
+ /**
26
+ * Zero-width structural carriers projected at their clean-text boundary.
27
+ *
28
+ * A single clean offset cannot identify both sides of an atom: the PM
29
+ * position immediately after the preceding character is before the atom,
30
+ * while the next character starts after it. Callers that select text must
31
+ * resolve through {@link resolveCleanTextRange}; indexing `offsets`
32
+ * directly can accidentally absorb the carrier into an adjacent range.
33
+ */
34
+ structuralBoundaries: readonly CleanTextStructuralBoundary[];
24
35
  };
36
+ type CleanTextStructuralBoundary = {
37
+ type: "pageBreakRun";
38
+ /** Clean-text offset at which the zero-width carrier occurs. */
39
+ offset: number;
40
+ /** PM range owned by the carrier. */
41
+ from: number;
42
+ to: number;
43
+ /** Preserved even though `clear` does not alter page-break layout. */
44
+ clear?: PageBreakRunAttrs["clear"];
45
+ /** Whether the post-tracked-changes projection retains this carrier. */
46
+ presentInCleanView: boolean;
47
+ };
48
+ type ResolveCleanTextRangeOptions = {
49
+ cleanBlock: CleanBlockText;
50
+ startOffset: number;
51
+ endOffset: number;
52
+ };
53
+ /**
54
+ * Resolve clean-text offsets without selecting a zero-width structural atom.
55
+ *
56
+ * A range wholly on one side of a boundary is biased away from the atom. A
57
+ * range spanning both sides is unrepresentable as a generic text selection
58
+ * and returns `null`; a structural operation must own that mutation instead.
59
+ */
60
+ declare const resolveCleanTextRange: ({ cleanBlock, startOffset, endOffset }: ResolveCleanTextRangeOptions) => {
61
+ from: number;
62
+ to: number;
63
+ } | null;
25
64
  declare const buildCleanBlockText: (blockNode: Node, blockFrom: number) => CleanBlockText;
26
65
  /**
27
66
  * A "redline-aware" view of a textblock: the same left-to-right traversal as
@@ -44,4 +83,4 @@ declare const buildCleanBlockText: (blockNode: Node, blockFrom: number) => Clean
44
83
  */
45
84
  declare const buildAnnotatedBlockText: (blockNode: Node) => string;
46
85
  //#endregion
47
- export { CleanBlockText, buildAnnotatedBlockText, buildCleanBlockText };
86
+ export { CleanBlockText, CleanTextStructuralBoundary, buildAnnotatedBlockText, buildCleanBlockText, resolveCleanTextRange };
@@ -1,4 +1,39 @@
1
+ import { expectPageBreakRunAttrs } from "../prosemirror/attrs/index.js";
1
2
  //#region src/ai-edits/clean-text.ts
3
+ const EMPTY_CLEAN_TEXT_STRUCTURAL_BOUNDARIES = Object.freeze([]);
4
+ /**
5
+ * Resolve clean-text offsets without selecting a zero-width structural atom.
6
+ *
7
+ * A range wholly on one side of a boundary is biased away from the atom. A
8
+ * range spanning both sides is unrepresentable as a generic text selection
9
+ * and returns `null`; a structural operation must own that mutation instead.
10
+ */
11
+ const resolveCleanTextRange = ({ cleanBlock, startOffset, endOffset }) => {
12
+ if (!Number.isInteger(startOffset) || !Number.isInteger(endOffset) || startOffset < 0 || endOffset < startOffset || endOffset > cleanBlock.text.length) return null;
13
+ const baseFrom = cleanBlock.offsets[startOffset];
14
+ const baseTo = cleanBlock.offsets[endOffset];
15
+ if (baseFrom === void 0 || baseTo === void 0) return null;
16
+ const { structuralBoundaries } = cleanBlock;
17
+ if (structuralBoundaries.length === 0) return baseFrom <= baseTo ? {
18
+ from: baseFrom,
19
+ to: baseTo
20
+ } : null;
21
+ let from = baseFrom;
22
+ let to = baseTo;
23
+ for (const boundary of structuralBoundaries) {
24
+ if (boundary.offset > startOffset && boundary.offset < endOffset) return null;
25
+ if (boundary.offset === startOffset) from = Math.max(from, boundary.to);
26
+ if (startOffset !== endOffset && boundary.offset === endOffset) to = Math.min(to, boundary.from);
27
+ }
28
+ if (startOffset === endOffset) return {
29
+ from,
30
+ to: from
31
+ };
32
+ return from <= to ? {
33
+ from,
34
+ to
35
+ } : null;
36
+ };
2
37
  const DELETION_MARK = "deletion";
3
38
  const INSERTION_MARK = "insertion";
4
39
  const COMMENT_MARK = "comment";
@@ -6,8 +41,22 @@ const HIDDEN_MARK = "hidden";
6
41
  const buildCleanBlockText = (blockNode, blockFrom) => {
7
42
  let text = "";
8
43
  const offsets = [];
44
+ let structuralBoundaries;
9
45
  let lastEnd = blockFrom + 1;
10
46
  blockNode.descendants((node, pos) => {
47
+ if (node.type.name === "pageBreakRun") {
48
+ const from = blockFrom + 1 + pos;
49
+ const { clear } = expectPageBreakRunAttrs(node);
50
+ (structuralBoundaries ??= []).push({
51
+ type: "pageBreakRun",
52
+ offset: text.length,
53
+ from,
54
+ to: from + node.nodeSize,
55
+ ...clear !== void 0 ? { clear } : {},
56
+ presentInCleanView: !node.marks.some(({ type }) => type.name === DELETION_MARK)
57
+ });
58
+ return false;
59
+ }
11
60
  if (!node.isText || node.text === void 0) return true;
12
61
  if (node.marks.some((mark) => mark.type.name === DELETION_MARK || mark.type.name === HIDDEN_MARK)) return false;
13
62
  const startPos = blockFrom + 1 + pos;
@@ -19,7 +68,8 @@ const buildCleanBlockText = (blockNode, blockFrom) => {
19
68
  offsets.push(lastEnd);
20
69
  return {
21
70
  text,
22
- offsets
71
+ offsets,
72
+ structuralBoundaries: structuralBoundaries ?? EMPTY_CLEAN_TEXT_STRUCTURAL_BOUNDARIES
23
73
  };
24
74
  };
25
75
  /**
@@ -89,4 +139,4 @@ const renderAnnotatedSegment = ({ annotation, text }) => {
89
139
  return inner;
90
140
  };
91
141
  //#endregion
92
- export { buildAnnotatedBlockText, buildCleanBlockText };
142
+ export { buildAnnotatedBlockText, buildCleanBlockText, resolveCleanTextRange };
@@ -361,6 +361,9 @@ declare class FolioDocxReviewer {
361
361
  private resolveEveryStory;
362
362
  /** The current document model with edits merged back in. */
363
363
  toDocument(): document_d_exports.Document;
364
+ private captureReviewerState;
365
+ private documentFromStateSnapshot;
366
+ private captureSaveSnapshot;
364
367
  /**
365
368
  * Serialise the reviewed document to a new `.docx` buffer. Tries a selective
366
369
  * patch first (only changed paragraphs are rewritten; every other byte of the
@@ -369,7 +372,6 @@ declare class FolioDocxReviewer {
369
372
  */
370
373
  toBuffer(): Promise<ArrayBuffer>;
371
374
  private assertResolvedStoriesSerialized;
372
- private getChangedNoteParaIds;
373
375
  /**
374
376
  * Attempt the selective patch, treating a throw the same as a decline. Odd
375
377
  * source XML can make the paragraph diff throw rather than return `null`; a
@@ -1,20 +1,25 @@
1
1
  import { applyFolioDocumentOperations } from "../document-operations.js";
2
2
  import { getEndnoteText, getFootnoteText, isSeparatorEndnote, isSeparatorFootnote } from "../docx/footnoteParser.js";
3
3
  import { getHeaderFooterText } from "../docx/headerFooterParser.js";
4
+ import { recreateProseNodeWithParagraphPropertySource, transferProseParagraphPropertySource } from "../docx/paragraphPropertySource.js";
4
5
  import { parseDocx } from "../docx/parser.js";
5
6
  import { createReply } from "../docx/replyToComment.js";
6
7
  import { repackDocx } from "../docx/rezip.js";
7
8
  import { attemptSelectiveSave } from "../docx/selectiveSave.js";
8
- import { acceptAIEditRevision, acceptAllChanges, rejectAIEditRevision, rejectAllChanges } from "../prosemirror/commands/comments.js";
9
+ import { pluginsForHeadlessRevisionResolution } from "../internal/headlessRevisionResolutionGuard.js";
10
+ import { withTrackedSectionEndpointRemoval } from "../internal/sectionEndpointResolution.js";
11
+ import { acceptAIEditRevision, rejectAIEditRevision, resolveAllChangesInHeadlessState } from "../prosemirror/commands/comments.js";
9
12
  import { proseDocToBlocks, updateDocumentContent } from "../prosemirror/conversion/fromProseDoc.js";
10
13
  import { footnoteToProseDoc, headerFooterToProseDoc, toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
11
14
  import { ensureBaseDirectionInState } from "../prosemirror/extensions/features/AutoBidiDetectionExtension.js";
12
- import { getChangedParagraphIds, hasStructuralChanges, hasUntrackedChanges } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
15
+ import { getChangedParagraphIds, getTrackedSectionEndpointRemoval, hasStructuralChanges, hasUntrackedChanges } from "../prosemirror/extensions/features/ParagraphChangeTrackerExtension.js";
16
+ import { createDocumentNumberingPlugin } from "../prosemirror/plugins/documentNumbering.js";
17
+ import { createDocumentStylesPlugin, getDocumentStyleResolver } from "../prosemirror/plugins/documentStyles.js";
13
18
  import { schema, singletonManager } from "../prosemirror/schema/index.js";
14
19
  import { deterministicHexId } from "../utils/hexId.js";
15
20
  import { buildAnnotatedBlockText } from "./clean-text.js";
16
21
  import { getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js";
17
- import { createFolioAIEditSnapshot, folioStoryTables, isFolioAIContentBlock, normalizeFolioAIBlockText } from "./snapshot.js";
22
+ import { createFolioAIEditSnapshotWithStyleResolver, folioStoryTables, isFolioAIContentBlock, normalizeFolioAIBlockText } from "./snapshot.js";
18
23
  import { matchTableGeometry } from "./table-geometry.js";
19
24
  import { TaggedError } from "better-result";
20
25
  import { Fragment } from "prosemirror-model";
@@ -109,14 +114,17 @@ const ensureDeterministicParaIdsInDoc = (doc) => {
109
114
  let paraId = deterministicHexId(`${child.textContent}:${ordinal}`);
110
115
  for (let salt = 1; seen.has(paraId); salt++) paraId = deterministicHexId(`${child.textContent}:${ordinal}:${salt}`);
111
116
  seen.add(paraId);
112
- next = child.type.create({
117
+ next = recreateProseNodeWithParagraphPropertySource(child, { attrs: {
113
118
  ...child.attrs,
114
- paraId
115
- }, child.content, child.marks);
119
+ paraId,
120
+ idStability: "positional"
121
+ } });
116
122
  }
123
+ const paraId = next.attrs["paraId"];
124
+ if (typeof paraId === "string") transferProseParagraphPropertySource(next, child, paraId);
117
125
  } else if (child.childCount > 0) {
118
126
  const content = rewrite(child);
119
- if (content !== child.content) next = child.copy(content);
127
+ if (content !== child.content) next = recreateProseNodeWithParagraphPropertySource(child, { content });
120
128
  }
121
129
  if (next !== child) changed = true;
122
130
  children.push(next);
@@ -124,7 +132,7 @@ const ensureDeterministicParaIdsInDoc = (doc) => {
124
132
  return changed ? Fragment.fromArray(children) : parent.content;
125
133
  };
126
134
  const content = rewrite(doc);
127
- return content === doc.content ? doc : doc.copy(content);
135
+ return content === doc.content ? doc : recreateProseNodeWithParagraphPropertySource(doc, { content });
128
136
  };
129
137
  const FOLIO_REVIEWED_VIEWS = Object.freeze([
130
138
  "original",
@@ -134,7 +142,24 @@ const FOLIO_REVIEWED_VIEWS = Object.freeze([
134
142
  const FOLIO_RESOLVED_REVIEWED_VIEWS = Object.freeze(["original", "final"]);
135
143
  var UnsupportedFolioReviewedViewError = class extends TaggedError("UnsupportedFolioReviewedViewError") {};
136
144
  var FolioDocumentStoryNotFoundError = class extends TaggedError("FolioDocumentStoryNotFoundError") {};
145
+ const FOLIO_RESOLVED_STORY_SERIALIZATION_MISMATCHES = Object.freeze({
146
+ storyMissing: "story-missing",
147
+ revisionMarkupRemains: "revision-markup-remains",
148
+ textProjection: "text-projection",
149
+ blockProjection: "block-projection"
150
+ });
137
151
  var FolioResolvedStorySerializationError = class extends TaggedError("FolioResolvedStorySerializationError") {};
152
+ /**
153
+ * `idStability` records how an id entered the current snapshot. A synthesized
154
+ * paraId is positional until save writes it into the package, then becomes an
155
+ * authored stable id when reopened. Compare the persisted block projection
156
+ * without changing that live snapshot identity contract.
157
+ */
158
+ const resolvedStoryBlockProjection = (block) => {
159
+ const persisted = { ...block };
160
+ delete persisted.idStability;
161
+ return persisted;
162
+ };
138
163
  const MAIN_STORY = Object.freeze({ type: "main" });
139
164
  const headerFooterStoryKey = ({ type, relationshipId }) => `${type}:${relationshipId}`;
140
165
  const noteStoryKey = ({ type, noteId }) => `${type}:${noteId}`;
@@ -142,19 +167,18 @@ const secondaryStoryKey = (story) => story.type === "header" || story.type === "
142
167
  const editableStoryKey = (story) => story.type === "main" ? "main" : secondaryStoryKey(story);
143
168
  const isFolioReviewedView = (value) => FOLIO_REVIEWED_VIEWS.some((view) => view === value);
144
169
  const isFolioResolvedReviewedView = (value) => FOLIO_RESOLVED_REVIEWED_VIEWS.some((view) => view === value);
145
- const applyCommandToState = (state, command) => {
146
- let nextState = state;
147
- command(state, (transaction) => {
148
- nextState = nextState.apply(transaction);
149
- });
150
- return nextState;
151
- };
152
170
  const resolveReviewedState = (state, view) => {
153
171
  if (view === "current-markup") return state;
154
- return applyCommandToState(state, view === "original" ? rejectAllChanges() : acceptAllChanges());
172
+ return resolveAllChangesInHeadlessState(state, view === "original" ? "reject" : "accept");
155
173
  };
174
+ const createHeadlessPlugins = (styles, numbering) => [
175
+ ...pluginsForHeadlessRevisionResolution(singletonManager.getPlugins()),
176
+ createDocumentStylesPlugin(styles),
177
+ createDocumentNumberingPlugin(numbering)
178
+ ];
179
+ const createStateSnapshot = (state) => createFolioAIEditSnapshotWithStyleResolver(state.doc, getDocumentStyleResolver(state));
156
180
  const formatStoryStateForLLM = (state, annotated) => {
157
- const snapshot = createFolioAIEditSnapshot(state.doc);
181
+ const snapshot = createStateSnapshot(state);
158
182
  const blocks = snapshot.blocks.filter(isFolioAIContentBlock);
159
183
  if (!annotated) return blocks.map(formatBlockForLLM).join("\n");
160
184
  const nodeByStart = /* @__PURE__ */ new Map();
@@ -248,7 +272,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
248
272
  preloadFonts: false,
249
273
  password: options.password
250
274
  });
251
- const plugins = singletonManager.getPlugins();
275
+ const plugins = createHeadlessPlugins(baseDocument.package.styles, baseDocument.package.numbering);
252
276
  const state = ensureBaseDirectionInState(EditorState.create({
253
277
  schema,
254
278
  doc: ensureDeterministicParaIdsInDoc(toProseDoc(baseDocument)),
@@ -266,7 +290,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
266
290
  * the anchor map the apply layer resolves operations against.
267
291
  */
268
292
  snapshot() {
269
- return createFolioAIEditSnapshot(this.state.doc);
293
+ return createStateSnapshot(this.state);
270
294
  }
271
295
  /**
272
296
  * The package's numbering, flattened to what a reader sees: one entry per
@@ -313,7 +337,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
313
337
  /** Snapshot one editable story into stable, operation-ready blocks. */
314
338
  snapshotStory(story) {
315
339
  const state = this.getEditableStoryState(story);
316
- return state ? createFolioAIEditSnapshot(state.doc) : null;
340
+ return state ? createStateSnapshot(state) : null;
317
341
  }
318
342
  /**
319
343
  * One story's tables, in the document order {@link snapshotStory} numbers
@@ -376,7 +400,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
376
400
  return {
377
401
  story,
378
402
  view,
379
- snapshot: createFolioAIEditSnapshot(state.doc),
403
+ snapshot: createStateSnapshot(state),
380
404
  text: formatStoryStateForLLM(state, view === "current-markup"),
381
405
  changes: getTrackedChangesFromDoc(state.doc)
382
406
  };
@@ -394,7 +418,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
394
418
  this.resolvedStoryExpectations.set(editableStoryKey(story), {
395
419
  story,
396
420
  text: formatStoryStateForLLM(resolvedState, false),
397
- blocks: createFolioAIEditSnapshot(resolvedState.doc).blocks
421
+ blocks: createStateSnapshot(resolvedState).blocks.map(resolvedStoryBlockProjection)
398
422
  });
399
423
  return true;
400
424
  }
@@ -413,6 +437,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
413
437
  },
414
438
  ...options.snapshot !== void 0 && { snapshot: options.snapshot },
415
439
  ...options.revisionStamp !== void 0 && { revisionStamp: options.revisionStamp },
440
+ ...options.wordDiff !== void 0 && { wordDiff: options.wordDiff },
416
441
  createUndoEntry: false
417
442
  });
418
443
  return {
@@ -432,6 +457,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
432
457
  batch,
433
458
  ...options.snapshot !== void 0 && { snapshot: options.snapshot },
434
459
  ...options.revisionStamp !== void 0 && { revisionStamp: options.revisionStamp },
460
+ ...options.wordDiff !== void 0 && { wordDiff: options.wordDiff },
435
461
  createUndoEntry: true
436
462
  });
437
463
  }
@@ -458,7 +484,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
458
484
  };
459
485
  const result = applyFolioDocumentOperations({
460
486
  view,
461
- snapshot: snapshot ?? createFolioAIEditSnapshot(beforeState.doc),
487
+ snapshot: snapshot ?? createStateSnapshot(beforeState),
462
488
  batch,
463
489
  story: story.type === "main" ? "main" : story,
464
490
  author: this.author,
@@ -524,7 +550,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
524
550
  * feed straight back into {@link applyOperations} and accept / reject.
525
551
  */
526
552
  getContent() {
527
- return this.snapshot().blocks;
553
+ return [...this.snapshot().blocks];
528
554
  }
529
555
  /**
530
556
  * The body as LLM-ready plain text: one line per block, each prefixed with
@@ -761,29 +787,73 @@ var FolioDocxReviewer = class FolioDocxReviewer {
761
787
  * document still carries a redline nobody can see from the body.
762
788
  */
763
789
  acceptAll() {
764
- return this.resolveEveryStory(acceptAllChanges());
790
+ return this.resolveEveryStory("accept");
765
791
  }
766
792
  /** Reject every tracked change in the package. See {@link acceptAll}. */
767
793
  rejectAll() {
768
- return this.resolveEveryStory(rejectAllChanges());
794
+ return this.resolveEveryStory("reject");
769
795
  }
770
- resolveEveryStory(command) {
796
+ resolveEveryStory(mode) {
771
797
  let count = 0;
772
798
  for (const { handle } of this.listStories()) {
773
799
  const state = this.getEditableStoryState(handle);
774
800
  if (!state) continue;
775
801
  count += getTrackedChangesFromDoc(state.doc).length;
776
- this.runStoryCommand(command, handle);
802
+ this.setEditableStoryState(handle, resolveAllChangesInHeadlessState(state, mode));
777
803
  }
778
804
  return count;
779
805
  }
780
806
  /** The current document model with edits merged back in. */
781
807
  toDocument() {
782
- const document = updateDocumentContent(this.baseDocument, this.state.doc);
783
- this.mergeEditedSecondaryStories(document);
784
- if (this.createdComments.length > 0 || this.resolvedOverrides.size > 0) document.package.document.comments = this.withResolvedOverrides([...document.package.document.comments ?? [], ...this.createdComments]);
808
+ return this.documentFromStateSnapshot(this.captureReviewerState());
809
+ }
810
+ captureReviewerState() {
811
+ const secondaryStoryStates = [];
812
+ for (const { handle, initialState, state } of this.secondaryStoryStates.values()) secondaryStoryStates.push({
813
+ handle,
814
+ initialState,
815
+ state
816
+ });
817
+ return {
818
+ mainState: this.state,
819
+ secondaryStoryStates,
820
+ createdComments: [...this.createdComments],
821
+ resolvedOverrides: new Map(this.resolvedOverrides),
822
+ resolvedStoryExpectations: [...this.resolvedStoryExpectations.values()]
823
+ };
824
+ }
825
+ documentFromStateSnapshot(snapshot) {
826
+ const document = updateDocumentContent(this.baseDocument, snapshot.mainState.doc);
827
+ this.mergeEditedSecondaryStories(document, snapshot.secondaryStoryStates);
828
+ if (snapshot.createdComments.length > 0 || snapshot.resolvedOverrides.size > 0) document.package.document.comments = this.withResolvedOverrides([...document.package.document.comments ?? [], ...snapshot.createdComments], snapshot.resolvedOverrides);
785
829
  return document;
786
830
  }
831
+ captureSaveSnapshot() {
832
+ const snapshot = this.captureReviewerState();
833
+ const changedParaIds = new Set(getChangedParagraphIds(snapshot.mainState));
834
+ let structuralChange = hasStructuralChanges(snapshot.mainState);
835
+ let untrackedChanges = hasUntrackedChanges(snapshot.mainState);
836
+ const changedNoteParaIds = /* @__PURE__ */ new Set();
837
+ for (const entry of snapshot.secondaryStoryStates) {
838
+ if (entry.handle.type !== "footnote" && entry.handle.type !== "endnote") continue;
839
+ for (const paraId of getChangedParagraphIds(entry.state)) {
840
+ changedParaIds.add(paraId);
841
+ changedNoteParaIds.add(paraId);
842
+ }
843
+ structuralChange ||= hasStructuralChanges(entry.state);
844
+ untrackedChanges ||= hasUntrackedChanges(entry.state);
845
+ }
846
+ return {
847
+ document: this.documentFromStateSnapshot(snapshot),
848
+ path: structuralChange || untrackedChanges ? { type: "full-repack" } : {
849
+ type: "selective-first",
850
+ changedParaIds
851
+ },
852
+ changedNoteParaIds,
853
+ sectionEndpointRemoval: getTrackedSectionEndpointRemoval(snapshot.mainState),
854
+ resolvedStoryExpectations: snapshot.resolvedStoryExpectations
855
+ };
856
+ }
787
857
  /**
788
858
  * Serialise the reviewed document to a new `.docx` buffer. Tries a selective
789
859
  * patch first (only changed paragraphs are rewritten; every other byte of the
@@ -791,67 +861,65 @@ var FolioDocxReviewer = class FolioDocxReviewer {
791
861
  * structural edits — the same two-tier path the editor's save uses.
792
862
  */
793
863
  async toBuffer() {
794
- const document = this.toDocument();
795
- const selective = await this.trySelectiveSave(document);
864
+ const save = this.captureSaveSnapshot();
865
+ const selective = save.path.type === "selective-first" ? await this.trySelectiveSave(save.document, save.path.changedParaIds) : null;
796
866
  if (selective) {
797
- await this.assertResolvedStoriesSerialized(selective);
867
+ await this.assertResolvedStoriesSerialized(selective, save.resolvedStoryExpectations);
798
868
  return selective;
799
869
  }
800
- const buffer = await repackDocx({
801
- ...document,
870
+ const repackDocument = {
871
+ ...save.document,
802
872
  originalBuffer: this.originalBuffer
803
- }, { changedNoteParaIds: this.getChangedNoteParaIds() });
804
- await this.assertResolvedStoriesSerialized(buffer);
873
+ };
874
+ const buffer = save.sectionEndpointRemoval ? await withTrackedSectionEndpointRemoval({
875
+ document: repackDocument,
876
+ resolution: save.sectionEndpointRemoval,
877
+ repack: () => repackDocx(repackDocument, { changedNoteParaIds: save.changedNoteParaIds })
878
+ }) : await repackDocx(repackDocument, { changedNoteParaIds: save.changedNoteParaIds });
879
+ await this.assertResolvedStoriesSerialized(buffer, save.resolvedStoryExpectations);
805
880
  return buffer;
806
881
  }
807
- async assertResolvedStoriesSerialized(buffer) {
808
- if (this.resolvedStoryExpectations.size === 0) return;
882
+ async assertResolvedStoriesSerialized(buffer, expectations) {
883
+ if (expectations.length === 0) return;
809
884
  const reopened = await FolioDocxReviewer.fromBuffer(buffer);
810
- for (const { story, text, blocks } of this.resolvedStoryExpectations.values()) {
885
+ for (const { story, text, blocks } of expectations) {
811
886
  const serialized = reopened.readReviewedStory({
812
887
  story,
813
888
  view: "current-markup"
814
889
  });
815
890
  const serializedState = reopened.getEditableStoryState(story);
816
- if (serialized && serializedState && serialized.changes.length === 0 && formatStoryStateForLLM(serializedState, false) === text && JSON.stringify(createFolioAIEditSnapshot(serializedState.doc).blocks) === JSON.stringify(blocks)) continue;
891
+ const serializedText = serializedState ? formatStoryStateForLLM(serializedState, false) : null;
892
+ const serializedBlocks = serializedState ? createStateSnapshot(serializedState).blocks.map(resolvedStoryBlockProjection) : null;
893
+ const mismatches = [];
894
+ if (!serialized || !serializedState) mismatches.push(FOLIO_RESOLVED_STORY_SERIALIZATION_MISMATCHES.storyMissing);
895
+ else {
896
+ if (serialized.changes.length > 0) mismatches.push(FOLIO_RESOLVED_STORY_SERIALIZATION_MISMATCHES.revisionMarkupRemains);
897
+ if (serializedText !== text) mismatches.push(FOLIO_RESOLVED_STORY_SERIALIZATION_MISMATCHES.textProjection);
898
+ if (JSON.stringify(serializedBlocks) !== JSON.stringify(blocks)) mismatches.push(FOLIO_RESOLVED_STORY_SERIALIZATION_MISMATCHES.blockProjection);
899
+ }
900
+ if (mismatches.length === 0) continue;
817
901
  throw new FolioResolvedStorySerializationError({
818
902
  message: "Resolved document story did not persist to the serialized DOCX.",
819
903
  story,
820
- expectedText: text,
821
- actualText: serializedState ? formatStoryStateForLLM(serializedState, false) : null,
904
+ mismatches,
905
+ expectedBlockCount: blocks.length,
906
+ actualBlockCount: serializedBlocks?.length ?? null,
822
907
  remainingChangeCount: serialized?.changes.length ?? null
823
908
  });
824
909
  }
825
910
  }
826
- getChangedNoteParaIds() {
827
- const changed = /* @__PURE__ */ new Set();
828
- for (const entry of this.secondaryStoryStates.values()) {
829
- if (entry.handle.type !== "footnote" && entry.handle.type !== "endnote") continue;
830
- for (const paraId of getChangedParagraphIds(entry.state)) changed.add(paraId);
831
- }
832
- return changed;
833
- }
834
911
  /**
835
912
  * Attempt the selective patch, treating a throw the same as a decline. Odd
836
913
  * source XML can make the paragraph diff throw rather than return `null`; a
837
914
  * full repack is the correct lossless fallback in both cases, so the fallback
838
915
  * is the graceful handling — no separate error surface is needed here.
839
916
  */
840
- async trySelectiveSave(document) {
841
- const changedParaIds = new Set(getChangedParagraphIds(this.state));
842
- let structuralChange = hasStructuralChanges(this.state);
843
- let untrackedChanges = hasUntrackedChanges(this.state);
844
- for (const entry of this.secondaryStoryStates.values()) {
845
- if (entry.handle.type !== "footnote" && entry.handle.type !== "endnote") continue;
846
- for (const paraId of getChangedParagraphIds(entry.state)) changedParaIds.add(paraId);
847
- structuralChange ||= hasStructuralChanges(entry.state);
848
- untrackedChanges ||= hasUntrackedChanges(entry.state);
849
- }
917
+ async trySelectiveSave(document, changedParaIds) {
850
918
  try {
851
919
  return await attemptSelectiveSave(document, this.originalBuffer, {
852
920
  changedParaIds,
853
- structuralChange,
854
- hasUntrackedChanges: untrackedChanges
921
+ structuralChange: false,
922
+ hasUntrackedChanges: false
855
923
  });
856
924
  } catch {
857
925
  return null;
@@ -872,7 +940,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
872
940
  const state = ensureBaseDirectionInState(EditorState.create({
873
941
  schema,
874
942
  doc: ensureDeterministicParaIdsInDoc(storyDoc),
875
- plugins: singletonManager.getPlugins()
943
+ plugins: createHeadlessPlugins(this.baseDocument.package.styles, this.baseDocument.package.numbering)
876
944
  }));
877
945
  this.secondaryStoryStates.set(key, {
878
946
  handle: story,
@@ -918,19 +986,19 @@ var FolioDocxReviewer = class FolioDocxReviewer {
918
986
  const sourceText = source.type === "footnote" ? getFootnoteText(source) : getEndnoteText(source);
919
987
  return normalizeFolioAIBlockText(state?.doc.textContent ?? sourceText);
920
988
  }
921
- mergeEditedSecondaryStories(document) {
989
+ mergeEditedSecondaryStories(document, secondaryStoryStates = this.secondaryStoryStates.values()) {
922
990
  let headers;
923
991
  let footers;
924
992
  let footnotes;
925
993
  let endnotes;
926
- for (const entry of this.secondaryStoryStates.values()) {
994
+ for (const entry of secondaryStoryStates) {
927
995
  if (entry.state === entry.initialState) continue;
928
996
  if (entry.handle.type === "header" || entry.handle.type === "footer") {
929
997
  const source = this.getHeaderFooterStory(entry.handle);
930
998
  if (!source) continue;
931
999
  const edited = {
932
1000
  ...source,
933
- content: proseDocToBlocks(entry.state.doc)
1001
+ content: proseDocToBlocks(entry.state.doc, source.content, document.package.styles)
934
1002
  };
935
1003
  if (entry.handle.type === "header") {
936
1004
  headers ??= new Map(document.package.headers);
@@ -947,7 +1015,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
947
1015
  if (!source) continue;
948
1016
  const edited = {
949
1017
  ...source,
950
- content: proseDocToBlocks(entry.state.doc)
1018
+ content: proseDocToBlocks(entry.state.doc, source.content, document.package.styles)
951
1019
  };
952
1020
  footnotes ??= [...document.package.footnotes ?? []];
953
1021
  const index = footnotes.findIndex((note) => note.id === noteId);
@@ -959,7 +1027,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
959
1027
  if (!source) continue;
960
1028
  const edited = {
961
1029
  ...source,
962
- content: proseDocToBlocks(entry.state.doc)
1030
+ content: proseDocToBlocks(entry.state.doc, source.content, document.package.styles)
963
1031
  };
964
1032
  endnotes ??= [...document.package.endnotes ?? []];
965
1033
  const index = endnotes.findIndex((note) => note.id === noteId);
@@ -971,10 +1039,10 @@ var FolioDocxReviewer = class FolioDocxReviewer {
971
1039
  if (endnotes) document.package.endnotes = endnotes;
972
1040
  }
973
1041
  /** Apply any {@link resolveComment} overrides recorded for these comments. */
974
- withResolvedOverrides(comments) {
975
- if (this.resolvedOverrides.size === 0) return [...comments];
1042
+ withResolvedOverrides(comments, resolvedOverrides = this.resolvedOverrides) {
1043
+ if (resolvedOverrides.size === 0) return [...comments];
976
1044
  return comments.map((comment) => {
977
- const override = this.resolvedOverrides.get(comment.id);
1045
+ const override = resolvedOverrides.get(comment.id);
978
1046
  return override === void 0 ? comment : {
979
1047
  ...comment,
980
1048
  done: override