@stll/folio-core 0.37.4 → 0.38.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 (131) hide show
  1. package/dist/ai-edits/apply.js +275 -88
  2. package/dist/ai-edits/clean-text.js +12 -1
  3. package/dist/ai-edits/headless.d.ts +110 -0
  4. package/dist/ai-edits/headless.js +569 -40
  5. package/dist/ai-edits/index.d.ts +2 -1
  6. package/dist/ai-edits/snapshot.d.ts +24 -1
  7. package/dist/ai-edits/snapshot.js +206 -9
  8. package/dist/ai-edits/table-geometry.d.ts +7 -7
  9. package/dist/ai-edits/table-geometry.js +43 -27
  10. package/dist/ai-edits/table-template.d.ts +2 -1
  11. package/dist/ai-edits/table-template.js +9 -3
  12. package/dist/ai-edits/types.d.ts +38 -16
  13. package/dist/compare/__fixtures__/body-sequence.d.ts +4 -0
  14. package/dist/compare/__fixtures__/body-sequence.js +5 -3
  15. package/dist/compare/compare.d.ts +26 -2
  16. package/dist/compare/compare.js +444 -16
  17. package/dist/compare/content-alignment.js +216 -137
  18. package/dist/compare/content-types.d.ts +36 -2
  19. package/dist/compare/content.d.ts +13 -10
  20. package/dist/compare/content.js +69 -23
  21. package/dist/compare/formatting.d.ts +4 -22
  22. package/dist/compare/formatting.js +8 -112
  23. package/dist/compare/inline-atom-resources.d.ts +6 -0
  24. package/dist/compare/inline-atom-resources.js +26 -0
  25. package/dist/compare/inline-atoms.d.ts +35 -0
  26. package/dist/compare/inline-atoms.js +383 -0
  27. package/dist/compare/inline-provenance.d.ts +36 -0
  28. package/dist/compare/inline-provenance.js +334 -0
  29. package/dist/compare/plan.d.ts +4 -1
  30. package/dist/compare/plan.js +32 -4
  31. package/dist/compare/scenario.d.ts +4 -6
  32. package/dist/compare/section-boundary-properties.d.ts +63 -0
  33. package/dist/compare/section-boundary-properties.js +235 -0
  34. package/dist/compare/style-resources.d.ts +30 -0
  35. package/dist/compare/style-resources.js +404 -0
  36. package/dist/compare/types.d.ts +50 -11
  37. package/dist/compare/types.js +3 -1
  38. package/dist/compare/verification.d.ts +3 -5
  39. package/dist/compare/verification.js +8 -39
  40. package/dist/compat/eigenpal.d.ts +4 -4
  41. package/dist/compat/eigenpal.js +3 -3
  42. package/dist/document-operations.d.ts +5 -3
  43. package/dist/document-operations.js +112 -5
  44. package/dist/docx/blockContentParser.js +14 -2
  45. package/dist/docx/drawingIdNormalization.js +43 -2
  46. package/dist/docx/drawingRelationships.d.ts +10 -0
  47. package/dist/docx/drawingRelationships.js +28 -0
  48. package/dist/docx/fieldParser.js +1 -1
  49. package/dist/docx/hyperlinkParser.js +3 -5
  50. package/dist/docx/imageRawXml.d.ts +8 -0
  51. package/dist/docx/imageRawXml.js +13 -0
  52. package/dist/docx/newImage.d.ts +2 -2
  53. package/dist/docx/newImage.js +3 -3
  54. package/dist/docx/numberingParser.d.ts +1 -13
  55. package/dist/docx/numberingParser.js +31 -37
  56. package/dist/docx/paragraphParser.js +12 -9
  57. package/dist/docx/paragraphPropertySource.d.ts +8 -5
  58. package/dist/docx/paragraphPropertySource.js +16 -4
  59. package/dist/docx/removeHeaderFooterParts.d.ts +14 -0
  60. package/dist/docx/removeHeaderFooterParts.js +87 -0
  61. package/dist/docx/rezip.d.ts +3 -1
  62. package/dist/docx/rezip.js +143 -41
  63. package/dist/docx/runParser.js +16 -5
  64. package/dist/docx/sectionParser.js +3 -0
  65. package/dist/docx/sectionReferenceHistory.d.ts +17 -0
  66. package/dist/docx/sectionReferenceHistory.js +59 -0
  67. package/dist/docx/selectiveSave.js +11 -31
  68. package/dist/docx/selectiveXmlPatch.d.ts +8 -1
  69. package/dist/docx/selectiveXmlPatch.js +62 -2
  70. package/dist/docx/serializer/borderSerializer.d.ts +4 -1
  71. package/dist/docx/serializer/borderSerializer.js +20 -18
  72. package/dist/docx/serializer/commentSerializer.js +1 -1
  73. package/dist/docx/serializer/numberingSerializer.js +1 -1
  74. package/dist/docx/serializer/paragraphSerializer.js +13 -177
  75. package/dist/docx/serializer/partNamespaces.d.ts +1 -1
  76. package/dist/docx/serializer/partNamespaces.js +4 -0
  77. package/dist/docx/serializer/runSerializer.d.ts +1 -5
  78. package/dist/docx/serializer/runSerializer.js +4 -130
  79. package/dist/docx/serializer/sectionPropertiesSerializer.js +5 -2
  80. package/dist/docx/serializer/stylesSerializer.js +1 -1
  81. package/dist/docx/serializer/textFormattingSerializer.d.ts +17 -0
  82. package/dist/docx/serializer/textFormattingSerializer.js +144 -0
  83. package/dist/docx/tableParser.js +21 -11
  84. package/dist/docx/xmlParser.d.ts +10 -1
  85. package/dist/docx/xmlParser.js +23 -1
  86. package/dist/index.d.ts +4 -4
  87. package/dist/index.js +3 -3
  88. package/dist/internal/compare/inline-presentation.d.ts +20 -0
  89. package/dist/internal/compare/inline-presentation.js +222 -0
  90. package/dist/internal/paragraphFormattingSerialization.d.ts +25 -0
  91. package/dist/internal/paragraphFormattingSerialization.js +164 -0
  92. package/dist/internal/sectionReferenceResolution.d.ts +21 -0
  93. package/dist/internal/sectionReferenceResolution.js +72 -0
  94. package/dist/layout-bridge/convert/toFlowBlocks.js +1 -0
  95. package/dist/markdown/renderRuns.js +1 -0
  96. package/dist/markdown/renderTable.js +1 -0
  97. package/dist/prosemirror/attrs/index.js +20 -1
  98. package/dist/prosemirror/commands/comments.d.ts +11 -1
  99. package/dist/prosemirror/commands/comments.js +68 -6
  100. package/dist/prosemirror/commands/propertyChangeScope.d.ts +9 -3
  101. package/dist/prosemirror/commands/propertyChangeScope.js +4 -10
  102. package/dist/prosemirror/conversion/fromProseDoc.js +22 -22
  103. package/dist/prosemirror/conversion/toProseDoc.js +16 -57
  104. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
  105. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +3 -0
  106. package/dist/prosemirror/imageCommit.js +7 -9
  107. package/dist/prosemirror/paragraphIndentation.d.ts +21 -0
  108. package/dist/prosemirror/paragraphIndentation.js +72 -0
  109. package/dist/prosemirror/plugins/documentNumbering.d.ts +3 -1
  110. package/dist/prosemirror/plugins/documentNumbering.js +7 -1
  111. package/dist/prosemirror/plugins/documentStyles.d.ts +3 -1
  112. package/dist/prosemirror/plugins/documentStyles.js +7 -1
  113. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +3 -1
  114. package/dist/prosemirror/rebaseParagraphRunFormatting.js +2 -1
  115. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +15 -2
  116. package/dist/prosemirror/runFormattingInlineCarriers.js +33 -3
  117. package/dist/prosemirror/runStyleFormatting.d.ts +15 -2
  118. package/dist/prosemirror/runStyleFormatting.js +76 -2
  119. package/dist/prosemirror/schema/marks.d.ts +3 -1
  120. package/dist/prosemirror/schema/marks.js +2 -0
  121. package/dist/prosemirror/schema/nodes.d.ts +3 -0
  122. package/dist/prosemirror/tableCellRevisionVisibility.d.ts +6 -0
  123. package/dist/prosemirror/tableCellRevisionVisibility.js +14 -0
  124. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -1
  125. package/dist/prosemirror/trackedRunInlineAtoms.js +1 -1
  126. package/dist/redline.js +15 -2
  127. package/dist/server.d.ts +3 -2
  128. package/dist/server.js +2 -2
  129. package/dist/version-comparison.d.ts +2 -0
  130. package/dist/version-comparison.js +30 -29
  131. package/package.json +2 -2
