@stll/folio-core 0.37.0 → 0.37.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -135,7 +135,12 @@ const hasRepresentableCommentAnchor = (item, mode) => {
135
135
  default: return item.from < item.to;
136
136
  }
137
137
  };
138
- const writesParagraphPropertyChange = (item) => item.operation.type === "setBlockParagraphProperties" || isResolvedReplaceBlockOperation(item) && REPLACE_BLOCK_IMPACT[item.replaceBlockImpact].changesStyle;
138
+ const writesParagraphPropertyChange = (item) => {
139
+ if (item.operation.type === "setBlockParagraphProperties") return true;
140
+ if (item.operation.type === "splitBlock") return item.operation.firstParagraphProperties !== void 0 || item.operation.secondParagraphProperties !== void 0;
141
+ if (item.operation.type === "mergeBlockWithNext") return item.operation.mergedParagraphProperties !== void 0;
142
+ return isResolvedReplaceBlockOperation(item) && REPLACE_BLOCK_IMPACT[item.replaceBlockImpact].changesStyle;
143
+ };
139
144
  const resolveReplaceBlockImpact = ({ changesText, changesStyle }) => {
140
145
  if (changesText) return changesStyle ? "textAndStyle" : "text";
141
146
  if (changesStyle) return "style";
@@ -266,6 +271,50 @@ const resolveFormattingFromStyle = ({ attrs, styleId, styleResolver }) => {
266
271
  if (attrs.alignmentFromStyle !== void 0) fallback.alignment = attrs.alignmentFromStyle;
267
272
  return Object.keys(fallback).length > 0 ? fallback : void 0;
268
273
  };
274
+ /**
275
+ * Apply one paragraph-property patch to a live node. A tracked patch stores
276
+ * the complete previous pPr: a partial snapshot cannot distinguish a property
277
+ * the change left alone from one it explicitly cleared when rejecting it.
278
+ */
279
+ const applyBlockParagraphProperties = ({ tr, position, node, properties, styleResolver, numbering = null, revisionInfo }) => {
280
+ const attrs = expectParagraphAttrs(node);
281
+ const resolvedFormattingFromStyle = properties.styleId === void 0 ? resolveFormattingFromStyle({
282
+ attrs,
283
+ styleId: attrs.styleId,
284
+ styleResolver
285
+ }) : resolveFormattingFromStyle({
286
+ attrs,
287
+ styleId: properties.styleId,
288
+ styleResolver
289
+ });
290
+ const patch = paragraphPropertiesPatch({
291
+ node,
292
+ properties,
293
+ resolvedFormattingFromStyle,
294
+ numbering
295
+ });
296
+ if (patch === null) return {
297
+ tr,
298
+ changed: false,
299
+ revisionId: null
300
+ };
301
+ const changeInfo = revisionInfo?.();
302
+ const change = changeInfo ? {
303
+ type: "paragraphPropertyChange",
304
+ info: changeInfo,
305
+ previousFormatting: paragraphPropertiesSnapshot(node)
306
+ } : null;
307
+ const existing = attrs._propertyChanges;
308
+ return {
309
+ tr: tr.setNodeMarkup(position, void 0, {
310
+ ...node.attrs,
311
+ ...patch,
312
+ ...change ? { _propertyChanges: [...Array.isArray(existing) ? existing : [], change] } : {}
313
+ }),
314
+ changed: true,
315
+ revisionId: change?.info.id ?? null
316
+ };
317
+ };
269
318
  const applyReplaceBlockStyleId = ({ item, tr, styleResolver, revisionInfo }) => {
270
319
  if (item.operation.type !== "replaceBlock" || item.operation.styleId === void 0) return {
271
320
  tr,
@@ -1291,7 +1340,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1291
1340
  const provisionalCommentId = item.commentText !== void 0 ? reserveProvisionalCommentId() : void 0;
1292
1341
  const commentMark = provisionalCommentId !== void 0 && commentType ? commentType.create({ commentId: provisionalCommentId }) : null;
1293
1342
  const stepsBefore = tr.steps.length;
1294
- let appliedRevisionIds;
1343
+ let appliedRevisionIds = [];
1295
1344
  if (isSuggested && !SUGGESTED_SUPPORTED_OPERATION_TYPES.has(item.operation.type)) {
1296
1345
  skipped.push({
1297
1346
  id: item.operation.id,
@@ -1900,85 +1949,89 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1900
1949
  case "setBlockParagraphProperties": {
1901
1950
  const blockPosition = tr.mapping.map(item.blockFrom);
1902
1951
  const liveBlock = tr.doc.nodeAt(blockPosition) ?? item.blockNode;
1903
- const liveAttrs = expectParagraphAttrs(liveBlock);
1904
- const resolvedFormattingFromStyle = item.operation.properties.styleId === void 0 ? resolveFormattingFromStyle({
1905
- attrs: liveAttrs,
1906
- styleId: liveAttrs.styleId,
1907
- styleResolver
1908
- }) : resolveFormattingFromStyle({
1909
- attrs: liveAttrs,
1910
- styleId: item.operation.properties.styleId,
1911
- styleResolver
1912
- });
1913
- const patch = paragraphPropertiesPatch({
1952
+ const appliedProperties = applyBlockParagraphProperties({
1953
+ tr,
1954
+ position: blockPosition,
1914
1955
  node: liveBlock,
1915
1956
  properties: item.operation.properties,
1916
- resolvedFormattingFromStyle,
1917
- numbering
1957
+ styleResolver,
1958
+ numbering,
1959
+ ...mode === "direct" ? {} : { revisionInfo: () => ({
1960
+ id: operationRevisionSeed++,
1961
+ author,
1962
+ date,
1963
+ ...trackedRevisionExtras
1964
+ }) }
1918
1965
  });
1919
- if (patch === null) {
1966
+ if (!appliedProperties.changed) {
1920
1967
  skipped.push({
1921
1968
  id: item.operation.id,
1922
1969
  reason: "noopOperation"
1923
1970
  });
1924
1971
  continue;
1925
1972
  }
1926
- if (mode === "direct") {
1927
- tr = tr.setNodeMarkup(blockPosition, void 0, {
1928
- ...liveBlock.attrs,
1929
- ...patch
1930
- });
1931
- break;
1932
- }
1933
- const revisionId = operationRevisionSeed++;
1934
- const change = {
1935
- type: "paragraphPropertyChange",
1936
- info: {
1937
- id: revisionId,
1938
- author,
1939
- date,
1940
- ...trackedRevisionExtras
1941
- },
1942
- previousFormatting: paragraphPropertiesSnapshot(liveBlock)
1943
- };
1944
- const existing = expectParagraphAttrs(liveBlock)._propertyChanges;
1945
- tr = tr.setNodeMarkup(blockPosition, void 0, {
1946
- ...liveBlock.attrs,
1947
- ...patch,
1948
- _propertyChanges: [...Array.isArray(existing) ? existing : [], change]
1949
- });
1950
- appliedRevisionIds = [revisionId];
1973
+ tr = appliedProperties.tr;
1974
+ if (appliedProperties.revisionId !== null) appliedRevisionIds = [appliedProperties.revisionId];
1951
1975
  break;
1952
1976
  }
1953
1977
  case "splitBlock": {
1954
1978
  if (mode === "direct") {
1955
1979
  if (item.to > item.from) tr = tr.delete(item.from, item.to);
1956
1980
  tr = tr.split(item.from);
1957
- break;
1958
- }
1959
- const revisionIdMark = operationRevisionSeed++;
1960
- const info = {
1961
- id: revisionIdMark,
1962
- author,
1963
- date,
1964
- ...trackedRevisionExtras
1965
- };
1966
- appliedRevisionIds = [revisionIdMark];
1967
- if (item.to > item.from && deletionType) {
1968
- const revisionIdSeparator = operationRevisionSeed++;
1969
- tr = tr.addMark(item.from, item.to, deletionType.create({
1970
- revisionId: revisionIdSeparator,
1981
+ } else {
1982
+ const revisionIdMark = operationRevisionSeed++;
1983
+ const info = {
1984
+ id: revisionIdMark,
1971
1985
  author,
1972
1986
  date,
1973
1987
  ...trackedRevisionExtras
1974
- }));
1975
- appliedRevisionIds = [revisionIdMark, revisionIdSeparator];
1988
+ };
1989
+ appliedRevisionIds = [revisionIdMark];
1990
+ if (item.to > item.from && deletionType) {
1991
+ const revisionIdSeparator = operationRevisionSeed++;
1992
+ tr = tr.addMark(item.from, item.to, deletionType.create({
1993
+ revisionId: revisionIdSeparator,
1994
+ author,
1995
+ date,
1996
+ ...trackedRevisionExtras
1997
+ }));
1998
+ appliedRevisionIds.push(revisionIdSeparator);
1999
+ }
2000
+ tr = tr.split(item.from);
2001
+ tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
2002
+ kind: "ins",
2003
+ info
2004
+ });
1976
2005
  }
1977
- tr = tr.split(item.from);
1978
- tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
1979
- kind: "ins",
1980
- info
2006
+ const allocateSplitParagraphPropertyRevisionInfo = () => ({
2007
+ id: operationRevisionSeed++,
2008
+ author,
2009
+ date,
2010
+ ...trackedRevisionExtras
1981
2011
  });
2012
+ const paragraphPropertyTargets = [{
2013
+ position: item.blockFrom,
2014
+ properties: item.operation.firstParagraphProperties
2015
+ }, {
2016
+ position: item.from + 1,
2017
+ properties: item.operation.secondParagraphProperties
2018
+ }];
2019
+ for (const { position, properties } of paragraphPropertyTargets) {
2020
+ if (!properties) continue;
2021
+ const paragraph = tr.doc.nodeAt(position);
2022
+ if (!paragraph) panic("A resolved split did not produce two paragraphs");
2023
+ const appliedProperties = applyBlockParagraphProperties({
2024
+ tr,
2025
+ position,
2026
+ node: paragraph,
2027
+ properties,
2028
+ styleResolver,
2029
+ numbering,
2030
+ ...mode === "direct" ? {} : { revisionInfo: allocateSplitParagraphPropertyRevisionInfo }
2031
+ });
2032
+ tr = appliedProperties.tr;
2033
+ if (appliedProperties.revisionId !== null) appliedRevisionIds.push(appliedProperties.revisionId);
2034
+ }
1982
2035
  break;
1983
2036
  }
1984
2037
  case "mergeBlockWithNext": {
@@ -1987,30 +2040,50 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1987
2040
  if (mode === "direct") {
1988
2041
  if (separator.length > 0) tr = tr.insertText(separator, insertAt);
1989
2042
  tr = tr.join(item.blockTo + separator.length);
1990
- break;
2043
+ } else {
2044
+ const revisionIdMark = operationRevisionSeed++;
2045
+ appliedRevisionIds = [revisionIdMark];
2046
+ if (separator.length > 0 && insertionType) {
2047
+ const revisionIdSeparator = operationRevisionSeed++;
2048
+ tr = tr.insertText(separator, insertAt);
2049
+ tr = tr.addMark(insertAt, insertAt + separator.length, insertionType.create({
2050
+ revisionId: revisionIdSeparator,
2051
+ author,
2052
+ date,
2053
+ ...trackedRevisionExtras
2054
+ }));
2055
+ appliedRevisionIds.push(revisionIdSeparator);
2056
+ }
2057
+ tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
2058
+ kind: "del",
2059
+ info: {
2060
+ id: revisionIdMark,
2061
+ author,
2062
+ date,
2063
+ ...trackedRevisionExtras
2064
+ }
2065
+ });
1991
2066
  }
1992
- const revisionIdMark = operationRevisionSeed++;
1993
- appliedRevisionIds = [revisionIdMark];
1994
- if (separator.length > 0 && insertionType) {
1995
- const revisionIdSeparator = operationRevisionSeed++;
1996
- tr = tr.insertText(separator, insertAt);
1997
- tr = tr.addMark(insertAt, insertAt + separator.length, insertionType.create({
1998
- revisionId: revisionIdSeparator,
1999
- author,
2000
- date,
2001
- ...trackedRevisionExtras
2002
- }));
2003
- appliedRevisionIds = [revisionIdMark, revisionIdSeparator];
2067
+ if (item.operation.mergedParagraphProperties) {
2068
+ const mergedParagraph = tr.doc.nodeAt(item.blockFrom);
2069
+ if (!mergedParagraph) panic("A resolved merge did not produce a paragraph");
2070
+ const appliedProperties = applyBlockParagraphProperties({
2071
+ tr,
2072
+ position: item.blockFrom,
2073
+ node: mergedParagraph,
2074
+ properties: item.operation.mergedParagraphProperties,
2075
+ styleResolver,
2076
+ numbering,
2077
+ ...mode === "direct" ? {} : { revisionInfo: () => ({
2078
+ id: operationRevisionSeed++,
2079
+ author,
2080
+ date,
2081
+ ...trackedRevisionExtras
2082
+ }) }
2083
+ });
2084
+ tr = appliedProperties.tr;
2085
+ if (appliedProperties.revisionId !== null) appliedRevisionIds.push(appliedProperties.revisionId);
2004
2086
  }
2005
- tr = tr.setNodeAttribute(item.blockFrom, "pPrMark", {
2006
- kind: "del",
2007
- info: {
2008
- id: revisionIdMark,
2009
- author,
2010
- date,
2011
- ...trackedRevisionExtras
2012
- }
2013
- });
2014
2087
  break;
2015
2088
  }
2016
2089
  case "commentOnBlock":
@@ -2043,7 +2116,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
2043
2116
  applied.push({
2044
2117
  id: item.operation.id,
2045
2118
  ...committedCommentId !== void 0 && { commentId: committedCommentId },
2046
- ...appliedRevisionIds !== void 0 && appliedRevisionIds[0] !== void 0 && {
2119
+ ...appliedRevisionIds[0] !== void 0 && {
2047
2120
  revisionId: appliedRevisionIds[0],
2048
2121
  revisionIds: appliedRevisionIds
2049
2122
  },
@@ -122,6 +122,22 @@ type FolioApplyDocumentOperationsToStoryOptions = FolioApplyDocumentOperationsOp
122
122
  */
123
123
  tableTemplates?: FolioTableTemplates;
124
124
  };
125
+ type FolioDocxComparisonProjectionMode = "with-revision-census" | "without-revision-census";
126
+ type FolioDocxComparisonStoryProjection = {
127
+ handle: FolioDocumentStoryHandle;
128
+ snapshot: FolioAIEditSnapshot | null;
129
+ };
130
+ type FolioDocxComparisonProjection = {
131
+ stories: readonly FolioDocxComparisonStoryProjection[];
132
+ revisions: {
133
+ highestId: number;
134
+ present: boolean;
135
+ };
136
+ };
137
+ type FolioDocxComparisonAccess = {
138
+ projectStories: (mode: FolioDocxComparisonProjectionMode) => FolioDocxComparisonProjection;
139
+ snapshotReviewedStory: (options?: FolioReadReviewedStoryOptions) => FolioAIEditSnapshot | null;
140
+ };
125
141
  declare const isFolioReviewedView: (value: unknown) => value is FolioReviewedView;
126
142
  declare const isFolioResolvedReviewedView: (value: unknown) => value is FolioResolvedReviewedView;
127
143
  /** Filter for {@link FolioDocxReviewer.getChanges}. */
@@ -222,6 +238,8 @@ declare class FolioDocxReviewer {
222
238
  getDocumentProperties(): Readonly<NonNullable<document_d_exports.Document["package"]["properties"]>> | null;
223
239
  /** Snapshot one editable story into stable, operation-ready blocks. */
224
240
  snapshotStory(story: FolioEditableDocumentStoryHandle): FolioAIEditSnapshot | null;
241
+ private snapshotReviewedStoryInternal;
242
+ private projectComparisonStoriesInternal;
225
243
  /**
226
244
  * One story's tables, in the document order {@link snapshotStory} numbers
227
245
  * them with, read through a reviewed view.
@@ -249,6 +267,7 @@ declare class FolioDocxReviewer {
249
267
  readReviewedStory(options?: FolioReadReviewedStoryOptions): FolioReviewedStory | null;
250
268
  /** Resolve one editable story to its original or final state. */
251
269
  resolveReviewedStory({ story, view }: FolioResolveReviewedStoryOptions): boolean;
270
+ private resolveReviewedStorySnapshotInternal;
252
271
  /**
253
272
  * Apply operations against the current state. Reuses the live-editor applier
254
273
  * verbatim via a headless `{ state, dispatch }` seam; the resulting state
@@ -292,6 +311,7 @@ declare class FolioDocxReviewer {
292
311
  * parts and separator notes are omitted.
293
312
  */
294
313
  getNotesAsText(): string;
314
+ private listStoryHandlesInternal;
295
315
  /** Discover every readable document story through a typed, serializable handle. */
296
316
  listStories(): FolioDocumentStory[];
297
317
  /** Read one discovered story; returns null when its handle is no longer present. */
@@ -386,6 +406,7 @@ declare class FolioDocxReviewer {
386
406
  private getNoteStory;
387
407
  private getHeaderFooterStoryText;
388
408
  private getNoteStoryText;
409
+ private getStoryText;
389
410
  private mergeEditedSecondaryStories;
390
411
  /** Apply any {@link resolveComment} overrides recorded for these comments. */
391
412
  private withResolvedOverrides;
@@ -401,6 +422,8 @@ declare class FolioDocxReviewer {
401
422
  private runCommand;
402
423
  private runStoryCommand;
403
424
  }
425
+ /** @internal Comparison-only access to atomic and fresh reviewer projections. */
426
+ declare const getFolioDocxComparisonAccess: (reviewer: FolioDocxReviewer) => FolioDocxComparisonAccess;
404
427
  /** Options for {@link applyFolioAIEditsToBuffer}. */
405
428
  type ApplyFolioAIEditsToBufferOptions = {
406
429
  /** Default author for tracked changes and comments. (default: `"AI"`) */
@@ -428,4 +451,4 @@ type ApplyFolioAIEditsToBufferResult = FolioAIEditApplyResult & {
428
451
  */
429
452
  declare const applyFolioAIEditsToBuffer: (buffer: ArrayBuffer, operations: FolioAIEditOperation[], options?: ApplyFolioAIEditsToBufferOptions) => Promise<ApplyFolioAIEditsToBufferResult>;
430
453
  //#endregion
431
- export { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FolioApplyDocumentOperationsOptions, FolioApplyDocumentOperationsToStoryOptions, FolioApplyOperationsOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, FolioDocxReviewer, FolioDocxReviewerOptions, FolioEditableDocumentStoryHandle, FolioGetContentAsTextOptions, FolioMatchStoryTableGeometryOptions, FolioNumberingLevel, FolioReadReviewedStoryOptions, FolioResolveReviewedStoryOptions, FolioResolvedReviewedView, type FolioReviewChange, FolioReviewChangeFilter, type FolioReviewChangeKind, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, FolioReviewedStory, FolioReviewedView, type FolioRevisionStamp, UnsupportedFolioReviewedViewError, applyFolioAIEditsToBuffer, isFolioResolvedReviewedView, isFolioReviewedView };
454
+ export { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FolioApplyDocumentOperationsOptions, FolioApplyDocumentOperationsToStoryOptions, FolioApplyOperationsOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocumentStoryNotFoundError, FolioDocxReviewer, FolioDocxReviewerOptions, FolioEditableDocumentStoryHandle, FolioGetContentAsTextOptions, FolioMatchStoryTableGeometryOptions, FolioNumberingLevel, FolioReadReviewedStoryOptions, FolioResolveReviewedStoryOptions, FolioResolvedReviewedView, type FolioReviewChange, FolioReviewChangeFilter, type FolioReviewChangeKind, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, FolioReviewedStory, FolioReviewedView, type FolioRevisionStamp, UnsupportedFolioReviewedViewError, applyFolioAIEditsToBuffer, getFolioDocxComparisonAccess, isFolioResolvedReviewedView, isFolioReviewedView };
@@ -18,10 +18,10 @@ import { createDocumentStylesPlugin, getDocumentStyleResolver } from "../prosemi
18
18
  import { schema, singletonManager } from "../prosemirror/schema/index.js";
19
19
  import { deterministicHexId } from "../utils/hexId.js";
20
20
  import { buildAnnotatedBlockText } from "./clean-text.js";
21
- import { getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js";
22
- import { createFolioAIEditSnapshotWithStyleResolver, folioStoryTables, isFolioAIContentBlock, normalizeFolioAIBlockText } from "./snapshot.js";
21
+ import { getCommentAnchorsFromDoc, getTrackedChangeStatsFromDoc, getTrackedChangesFromDoc, getTrackedChangesFromSnapshot } from "./read.js";
22
+ import { createFolioAIEditSnapshotWithStyleResolver, folioStoryTables, isFolioAIContentBlock, normalizeFolioAIBlockText, sourceDocumentOf } from "./snapshot.js";
23
23
  import { matchTableGeometry } from "./table-geometry.js";
24
- import { TaggedError } from "better-result";
24
+ import { TaggedError, panic } from "better-result";
25
25
  import { Fragment } from "prosemirror-model";
26
26
  import { EditorState } from "prosemirror-state";
27
27
  //#region src/ai-edits/headless.ts
@@ -160,6 +160,7 @@ const resolvedStoryBlockProjection = (block) => {
160
160
  delete persisted.idStability;
161
161
  return persisted;
162
162
  };
163
+ const comparisonAccessByReviewer = /* @__PURE__ */ new WeakMap();
163
164
  const MAIN_STORY = Object.freeze({ type: "main" });
164
165
  const headerFooterStoryKey = ({ type, relationshipId }) => `${type}:${relationshipId}`;
165
166
  const noteStoryKey = ({ type, noteId }) => `${type}:${noteId}`;
@@ -177,12 +178,11 @@ const createHeadlessPlugins = (styles, numbering) => [
177
178
  createDocumentNumberingPlugin(numbering)
178
179
  ];
179
180
  const createStateSnapshot = (state) => createFolioAIEditSnapshotWithStyleResolver(state.doc, getDocumentStyleResolver(state));
180
- const formatStoryStateForLLM = (state, annotated) => {
181
- const snapshot = createStateSnapshot(state);
181
+ const formatStorySnapshotForLLM = (snapshot, annotated) => {
182
182
  const blocks = snapshot.blocks.filter(isFolioAIContentBlock);
183
183
  if (!annotated) return blocks.map(formatBlockForLLM).join("\n");
184
184
  const nodeByStart = /* @__PURE__ */ new Map();
185
- state.doc.descendants((node, pos) => {
185
+ sourceDocumentOf(snapshot).descendants((node, pos) => {
186
186
  if (!node.isTextblock) return true;
187
187
  nodeByStart.set(pos, node);
188
188
  return false;
@@ -264,6 +264,10 @@ var FolioDocxReviewer = class FolioDocxReviewer {
264
264
  this.state = args.state;
265
265
  this.author = args.author;
266
266
  this.usedCommentIds = new Set((args.baseDocument.package.document.comments ?? []).map(({ id }) => id));
267
+ comparisonAccessByReviewer.set(this, Object.freeze({
268
+ projectStories: (mode) => this.projectComparisonStoriesInternal(mode),
269
+ snapshotReviewedStory: (options) => this.snapshotReviewedStoryInternal(options)
270
+ }));
267
271
  }
268
272
  /** Parse a `.docx` buffer into a reviewer. */
269
273
  static async fromBuffer(buffer, options = {}) {
@@ -339,6 +343,43 @@ var FolioDocxReviewer = class FolioDocxReviewer {
339
343
  const state = this.getEditableStoryState(story);
340
344
  return state ? createStateSnapshot(state) : null;
341
345
  }
346
+ snapshotReviewedStoryInternal(options = {}) {
347
+ const story = options.story ?? MAIN_STORY;
348
+ const view = options.view ?? "final";
349
+ if (!isFolioReviewedView(view)) throw new UnsupportedFolioReviewedViewError({
350
+ message: "Unsupported reviewed document view.",
351
+ receivedView: view
352
+ });
353
+ const sourceState = this.getEditableStoryState(story);
354
+ return sourceState ? createStateSnapshot(resolveReviewedState(sourceState, view)) : null;
355
+ }
356
+ projectComparisonStoriesInternal(mode) {
357
+ const handles = this.listStoryHandlesInternal();
358
+ let highestId = 0;
359
+ let present = false;
360
+ if (mode === "with-revision-census") for (const handle of handles) {
361
+ const state = this.getEditableStoryState(handle);
362
+ if (!state) continue;
363
+ const stats = getTrackedChangeStatsFromDoc(state.doc);
364
+ highestId = Math.max(highestId, stats.highestId);
365
+ present ||= stats.present;
366
+ }
367
+ const stories = [];
368
+ for (const handle of handles) stories.push({
369
+ handle,
370
+ snapshot: this.resolveReviewedStorySnapshotInternal({
371
+ story: handle,
372
+ view: "final"
373
+ })
374
+ });
375
+ return {
376
+ stories,
377
+ revisions: {
378
+ highestId,
379
+ present
380
+ }
381
+ };
382
+ }
342
383
  /**
343
384
  * One story's tables, in the document order {@link snapshotStory} numbers
344
385
  * them with, read through a reviewed view.
@@ -390,37 +431,42 @@ var FolioDocxReviewer = class FolioDocxReviewer {
390
431
  readReviewedStory(options = {}) {
391
432
  const story = options.story ?? MAIN_STORY;
392
433
  const view = options.view ?? "final";
393
- if (!isFolioReviewedView(view)) throw new UnsupportedFolioReviewedViewError({
394
- message: "Unsupported reviewed document view.",
395
- receivedView: view
434
+ const snapshot = this.snapshotReviewedStoryInternal({
435
+ story,
436
+ view
396
437
  });
397
- const sourceState = this.getEditableStoryState(story);
398
- if (!sourceState) return null;
399
- const state = resolveReviewedState(sourceState, view);
438
+ if (!snapshot) return null;
400
439
  return {
401
440
  story,
402
441
  view,
403
- snapshot: createStateSnapshot(state),
404
- text: formatStoryStateForLLM(state, view === "current-markup"),
405
- changes: getTrackedChangesFromDoc(state.doc)
442
+ snapshot,
443
+ text: formatStorySnapshotForLLM(snapshot, view === "current-markup"),
444
+ changes: getTrackedChangesFromSnapshot(snapshot)
406
445
  };
407
446
  }
408
447
  /** Resolve one editable story to its original or final state. */
409
448
  resolveReviewedStory({ story = MAIN_STORY, view }) {
449
+ return this.resolveReviewedStorySnapshotInternal({
450
+ story,
451
+ view
452
+ }) !== null;
453
+ }
454
+ resolveReviewedStorySnapshotInternal({ story = MAIN_STORY, view }) {
410
455
  if (!isFolioResolvedReviewedView(view)) throw new UnsupportedFolioReviewedViewError({
411
456
  message: "Only original and final views can replace editable story state.",
412
457
  receivedView: view
413
458
  });
414
459
  const sourceState = this.getEditableStoryState(story);
415
- if (!sourceState) return false;
460
+ if (!sourceState) return null;
416
461
  const resolvedState = resolveReviewedState(sourceState, view);
417
462
  this.setEditableStoryState(story, resolvedState);
463
+ const snapshot = createStateSnapshot(resolvedState);
418
464
  this.resolvedStoryExpectations.set(editableStoryKey(story), {
419
465
  story,
420
- text: formatStoryStateForLLM(resolvedState, false),
421
- blocks: createStateSnapshot(resolvedState).blocks.map(resolvedStoryBlockProjection)
466
+ text: formatStorySnapshotForLLM(snapshot, false),
467
+ blocks: snapshot.blocks.map(resolvedStoryBlockProjection)
422
468
  });
423
- return true;
469
+ return snapshot;
424
470
  }
425
471
  /**
426
472
  * Apply operations against the current state. Reuses the live-editor applier
@@ -564,7 +610,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
564
610
  * (clean) output flattens tracked changes and is unchanged.
565
611
  */
566
612
  getContentAsText(options = {}) {
567
- return formatStoryStateForLLM(this.state, options.annotated === true);
613
+ return formatStorySnapshotForLLM(this.snapshot(), options.annotated === true);
568
614
  }
569
615
  /**
570
616
  * Header / footer and footnote / endnote text as labeled, LLM-ready lines,
@@ -608,54 +654,33 @@ var FolioDocxReviewer = class FolioDocxReviewer {
608
654
  }
609
655
  return lines.join("\n");
610
656
  }
657
+ listStoryHandlesInternal() {
658
+ const pkg = this.baseDocument.package;
659
+ const handles = [{ type: "main" }];
660
+ for (const relationshipId of pkg.headers?.keys() ?? []) handles.push({
661
+ type: "header",
662
+ relationshipId
663
+ });
664
+ for (const relationshipId of pkg.footers?.keys() ?? []) handles.push({
665
+ type: "footer",
666
+ relationshipId
667
+ });
668
+ for (const footnote of pkg.footnotes ?? []) if (!isSeparatorFootnote(footnote)) handles.push({
669
+ type: "footnote",
670
+ noteId: footnote.id
671
+ });
672
+ for (const endnote of pkg.endnotes ?? []) if (!isSeparatorEndnote(endnote)) handles.push({
673
+ type: "endnote",
674
+ noteId: endnote.id
675
+ });
676
+ return handles;
677
+ }
611
678
  /** Discover every readable document story through a typed, serializable handle. */
612
679
  listStories() {
613
- const pkg = this.baseDocument.package;
614
- const stories = [{
615
- handle: { type: "main" },
616
- text: this.getContentAsText()
617
- }];
618
- for (const [relationshipId, header] of pkg.headers ?? []) {
619
- const handle = {
620
- type: "header",
621
- relationshipId
622
- };
623
- stories.push({
624
- handle,
625
- text: this.getHeaderFooterStoryText(handle, header)
626
- });
627
- }
628
- for (const [relationshipId, footer] of pkg.footers ?? []) {
629
- const handle = {
630
- type: "footer",
631
- relationshipId
632
- };
633
- stories.push({
634
- handle,
635
- text: this.getHeaderFooterStoryText(handle, footer)
636
- });
637
- }
638
- for (const footnote of pkg.footnotes ?? []) if (!isSeparatorFootnote(footnote)) {
639
- const handle = {
640
- type: "footnote",
641
- noteId: footnote.id
642
- };
643
- stories.push({
644
- handle,
645
- text: this.getNoteStoryText(handle, footnote)
646
- });
647
- }
648
- for (const endnote of pkg.endnotes ?? []) if (!isSeparatorEndnote(endnote)) {
649
- const handle = {
650
- type: "endnote",
651
- noteId: endnote.id
652
- };
653
- stories.push({
654
- handle,
655
- text: this.getNoteStoryText(handle, endnote)
656
- });
657
- }
658
- return stories;
680
+ return this.listStoryHandlesInternal().map((handle) => ({
681
+ handle,
682
+ text: this.getStoryText(handle)
683
+ }));
659
684
  }
660
685
  /** Read one discovered story; returns null when its handle is no longer present. */
661
686
  readStory(handle) {
@@ -887,11 +912,10 @@ var FolioDocxReviewer = class FolioDocxReviewer {
887
912
  story,
888
913
  view: "current-markup"
889
914
  });
890
- const serializedState = reopened.getEditableStoryState(story);
891
- const serializedText = serializedState ? formatStoryStateForLLM(serializedState, false) : null;
892
- const serializedBlocks = serializedState ? createStateSnapshot(serializedState).blocks.map(resolvedStoryBlockProjection) : null;
915
+ const serializedText = serialized ? formatStorySnapshotForLLM(serialized.snapshot, false) : null;
916
+ const serializedBlocks = serialized ? serialized.snapshot.blocks.map(resolvedStoryBlockProjection) : null;
893
917
  const mismatches = [];
894
- if (!serialized || !serializedState) mismatches.push(FOLIO_RESOLVED_STORY_SERIALIZATION_MISMATCHES.storyMissing);
918
+ if (!serialized) mismatches.push(FOLIO_RESOLVED_STORY_SERIALIZATION_MISMATCHES.storyMissing);
895
919
  else {
896
920
  if (serialized.changes.length > 0) mismatches.push(FOLIO_RESOLVED_STORY_SERIALIZATION_MISMATCHES.revisionMarkupRemains);
897
921
  if (serializedText !== text) mismatches.push(FOLIO_RESOLVED_STORY_SERIALIZATION_MISMATCHES.textProjection);
@@ -986,6 +1010,17 @@ var FolioDocxReviewer = class FolioDocxReviewer {
986
1010
  const sourceText = source.type === "footnote" ? getFootnoteText(source) : getEndnoteText(source);
987
1011
  return normalizeFolioAIBlockText(state?.doc.textContent ?? sourceText);
988
1012
  }
1013
+ getStoryText(story) {
1014
+ if (story.type === "main") return this.getContentAsText();
1015
+ if (story.type === "header" || story.type === "footer") {
1016
+ const source = this.getHeaderFooterStory(story);
1017
+ if (!source) return panic("A listed document story no longer exists", { story });
1018
+ return this.getHeaderFooterStoryText(story, source);
1019
+ }
1020
+ const source = this.getNoteStory(story);
1021
+ if (!source) return panic("A listed document story no longer exists", { story });
1022
+ return this.getNoteStoryText(story, source);
1023
+ }
989
1024
  mergeEditedSecondaryStories(document, secondaryStoryStates = this.secondaryStoryStates.values()) {
990
1025
  let headers;
991
1026
  let footers;
@@ -1087,6 +1122,8 @@ var FolioDocxReviewer = class FolioDocxReviewer {
1087
1122
  return handled;
1088
1123
  }
1089
1124
  };
1125
+ /** @internal Comparison-only access to atomic and fresh reviewer projections. */
1126
+ const getFolioDocxComparisonAccess = (reviewer) => comparisonAccessByReviewer.get(reviewer) ?? panic("Comparison access was requested for an uninitialized DOCX reviewer");
1090
1127
  /**
1091
1128
  * One-shot headless review: parse `buffer`, apply `operations`, and return the
1092
1129
  * reviewed `.docx` buffer alongside the applied / skipped breakdown. Convenience
@@ -1106,4 +1143,4 @@ const applyFolioAIEditsToBuffer = async (buffer, operations, options = {}) => {
1106
1143
  };
1107
1144
  };
1108
1145
  //#endregion
1109
- export { FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FolioDocumentStoryNotFoundError, FolioDocxReviewer, UnsupportedFolioReviewedViewError, applyFolioAIEditsToBuffer, isFolioResolvedReviewedView, isFolioReviewedView };
1146
+ export { FOLIO_RESOLVED_REVIEWED_VIEWS, FOLIO_REVIEWED_VIEWS, FolioDocumentStoryNotFoundError, FolioDocxReviewer, UnsupportedFolioReviewedViewError, applyFolioAIEditsToBuffer, getFolioDocxComparisonAccess, isFolioResolvedReviewedView, isFolioReviewedView };