@stll/folio-core 0.37.1 → 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.
- package/dist/ai-edits/headless.d.ts +24 -1
- package/dist/ai-edits/headless.js +108 -71
- package/dist/ai-edits/read.d.ts +9 -1
- package/dist/ai-edits/read.js +34 -16
- package/dist/ai-edits/snapshot.d.ts +7 -5
- package/dist/ai-edits/snapshot.js +39 -12
- package/dist/compare/compare.d.ts +2 -3
- package/dist/compare/compare.js +41 -64
- package/dist/compare/content-alignment.js +0 -1
- package/dist/compare/scenario.d.ts +11 -2
- package/dist/compare/scenario.js +6 -2
- package/dist/display-list/primitives.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
434
|
+
const snapshot = this.snapshotReviewedStoryInternal({
|
|
435
|
+
story,
|
|
436
|
+
view
|
|
396
437
|
});
|
|
397
|
-
|
|
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
|
|
404
|
-
text:
|
|
405
|
-
changes:
|
|
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
|
|
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:
|
|
421
|
-
blocks:
|
|
466
|
+
text: formatStorySnapshotForLLM(snapshot, false),
|
|
467
|
+
blocks: snapshot.blocks.map(resolvedStoryBlockProjection)
|
|
422
468
|
});
|
|
423
|
-
return
|
|
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
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
|
891
|
-
const
|
|
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
|
|
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 };
|
package/dist/ai-edits/read.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FolioAIEditSnapshot } from "./types.js";
|
|
1
2
|
import { FolioNodeRevisionKind } from "../prosemirror/revisionCarriers.js";
|
|
2
3
|
import { Node } from "prosemirror-model";
|
|
3
4
|
//#region src/ai-edits/read.d.ts
|
|
@@ -56,6 +57,8 @@ type FolioCommentAnchor = {
|
|
|
56
57
|
/** The document text the comment is anchored to. */
|
|
57
58
|
quote: string;
|
|
58
59
|
};
|
|
60
|
+
/** Read revisions from the exact immutable document that produced a snapshot. */
|
|
61
|
+
declare const getTrackedChangesFromSnapshot: (snapshot: FolioAIEditSnapshot) => FolioReviewChange[];
|
|
59
62
|
/**
|
|
60
63
|
* The tracked changes present in the body, read from inline marks and
|
|
61
64
|
* structural node attributes. Runs of one inline revision within a block fold
|
|
@@ -66,6 +69,11 @@ type FolioCommentAnchor = {
|
|
|
66
69
|
* fold into the row's entry rather than reporting a second time.
|
|
67
70
|
*/
|
|
68
71
|
declare const getTrackedChangesFromDoc: (doc: Node) => FolioReviewChange[];
|
|
72
|
+
/** @internal Read revision statistics without paying for an unrelated block projection. */
|
|
73
|
+
declare const getTrackedChangeStatsFromDoc: (doc: Node) => {
|
|
74
|
+
highestId: number;
|
|
75
|
+
present: boolean;
|
|
76
|
+
};
|
|
69
77
|
/**
|
|
70
78
|
* The comment anchors present in the body, read from the `comment` mark the
|
|
71
79
|
* editor renders. Each entry carries the anchored text and containing block id;
|
|
@@ -73,4 +81,4 @@ declare const getTrackedChangesFromDoc: (doc: Node) => FolioReviewChange[];
|
|
|
73
81
|
*/
|
|
74
82
|
declare const getCommentAnchorsFromDoc: (doc: Node) => FolioCommentAnchor[];
|
|
75
83
|
//#endregion
|
|
76
|
-
export { FOLIO_REVIEW_CHANGE_KINDS, FolioCommentAnchor, FolioReviewChange, FolioReviewChangeKind, getCommentAnchorsFromDoc, getTrackedChangesFromDoc };
|
|
84
|
+
export { FOLIO_REVIEW_CHANGE_KINDS, FolioCommentAnchor, FolioReviewChange, FolioReviewChangeKind, getCommentAnchorsFromDoc, getTrackedChangeStatsFromDoc, getTrackedChangesFromDoc, getTrackedChangesFromSnapshot };
|
package/dist/ai-edits/read.js
CHANGED
|
@@ -2,7 +2,7 @@ import { expectRunPropertyChangeMarkAttrs } from "../prosemirror/attrs/index.js"
|
|
|
2
2
|
import { getFolioNodeRevisionCarriers } from "../prosemirror/revisionCarriers.js";
|
|
3
3
|
import { expandRunFormattingCarrier, runFormattingCarrierReviewText, runFormattingInlineAtomDisposition } from "../prosemirror/runFormattingInlineCarriers.js";
|
|
4
4
|
import { getTableCellMergeChange } from "../prosemirror/tableCellMergeRevision.js";
|
|
5
|
-
import { createFolioAIEditSnapshot } from "./snapshot.js";
|
|
5
|
+
import { createFolioAIEditSnapshot, sourceDocumentOf } from "./snapshot.js";
|
|
6
6
|
//#region src/ai-edits/read.ts
|
|
7
7
|
/**
|
|
8
8
|
* Runtime census of every tracked-change discriminator the reviewer exposes.
|
|
@@ -31,11 +31,16 @@ const FOLIO_REVIEW_CHANGE_KINDS = Object.freeze({
|
|
|
31
31
|
});
|
|
32
32
|
/** Map each snapshot block's start position to its stable id. */
|
|
33
33
|
const blockStartIdsFromDoc = (doc) => {
|
|
34
|
+
return blockStartIdsFromSnapshot(createFolioAIEditSnapshot(doc));
|
|
35
|
+
};
|
|
36
|
+
/** Map an existing snapshot's block starts without projecting its document again. */
|
|
37
|
+
const blockStartIdsFromSnapshot = (snapshot) => {
|
|
34
38
|
const starts = /* @__PURE__ */ new Map();
|
|
35
|
-
for (const anchor of Object.values(
|
|
39
|
+
for (const anchor of Object.values(snapshot.anchors)) starts.set(anchor.from, anchor.id);
|
|
36
40
|
return starts;
|
|
37
41
|
};
|
|
38
42
|
const firstBlockIdWithin = ({ node, nodePos, blockStarts }) => {
|
|
43
|
+
if (!blockStarts) return null;
|
|
39
44
|
let blockId = null;
|
|
40
45
|
node.descendants((child, relativePos) => {
|
|
41
46
|
if (blockId !== null || !child.isTextblock) return blockId === null;
|
|
@@ -51,19 +56,10 @@ const firstBlockIdWithin = ({ node, nodePos, blockStarts }) => {
|
|
|
51
56
|
* matches neither and keeps its own entry.
|
|
52
57
|
*/
|
|
53
58
|
const belongsToRowRevision = (scope, revision) => revision.id === scope.change.id || revision.author === scope.change.author && revision.date === scope.change.date;
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
* structural node attributes. Runs of one inline revision within a block fold
|
|
57
|
-
* into a single entry.
|
|
58
|
-
*
|
|
59
|
-
* A tracked row insertion or deletion marks the row AND every run in its
|
|
60
|
-
* cells, the way Word writes it. Both halves are one change, so the run marks
|
|
61
|
-
* fold into the row's entry rather than reporting a second time.
|
|
62
|
-
*/
|
|
63
|
-
const getTrackedChangesFromDoc = (doc) => {
|
|
59
|
+
/** Shared revision interpreter; a null block map omits unrelated block-id projection. */
|
|
60
|
+
const getTrackedChangesFromProjectedDoc = (doc, blockStarts) => {
|
|
64
61
|
const insertionType = doc.type.schema.marks["insertion"];
|
|
65
62
|
const deletionType = doc.type.schema.marks["deletion"];
|
|
66
|
-
const blockStarts = blockStartIdsFromDoc(doc);
|
|
67
63
|
const grouped = /* @__PURE__ */ new Map();
|
|
68
64
|
const structuralChangeCellPositions = /* @__PURE__ */ new Map();
|
|
69
65
|
const rowRevisionScopes = [];
|
|
@@ -73,7 +69,7 @@ const getTrackedChangesFromDoc = (doc) => {
|
|
|
73
69
|
while (pos >= (rowRevisionScopes.at(-1)?.end ?? Number.POSITIVE_INFINITY)) rowRevisionScopes.pop();
|
|
74
70
|
const nodeRevisionCarriers = getFolioNodeRevisionCarriers(node, pos);
|
|
75
71
|
if (nodeRevisionCarriers.length > 0) {
|
|
76
|
-
const blockId = node.isTextblock ? blockStarts
|
|
72
|
+
const blockId = node.isTextblock ? blockStarts?.get(pos) ?? null : firstBlockIdWithin({
|
|
77
73
|
node,
|
|
78
74
|
nodePos: pos,
|
|
79
75
|
blockStarts
|
|
@@ -171,7 +167,7 @@ const getTrackedChangesFromDoc = (doc) => {
|
|
|
171
167
|
}
|
|
172
168
|
}
|
|
173
169
|
if (node.isTextblock) {
|
|
174
|
-
currentBlockId = blockStarts
|
|
170
|
+
currentBlockId = blockStarts?.get(pos) ?? null;
|
|
175
171
|
return true;
|
|
176
172
|
}
|
|
177
173
|
if (!node.isInline) return;
|
|
@@ -244,6 +240,28 @@ const getTrackedChangesFromDoc = (doc) => {
|
|
|
244
240
|
});
|
|
245
241
|
return [...grouped.values()];
|
|
246
242
|
};
|
|
243
|
+
/** Read revisions from the exact immutable document that produced a snapshot. */
|
|
244
|
+
const getTrackedChangesFromSnapshot = (snapshot) => getTrackedChangesFromProjectedDoc(sourceDocumentOf(snapshot), blockStartIdsFromSnapshot(snapshot));
|
|
245
|
+
/**
|
|
246
|
+
* The tracked changes present in the body, read from inline marks and
|
|
247
|
+
* structural node attributes. Runs of one inline revision within a block fold
|
|
248
|
+
* into a single entry.
|
|
249
|
+
*
|
|
250
|
+
* A tracked row insertion or deletion marks the row AND every run in its
|
|
251
|
+
* cells, the way Word writes it. Both halves are one change, so the run marks
|
|
252
|
+
* fold into the row's entry rather than reporting a second time.
|
|
253
|
+
*/
|
|
254
|
+
const getTrackedChangesFromDoc = (doc) => getTrackedChangesFromProjectedDoc(doc, blockStartIdsFromDoc(doc));
|
|
255
|
+
/** @internal Read revision statistics without paying for an unrelated block projection. */
|
|
256
|
+
const getTrackedChangeStatsFromDoc = (doc) => {
|
|
257
|
+
let highestId = 0;
|
|
258
|
+
const changes = getTrackedChangesFromProjectedDoc(doc, null);
|
|
259
|
+
for (const change of changes) highestId = Math.max(highestId, change.id);
|
|
260
|
+
return {
|
|
261
|
+
highestId,
|
|
262
|
+
present: changes.length > 0
|
|
263
|
+
};
|
|
264
|
+
};
|
|
247
265
|
/**
|
|
248
266
|
* The comment anchors present in the body, read from the `comment` mark the
|
|
249
267
|
* editor renders. Each entry carries the anchored text and containing block id;
|
|
@@ -281,4 +299,4 @@ const getCommentAnchorsFromDoc = (doc) => {
|
|
|
281
299
|
return [...anchors.values()];
|
|
282
300
|
};
|
|
283
301
|
//#endregion
|
|
284
|
-
export { FOLIO_REVIEW_CHANGE_KINDS, getCommentAnchorsFromDoc, getTrackedChangesFromDoc };
|
|
302
|
+
export { FOLIO_REVIEW_CHANGE_KINDS, getCommentAnchorsFromDoc, getTrackedChangeStatsFromDoc, getTrackedChangesFromDoc, getTrackedChangesFromSnapshot };
|
|
@@ -5,6 +5,10 @@ import { Node } from "prosemirror-model";
|
|
|
5
5
|
//#region src/ai-edits/snapshot.d.ts
|
|
6
6
|
/** @internal Numbering references collected during the snapshot's document walk. */
|
|
7
7
|
declare const numberingReferenceKeysOf: (snapshot: FolioAIEditSnapshot) => readonly string[];
|
|
8
|
+
/** @internal Tables collected during the snapshot's document walk. */
|
|
9
|
+
declare const storyTablesOf: (snapshot: FolioAIEditSnapshot) => readonly FolioStoryTable[];
|
|
10
|
+
/** @internal The immutable ProseMirror document that produced this snapshot. */
|
|
11
|
+
declare const sourceDocumentOf: (snapshot: FolioAIEditSnapshot) => Node;
|
|
8
12
|
declare const normalizeFolioAIBlockText: (text: string) => string;
|
|
9
13
|
/**
|
|
10
14
|
* Whether a block carries text a reader would see.
|
|
@@ -70,14 +74,12 @@ type FolioStoryTable = {
|
|
|
70
74
|
* Every table of one story in document order, nested tables included and
|
|
71
75
|
* hidden rows' subtrees excluded.
|
|
72
76
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* it checks its own work against all read this list, so no second walk can
|
|
76
|
-
* number the same document differently.
|
|
77
|
+
* Uses the same traversal classification as the snapshot's integrated census,
|
|
78
|
+
* so both surfaces skip and number the same nodes in the same order.
|
|
77
79
|
*/
|
|
78
80
|
declare const folioStoryTables: (doc: Node) => FolioStoryTable[];
|
|
79
81
|
declare const createFolioAIEditSnapshot: (doc: Node) => FolioAIEditSnapshot;
|
|
80
82
|
/** @internal Use for an EditorState that owns the document's style resolver. */
|
|
81
83
|
declare const createFolioAIEditSnapshotWithStyleResolver: (doc: Node, styleResolver: RunStyleResolver | null) => FolioAIEditSnapshot;
|
|
82
84
|
//#endregion
|
|
83
|
-
export { FolioStoryTable, createFolioAIEditSnapshot, createFolioAIEditSnapshotWithStyleResolver, createFolioAITextRangeHandle, folioStoryTables, hashFolioAIBlockStructuralBoundaries, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, numberingReferenceKeysOf, projectFolioAIBlockStructuralBoundaries, trailingBodyBlockId };
|
|
85
|
+
export { FolioStoryTable, createFolioAIEditSnapshot, createFolioAIEditSnapshotWithStyleResolver, createFolioAITextRangeHandle, folioStoryTables, hashFolioAIBlockStructuralBoundaries, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, numberingReferenceKeysOf, projectFolioAIBlockStructuralBoundaries, sourceDocumentOf, storyTablesOf, trailingBodyBlockId };
|
|
@@ -9,9 +9,14 @@ import { buildCleanBlockText } from "./clean-text.js";
|
|
|
9
9
|
import { panic } from "better-result";
|
|
10
10
|
import { TableMap } from "prosemirror-tables";
|
|
11
11
|
//#region src/ai-edits/snapshot.ts
|
|
12
|
-
const
|
|
12
|
+
const metadataBySnapshot = /* @__PURE__ */ new WeakMap();
|
|
13
|
+
const metadataOf = (snapshot) => metadataBySnapshot.get(snapshot) ?? panic("Metadata was requested for a snapshot that did not record it");
|
|
13
14
|
/** @internal Numbering references collected during the snapshot's document walk. */
|
|
14
|
-
const numberingReferenceKeysOf = (snapshot) =>
|
|
15
|
+
const numberingReferenceKeysOf = (snapshot) => metadataOf(snapshot).numberingReferenceKeys;
|
|
16
|
+
/** @internal Tables collected during the snapshot's document walk. */
|
|
17
|
+
const storyTablesOf = (snapshot) => metadataOf(snapshot).storyTables;
|
|
18
|
+
/** @internal The immutable ProseMirror document that produced this snapshot. */
|
|
19
|
+
const sourceDocumentOf = (snapshot) => metadataOf(snapshot).sourceDocument;
|
|
15
20
|
const normalizeFolioAIBlockText = (text) => text.replace(/\s+/gu, " ").trim();
|
|
16
21
|
/**
|
|
17
22
|
* Whether a block carries text a reader would see.
|
|
@@ -96,21 +101,28 @@ const TABLE_ROLE_ROW = "row";
|
|
|
96
101
|
* whole subtree, so a table nested inside a hidden row is hidden with it.
|
|
97
102
|
*/
|
|
98
103
|
const isHiddenTableRow = (node) => node.type.name === TABLE_ROW_NODE_NAME && node.attrs["hidden"] === true;
|
|
104
|
+
const STORY_TABLE_CONTAINER = "container";
|
|
105
|
+
const STORY_TABLE_HIDDEN_SUBTREE = "hidden-subtree";
|
|
106
|
+
const STORY_TABLE = "table";
|
|
107
|
+
/** Shared visibility and table-role rule for both story projection walks. */
|
|
108
|
+
const classifyNonTextblockStoryTableNode = (node) => {
|
|
109
|
+
if (isHiddenTableRow(node)) return STORY_TABLE_HIDDEN_SUBTREE;
|
|
110
|
+
return node.type.spec["tableRole"] === TABLE_ROLE_TABLE ? STORY_TABLE : STORY_TABLE_CONTAINER;
|
|
111
|
+
};
|
|
99
112
|
/**
|
|
100
113
|
* Every table of one story in document order, nested tables included and
|
|
101
114
|
* hidden rows' subtrees excluded.
|
|
102
115
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* it checks its own work against all read this list, so no second walk can
|
|
106
|
-
* number the same document differently.
|
|
116
|
+
* Uses the same traversal classification as the snapshot's integrated census,
|
|
117
|
+
* so both surfaces skip and number the same nodes in the same order.
|
|
107
118
|
*/
|
|
108
119
|
const folioStoryTables = (doc) => {
|
|
109
120
|
const tables = [];
|
|
110
121
|
doc.descendants((node, pos) => {
|
|
111
122
|
if (node.isTextblock) return false;
|
|
112
|
-
|
|
113
|
-
if (
|
|
123
|
+
const disposition = classifyNonTextblockStoryTableNode(node);
|
|
124
|
+
if (disposition === STORY_TABLE_HIDDEN_SUBTREE) return false;
|
|
125
|
+
if (disposition === STORY_TABLE) tables.push({
|
|
114
126
|
index: tables.length,
|
|
115
127
|
start: pos,
|
|
116
128
|
node
|
|
@@ -154,14 +166,25 @@ const createFolioAIEditSnapshotInternal = (doc, styleResolver) => {
|
|
|
154
166
|
const hashCounts = /* @__PURE__ */ new Map();
|
|
155
167
|
const usedBlockIds = /* @__PURE__ */ new Set();
|
|
156
168
|
const numberingReferenceKeys = /* @__PURE__ */ new Set();
|
|
157
|
-
const
|
|
169
|
+
const tables = [];
|
|
170
|
+
const tableIndexByStart = /* @__PURE__ */ new Map();
|
|
158
171
|
const path = [];
|
|
159
172
|
let blockIndex = 0;
|
|
160
173
|
let blankIndex = 0;
|
|
161
174
|
doc.descendants((node, pos, _parent, index) => {
|
|
162
175
|
while (pos >= (path.at(-1)?.end ?? Number.POSITIVE_INFINITY)) path.pop();
|
|
163
176
|
if (!node.isTextblock) {
|
|
164
|
-
|
|
177
|
+
const disposition = classifyNonTextblockStoryTableNode(node);
|
|
178
|
+
if (disposition === STORY_TABLE_HIDDEN_SUBTREE) return false;
|
|
179
|
+
if (disposition === STORY_TABLE) {
|
|
180
|
+
const tableIndex = tables.length;
|
|
181
|
+
tables.push({
|
|
182
|
+
index: tableIndex,
|
|
183
|
+
start: pos,
|
|
184
|
+
node
|
|
185
|
+
});
|
|
186
|
+
tableIndexByStart.set(pos, tableIndex);
|
|
187
|
+
}
|
|
165
188
|
if (!node.isLeaf) path.push({
|
|
166
189
|
node,
|
|
167
190
|
start: pos,
|
|
@@ -254,7 +277,11 @@ const createFolioAIEditSnapshotInternal = (doc, styleResolver) => {
|
|
|
254
277
|
blocks,
|
|
255
278
|
anchors
|
|
256
279
|
};
|
|
257
|
-
|
|
280
|
+
metadataBySnapshot.set(snapshot, {
|
|
281
|
+
numberingReferenceKeys: [...numberingReferenceKeys],
|
|
282
|
+
sourceDocument: doc,
|
|
283
|
+
storyTables: tables
|
|
284
|
+
});
|
|
258
285
|
return snapshot;
|
|
259
286
|
};
|
|
260
287
|
const createFolioAIEditSnapshot = (doc) => createFolioAIEditSnapshotInternal(doc, null);
|
|
@@ -479,4 +506,4 @@ const isEmptyPreviewRunStyle = ({ bold, italic, underline, strike, fontFamily, f
|
|
|
479
506
|
const sameDirectFormatting = (left, right) => left === void 0 && isEmptyPreviewRunStyle(right) || left !== void 0 && left.bold === right.bold && left.italic === right.italic && left.underline === right.underline && left.strike === right.strike && left.fontFamily === right.fontFamily && left.fontSizePt === right.fontSizePt && left.color === right.color;
|
|
480
507
|
const isUnstyledPreviewRun = ({ bold, italic, underline, strike, fontFamily, fontSizePt, color }) => bold === void 0 && italic === void 0 && underline === void 0 && strike === void 0 && fontFamily === void 0 && fontSizePt === void 0 && color === void 0;
|
|
481
508
|
//#endregion
|
|
482
|
-
export { createFolioAIEditSnapshot, createFolioAIEditSnapshotWithStyleResolver, createFolioAITextRangeHandle, folioStoryTables, hashFolioAIBlockStructuralBoundaries, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, numberingReferenceKeysOf, projectFolioAIBlockStructuralBoundaries, trailingBodyBlockId };
|
|
509
|
+
export { createFolioAIEditSnapshot, createFolioAIEditSnapshotWithStyleResolver, createFolioAITextRangeHandle, folioStoryTables, hashFolioAIBlockStructuralBoundaries, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, numberingReferenceKeysOf, projectFolioAIBlockStructuralBoundaries, sourceDocumentOf, storyTablesOf, trailingBodyBlockId };
|
|
@@ -31,7 +31,6 @@ type ParsedComparison = {
|
|
|
31
31
|
baseBuffer: ArrayBuffer;
|
|
32
32
|
baseCarriedRevisions: boolean;
|
|
33
33
|
reviewer: FolioDocxReviewer;
|
|
34
|
-
targetReviewer: FolioDocxReviewer;
|
|
35
34
|
revisionStamp: FolioRevisionStamp;
|
|
36
35
|
packageDate: Date;
|
|
37
36
|
pairs: readonly ComparedStoryPair[];
|
|
@@ -50,7 +49,7 @@ type PlannedStoryComparison = {
|
|
|
50
49
|
* Stage 2: align every paired story and derive its operations. Pure — no
|
|
51
50
|
* parsing, no serialization, no clock.
|
|
52
51
|
*/
|
|
53
|
-
declare const planComparison: ({ pairs
|
|
52
|
+
declare const planComparison: ({ pairs }: ParsedComparison) => Result<readonly PlannedStoryComparison[], CompareDocxOperationLimitError>;
|
|
54
53
|
declare const getCompareSkipDisposition: (reason: FolioAIEditSkipReason) => "fatal" | "unwritable";
|
|
55
54
|
/** What stage 3 produced: the change list, whether it was proven, and whether it wrote anything. */
|
|
56
55
|
type AppliedComparison = {
|
|
@@ -77,7 +76,7 @@ type AppliedComparison = {
|
|
|
77
76
|
* what you could not represent" and "give me nothing unless you can prove it"
|
|
78
77
|
* are both legitimate asks and only the caller knows which one it is making.
|
|
79
78
|
*/
|
|
80
|
-
declare const applyComparison: ({ reviewer,
|
|
79
|
+
declare const applyComparison: ({ reviewer, revisionStamp, granularity, numberingChanges }: ParsedComparison, planned: readonly PlannedStoryComparison[]) => Result<AppliedComparison, CompareDocxApplyError>;
|
|
81
80
|
/**
|
|
82
81
|
* Stage 4: the result package, with every ZIP entry date pinned.
|
|
83
82
|
*
|
package/dist/compare/compare.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { FolioDocxReviewer } from "../ai-edits/headless.js";
|
|
2
|
-
import { numberingReferenceKeysOf } from "../ai-edits/snapshot.js";
|
|
1
|
+
import { FolioDocxReviewer, getFolioDocxComparisonAccess } from "../ai-edits/headless.js";
|
|
2
|
+
import { numberingReferenceKeysOf, 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";
|
|
@@ -60,25 +60,6 @@ const parseSide = async (buffer, side, author) => await Result.tryPromise({
|
|
|
60
60
|
cause
|
|
61
61
|
})
|
|
62
62
|
});
|
|
63
|
-
/** Read before either side is resolved, so it describes the package as it arrived. */
|
|
64
|
-
const existingRevisionsOf = (reviewer) => {
|
|
65
|
-
let highest = 0;
|
|
66
|
-
let present = false;
|
|
67
|
-
for (const { handle } of reviewer.listStories()) {
|
|
68
|
-
const story = reviewer.readReviewedStory({
|
|
69
|
-
story: handle,
|
|
70
|
-
view: "current-markup"
|
|
71
|
-
});
|
|
72
|
-
for (const change of story?.changes ?? []) {
|
|
73
|
-
highest = Math.max(highest, change.id);
|
|
74
|
-
present = true;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
idSeed: highest + 1,
|
|
79
|
-
present
|
|
80
|
-
};
|
|
81
|
-
};
|
|
82
63
|
/** Verify formatting only where the plan claims a text-equal formatting change. */
|
|
83
64
|
const formattingRoundTripFailure = ({ invariant, story, changes, actualBlocks, expectedBlocks, expectedBlockId }) => {
|
|
84
65
|
const expectedIndexById = new Map(expectedBlocks.map(({ id }, index) => [id, index]));
|
|
@@ -146,15 +127,20 @@ const parseComparison = async (base, target, options) => {
|
|
|
146
127
|
if (targetParse.isErr()) return Result.err(targetParse.error);
|
|
147
128
|
const reviewer = baseParse.value;
|
|
148
129
|
const targetReviewer = targetParse.value;
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
130
|
+
const baseProjection = getFolioDocxComparisonAccess(reviewer).projectStories("with-revision-census");
|
|
131
|
+
const targetProjection = getFolioDocxComparisonAccess(targetReviewer).projectStories("without-revision-census");
|
|
132
|
+
const baseStories = [];
|
|
133
|
+
const targetStories = [];
|
|
134
|
+
const baseSnapshots = /* @__PURE__ */ new Map();
|
|
135
|
+
const targetSnapshots = /* @__PURE__ */ new Map();
|
|
136
|
+
for (const { handle, snapshot } of baseProjection.stories) {
|
|
137
|
+
baseStories.push(handle);
|
|
138
|
+
baseSnapshots.set(handle, snapshot);
|
|
139
|
+
}
|
|
140
|
+
for (const { handle, snapshot } of targetProjection.stories) {
|
|
141
|
+
targetStories.push(handle);
|
|
142
|
+
targetSnapshots.set(handle, snapshot);
|
|
143
|
+
}
|
|
158
144
|
const pairs = [];
|
|
159
145
|
const unsupported = [];
|
|
160
146
|
const referencedNumberingLevels = /* @__PURE__ */ new Set();
|
|
@@ -162,10 +148,10 @@ const parseComparison = async (base, target, options) => {
|
|
|
162
148
|
if (!snapshot) return;
|
|
163
149
|
for (const referenceKey of numberingReferenceKeysOf(snapshot)) referencedNumberingLevels.add(referenceKey);
|
|
164
150
|
};
|
|
165
|
-
for (const { baseStory, revisedStory: targetStory } of pairFolioDocumentStories(
|
|
151
|
+
for (const { baseStory, revisedStory: targetStory } of pairFolioDocumentStories(baseStories, targetStories)) {
|
|
166
152
|
if (!baseStory) {
|
|
167
153
|
if (!targetStory) panic("A story pair contained neither a base nor a target story");
|
|
168
|
-
collectNumberingReferences(
|
|
154
|
+
collectNumberingReferences(targetSnapshots.get(targetStory));
|
|
169
155
|
unsupported.push({
|
|
170
156
|
reason: "story-missing-in-base",
|
|
171
157
|
baseStory: null,
|
|
@@ -174,7 +160,7 @@ const parseComparison = async (base, target, options) => {
|
|
|
174
160
|
continue;
|
|
175
161
|
}
|
|
176
162
|
if (!targetStory) {
|
|
177
|
-
collectNumberingReferences(
|
|
163
|
+
collectNumberingReferences(baseSnapshots.get(baseStory));
|
|
178
164
|
unsupported.push({
|
|
179
165
|
reason: "story-missing-in-target",
|
|
180
166
|
baseStory,
|
|
@@ -182,8 +168,8 @@ const parseComparison = async (base, target, options) => {
|
|
|
182
168
|
});
|
|
183
169
|
continue;
|
|
184
170
|
}
|
|
185
|
-
const baseSnapshot =
|
|
186
|
-
const targetSnapshot =
|
|
171
|
+
const baseSnapshot = baseSnapshots.get(baseStory);
|
|
172
|
+
const targetSnapshot = targetSnapshots.get(targetStory);
|
|
187
173
|
collectNumberingReferences(baseSnapshot);
|
|
188
174
|
collectNumberingReferences(targetSnapshot);
|
|
189
175
|
if (!baseSnapshot || !targetSnapshot) {
|
|
@@ -204,12 +190,11 @@ const parseComparison = async (base, target, options) => {
|
|
|
204
190
|
return Result.ok({
|
|
205
191
|
granularity: options.granularity ?? "word",
|
|
206
192
|
baseBuffer: base,
|
|
207
|
-
baseCarriedRevisions:
|
|
193
|
+
baseCarriedRevisions: baseProjection.revisions.present,
|
|
208
194
|
reviewer,
|
|
209
|
-
targetReviewer,
|
|
210
195
|
revisionStamp: {
|
|
211
196
|
date: options.timestamp,
|
|
212
|
-
idSeed:
|
|
197
|
+
idSeed: baseProjection.revisions.highestId + 1
|
|
213
198
|
},
|
|
214
199
|
packageDate,
|
|
215
200
|
pairs,
|
|
@@ -217,8 +202,8 @@ const parseComparison = async (base, target, options) => {
|
|
|
217
202
|
unsupported
|
|
218
203
|
});
|
|
219
204
|
};
|
|
220
|
-
const planCopiesNonPortableWholeTable = (
|
|
221
|
-
const targetTables = new Map(
|
|
205
|
+
const planCopiesNonPortableWholeTable = (pair, plan) => {
|
|
206
|
+
const targetTables = new Map(storyTablesOf(pair.targetSnapshot).map(({ index, node }) => [index, node]));
|
|
222
207
|
return plan.tableTemplates.some(({ targetRowIndex, targetTableIndex }) => {
|
|
223
208
|
if (targetRowIndex !== void 0) return false;
|
|
224
209
|
const table = targetTables.get(targetTableIndex);
|
|
@@ -229,7 +214,7 @@ const planCopiesNonPortableWholeTable = (targetReviewer, pair, plan) => {
|
|
|
229
214
|
* Stage 2: align every paired story and derive its operations. Pure — no
|
|
230
215
|
* parsing, no serialization, no clock.
|
|
231
216
|
*/
|
|
232
|
-
const planComparison = ({ pairs
|
|
217
|
+
const planComparison = ({ pairs }) => {
|
|
233
218
|
const planned = [];
|
|
234
219
|
const workSession = createContentComparisonWorkSession();
|
|
235
220
|
let remainingOperations = MAX_COMPARE_OPERATIONS;
|
|
@@ -246,7 +231,7 @@ const planComparison = ({ pairs, targetReviewer }) => {
|
|
|
246
231
|
wholeTableReplacement: "allow",
|
|
247
232
|
workSession
|
|
248
233
|
});
|
|
249
|
-
if (plan && planCopiesNonPortableWholeTable(
|
|
234
|
+
if (plan && planCopiesNonPortableWholeTable(pair, plan)) {
|
|
250
235
|
workSession.alignment.remainingLcsCells = remainingLcsCells;
|
|
251
236
|
workSession.alignment.remainingStructuralTokenLookups = remainingStructuralTokenLookups;
|
|
252
237
|
workSession.remainingMoveComparisons = remainingMoveComparisons;
|
|
@@ -343,7 +328,8 @@ const resolveTableTemplates = (targetTables, requests) => {
|
|
|
343
328
|
* what you could not represent" and "give me nothing unless you can prove it"
|
|
344
329
|
* are both legitimate asks and only the caller knows which one it is making.
|
|
345
330
|
*/
|
|
346
|
-
const applyComparison = ({ reviewer,
|
|
331
|
+
const applyComparison = ({ reviewer, revisionStamp, granularity, numberingChanges }, planned) => {
|
|
332
|
+
const comparisonAccess = getFolioDocxComparisonAccess(reviewer);
|
|
347
333
|
const changes = [...numberingChanges];
|
|
348
334
|
const failures = [];
|
|
349
335
|
let idSeed = revisionStamp.idSeed;
|
|
@@ -352,12 +338,9 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
|
|
|
352
338
|
for (const { pair, plan } of planned) {
|
|
353
339
|
changes.push(...plan.changes);
|
|
354
340
|
if (plan.operations.length === 0 && plan.tableGeometryPairings.length === 0) continue;
|
|
355
|
-
const baseBefore =
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
})?.snapshot.blocks ?? [];
|
|
359
|
-
const baseBeforeGeometry = projectTableGeometry(reviewer.storyTables({ story: pair.baseStory }));
|
|
360
|
-
const targetTables = new Map(targetReviewer.storyTables({ story: pair.targetStory }).map(({ index, node }) => [index, node]));
|
|
341
|
+
const baseBefore = pair.baseSnapshot.blocks;
|
|
342
|
+
const baseBeforeGeometry = projectTableGeometry(storyTablesOf(pair.baseSnapshot));
|
|
343
|
+
const targetTables = new Map(storyTablesOf(pair.targetSnapshot).map(({ index, node }) => [index, node]));
|
|
361
344
|
const afterGeometry = reviewer.matchStoryTableGeometry({
|
|
362
345
|
story: pair.baseStory,
|
|
363
346
|
targetTables,
|
|
@@ -396,14 +379,14 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
|
|
|
396
379
|
skipped: refused
|
|
397
380
|
}));
|
|
398
381
|
}
|
|
399
|
-
const
|
|
382
|
+
const acceptedSnapshot = comparisonAccess.snapshotReviewedStory({
|
|
400
383
|
story: pair.baseStory,
|
|
401
384
|
view: "final"
|
|
402
385
|
});
|
|
403
386
|
const acceptFailure = classifyProjectionMismatch({
|
|
404
387
|
invariant: "accept-reproduces-target",
|
|
405
388
|
story: pair.baseStory,
|
|
406
|
-
actual:
|
|
389
|
+
actual: acceptedSnapshot?.blocks ?? [],
|
|
407
390
|
expected: pair.targetSnapshot.blocks
|
|
408
391
|
});
|
|
409
392
|
if (acceptFailure) failures.push(acceptFailure);
|
|
@@ -412,20 +395,20 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
|
|
|
412
395
|
invariant: "accept-reproduces-target",
|
|
413
396
|
story: pair.baseStory,
|
|
414
397
|
changes: plan.changes,
|
|
415
|
-
actualBlocks:
|
|
398
|
+
actualBlocks: acceptedSnapshot?.blocks ?? [],
|
|
416
399
|
expectedBlocks: pair.targetSnapshot.blocks,
|
|
417
400
|
expectedBlockId: ({ targetBlockId }) => targetBlockId
|
|
418
401
|
});
|
|
419
402
|
if (formattingFailure) failures.push(formattingFailure);
|
|
420
403
|
}
|
|
421
|
-
const
|
|
404
|
+
const rejectedSnapshot = comparisonAccess.snapshotReviewedStory({
|
|
422
405
|
story: pair.baseStory,
|
|
423
406
|
view: "original"
|
|
424
407
|
});
|
|
425
408
|
const rejectFailure = classifyProjectionMismatch({
|
|
426
409
|
invariant: "reject-reproduces-base",
|
|
427
410
|
story: pair.baseStory,
|
|
428
|
-
actual:
|
|
411
|
+
actual: rejectedSnapshot?.blocks ?? [],
|
|
429
412
|
expected: baseBefore
|
|
430
413
|
});
|
|
431
414
|
if (rejectFailure) failures.push(rejectFailure);
|
|
@@ -434,7 +417,7 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
|
|
|
434
417
|
invariant: "reject-reproduces-base",
|
|
435
418
|
story: pair.baseStory,
|
|
436
419
|
changes: plan.changes,
|
|
437
|
-
actualBlocks:
|
|
420
|
+
actualBlocks: rejectedSnapshot?.blocks ?? [],
|
|
438
421
|
expectedBlocks: pair.baseSnapshot.blocks,
|
|
439
422
|
expectedBlockId: ({ baseBlockId }) => baseBlockId
|
|
440
423
|
});
|
|
@@ -443,20 +426,14 @@ const applyComparison = ({ reviewer, targetReviewer, revisionStamp, granularity,
|
|
|
443
426
|
const geometryAcceptFailure = classifyGeometryMismatch({
|
|
444
427
|
invariant: "accept-reproduces-target",
|
|
445
428
|
story: pair.baseStory,
|
|
446
|
-
actual: projectTableGeometry(
|
|
447
|
-
|
|
448
|
-
view: "final"
|
|
449
|
-
})),
|
|
450
|
-
expected: projectTableGeometry(targetReviewer.storyTables({ story: pair.targetStory }))
|
|
429
|
+
actual: projectTableGeometry(acceptedSnapshot ? storyTablesOf(acceptedSnapshot) : []),
|
|
430
|
+
expected: projectTableGeometry(storyTablesOf(pair.targetSnapshot))
|
|
451
431
|
});
|
|
452
432
|
if (geometryAcceptFailure) failures.push(geometryAcceptFailure);
|
|
453
433
|
const geometryRejectFailure = classifyGeometryMismatch({
|
|
454
434
|
invariant: "reject-reproduces-base",
|
|
455
435
|
story: pair.baseStory,
|
|
456
|
-
actual: projectTableGeometry(
|
|
457
|
-
story: pair.baseStory,
|
|
458
|
-
view: "original"
|
|
459
|
-
})),
|
|
436
|
+
actual: projectTableGeometry(rejectedSnapshot ? storyTablesOf(rejectedSnapshot) : []),
|
|
460
437
|
expected: baseBeforeGeometry
|
|
461
438
|
});
|
|
462
439
|
if (geometryRejectFailure) failures.push(geometryRejectFailure);
|
|
@@ -601,7 +601,6 @@ const trustedContentSequencePairs = ({ exactPairs, stablePairs, revisedLength, p
|
|
|
601
601
|
return [...primary, ...secondary].toSorted((left, right) => left.baseIndex - right.baseIndex || left.revisedIndex - right.revisedIndex);
|
|
602
602
|
};
|
|
603
603
|
const persistedContentSequencePairs = ({ base, revised, anchors, canPair }) => {
|
|
604
|
-
if (anchors.length === 0) return /* @__PURE__ */ new Set();
|
|
605
604
|
const uniqueIndexesByFirstId = (items) => {
|
|
606
605
|
const indexes = /* @__PURE__ */ new Map();
|
|
607
606
|
items.forEach(({ profile }, itemIndex) => {
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { FolioAIInlineFormattingPatch } from "../ai-edits/types.js";
|
|
1
|
+
import { FolioAIEditOperation, FolioAIInlineFormattingPatch } from "../ai-edits/types.js";
|
|
2
2
|
import { CompareDocxParseError, CompareDocxSerializeError } from "./types.js";
|
|
3
3
|
import { Result } from "better-result";
|
|
4
4
|
//#region src/compare/scenario.d.ts
|
|
5
|
+
type InsertTableRowOperation = Extract<FolioAIEditOperation, {
|
|
6
|
+
type: "insertTableRow";
|
|
7
|
+
}>;
|
|
8
|
+
type TableRowPosition = NonNullable<InsertTableRowOperation["position"]>;
|
|
9
|
+
declare const EDIT_SCRIPT_TABLE_ROW_POSITION: {
|
|
10
|
+
readonly after: "after";
|
|
11
|
+
readonly before: "before";
|
|
12
|
+
};
|
|
5
13
|
type EditScriptStep = {
|
|
6
14
|
type: "insertParagraphAfter";
|
|
7
15
|
blockIndex: number;
|
|
@@ -46,6 +54,7 @@ type EditScriptStep = {
|
|
|
46
54
|
} | {
|
|
47
55
|
type: "insertTableRow";
|
|
48
56
|
blockIndex: number;
|
|
57
|
+
position?: TableRowPosition;
|
|
49
58
|
cellTexts: readonly string[];
|
|
50
59
|
} | {
|
|
51
60
|
type: "deleteTableRow";
|
|
@@ -74,4 +83,4 @@ type EditScriptResult = {
|
|
|
74
83
|
*/
|
|
75
84
|
declare const applyEditScript: (base: ArrayBuffer, script: EditScript) => Promise<Result<EditScriptResult, CompareDocxParseError | CompareDocxSerializeError>>;
|
|
76
85
|
//#endregion
|
|
77
|
-
export { EDIT_SCRIPT_UNRESOLVED_REASONS, EditScript, EditScriptResult, EditScriptStep, EditScriptUnresolvedReason, EditScriptUnresolvedStep, applyEditScript };
|
|
86
|
+
export { EDIT_SCRIPT_TABLE_ROW_POSITION, EDIT_SCRIPT_UNRESOLVED_REASONS, EditScript, EditScriptResult, EditScriptStep, EditScriptUnresolvedReason, EditScriptUnresolvedStep, applyEditScript };
|
package/dist/compare/scenario.js
CHANGED
|
@@ -24,6 +24,10 @@ import { Result, panic } from "better-result";
|
|
|
24
24
|
* dropped: a script silently doing less than it says would weaken every
|
|
25
25
|
* property built on it.
|
|
26
26
|
*/
|
|
27
|
+
const EDIT_SCRIPT_TABLE_ROW_POSITION = {
|
|
28
|
+
after: "after",
|
|
29
|
+
before: "before"
|
|
30
|
+
};
|
|
27
31
|
const EDIT_SCRIPT_UNRESOLVED_REASONS = Object.freeze([
|
|
28
32
|
"block-out-of-range",
|
|
29
33
|
"block-not-in-table",
|
|
@@ -160,7 +164,7 @@ const planStep = ({ step, blocks, nextOperationId }) => {
|
|
|
160
164
|
id: nextOperationId(),
|
|
161
165
|
type: "insertTableRow",
|
|
162
166
|
blockId: block.id,
|
|
163
|
-
position:
|
|
167
|
+
position: step.position ?? EDIT_SCRIPT_TABLE_ROW_POSITION.after,
|
|
164
168
|
cellTexts: [...step.cellTexts]
|
|
165
169
|
}]
|
|
166
170
|
} : {
|
|
@@ -260,4 +264,4 @@ const applyEditScript = async (base, script) => {
|
|
|
260
264
|
});
|
|
261
265
|
};
|
|
262
266
|
//#endregion
|
|
263
|
-
export { EDIT_SCRIPT_UNRESOLVED_REASONS, applyEditScript };
|
|
267
|
+
export { EDIT_SCRIPT_TABLE_ROW_POSITION, EDIT_SCRIPT_UNRESOLVED_REASONS, applyEditScript };
|
|
@@ -15,7 +15,7 @@ import { hasComplexScript, isComplexScriptCodePoint } from "../utils/scriptSegme
|
|
|
15
15
|
* advances rightward from its origin whatever the paragraph does.
|
|
16
16
|
*/
|
|
17
17
|
declare const glyphCellOffsetsPx: (run: DisplayGlyphRun) => readonly number[];
|
|
18
|
-
declare const DISPLAY_PRIMITIVE_KINDS: ("
|
|
18
|
+
declare const DISPLAY_PRIMITIVE_KINDS: ("glyphRun" | "rect" | "line" | "image" | "clipGroup" | "rotateGroup" | "opacityGroup")[];
|
|
19
19
|
/**
|
|
20
20
|
* Dash geometry of each stroke pattern, as multiples of the stroke thickness.
|
|
21
21
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
4
4
|
"description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"document-model",
|