@@ -1,15 +1,22 @@
1
1
  import { FolioDocxReviewer, getFolioDocxComparisonAccess } from "../ai-edits/headless.js";
2
- import { numberingReferenceKeysOf, storyTablesOf } from "../ai-edits/snapshot.js";
2
+ import { detachFolioAIEditSnapshotExternalHyperlinks, numberingReferenceKeysOf, remapFolioAIEditSnapshotNumberingReferences, remapFolioAIEditSnapshotStyleReferences, sourceDocumentOf, storyTablesOf } from "../ai-edits/snapshot.js";
3
3
  import { projectTableGeometry } from "../ai-edits/table-geometry.js";
4
4
  import { tableTemplateCanCrossPackageLosslessly } from "../ai-edits/table-template.js";
5
5
  import { createScopedWordDiffOptions } from "../ai-edits/word-diff.js";
6
6
  import "../document-operations.js";
7
7
  import { pairFolioDocumentStories } from "../document-stories.js";
8
+ import { sectionReferenceHistory } from "../docx/sectionReferenceHistory.js";
9
+ import { sameCanonicalInlinePresentation } from "../internal/compare/inline-presentation.js";
10
+ import { createStyleResolver } from "../prosemirror/styles/styleResolver.js";
11
+ import { canonicalJson } from "../utils/canonicalJson.js";
8
12
  import { createContentComparisonWorkSession } from "./content.js";
13
+ import { sameInlineAtoms } from "./inline-atoms.js";
14
+ import { sameAuthoredInlineProvenance } from "./inline-provenance.js";
9
15
  import { planStoryCompare } from "./plan.js";
10
16
  import { withFixedPackageDates } from "./reproducible-package.js";
17
+ import { compareSectionBoundaryProperties } from "./section-boundary-properties.js";
11
18
  import { CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError } from "./types.js";
