@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.
- package/dist/ai-edits/apply.js +157 -84
- 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/ai-edits/types.d.ts +15 -0
- 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/plan.js +32 -2
- 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/dist/document-operations.d.ts +2 -2
- package/dist/document-operations.js +44 -14
- package/dist/internal/headlessRevisionResolution.d.ts +2 -2
- package/dist/internal/headlessRevisionResolution.js +30 -9
- package/dist/prosemirror/commands/comments.js +2 -25
- package/package.json +1 -1
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 };
|
package/dist/ai-edits/types.d.ts
CHANGED
|
@@ -312,6 +312,16 @@ type FolioAIEditOperation = FolioAIEditReviewMeta & {
|
|
|
312
312
|
*/
|
|
313
313
|
separator?: string;
|
|
314
314
|
blockId: string;
|
|
315
|
+
/**
|
|
316
|
+
* Paragraph properties for the first result. Omitted properties keep
|
|
317
|
+
* the source paragraph's value.
|
|
318
|
+
*/
|
|
319
|
+
firstParagraphProperties?: FolioAIBlockParagraphProperties;
|
|
320
|
+
/**
|
|
321
|
+
* Paragraph properties for the second result. Omitted properties keep
|
|
322
|
+
* the source paragraph's value.
|
|
323
|
+
*/
|
|
324
|
+
secondParagraphProperties?: FolioAIBlockParagraphProperties;
|
|
315
325
|
} |
|
|
316
326
|
/**
|
|
317
327
|
* Add a whole table next to the anchor block, its rows marked inserted in
|
|
@@ -377,6 +387,11 @@ type FolioAIEditOperation = FolioAIEditReviewMeta & {
|
|
|
377
387
|
*/
|
|
378
388
|
separator?: string;
|
|
379
389
|
blockId: string;
|
|
390
|
+
/**
|
|
391
|
+
* Paragraph properties for the joined result. Omitted properties keep
|
|
392
|
+
* the first paragraph's value.
|
|
393
|
+
*/
|
|
394
|
+
mergedParagraphProperties?: FolioAIBlockParagraphProperties;
|
|
380
395
|
} | {
|
|
381
396
|
id: string;
|
|
382
397
|
type: "commentOnBlock";
|
|
@@ -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) => {
|
package/dist/compare/plan.js
CHANGED
|
@@ -612,6 +612,10 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations,
|
|
|
612
612
|
const paragraphMarkPlan = paragraphMarkPlans.get(stepIndex);
|
|
613
613
|
if (paragraphMarkPlan?.type === "split") {
|
|
614
614
|
const { baseBlock, revisedBlocks: splitInto, offset, separator } = paragraphMarkPlan;
|
|
615
|
+
const firstParagraphFormatting = changedFolioContentParagraphFormatting(baseBlock, splitInto[0]);
|
|
616
|
+
const secondParagraphFormatting = changedFolioContentParagraphFormatting(baseBlock, splitInto[1]);
|
|
617
|
+
const firstParagraphProperties = firstParagraphFormatting ? toFolioAIBlockParagraphProperties(firstParagraphFormatting) : void 0;
|
|
618
|
+
const secondParagraphProperties = secondParagraphFormatting ? toFolioAIBlockParagraphProperties(secondParagraphFormatting) : void 0;
|
|
615
619
|
changes.push({
|
|
616
620
|
kind: "split",
|
|
617
621
|
location: locationOf(story, baseBlock),
|
|
@@ -619,17 +623,35 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations,
|
|
|
619
623
|
targetBlockIds: splitInto.map(({ id }) => id),
|
|
620
624
|
text: baseBlock.text
|
|
621
625
|
});
|
|
626
|
+
if (firstParagraphProperties) changes.push({
|
|
627
|
+
kind: "paragraph-format",
|
|
628
|
+
location: locationOf(story, baseBlock),
|
|
629
|
+
baseBlockId: baseBlock.id,
|
|
630
|
+
targetBlockId: splitInto[0].id,
|
|
631
|
+
properties: firstParagraphProperties
|
|
632
|
+
});
|
|
633
|
+
if (secondParagraphProperties) changes.push({
|
|
634
|
+
kind: "paragraph-format",
|
|
635
|
+
location: locationOf(story, baseBlock),
|
|
636
|
+
baseBlockId: baseBlock.id,
|
|
637
|
+
targetBlockId: splitInto[1].id,
|
|
638
|
+
properties: secondParagraphProperties
|
|
639
|
+
});
|
|
622
640
|
operations.push({
|
|
623
641
|
id: nextOperationId(),
|
|
624
642
|
type: "splitBlock",
|
|
625
643
|
blockId: baseBlock.id,
|
|
626
644
|
offset,
|
|
627
|
-
...separator.length > 0 && { separator }
|
|
645
|
+
...separator.length > 0 && { separator },
|
|
646
|
+
...firstParagraphProperties && { firstParagraphProperties },
|
|
647
|
+
...secondParagraphProperties && { secondParagraphProperties }
|
|
628
648
|
});
|
|
629
649
|
continue;
|
|
630
650
|
}
|
|
631
651
|
if (paragraphMarkPlan?.type === "merge") {
|
|
632
652
|
const { baseBlocks, revisedBlock: targetBlock, separator } = paragraphMarkPlan;
|
|
653
|
+
const mergedParagraphFormatting = changedFolioContentParagraphFormatting(baseBlocks[0], targetBlock);
|
|
654
|
+
const mergedParagraphProperties = mergedParagraphFormatting ? toFolioAIBlockParagraphProperties(mergedParagraphFormatting) : void 0;
|
|
633
655
|
changes.push({
|
|
634
656
|
kind: "merge",
|
|
635
657
|
location: locationOf(story, baseBlocks[0]),
|
|
@@ -637,11 +659,19 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations,
|
|
|
637
659
|
targetBlockId: targetBlock.id,
|
|
638
660
|
text: targetBlock.text
|
|
639
661
|
});
|
|
662
|
+
if (mergedParagraphProperties) changes.push({
|
|
663
|
+
kind: "paragraph-format",
|
|
664
|
+
location: locationOf(story, baseBlocks[0]),
|
|
665
|
+
baseBlockId: baseBlocks[0].id,
|
|
666
|
+
targetBlockId: targetBlock.id,
|
|
667
|
+
properties: mergedParagraphProperties
|
|
668
|
+
});
|
|
640
669
|
operations.push({
|
|
641
670
|
id: nextOperationId(),
|
|
642
671
|
type: "mergeBlockWithNext",
|
|
643
672
|
blockId: baseBlocks[0].id,
|
|
644
|
-
...separator.length > 0 && { separator }
|
|
673
|
+
...separator.length > 0 && { separator },
|
|
674
|
+
...mergedParagraphProperties && { mergedParagraphProperties }
|
|
645
675
|
});
|
|
646
676
|
continue;
|
|
647
677
|
}
|