@beyondwork/docx-react-component 1.0.46 → 1.0.47

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.46",
4
+ "version": "1.0.47",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
6
  "packageManager": "pnpm@10.30.3",
7
7
  "type": "module",
@@ -34,7 +34,10 @@ import {
34
34
  outdentParagraphAtSelection,
35
35
  splitParagraph,
36
36
  } from "./text-commands.ts";
37
- import type { RevisionRecord as CanonicalRevisionRecord } from "../../model/canonical-document.ts";
37
+ import type {
38
+ BlockNode,
39
+ RevisionRecord as CanonicalRevisionRecord,
40
+ } from "../../model/canonical-document.ts";
38
41
  import { remapCommentThreads } from "../../review/store/comment-remapping.ts";
39
42
  import { collectScopeTagTouches } from "../../review/store/scope-tag-diff.ts";
40
43
  import { applyRevisionRuntimeCommand } from "../../runtime/revision-runtime.ts";
@@ -86,6 +89,15 @@ import {
86
89
  import { insertPageBreak, insertTable } from "./text-commands.ts";
87
90
  import type { TableSelectionDescriptor } from "../../runtime/table-commands.ts";
88
91
 
92
+ export type ContentChildrenPatch =
93
+ | { kind: "keep-all" }
94
+ | { kind: "replace-all"; children: BlockNode[] }
95
+ | {
96
+ kind: "index-map";
97
+ baseLength: number;
98
+ entries: ReadonlyArray<{ index: number; block: BlockNode }>;
99
+ };
100
+
89
101
  export interface CommandOrigin {
90
102
  source:
91
103
  | "keyboard"
@@ -112,6 +124,15 @@ export type EditorCommand =
112
124
  protectionSelection?: SelectionSnapshot;
113
125
  origin?: CommandOrigin;
114
126
  }
127
+ | {
128
+ type: "document.patch";
129
+ patch: Partial<CanonicalDocumentEnvelope>;
130
+ contentChildren?: ContentChildrenPatch;
131
+ mapping?: TransactionMapping;
132
+ selection?: SelectionSnapshot;
133
+ protectionSelection?: SelectionSnapshot;
134
+ origin?: CommandOrigin;
135
+ }
115
136
  | {
116
137
  type: "text.insert";
117
138
  text: string;
@@ -482,6 +503,53 @@ export function executeEditorCommand(
482
503
  },
483
504
  );
484
505
  }
506
+ case "document.patch": {
507
+ const mapping = command.mapping ?? createEmptyMapping();
508
+ const nextDocument = applyDocumentPatch(
509
+ state.document,
510
+ command.patch,
511
+ command.contentChildren,
512
+ );
513
+ const selection =
514
+ command.selection ?? remapSelection(state.selection, mapping);
515
+ const reviewState = remapReviewStateAfterContentChange(
516
+ state,
517
+ nextDocument,
518
+ mapping,
519
+ );
520
+
521
+ return createTransaction(
522
+ {
523
+ ...state,
524
+ document: reviewState.document,
525
+ selection,
526
+ warnings: reviewState.warnings,
527
+ runtime: {
528
+ ...state.runtime,
529
+ activeCommentId: reviewState.activeCommentId,
530
+ },
531
+ compatibility: {
532
+ ...state.compatibility,
533
+ generatedAt: context.timestamp,
534
+ warnings: reviewState.warnings,
535
+ featureEntries: state.compatibility.featureEntries.map((entry) =>
536
+ entry.affectedAnchor
537
+ ? {
538
+ ...entry,
539
+ affectedAnchor: mapAnchor(entry.affectedAnchor, mapping),
540
+ }
541
+ : entry,
542
+ ),
543
+ },
544
+ },
545
+ {
546
+ historyBoundary: "push",
547
+ markDirty: true,
548
+ mapping,
549
+ effects: reviewState.effects,
550
+ },
551
+ );
552
+ }
485
553
  case "text.insert": {
486
554
  const suggestingResult = context.documentMode === "suggesting"
487
555
  ? applySuggestingInsert(state, command.text, context)
@@ -1141,6 +1209,57 @@ export function executeEditorCommand(
1141
1209
  }
1142
1210
  }