12
- import { classifyGeometryMismatch, classifyProjectionMismatch, projectSupportedInlineFormatting, revisedFinalParagraphMarks } from "./verification.js";
19
+ import { classifyGeometryMismatch, classifyProjectionMismatch, revisedFinalParagraphMarks } from "./verification.js";
13
20
  import { Result, panic } from "better-result";
14
21
  //#region src/compare/compare.ts
15
22
  /**
@@ -78,7 +85,7 @@ const formattingRoundTripFailure = ({ invariant, story, changes, actualBlocks, e
78
85
  story,
79
86
  detail: "a text-equal aligned block could not be projected for formatting verification"
80
87
  };
81
- if (projectSupportedInlineFormatting(actual) !== projectSupportedInlineFormatting(expected)) return {
88
+ if (!sameCanonicalInlinePresentation(actual, expected)) return {
82
89
  invariant,
83
90
  cause: "inline-formatting",
84
91
  story,
@@ -113,8 +120,86 @@ const compareNumbering = (base, target, referenced) => {
113
120
  });
114
121
  return changes.toSorted((left, right) => left.numId - right.numId || left.level - right.level);
115
122
  };
123
+ const withoutSectionChangeHistory = ({ propertyChanges: _propertyChanges, ...properties }) => properties;
124
+ const sectionRelationshipMap = (pairs) => {
125
+ const mapped = /* @__PURE__ */ new Map();
126
+ for (const { baseStory, targetStory } of pairs) {
127
+ if (baseStory.type !== "header" && baseStory.type !== "footer" || targetStory.type !== "header" && targetStory.type !== "footer" || baseStory.type !== targetStory.type) continue;
128
+ mapped.set(targetStory.relationshipId, baseStory.relationshipId);
129
+ }
130
+ return mapped;
131
+ };
132
+ const remapSectionReferences = (references, relationshipIds) => {
133
+ if (references === void 0) return void 0;
134
+ const remapped = [];
135
+ for (const reference of references) {
136
+ const relationshipId = relationshipIds.get(reference.rId);
137
+ if (relationshipId === void 0) return null;
138
+ remapped.push({
139
+ ...reference,
140
+ rId: relationshipId
141
+ });
142
+ }
143
+ return remapped;
144
+ };
145
+ const safelyStageSectionProperties = ({ current, target, relationshipIds, revisionFormat }) => {
146
+ const targetHeaders = remapSectionReferences(target.headerReferences, relationshipIds);
147
+ const targetFooters = remapSectionReferences(target.footerReferences, relationshipIds);
148
+ if (targetHeaders === null || targetFooters === null || revisionFormat === "word" && (canonicalJson(targetHeaders) !== canonicalJson(current.headerReferences) || canonicalJson(targetFooters) !== canonicalJson(current.footerReferences)) || target.printerSettingsRelationshipId !== current.printerSettingsRelationshipId) return null;
149
+ const safeTarget = {
150
+ ...withoutSectionChangeHistory(target),
151
+ ...targetHeaders !== void 0 && { headerReferences: targetHeaders },
152
+ ...targetFooters !== void 0 && { footerReferences: targetFooters },
153
+ ...current.printerSettingsRelationshipId !== void 0 && { printerSettingsRelationshipId: current.printerSettingsRelationshipId }
154
+ };
155
+ return {
156
+ previous: withoutSectionChangeHistory(current),
157
+ target: safeTarget
158
+ };
159
+ };
160
+ const stageInsertedSectionProperties = ({ target, relationshipIds }) => {
161
+ if (target.printerSettingsRelationshipId !== void 0) return null;
162
+ const headers = remapSectionReferences(target.headerReferences, relationshipIds);
163
+ const footers = remapSectionReferences(target.footerReferences, relationshipIds);
164
+ if (headers === null || footers === null) return null;
165
+ return {
166
+ ...withoutSectionChangeHistory(target),
167
+ ...headers !== void 0 && { headerReferences: headers },
168
+ ...footers !== void 0 && { footerReferences: footers }
169
+ };
170
+ };
171
+ const compareFinalSectionProperties = ({ base, target, pairs, revisionFormat }) => {
172
+ if (base === void 0 || target === void 0) return base === target ? { status: "same" } : {
173
+ status: "unsupported",
174
+ detail: "final section presence differs"
175
+ };
176
+ if (base.propertyChanges !== void 0 || target.propertyChanges !== void 0) return {
177
+ status: "unsupported",
178
+ detail: "input final section already carries tracked changes"
179
+ };
180
+ const staged = safelyStageSectionProperties({
181
+ current: base,
182
+ target,
183
+ relationshipIds: sectionRelationshipMap(pairs),
184
+ revisionFormat
185
+ });
186
+ if (!staged) return {
187
+ status: "unsupported",
188
+ detail: "final section relationship references differ"
189
+ };
190
+ return canonicalJson(staged.previous) === canonicalJson(staged.target) ? { status: "same" } : {
191
+ status: "stage",
192
+ ...staged
193
+ };
194
+ };
116
195
  /** Stage 1: both packages to editor models, paired story by story. */