1143
1211
 
1212
+ export function applyDocumentPatch(
1213
+ base: CanonicalDocumentEnvelope,
1214
+ patch: Partial<CanonicalDocumentEnvelope>,
1215
+ contentChildren?: ContentChildrenPatch,
1216
+ ): CanonicalDocumentEnvelope {
1217
+ const nextContent = resolvePatchedContent(base, patch, contentChildren);
1218
+ const merged: CanonicalDocumentEnvelope = { ...base, ...patch };
1219
+ if (nextContent) {
1220
+ merged.content = nextContent;
1221
+ }
1222
+ return merged;
1223
+ }
1224
+
1225
+ function resolvePatchedContent(
1226
+ base: CanonicalDocumentEnvelope,
1227
+ patch: Partial<CanonicalDocumentEnvelope>,
1228
+ contentChildren: ContentChildrenPatch | undefined,
1229
+ ): CanonicalDocumentEnvelope["content"] | null {
1230
+ if (!contentChildren) {
1231
+ return patch.content ?? null;
1232
+ }
1233
+ if (contentChildren.kind === "keep-all") {
1234
+ return { ...base.content, children: base.content.children };
1235
+ }
1236
+ if (contentChildren.kind === "replace-all") {
1237
+ return {
1238
+ ...(patch.content ?? base.content),
1239
+ children: contentChildren.children,
1240
+ };
1241
+ }
1242
+ const baseChildren = base.content.children;
1243
+ if (contentChildren.baseLength !== baseChildren.length) {
1244
+ throw new Error(
1245
+ `applyDocumentPatch: index-map baseLength ${contentChildren.baseLength} does not match current children length ${baseChildren.length}`,
1246
+ );
1247
+ }
1248
+ const nextChildren: BlockNode[] = baseChildren.slice();
1249
+ for (const entry of contentChildren.entries) {
1250
+ if (entry.index < 0 || entry.index >= nextChildren.length) {
1251
+ throw new Error(
1252
+ `applyDocumentPatch: index-map entry index ${entry.index} out of range [0, ${nextChildren.length})`,
1253
+ );
1254
+ }
1255
+ nextChildren[entry.index] = entry.block;
1256
+ }
1257
+ return {
1258
+ ...(patch.content ?? base.content),
1259
+ children: nextChildren,
1260
+ };
1261
+ }
1262
+
1144
1263
  function buildDocumentReplaceTransaction(
1145
1264
  state: EditorState,
1146
1265
  context: CommandExecutionContext,
@@ -59,14 +59,12 @@ import {
59
59
  getDocumentBackedWorkflowMetadata,
60
60
  parseWorkflowPayloadEnvelopeFromPackage,
61
61
  resolvePayloadPartPath,
62
+ resolveWorkflowPayloadPartPaths,
62
63
  WORKFLOW_PAYLOAD_CONTENT_TYPE,
63
64
  WORKFLOW_PAYLOAD_CUSTOM_PROPS_CONTENT_TYPE,
64
65
  WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
65
66
  WORKFLOW_PAYLOAD_CUSTOM_PROPS_RELATIONSHIP_TYPE,
66
67
  WORKFLOW_PAYLOAD_ITEM_PROPS_CONTENT_TYPE,
67
- WORKFLOW_PAYLOAD_ITEM_PROPS_PART_PATH,
68
- WORKFLOW_PAYLOAD_PART_PATH,
69
- WORKFLOW_PAYLOAD_RELATIONSHIP_TYPE,
70
68
  } from "./ooxml/workflow-payload.ts";
71
69
  import {
72
70
  classifyCorruptPackageError,
@@ -1736,12 +1734,17 @@ function exportDocxEditorSession(
1736
1734
  }
1737
1735
  }
1738
1736
 
1737
+ const workflowPayloadPartPaths = resolveWorkflowPayloadPartPaths(
1738
+ state.sourcePackage,
1739
+ sessionState.documentId,
1740
+ );
1741
+
1739
1742
  const exportSession = createExportSession(state.sourcePackage, [
1740
1743
  state.sourceDocumentPartPath,
1741
1744
  APP_PROPERTIES_PART_PATH,
1742
1745
  CORE_PROPERTIES_PART_PATH,
1743
- WORKFLOW_PAYLOAD_PART_PATH,
1744
- WORKFLOW_PAYLOAD_ITEM_PROPS_PART_PATH,
1746
+ workflowPayloadPartPaths.payloadPartPath,
1747
+ workflowPayloadPartPaths.itemPropsPartPath,
1745
1748
  WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
1746
1749
  numberingPartPath,
1747
1750
  commentsPartPath,
@@ -1974,7 +1977,14 @@ function exportDocxEditorSession(
1974
1977
  ensureHostMetadataParts(exportSession, state.sourcePackage, currentDocument);
1975
1978
  // Schema 1.2: pass through editorState payload collected by the runtime channel.
1976
1979
  const internalEditorState = (options as { _editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload } | undefined)?._editorState;
1977
- ensureWorkflowPayloadParts(exportSession, sessionState, currentDocument, state.sourcePackage, internalEditorState);
1980
+ ensureWorkflowPayloadParts(
1981
+ exportSession,
1982
+ sessionState,
1983
+ currentDocument,
1984
+ state.sourcePackage,
1985
+ internalEditorState,
1986
+ workflowPayloadPartPaths,
1987
+ );
1978
1988
 
1979
1989
  return {
1980
1990
  bytes: exportSession.serialize(),
@@ -3701,6 +3711,7 @@ function ensureWorkflowPayloadParts(
3701
3711
  document: CanonicalDocumentEnvelope,
3702
3712
  sourcePackage: OpcPackage,
3703
3713
  editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload,
3714
+ precomputedPartPaths?: { payloadPartPath: string; itemPropsPartPath: string },
3704
3715
  ): void {
3705
3716
  const payloadParts = buildWorkflowPayloadParts({
3706
3717
  sourcePackage,
@@ -3716,19 +3727,37 @@ function ensureWorkflowPayloadParts(
3716
3727
  return;
3717
3728
  }
3718
3729
 
3730
+ // Export ownership is resolved before session creation, so any precomputed
3731
+ // paths must stay in lockstep with buildWorkflowPayloadParts for the same
3732
+ // source package and documentId.
3733
+ if (precomputedPartPaths) {
3734
+ if (
3735
+ precomputedPartPaths.payloadPartPath !== payloadParts.payloadPartPath ||
3736
+ precomputedPartPaths.itemPropsPartPath !== payloadParts.itemPropsPartPath
3737
+ ) {
3738
+ throw new Error(
3739
+ `ensureWorkflowPayloadParts: precomputed payload part paths `
3740
+ + `(${precomputedPartPaths.payloadPartPath} / ${precomputedPartPaths.itemPropsPartPath}) `
3741
+ + `do not match buildWorkflowPayloadParts output `
3742
+ + `(${payloadParts.payloadPartPath} / ${payloadParts.itemPropsPartPath}). `
3743
+ + `This is a bug in the export orchestration.`,
3744
+ );
3745
+ }
3746
+ }
3747
+
3719
3748
  const payloadPart = sourcePackage.parts.get(payloadParts.payloadPartPath);
3720
3749
  const itemPropsPart = sourcePackage.parts.get(payloadParts.itemPropsPartPath);
3721
3750
  const customPropsPart = sourcePackage.parts.get(WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH);
3722
3751
 
3723
3752
  exportSession.replaceOwnedPart({
3724
- path: payloadParts.payloadPartPath,
3753
+ path: precomputedPartPaths?.payloadPartPath ?? payloadParts.payloadPartPath,
3725
3754
  bytes: new TextEncoder().encode(payloadParts.payloadPartXml),
3726
3755
  contentType: payloadPart?.contentType ?? WORKFLOW_PAYLOAD_CONTENT_TYPE,
3727
3756
  relationships: payloadParts.payloadRelationships,
3728
3757
  compression: payloadPart?.compression,
3729
3758
  });
3730
3759
  exportSession.replaceOwnedPart({
3731
- path: payloadParts.itemPropsPartPath,
3760
+ path: precomputedPartPaths?.itemPropsPartPath ?? payloadParts.itemPropsPartPath,
3732
3761
  bytes: new TextEncoder().encode(payloadParts.itemPropsXml),
3733
3762
  contentType: itemPropsPart?.contentType ?? WORKFLOW_PAYLOAD_ITEM_PROPS_CONTENT_TYPE,
3734
3763
  compression: itemPropsPart?.compression,
@@ -1158,6 +1158,33 @@ function resolveDescriptor(sourcePackage: OpcPackage, documentId: string): Embed
1158
1158
  };
1159
1159
  }
1160
1160
 
1161
+ /**
1162
+ * Resolve the OPC part paths the workflow-payload serializer will write
1163
+ * to on export, based on the source package's existing customXml slots.
1164
+ *
1165
+ * - If the source already contains a BW workflow payload, the existing
1166
+ * `/customXml/itemN.xml` path is reused (via `resolvePayloadPartPath`).
1167
+ * - Otherwise, `findNextAvailableItemNumber(sourcePackage)` picks the
1168
+ * next free slot (e.g. `item5.xml` when `item1..4.xml` already exist
1169
+ * in the source).
1170
+ *
1171
+ * The returned paths MUST be passed to `createExportSession` as owned
1172
+ * paths so `replaceOwnedPart(...)` does not throw when
1173
+ * `ensureWorkflowPayloadParts` later writes to them. The custom-props
1174
+ * part lives at the fixed path `WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH`
1175
+ * and is not part of this resolver.
1176
+ */
1177
+ export function resolveWorkflowPayloadPartPaths(
1178
+ sourcePackage: OpcPackage,
1179
+ documentId: string,
1180
+ ): { payloadPartPath: string; itemPropsPartPath: string } {
1181
+ const descriptor = resolveDescriptor(sourcePackage, documentId);
1182
+ return {
1183
+ payloadPartPath: descriptor.payloadPartPath,
1184
+ itemPropsPartPath: descriptor.itemPropsPartPath,
1185
+ };
1186
+ }
1187
+
1161
1188
  export function resolvePayloadPartPath(sourcePackage: OpcPackage): string | null {
1162
1189
  const mirroredItemId = readCustomPropertyValue(
1163
1190
  sourcePackage,
@@ -18,11 +18,15 @@ import type { EditorStoryTarget } from "../../api/public-types.ts";
18
18
  * Events are meant to be small deltas (text insertions, comment adds,
19
19
  * change accepts, etc.). They are NOT full-document snapshots.
20
20
  *
21
- * `document.replace` is a special case: some formatting/table/list
22
- * operations currently produce `document.replace` commands that carry
23
- * a full `CanonicalDocumentEnvelope`. These events are bandwidth-heavy
24
- * but functionally correct. A follow-up will promote them to first-class
25
- * delta commands.
21
+ * `document.replace` carries a full `CanonicalDocumentEnvelope` and is
22
+ * therefore bandwidth-heavy. The collab sync bridge transparently
23
+ * converts broadcast `document.replace` commands to `document.patch`
24
+ * deltas by diffing against the pre-commit document snapshot, so the
25
+ * shared event log only ever contains the changed top-level keys (and,
26
+ * for `content.children`, only the changed block indices). Remote
27
+ * replicas apply `document.patch` by merging the delta onto their
28
+ * current canonical state — see `applyDocumentPatch` in
29
+ * `core/commands/index.ts`.
26
30
  */
27
31
  export interface CommandEvent {
28
32
  /** UUID — dedup key for defensive idempotency. */
@@ -74,6 +78,7 @@ export const BROADCAST_COMMAND_TYPES: ReadonlySet<EditorCommand["type"]> = new S
74
78
  "text.insert-hard-break",
75
79
  "paragraph.split",
76
80
  "document.replace",
81
+ "document.patch",
77
82
  "comment.add",
78
83
  "comment.resolve",
79
84
  "comment.reopen",
@@ -2,9 +2,12 @@ import * as Y from "yjs";
2
2
 
3
3
  import type {
4
4
  CommandExecutionContext,
5
+ ContentChildrenPatch,
5
6
  EditorCommand,
6
7
  EditorTransaction,
7
8
  } from "../../core/commands/index.ts";
9
+ import type { CanonicalDocumentEnvelope } from "../../core/state/editor-state.ts";
10
+ import type { BlockNode } from "../../model/canonical-document.ts";
8
11
  import type {
9
12
  CommandAppliedMeta,
10
13
  DocumentRuntime,
@@ -17,6 +20,122 @@ import {
17
20
  type CommandEvent,
18
21
  } from "./event-types.ts";
19
22
 
23
+ const PATCHABLE_TOP_LEVEL_KEYS = [
24
+ "updatedAt",
25
+ "metadata",
26
+ "styles",
27
+ "numbering",
28
+ "media",
29
+ "content",
30
+ "review",
31
+ "preservation",
32
+ "diagnostics",
33
+ "subParts",
34
+ "fieldRegistry",
35
+ ] as const satisfies ReadonlyArray<keyof CanonicalDocumentEnvelope>;
36
+
37
+ export function compressDocumentReplaceForBroadcast(
38
+ command: EditorCommand,
39
+ priorDocument: CanonicalDocumentEnvelope,
40
+ ): EditorCommand {
41
+ if (command.type !== "document.replace") {
42
+ return command;
43
+ }
44
+ const next = command.document;
45
+ if (next === priorDocument) {
46
+ return command;
47
+ }
48
+
49
+ const patch: Partial<CanonicalDocumentEnvelope> = {};
50
+ let anyTopLevelChange = false;
51
+ for (const key of PATCHABLE_TOP_LEVEL_KEYS) {
52
+ if (key === "content") continue;
53
+ if (next[key] !== priorDocument[key]) {
54
+ (patch as Record<string, unknown>)[key] = next[key];
55
+ anyTopLevelChange = true;
56
+ }
57
+ }
58
+
59
+ const contentChildrenPatch = diffContentChildren(
60
+ priorDocument.content.children,
61
+ next.content.children,
62
+ );
63
+ const contentMetaChanged =
64
+ next.content !== priorDocument.content &&
65
+ !contentReferenceEqualExceptChildren(priorDocument.content, next.content);
66
+ if (contentMetaChanged) {
67
+ const { children: _children, ...rest } = next.content;
68
+ patch.content = { ...rest, children: [] } as CanonicalDocumentEnvelope["content"];
69
+ }
70
+
71
+ const contentChanged =
72
+ contentChildrenPatch.kind !== "keep-all" || contentMetaChanged;
73
+ if (!contentChanged && !anyTopLevelChange) {
74
+ return {
75
+ type: "document.patch",
76
+ patch: {},
77
+ contentChildren: { kind: "keep-all" },
78
+ mapping: command.mapping,
79
+ selection: command.selection,
80
+ protectionSelection: command.protectionSelection,
81
+ origin: command.origin,
82
+ };
83
+ }
84
+
85
+ return {
86
+ type: "document.patch",
87
+ patch,
88
+ contentChildren: contentChildrenPatch,
89
+ mapping: command.mapping,
90
+ selection: command.selection,
91
+ protectionSelection: command.protectionSelection,
92
+ origin: command.origin,
93
+ };
94
+ }
95
+
96
+ function contentReferenceEqualExceptChildren(
97
+ a: CanonicalDocumentEnvelope["content"],
98
+ b: CanonicalDocumentEnvelope["content"],
99
+ ): boolean {
100
+ const aKeys = Object.keys(a);
101
+ const bKeys = Object.keys(b);
102
+ if (aKeys.length !== bKeys.length) return false;
103
+ for (const key of aKeys) {
104
+ if (key === "children") continue;
105
+ if ((a as unknown as Record<string, unknown>)[key] !== (b as unknown as Record<string, unknown>)[key]) {
106
+ return false;
107
+ }
108
+ }
109
+ return true;
110
+ }
111
+
112
+ function diffContentChildren(
113
+ prior: readonly BlockNode[],
114
+ next: readonly BlockNode[],
115
+ ): ContentChildrenPatch {
116
+ if (prior === next) {
117
+ return { kind: "keep-all" };
118
+ }
119
+ if (prior.length === next.length) {
120
+ const entries: Array<{ index: number; block: BlockNode }> = [];
121
+ for (let i = 0; i < next.length; i += 1) {
122
+ const priorBlock = prior[i];
123
+ const nextBlock = next[i];
124
+ if (priorBlock === undefined || nextBlock === undefined) {
125
+ return { kind: "replace-all", children: next.slice() };
126
+ }
127
+ if (priorBlock !== nextBlock) {
128
+ entries.push({ index: i, block: nextBlock });
129
+ }
130
+ }
131
+ if (entries.length === 0) {
132
+ return { kind: "keep-all" };
133
+ }
134
+ return { kind: "index-map", baseLength: prior.length, entries };
135
+ }
136
+ return { kind: "replace-all", children: next.slice() };
137
+ }
138
+
20
139
  export type RuntimeCommandAppliedListener = (
21
140
  command: EditorCommand,
22
141
  transaction: EditorTransaction,
@@ -73,8 +192,13 @@ export function createRuntimeCollabSync(
73
192
  return;
74
193
  }
75
194
 
76
- const event = createCommandEvent({
195
+ const broadcastCommand = compressDocumentReplaceForBroadcast(
77
196
  command,
197
+ meta.priorDocument,
198
+ );
199
+
200
+ const event = createCommandEvent({
201
+ command: broadcastCommand,
78
202
  originClientId: ydoc.clientID,
79
203
  authorId,
80
204
  timestamp: context.timestamp,
@@ -406,6 +406,7 @@ export interface DocumentRuntime {
406
406
  export interface CommandAppliedMeta {
407
407
  preSelection: import("../core/state/editor-state.ts").SelectionSnapshot;
408
408
  activeStory: EditorStoryTarget;
409
+ priorDocument: CanonicalDocumentEnvelope;
409
410
  }
410
411
 
411
412
  export interface CreateDocumentRuntimeOptions {
@@ -2111,6 +2112,7 @@ export function createDocumentRuntime(
2111
2112
  options.onCommandApplied?.(command, noopTransaction, context, {
2112
2113
  preSelection: state.selection,
2113
2114
  activeStory,
2115
+ priorDocument: state.document,
2114
2116
  });
2115
2117
  return;
2116
2118
  }
@@ -2124,11 +2126,13 @@ export function createDocumentRuntime(
2124
2126
  } as const;
2125
2127
  const preSelection = commandSelection;
2126
2128
  const preActiveStory = activeStory;
2129
+ const priorDocument = state.document;
2127
2130
  const transaction = executeEditorCommand(state, command, context);
2128
2131
  commit(transaction);
2129
2132
  options.onCommandApplied?.(command, transaction, context, {
2130
2133
  preSelection,
2131
2134
  activeStory: preActiveStory,
2135
+ priorDocument,
2132
2136
  });
2133
2137
  } catch (error) {
2134
2138
  emitError(toRuntimeError(error));
@@ -3283,12 +3287,14 @@ export function createDocumentRuntime(
3283
3287
 
3284
3288
  const preSelection = selection;
3285
3289
  const preActiveStory = activeStory;
3290
+ const priorDocument = state.document;
3286
3291
  if (activeStory.kind === "main") {
3287
3292
  const mainTransaction = executeEditorCommand(baseState, command, context);
3288
3293
  commit(mainTransaction);
3289
3294
  options.onCommandApplied?.(command, mainTransaction, context, {
3290
3295
  preSelection,
3291
3296
  activeStory: preActiveStory,
3297
+ priorDocument,
3292
3298
  });
3293
3299
  return classifyAck({
3294
3300
  command,
@@ -3370,6 +3376,7 @@ export function createDocumentRuntime(
3370
3376
  options.onCommandApplied?.(broadcastCommand, mergedTransaction, context, {
3371
3377
  preSelection,
3372
3378
  activeStory: preActiveStory,
3379
+ priorDocument,
3373
3380
  });
3374
3381
  return classifyAck({
3375
3382
  command,
@@ -24,8 +24,26 @@
24
24
  * internal cachedGraph + cachedKey without triggering fullRebuild.
25
25
  * Does not change geometry — but the public interface changed, so
26
26
  * persisted envelopes MUST re-derive their cacheKey under 3.
27
+ * 4 — PR #188 `fix(export)` bumped the version to satisfy the
28
+ * `src/runtime/layout/**` gate, even though the font-loader
29
+ * type-def restoration intended for that PR did not survive the
30
+ * squash-merge. Runtime and cached geometry unchanged.
31
+ * 5 — PR #187 `joakim/commentevents` restores the local `Minimal*`
32
+ * font-loader type defs under `src/runtime/layout/docx-font-loader.ts`
33
+ * (re-application of previously-shipped PRs #162/#163 which a
34
+ * subsequent merge reverted) so downstream consumers whose
35
+ * TS `lib` does not expose `FontFaceSet.add` can type-check the
36
+ * package, and adds the `comments_changed` event plumbing in
37
+ * runtime domains outside `src/runtime/layout/**`. TypeScript-
38
+ * surface-only from the layout engine's perspective: no cached
39
+ * geometry or cache-key derivation changes. The bump exists
40
+ * solely to satisfy the
41
+ * `scripts/ci-check-layout-engine-version.mjs` gate because a
42
+ * file under `src/runtime/layout/**` changed. Safe to treat
43
+ * versions 3, 4, and 5 as cache-compatible if a migration ever
44
+ * needs to collapse them.
27
45
  */
28
- export const LAYOUT_ENGINE_VERSION = 3 as const;
46
+ export const LAYOUT_ENGINE_VERSION = 5 as const;
29
47
 
30
48
  /**
31
49
  * Serialization schema version for the LayCache payload (the cache envelope
@@ -3,9 +3,9 @@
3
3
  * as `adjustedRange`. The semantics: the union of all step post-edit ranges
4
4
  * (where each step covers `[step.from, step.from + step.insertSize]` in the
5
5
  * new document). When the transaction has no steps (e.g., a non-main-story
6
- * `document.replace` that uses `createEmptyMapping()`), fall back to the
7
- * normalized prior selection range so consumers still receive a meaningful
8
- * pointer.
6
+ * `document.replace` or `document.patch` that uses `createEmptyMapping()`),
7
+ * fall back to the normalized prior selection range so consumers still
8
+ * receive a meaningful pointer.
9
9
  *
10
10
  * Note: only `step.from` and `step.insertSize` are considered; `step.to`
11
11
  * (the pre-edit end of the replaced range) is intentionally ignored. A pure