117
196
  const parseComparison = async (base, target, options) => {
197
+ const revisionFormat = options.revisionFormat ?? "word";
198
+ if (revisionFormat !== "word" && revisionFormat !== "folio-exact") return Result.err(new InvalidCompareDocxOptionsError({
199
+ message: "revisionFormat must be word or folio-exact.",
200
+ option: "revisionFormat",
201
+ receivedValue: revisionFormat
202
+ }));
118
203
  const packageDate = new Date(options.timestamp);
119
204
  if (Number.isNaN(packageDate.getTime())) return Result.err(new InvalidCompareDocxOptionsError({
120
205
  message: "timestamp must be a date the package can be stamped with.",
@@ -142,13 +227,34 @@ const parseComparison = async (base, target, options) => {
142
227
  targetSnapshots.set(handle, snapshot);
143
228
  }
144
229
  const pairs = [];
230
+ const importedHeaderFooterTargetSnapshots = /* @__PURE__ */ new Set();
145
231
  const unsupported = [];
146
232
  const referencedNumberingLevels = /* @__PURE__ */ new Set();
233
+ let terminalTableCarrierStaged = false;
147
234
  const collectNumberingReferences = (snapshot) => {
148
235
  if (!snapshot) return;
149
236
  for (const referenceKey of numberingReferenceKeysOf(snapshot)) referencedNumberingLevels.add(referenceKey);
150
237
  };
151
- for (const { baseStory, revisedStory: targetStory } of pairFolioDocumentStories(baseStories, targetStories)) {
238
+ for (const storyPair of pairFolioDocumentStories(baseStories, targetStories)) {
239
+ let { baseStory, revisedStory: targetStory } = storyPair;
240
+ let targetHeaderFooterWasImported = false;
241
+ let targetHeaderFooterNeedsExternalHyperlinkRebinding = false;
242
+ if (!baseStory && targetStory && (targetStory.type === "header" || targetStory.type === "footer")) {
243
+ baseStory = await getFolioDocxComparisonAccess(reviewer).createComparisonHeaderFooter(targetReviewer, targetStory);
244
+ if (baseStory) {
245
+ targetHeaderFooterWasImported = true;
246
+ targetHeaderFooterNeedsExternalHyperlinkRebinding = true;
247
+ baseSnapshots.set(baseStory, reviewer.snapshotStory(baseStory));
248
+ }
249
+ }
250
+ if (!targetStory && baseStory && revisionFormat === "folio-exact" && (baseStory.type === "header" || baseStory.type === "footer")) continue;
251
+ if (!targetStory && baseStory && (baseStory.type === "header" || baseStory.type === "footer")) {
252
+ targetStory = await getFolioDocxComparisonAccess(targetReviewer).createComparisonHeaderFooter(reviewer, baseStory);
253
+ if (targetStory) {
254
+ targetHeaderFooterNeedsExternalHyperlinkRebinding = true;
255
+ targetSnapshots.set(targetStory, targetReviewer.snapshotStory(targetStory));
256
+ }
257
+ }
152
258
  if (!baseStory) {
153
259
  if (!targetStory) panic("A story pair contained neither a base nor a target story");
154
260
  collectNumberingReferences(targetSnapshots.get(targetStory));
@@ -168,8 +274,8 @@ const parseComparison = async (base, target, options) => {
168
274
  });
169
275
  continue;
170
276
  }
171
- const baseSnapshot = baseSnapshots.get(baseStory);
172
- const targetSnapshot = targetSnapshots.get(targetStory);
277
+ let baseSnapshot = baseSnapshots.get(baseStory);
278
+ let targetSnapshot = targetSnapshots.get(targetStory);
173
279
  collectNumberingReferences(baseSnapshot);
174
280
  collectNumberingReferences(targetSnapshot);
175
281
  if (!baseSnapshot || !targetSnapshot) {
@@ -180,15 +286,70 @@ const parseComparison = async (base, target, options) => {
180
286
  });
181
287
  continue;
182
288
  }
289
+ const rawBaseSnapshot = baseSnapshot;
290
+ if (revisionFormat === "folio-exact" && baseStory.type === "main" && getFolioDocxComparisonAccess(reviewer).stageTerminalTableReviewCarrier(sourceDocumentOf(targetSnapshot))) {
291
+ const staged = reviewer.snapshotStory(baseStory);
292
+ if (!staged) panic("A staged terminal-table carrier lost the main story snapshot");
293
+ baseSnapshot = staged;
294
+ baseSnapshots.set(baseStory, staged);
295
+ terminalTableCarrierStaged = true;
296
+ }
297
+ if (targetHeaderFooterNeedsExternalHyperlinkRebinding) {
298
+ const detached = detachFolioAIEditSnapshotExternalHyperlinks(targetSnapshot);
299
+ if (!detached) {
300
+ unsupported.push({
301
+ reason: "story-not-editable",
302
+ baseStory,
303
+ targetStory
304
+ });
305
+ continue;
306
+ }
307
+ targetSnapshot = detached;
308
+ targetSnapshots.set(targetStory, targetSnapshot);
309
+ }
310
+ if (targetHeaderFooterWasImported) importedHeaderFooterTargetSnapshots.add(targetSnapshot);
183
311
  pairs.push({
184
312
  baseStory,
185
313
  targetStory,
186
314
  baseSnapshot,
315
+ ...terminalTableCarrierStaged && rawBaseSnapshot !== baseSnapshot && { rawBaseSnapshot },
187
316
  targetSnapshot
188
317
  });
189
318
  }
319
+ const styleImport = getFolioDocxComparisonAccess(reviewer).stageTargetStyles(targetReviewer, pairs.map(({ targetSnapshot }) => targetSnapshot), [...importedHeaderFooterTargetSnapshots]);
320
+ const styleAlignedPairs = styleImport.status === "unalignable" ? pairs : pairs.map(({ baseStory, targetStory, baseSnapshot, rawBaseSnapshot, targetSnapshot }) => ({
321
+ baseStory,
322
+ targetStory,
323
+ baseSnapshot,
324
+ rawBaseSnapshot: rawBaseSnapshot ?? baseSnapshot,
325
+ targetSnapshot: remapFolioAIEditSnapshotStyleReferences({
326
+ snapshot: targetSnapshot,
327
+ styleIdMap: styleImport.styleIdMap,
328
+ defaultParagraphStyleId: styleImport.defaultParagraphStyleId,
329
+ importedStyleResolver: createStyleResolver(styleImport.styles),
330
+ reconcileAuthoredFormatting: true
331
+ })
332
+ }));
333
+ const targetNumbering = getFolioDocxComparisonAccess(targetReviewer).numberingDefinitions();
334
+ const targetNumberingReferences = styleAlignedPairs.flatMap(({ targetSnapshot }) => targetSnapshot.blocks.flatMap((block) => block.listReference ? [block.listReference] : []));
335
+ const targetNumberingReferenceMap = getFolioDocxComparisonAccess(reviewer).planTargetNumberingReferences(targetNumbering, targetNumberingReferences);
336
+ const comparisonPairs = targetNumberingReferenceMap === null ? styleAlignedPairs : styleAlignedPairs.map(({ baseStory, targetStory, baseSnapshot, rawBaseSnapshot, targetSnapshot }) => ({
337
+ baseStory,
338
+ targetStory,
339
+ baseSnapshot,
340
+ rawBaseSnapshot: rawBaseSnapshot ?? baseSnapshot,
341
+ targetSnapshot: remapFolioAIEditSnapshotNumberingReferences(targetSnapshot, targetNumberingReferenceMap)
342
+ }));
343
+ const finalSectionComparison = compareFinalSectionProperties({
344
+ base: getFolioDocxComparisonAccess(reviewer).finalSectionProperties(),
345
+ target: getFolioDocxComparisonAccess(targetReviewer).finalSectionProperties(),
346
+ pairs: comparisonPairs,
347
+ revisionFormat
348
+ });
190
349
  return Result.ok({
191
350
  granularity: options.granularity ?? "word",
351
+ revisionFormat,
352
+ terminalTableCarrierStaged,
192
353
  baseBuffer: base,
193
354
  baseCarriedRevisions: baseProjection.revisions.present,
194
355
  reviewer,
@@ -197,8 +358,13 @@ const parseComparison = async (base, target, options) => {
197
358
  idSeed: baseProjection.revisions.highestId + 1
198
359
  },
199
360
  packageDate,
200
- pairs,
361
+ pairs: comparisonPairs,
201
362
  numberingChanges: compareNumbering(reviewer, targetReviewer, referencedNumberingLevels),
363
+ targetNumbering,
364
+ targetNumberingReferences,
365
+ targetNumberingReferenceMap: targetNumberingReferenceMap ?? /* @__PURE__ */ new Map(),
366
+ finalSectionComparison,
367
+ styleImportFailure: styleImport.status === "unalignable" ? styleImport.detail : void 0,
202
368
  unsupported
203
369
  });
204
370
  };
@@ -328,18 +494,58 @@ const resolveTableTemplates = (targetTables, requests) => {
328
494
  * what you could not represent" and "give me nothing unless you can prove it"
329
495
  * are both legitimate asks and only the caller knows which one it is making.
330
496
  */
331
- const applyComparison = ({ reviewer, revisionStamp, granularity, numberingChanges }, planned) => {
497
+ const applyComparison = ({ reviewer, revisionStamp, granularity, revisionFormat, terminalTableCarrierStaged, numberingChanges, targetNumbering, targetNumberingReferences, targetNumberingReferenceMap, finalSectionComparison, styleImportFailure, pairs }, planned) => {
498
+ const relationshipIds = sectionRelationshipMap(pairs);
499
+ const plannedOperationCount = planned.reduce((count, { plan }) => count + plan.operations.length, 0);
500
+ const finalSectionOperationCount = finalSectionComparison.status === "stage" ? 1 : 0;
501
+ if (plannedOperationCount + finalSectionOperationCount > 1e4) return Result.err(new CompareDocxOperationLimitError({
502
+ message: "The comparison needs more operations than the engine generates.",
503
+ limit: MAX_COMPARE_OPERATIONS
504
+ }));
332
505
  const comparisonAccess = getFolioDocxComparisonAccess(reviewer);
506
+ const numberingStage = comparisonAccess.stageTargetNumbering(targetNumbering, targetNumberingReferences, targetNumberingReferenceMap);
333
507
  const changes = [...numberingChanges];
334
508
  const failures = [];
335
- let idSeed = revisionStamp.idSeed;
336
- let documentChanged = false;
509
+ if (styleImportFailure !== void 0) failures.push({
510
+ invariant: "accept-reproduces-target",
511
+ cause: "style",
512
+ story: { type: "main" },
513
+ detail: styleImportFailure
514
+ });
515
+ const finalSectionChanged = finalSectionComparison.status === "stage" && comparisonAccess.stageFinalSectionProperties({
516
+ target: finalSectionComparison.target,
517
+ previous: finalSectionComparison.previous,
518
+ revision: revisionStamp
519
+ });
520
+ if (finalSectionChanged) changes.push({
521
+ kind: "section-properties",
522
+ location: { story: { type: "main" } }
523
+ });
524
+ if (finalSectionComparison.status === "unsupported") failures.push({
525
+ invariant: "accept-reproduces-target",
526
+ cause: "section-properties",
527
+ story: { type: "main" },
528
+ detail: finalSectionComparison.detail
529
+ });
530
+ let idSeed = revisionStamp.idSeed + (finalSectionChanged ? 1 : 0);
531
+ if (numberingStage === "conflict") failures.push({
532
+ invariant: "accept-reproduces-target",
533
+ cause: "list-level",
534
+ story: { type: "main" },
535
+ detail: "target numbering definitions cannot be imported without changing existing references"
536
+ });
537
+ const folioRequirements = /* @__PURE__ */ new Set();
538
+ if (finalSectionChanged && finalSectionComparison.status === "stage" && sectionReferenceHistory(finalSectionComparison) !== void 0) folioRequirements.add("section-reference-history");
539
+ if (terminalTableCarrierStaged) folioRequirements.add("terminal-table-carrier");
540
+ let documentChanged = numberingStage === "staged" || finalSectionChanged;
541
+ let remainingProvenanceRanges = MAX_COMPARE_OPERATIONS - plannedOperationCount - finalSectionOperationCount;
337
542
  const wordDiff = createScopedWordDiffOptions({ granularity });
338
543
  for (const { pair, plan } of planned) {
339
544
  changes.push(...plan.changes);
340
- if (plan.operations.length === 0 && plan.tableGeometryPairings.length === 0) continue;
341
- const baseBefore = pair.baseSnapshot.blocks;
342
- const baseBeforeGeometry = projectTableGeometry(storyTablesOf(pair.baseSnapshot));
545
+ failures.push(...plan.verificationFailures);
546
+ const rawBaseSnapshot = pair.rawBaseSnapshot ?? pair.baseSnapshot;
547
+ const baseBefore = rawBaseSnapshot.blocks;
548
+ const baseBeforeGeometry = projectTableGeometry(storyTablesOf(rawBaseSnapshot));
343
549
  const targetTables = new Map(storyTablesOf(pair.targetSnapshot).map(({ index, node }) => [index, node]));
344
550
  const afterGeometry = reviewer.matchStoryTableGeometry({
345
551
  story: pair.baseStory,
@@ -353,7 +559,6 @@ const applyComparison = ({ reviewer, revisionStamp, granularity, numberingChange
353
559
  const geometryChanged = afterGeometry > idSeed;
354
560
  documentChanged ||= geometryChanged;
355
561
  idSeed = afterGeometry;
356
- if (plan.operations.length === 0 && !geometryChanged) continue;
357
562
  if (plan.operations.length > 0) {
358
563
  const { skipped, nextRevisionId } = reviewer.applyDocumentOperationsToStory({
359
564
  story: pair.baseStory,
@@ -379,6 +584,171 @@ const applyComparison = ({ reviewer, revisionStamp, granularity, numberingChange
379
584
  skipped: refused
380
585
  }));
381
586
  }
587
+ const describedTargetIds = /* @__PURE__ */ new Set();
588
+ for (const change of plan.changes) switch (change.kind) {
589
+ case "insert":
590
+ case "replace":
591
+ case "move":
592
+ case "merge":
593
+ case "paragraph-format":
594
+ case "format":
595
+ case "run-format":
596
+ case "inline-atom":
597
+ describedTargetIds.add(change.targetBlockId);
598
+ break;
599
+ case "split":
600
+ case "table-insert":
601
+ case "table-row-insert":
602
+ case "table-column-insert":
603
+ for (const id of change.targetBlockIds) describedTargetIds.add(id);
604
+ break;
605
+ case "delete":
606
+ case "section-properties":
607
+ case "table-delete":
608
+ case "table-row-delete":
609
+ case "table-column-delete":
610
+ case "numbering": break;
611
+ default: return panic("Unhandled comparison change", { change });
612
+ }
613
+ const atoms = comparisonAccess.matchInlineAtoms({
614
+ story: pair.baseStory,
615
+ targetSnapshot: pair.targetSnapshot,
616
+ revisionStamp: {
617
+ date: revisionStamp.date,
618
+ idSeed
619
+ },
620
+ originalRevisionIdSeed: revisionStamp.idSeed,
621
+ maxRanges: remainingProvenanceRanges
622
+ });
623
+ switch (atoms.status) {
624
+ case "matched": {
625
+ idSeed = atoms.nextRevisionId;
626
+ remainingProvenanceRanges -= atoms.rangeCount;
627
+ documentChanged ||= atoms.documentChanged;
628
+ const changedAtomTargetIds = new Set(atoms.changedTargetBlockIds);
629
+ for (const block of pair.targetSnapshot.blocks) {
630
+ if (!changedAtomTargetIds.has(block.id) || describedTargetIds.has(block.id)) continue;
631
+ changes.push({
632
+ kind: "inline-atom",
633
+ location: {
634
+ story: pair.baseStory,
635
+ ...block.table ? { cell: block.table } : {}
636
+ },
637
+ targetBlockId: block.id,
638
+ text: block.text
639
+ });
640
+ }
641
+ break;
642
+ }
643
+ case "unalignable":
644
+ failures.push({
645
+ invariant: "accept-reproduces-target",
646
+ cause: "inline-structure",
647
+ story: pair.baseStory,
648
+ detail: "supported inline atoms could not be aligned to the revised content"
649
+ });
650
+ break;
651
+ case "budget-exceeded": return Result.err(new CompareDocxOperationLimitError({
652
+ message: "The comparison exceeds its inline presentation range budget.",
653
+ limit: MAX_COMPARE_OPERATIONS
654
+ }));
655
+ default: return panic("Unhandled inline atom projection result", { result: atoms });
656
+ }
657
+ const provenance = comparisonAccess.matchInlineProvenance({
658
+ story: pair.baseStory,
659
+ targetSnapshot: pair.targetSnapshot,
660
+ revisionStamp: {
661
+ date: revisionStamp.date,
662
+ idSeed
663
+ },
664
+ originalRevisionIdSeed: revisionStamp.idSeed,
665
+ maxRanges: remainingProvenanceRanges
666
+ });
667
+ switch (provenance.status) {
668
+ case "matched": {
669
+ idSeed = provenance.nextRevisionId;
670
+ remainingProvenanceRanges -= provenance.rangeCount;
671
+ documentChanged ||= provenance.documentChanged;
672
+ const changedTargetIds = new Set(provenance.changedTargetBlockIds);
673
+ for (const block of pair.targetSnapshot.blocks) {
674
+ if (!changedTargetIds.has(block.id) || describedTargetIds.has(block.id)) continue;
675
+ changes.push({
676
+ kind: "run-format",
677
+ location: {
678
+ story: pair.baseStory,
679
+ ...block.table ? { cell: block.table } : {}
680
+ },
681
+ targetBlockId: block.id,
682
+ text: block.text
683
+ });
684
+ }
685
+ break;
686
+ }
687
+ case "unalignable":
688
+ failures.push({
689
+ invariant: "accept-reproduces-target",
690
+ cause: "inline-formatting",
691
+ story: pair.baseStory,
692
+ detail: "authored run properties could not be aligned to the revised content"
693
+ });
694
+ break;
695
+ case "budget-exceeded": return Result.err(new CompareDocxOperationLimitError({
696
+ message: "The comparison exceeds its inline presentation range budget.",
697
+ limit: MAX_COMPARE_OPERATIONS
698
+ }));
699
+ default: return panic("Unhandled authored inline projection result", { result: provenance });
700
+ }
701
+ if (pair.baseStory.type === "main") {
702
+ let boundaryRequiresFolio = false;
703
+ const insertedBoundaries = comparisonAccess.stageMappedSectionBoundaries({
704
+ target: sourceDocumentOf(pair.targetSnapshot),
705
+ originalRevisionIdSeed: revisionStamp.idSeed,
706
+ maxRanges: remainingProvenanceRanges,
707
+ revision: {
708
+ date: revisionStamp.date,
709
+ idSeed
710
+ },
711
+ mapTargetProperties: ({ kind, current, target }) => {
712
+ if (kind === "inserted") {
713
+ const inserted = stageInsertedSectionProperties({
714
+ target,
715
+ relationshipIds
716
+ });
717
+ return inserted ? {
718
+ kind: "inserted",
719
+ target: inserted
720
+ } : null;
721
+ }
722
+ if (current === void 0) return null;
723
+ const staged = safelyStageSectionProperties({
724
+ current,
725
+ target,
726
+ relationshipIds,
727
+ revisionFormat
728
+ });
729
+ if (staged && sectionReferenceHistory(staged) !== void 0) boundaryRequiresFolio = true;
730
+ return staged ? {
731
+ kind: "retained",
732
+ ...staged
733
+ } : null;
734
+ }
735
+ });
736
+ switch (insertedBoundaries.status) {
737
+ case "matched":
738
+ remainingProvenanceRanges -= insertedBoundaries.rangeCount;
739
+ idSeed = insertedBoundaries.nextRevisionId;
740
+ documentChanged ||= insertedBoundaries.documentChanged;
741
+ if (boundaryRequiresFolio) folioRequirements.add("section-reference-history");
742
+ break;
743
+ case "unalignable": break;
744
+ case "budget-exceeded": return Result.err(new CompareDocxOperationLimitError({
745
+ message: "The comparison exceeds its section boundary range budget.",
746
+ limit: MAX_COMPARE_OPERATIONS
747
+ }));
748
+ default: return panic("Unhandled inserted section boundary result", { result: insertedBoundaries });
749
+ }
750
+ }
751
+ if (plan.operations.length === 0 && !geometryChanged && atoms.status === "matched" && atoms.rangeCount === 0 && provenance.status === "matched" && provenance.rangeCount === 0 && pair.baseStory.type !== "main") continue;
382
752
  const acceptedSnapshot = comparisonAccess.snapshotReviewedStory({
383
753
  story: pair.baseStory,
384
754
  view: "final"
@@ -401,6 +771,34 @@ const applyComparison = ({ reviewer, revisionStamp, granularity, numberingChange
401
771
  });
402
772
  if (formattingFailure) failures.push(formattingFailure);
403
773
  }
774
+ if (acceptedSnapshot && !sameAuthoredInlineProvenance(acceptedSnapshot, pair.targetSnapshot)) failures.push({
775
+ invariant: "accept-reproduces-target",
776
+ cause: "inline-formatting",
777
+ story: pair.baseStory,
778
+ detail: "accepted authored run properties differ from the revised document"
779
+ });
780
+ if (acceptedSnapshot && !sameInlineAtoms(sourceDocumentOf(acceptedSnapshot), sourceDocumentOf(pair.targetSnapshot))) failures.push({
781
+ invariant: "accept-reproduces-target",
782
+ cause: "inline-structure",
783
+ story: pair.baseStory,
784
+ detail: "accepted inline atoms differ from the revised document"
785
+ });
786
+ if (pair.baseStory.type === "main" && acceptedSnapshot) {
787
+ const sectionBoundaries = compareSectionBoundaryProperties({
788
+ current: sourceDocumentOf(acceptedSnapshot),
789
+ target: sourceDocumentOf(pair.targetSnapshot),
790
+ mapTargetProperties: (target) => stageInsertedSectionProperties({
791
+ target,
792
+ relationshipIds
793
+ })
794
+ });
795
+ if (sectionBoundaries.status === "unalignable" || sectionBoundaries.changes.length > 0) failures.push({
796
+ invariant: "accept-reproduces-target",
797
+ cause: "section-properties",
798
+ story: pair.baseStory,
799
+ detail: sectionBoundaries.status === "unalignable" ? sectionBoundaries.detail : "accepted paragraph section properties differ from the revised document"
800
+ });
801
+ }
404
802
  const rejectedSnapshot = comparisonAccess.snapshotReviewedStory({
405
803
  story: pair.baseStory,
406
804
  view: "original"
@@ -418,11 +816,35 @@ const applyComparison = ({ reviewer, revisionStamp, granularity, numberingChange
418
816
  story: pair.baseStory,
419
817
  changes: plan.changes,
420
818
  actualBlocks: rejectedSnapshot?.blocks ?? [],
421
- expectedBlocks: pair.baseSnapshot.blocks,
819
+ expectedBlocks: rawBaseSnapshot.blocks,
422
820
  expectedBlockId: ({ baseBlockId }) => baseBlockId
423
821
  });
424
822
  if (formattingFailure) failures.push(formattingFailure);
425
823
  }
824
+ if (rejectedSnapshot && !sameAuthoredInlineProvenance(rejectedSnapshot, rawBaseSnapshot)) failures.push({
825
+ invariant: "reject-reproduces-base",
826
+ cause: "inline-formatting",
827
+ story: pair.baseStory,
828
+ detail: "rejected authored run properties differ from the base document"
829
+ });
830
+ if (rejectedSnapshot && !sameInlineAtoms(sourceDocumentOf(rejectedSnapshot), sourceDocumentOf(rawBaseSnapshot))) failures.push({
831
+ invariant: "reject-reproduces-base",
832
+ cause: "inline-structure",
833
+ story: pair.baseStory,
834
+ detail: "rejected inline atoms differ from the base document"
835
+ });
836
+ if (pair.baseStory.type === "main" && rejectedSnapshot) {
837
+ const sectionBoundaries = compareSectionBoundaryProperties({
838
+ current: sourceDocumentOf(rejectedSnapshot),
839
+ target: sourceDocumentOf(rawBaseSnapshot)
840
+ });
841
+ if (sectionBoundaries.status === "unalignable" || sectionBoundaries.changes.length > 0) failures.push({
842
+ invariant: "reject-reproduces-base",
843
+ cause: "section-properties",
844
+ story: pair.baseStory,
845
+ detail: sectionBoundaries.status === "unalignable" ? sectionBoundaries.detail : "rejected paragraph section properties differ from the base document"
846
+ });
847
+ }
426
848
  const geometryAcceptFailure = classifyGeometryMismatch({
427
849
  invariant: "accept-reproduces-target",
428
850
  story: pair.baseStory,
@@ -438,13 +860,18 @@ const applyComparison = ({ reviewer, revisionStamp, granularity, numberingChange
438
860
  });
439
861
  if (geometryRejectFailure) failures.push(geometryRejectFailure);
440
862
  }
863
+ const firstFolioRequirement = [...folioRequirements].at(0);
441
864
  return Result.ok({
442
865
  changes,
443
866
  verification: failures.length === 0 ? { status: "verified" } : {
444
867
  status: "unverified",
445
868
  failures
446
869
  },
447
- documentChanged
870
+ documentChanged,
871
+ compatibility: firstFolioRequirement === void 0 ? { status: "standard-ooxml" } : {
872
+ status: "requires-folio",
873
+ reasons: [firstFolioRequirement, ...[...folioRequirements].slice(1)]
874
+ }
448
875
  });
449
876
  };
450
877
  /**
@@ -519,6 +946,7 @@ const compareDocx = async (base, target, options) => {
519
946
  buffer: serialized.value,
520
947
  changes,
521
948
  verification,
949
+ compatibility: applied.value.compatibility,
522
950
  unsupported: parsed.value.unsupported
523
951
  });
524
952
  };