web-doc 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/THIRD_PARTY_NOTICES.md +10 -3
  2. package/dist/contracts.d.ts +6 -0
  3. package/dist/edit/ai/outline.d.ts +47 -0
  4. package/dist/edit/ai/outline.js +338 -0
  5. package/dist/edit/ai/targets.d.ts +16 -0
  6. package/dist/edit/ai/targets.js +309 -0
  7. package/dist/edit/ai/tools.d.ts +28 -0
  8. package/dist/edit/ai/tools.js +605 -0
  9. package/dist/edit/ai/types.d.ts +175 -0
  10. package/dist/edit/ai/types.js +1 -0
  11. package/dist/edit/docx/engine.d.ts +10 -4
  12. package/dist/edit/docx/engine.js +80 -6
  13. package/dist/edit/docx/model.d.ts +2 -0
  14. package/dist/edit/docx/model.js +11 -0
  15. package/dist/edit/docx/operations.d.ts +10 -0
  16. package/dist/edit/docx/provider.d.ts +4 -1
  17. package/dist/edit/docx/provider.js +3 -0
  18. package/dist/edit/docx/session.d.ts +12 -1
  19. package/dist/edit/docx/session.js +41 -0
  20. package/dist/edit/docx/structure-ops.js +39 -4
  21. package/dist/edit/docx/table-ops.js +26 -4
  22. package/dist/edit/docx/text-ops.d.ts +22 -0
  23. package/dist/edit/docx/text-ops.js +49 -16
  24. package/dist/edit/docx/text.js +21 -0
  25. package/dist/edit/docx/tracked.d.ts +52 -0
  26. package/dist/edit/docx/tracked.js +347 -0
  27. package/dist/edit/docx/types.d.ts +16 -1
  28. package/dist/edit/docx/write.d.ts +19 -5
  29. package/dist/edit/docx/write.js +31 -8
  30. package/dist/edit/engine.d.ts +14 -3
  31. package/dist/edit/history.d.ts +27 -3
  32. package/dist/edit/history.js +29 -7
  33. package/dist/edit/pdf/range-map.d.ts +4 -2
  34. package/dist/edit/pdf/session.d.ts +10 -0
  35. package/dist/edit/pdf/session.js +39 -0
  36. package/dist/edit/pptx/handler.js +10 -2
  37. package/dist/edit/pptx/session.d.ts +10 -0
  38. package/dist/edit/pptx/session.js +31 -0
  39. package/dist/edit/session.d.ts +11 -0
  40. package/dist/edit/session.js +269 -27
  41. package/dist/edit/types.d.ts +17 -3
  42. package/dist/edit/worker-engine.d.ts +2 -2
  43. package/dist/edit/worker-engine.js +2 -2
  44. package/dist/fuzzy-alignment.d.ts +11 -4
  45. package/dist/fuzzy-alignment.js +3 -9
  46. package/dist/headless.d.ts +1 -1
  47. package/dist/headless.js +4 -1
  48. package/dist/index.d.ts +5 -2
  49. package/dist/index.js +11 -2
  50. package/dist/limits.js +3 -0
  51. package/dist/worker-protocol.d.ts +1 -1
  52. package/dist/workers/fuzzy-search-worker.js +1 -1
  53. package/dist/workers/ooxml-edit-worker.js +1400 -859
  54. package/dist/workers/pdf-edit-worker.js +4 -1
  55. package/package.json +1 -1
@@ -1,7 +1,10 @@
1
1
  import { linkedAbortController } from "../abort.js";
2
2
  import { abortError, ViewerError } from "../errors.js";
3
+ import { readDescription, readOutline } from "./ai/outline.js";
4
+ import { resolveTargets } from "./ai/targets.js";
5
+ import { buildToolSet, callTool as runTool } from "./ai/tools.js";
3
6
  import { assetIdOf, AssetStore, binaryFields, isAssetReference, } from "./assets.js";
4
- import { EditHistory } from "./history.js";
7
+ import { batchOf, EditHistory, modeOf } from "./history.js";
5
8
  import { assertBatchSize, checkOperations, freezeOperations, invalidOperationError, parseReference, } from "./operations.js";
6
9
  /**
7
10
  * The format-independent editing session: validation, history, revisions,
@@ -17,15 +20,20 @@ export class EditSessionController {
17
20
  #original;
18
21
  #originalPageCount;
19
22
  #ending = new AbortController();
20
- sessionId = newSessionId();
23
+ sessionId = randomId();
21
24
  /** Bytes of the last committed state; what the viewer shows and what a broken session saves. */
22
25
  #committedBytes;
23
26
  /** Materialized bytes of some committed states, by state id, so restores replay less. */
24
27
  #checkpoints = new Map();
25
28
  #checkpointBytes = 0;
29
+ /** Named checkpoints by id, in creation order. */
30
+ #named = new Map();
31
+ /** State ids named checkpoints pin, with how many name each; never evicted. */
32
+ #pinned = new Map();
26
33
  /** Binary payloads of this session's batches, by content id. */
27
34
  #assets = new AssetStore();
28
35
  #state;
36
+ #tools;
29
37
  #queue = Promise.resolve();
30
38
  #revision = 0;
31
39
  #savedStateId = 0;
@@ -46,6 +54,9 @@ export class EditSessionController {
46
54
  get state() {
47
55
  return this.#state;
48
56
  }
57
+ get limits() {
58
+ return this.#host.limits;
59
+ }
49
60
  applyJson(operations, options = {}) {
50
61
  return this.apply(operations, options);
51
62
  }
@@ -58,6 +69,9 @@ export class EditSessionController {
58
69
  return this.#enqueue(options.signal, async (signal) => {
59
70
  this.#assertRevision(options);
60
71
  assertBatchSize(batch, this.#host.limits.maxEditOperations);
72
+ const modeIssues = checkChangeMode(this.format, options);
73
+ if (modeIssues.length > 0)
74
+ throw invalidOperationError(modeIssues);
61
75
  const shapeIssues = checkOperations(batch, this.schemas);
62
76
  if (shapeIssues.length > 0)
63
77
  throw invalidOperationError(shapeIssues);
@@ -65,7 +79,8 @@ export class EditSessionController {
65
79
  if (referenceIssues.length > 0)
66
80
  throw invalidOperationError(referenceIssues);
67
81
  const interned = await this.#intern(batch, signal);
68
- const engineIssues = await this.#engine.validate(interned, signal);
82
+ const mode = modeOf(options);
83
+ const engineIssues = await this.#engine.validate(interned, signal, mode);
69
84
  throwIfAborted(signal);
70
85
  if (engineIssues.length > 0)
71
86
  throw invalidOperationError(engineIssues);
@@ -74,6 +89,7 @@ export class EditSessionController {
74
89
  const engineBatch = {
75
90
  stateId: this.#history.nextStateId,
76
91
  operations: interned,
92
+ ...mode,
77
93
  };
78
94
  if (options.dryRun) {
79
95
  const change = await this.#transaction(signal, "apply", async () => {
@@ -99,6 +115,7 @@ export class EditSessionController {
99
115
  });
100
116
  this.#history.push({
101
117
  operations: interned,
118
+ ...mode,
102
119
  ...(options.label === undefined ? {} : { label: options.label }),
103
120
  createdIds: change.createdIds,
104
121
  removedIds: change.removedIds,
@@ -230,6 +247,88 @@ export class EditSessionController {
230
247
  const { signal: own, ...engineOptions } = options;
231
248
  return this.#enqueue(own, async (signal) => this.#items(await this.#engine.findText(query, engineOptions, signal)));
232
249
  }
250
+ getOutline(options) {
251
+ return readOutline(this, this.#host.limits, options);
252
+ }
253
+ describe(options) {
254
+ return readDescription(this, this.#host.limits, options);
255
+ }
256
+ resolveTargets(query, options) {
257
+ return resolveTargets(this, query, options);
258
+ }
259
+ createCheckpoint(label) {
260
+ return this.#enqueue(undefined, async () => {
261
+ const limit = this.#host.limits.maxEditCheckpoints;
262
+ if (this.#named.size >= limit)
263
+ throw new ViewerError("resource-limit", "Too many edit checkpoints; drop one first", { details: { limit } });
264
+ const stateId = this.#history.stateId;
265
+ const checkpoint = Object.freeze({
266
+ id: randomId(),
267
+ ...(label === undefined ? {} : { label }),
268
+ revision: this.#revision,
269
+ createdAt: new Date().toISOString(),
270
+ });
271
+ const entries = this.#history.entriesAt(this.#history.position);
272
+ this.#named.set(checkpoint.id, {
273
+ checkpoint,
274
+ stateId,
275
+ pageCount: this.#history.pageCount,
276
+ entries,
277
+ batches: batchesFromOriginal(entries),
278
+ });
279
+ this.#pin(stateId, this.#committedBytes);
280
+ return checkpoint;
281
+ });
282
+ }
283
+ listCheckpoints() {
284
+ return Object.freeze([...this.#named.values()].map((named) => named.checkpoint));
285
+ }
286
+ restoreCheckpoint(id, options = {}) {
287
+ return this.#enqueue(options.signal, async (signal) => {
288
+ this.#assertRevision(options);
289
+ const named = this.#named.get(id);
290
+ if (!named)
291
+ throw new ViewerError("invalid-operation", `Unknown edit checkpoint ${id}`, { details: { checkpointId: id } });
292
+ if (named.stateId === this.#history.stateId)
293
+ return this.#noop();
294
+ const before = this.#history.pageCount;
295
+ const changedPages = allPages(Math.max(before, named.pageCount));
296
+ const shown = await this.#transaction(signal, "apply", async () => {
297
+ await this.#engine.restore(this.#targetFor(named.entries), signal);
298
+ return this.#show(signal, changedPages);
299
+ });
300
+ const diff = entryDiff(this.#history.entriesAt(this.#history.position), named.entries);
301
+ this.#history.push({
302
+ operations: [],
303
+ createdIds: diff.created,
304
+ removedIds: diff.removed,
305
+ changedPages: shown.changedPages,
306
+ pageCountBefore: before,
307
+ pageCountAfter: shown.pageCount,
308
+ base: { stateId: named.stateId, batches: named.batches },
309
+ }, named.stateId);
310
+ this.#commit("restore", shown.changedPages, shown);
311
+ return this.#receipt(false, 0, diff.created, {
312
+ removedIds: diff.removed,
313
+ changedPages: shown.changedPages,
314
+ pageCount: shown.pageCount,
315
+ warnings: [],
316
+ });
317
+ });
318
+ }
319
+ dropCheckpoint(id) {
320
+ const named = this.#named.get(id);
321
+ if (!named)
322
+ return;
323
+ this.#named.delete(id);
324
+ this.#unpin(named.stateId);
325
+ }
326
+ get tools() {
327
+ return (this.#tools ??= buildToolSet(this.format, this.schemas));
328
+ }
329
+ callTool(call, options) {
330
+ return runTool(this, this.tools, call, options);
331
+ }
233
332
  readItem(options, task) {
234
333
  return this.#enqueue(options?.signal, async (signal) => Object.freeze({
235
334
  sessionId: this.sessionId,
@@ -412,7 +511,7 @@ export class EditSessionController {
412
511
  }
413
512
  #commit(reason, changedPages, shown) {
414
513
  this.#committedBytes = shown.bytes;
415
- if (reason === "apply")
514
+ if (reason === "apply" || reason === "restore")
416
515
  this.#keepCheckpoint(shown.bytes);
417
516
  else if (reason === "reset")
418
517
  this.#dropCheckpoints();
@@ -500,49 +599,107 @@ export class EditSessionController {
500
599
  #keepCheckpoint(bytes) {
501
600
  const stride = Math.max(1, Math.floor(this.#host.limits.maxEditHistory / 4));
502
601
  const stateId = this.#history.stateId;
503
- // Entries dropped by a new change after an undo can never be restored.
504
- const reachable = new Set(this.#history.stateIds);
602
+ // Entries dropped by a new change after an undo can never be restored;
603
+ // a pinned state stays whatever the history does.
604
+ const reachable = this.#reachable();
505
605
  for (const [id, kept] of this.#checkpoints)
506
- if (!reachable.has(id))
606
+ if (!reachable.has(id) && !this.#pinned.has(id))
507
607
  this.#forgetCheckpoint(id, kept);
508
- if (stateId % stride !== 0)
608
+ if (stateId === 0 ||
609
+ this.#checkpoints.has(stateId) ||
610
+ stateId % stride !== 0)
509
611
  return;
612
+ this.#retain(stateId, bytes);
613
+ }
614
+ /** Stores a state's bytes when the budget, less the pinned states, can hold them. */
615
+ #retain(stateId, bytes) {
510
616
  const budget = this.#host.limits.maxEditCheckpointBytes;
511
617
  if (bytes.byteLength > budget)
512
618
  return;
513
- const oldestFirst = [...this.#checkpoints.keys()].sort((a, b) => a - b);
619
+ const evictable = [...this.#checkpoints.keys()]
620
+ .filter((id) => !this.#pinned.has(id))
621
+ .sort((a, b) => a - b);
514
622
  while (this.#checkpointBytes + bytes.byteLength > budget &&
515
- oldestFirst.length > 0) {
516
- const id = oldestFirst.shift();
623
+ evictable.length > 0) {
624
+ const id = evictable.shift();
517
625
  this.#forgetCheckpoint(id, this.#checkpoints.get(id));
518
626
  }
627
+ if (this.#checkpointBytes + bytes.byteLength > budget)
628
+ return;
519
629
  this.#checkpoints.set(stateId, bytes);
520
630
  this.#checkpointBytes += bytes.byteLength;
521
631
  }
632
+ /**
633
+ * Pins a state for a named checkpoint: its bytes are kept while any
634
+ * checkpoint names it, or rebuilt by replay when the budget cannot hold
635
+ * them. The original (state 0) needs no bytes.
636
+ */
637
+ #pin(stateId, bytes) {
638
+ if (stateId === 0)
639
+ return;
640
+ this.#pinned.set(stateId, (this.#pinned.get(stateId) ?? 0) + 1);
641
+ if (!this.#checkpoints.has(stateId))
642
+ this.#retain(stateId, bytes);
643
+ }
644
+ #unpin(stateId) {
645
+ const count = this.#pinned.get(stateId);
646
+ if (!count)
647
+ return;
648
+ if (count > 1) {
649
+ this.#pinned.set(stateId, count - 1);
650
+ return;
651
+ }
652
+ this.#pinned.delete(stateId);
653
+ const kept = this.#checkpoints.get(stateId);
654
+ if (kept && !this.#reachable().has(stateId))
655
+ this.#forgetCheckpoint(stateId, kept);
656
+ }
657
+ /** State ids a replay may start from: every entry's, and the base of every restore. */
658
+ #reachable() {
659
+ const reachable = new Set();
660
+ for (const entry of this.#history.allEntries) {
661
+ reachable.add(entry.stateId);
662
+ if (entry.base)
663
+ reachable.add(entry.base.stateId);
664
+ }
665
+ return reachable;
666
+ }
522
667
  #forgetCheckpoint(id, bytes) {
523
668
  this.#checkpoints.delete(id);
524
669
  this.#checkpointBytes -= bytes.byteLength;
525
670
  }
671
+ /** After a reset nothing in the history is reachable; pinned states stay. */
526
672
  #dropCheckpoints() {
527
- this.#checkpoints.clear();
528
- this.#checkpointBytes = 0;
673
+ for (const [id, kept] of this.#checkpoints)
674
+ if (!this.#pinned.has(id))
675
+ this.#forgetCheckpoint(id, kept);
529
676
  }
530
677
  /**
531
678
  * The cheapest way to rebuild the state at `position`: the newest
532
- * checkpoint at or before it, plus the batches after that checkpoint.
679
+ * retained bytes at or before it, plus the batches after them.
533
680
  */
534
681
  #restoreTarget(position = this.#history.position) {
535
- const entries = this.#history.entriesAt(position);
536
- let last = entries.length - 1;
537
- while (last >= 0 && !this.#checkpoints.has(entries[last].stateId))
538
- last -= 1;
539
- const batches = entries.slice(last + 1).map((entry) => ({
540
- stateId: entry.stateId,
541
- operations: entry.operations,
542
- }));
543
- return last >= 0
544
- ? { base: this.#checkpoints.get(entries[last].stateId), batches }
545
- : { batches };
682
+ return this.#targetFor(this.#history.entriesAt(position));
683
+ }
684
+ /**
685
+ * Walks `entries` from the end: the first with retained bytes is the base;
686
+ * a restore entry without them starts from the original plus the batches
687
+ * that built its checkpoint.
688
+ */
689
+ #targetFor(entries) {
690
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
691
+ const entry = entries[index];
692
+ const after = () => batchesOf(entries.slice(index + 1));
693
+ const retained = this.#checkpoints.get(entry.stateId);
694
+ if (retained)
695
+ return { base: retained, batches: after() };
696
+ // A restore entry carries its checkpoint's state id, so its bytes
697
+ // were just looked up; without them the checkpoint's batches rebuild
698
+ // it from the original.
699
+ if (entry.base)
700
+ return { batches: [...entry.base.batches, ...after()] };
701
+ }
702
+ return { batches: batchesOf(entries) };
546
703
  }
547
704
  #stateToken(stateId) {
548
705
  return `${this.sessionId}:${stateId}`;
@@ -667,6 +824,43 @@ function checkBatchReferences(operations) {
667
824
  });
668
825
  return issues;
669
826
  }
827
+ /**
828
+ * Tracked changes exist in DOCX only and always name their author (decision
829
+ * 8 of the ai-edit module): the batch is refused before any engine work.
830
+ */
831
+ function checkChangeMode(format, options) {
832
+ const issues = [];
833
+ // A timestamp is written into the file where a format records one, so
834
+ // it must be a date-time the file can hold whatever the mode.
835
+ if (options.timestamp !== undefined &&
836
+ (!DATE_TIME.test(options.timestamp) ||
837
+ Number.isNaN(Date.parse(options.timestamp))))
838
+ issues.push({
839
+ operationIndex: -1,
840
+ path: "/timestamp",
841
+ code: "invalid-value",
842
+ message: "The timestamp must be an ISO 8601 date-time",
843
+ });
844
+ if (options.changeMode !== "tracked")
845
+ return issues;
846
+ if (format !== "docx")
847
+ issues.push({
848
+ operationIndex: -1,
849
+ path: "",
850
+ code: "unsupported-change-mode",
851
+ message: `${format.toUpperCase()} has no tracked changes; apply directly and review with checkpoints`,
852
+ });
853
+ else if (!options.author || options.author.trim().length === 0)
854
+ issues.push({
855
+ operationIndex: -1,
856
+ path: "/author",
857
+ code: "required",
858
+ message: "Tracked changes name their author; pass ApplyOptions.author",
859
+ });
860
+ return issues;
861
+ }
862
+ /** ISO 8601 as `xsd:dateTime` takes it. */
863
+ const DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
670
864
  /** The renderer owns the page count; an engine that disagrees is reported, not trusted. */
671
865
  function pageCountWarning(change, shown) {
672
866
  if (change.pageCount === undefined || change.pageCount === shown.pageCount)
@@ -679,8 +873,56 @@ function pageCountWarning(change, shown) {
679
873
  },
680
874
  ];
681
875
  }
682
- /** 128 random bits as URL-safe base64; unique across sessions and reloads. */
683
- function newSessionId() {
876
+ /** The engine batches of entries that carry operations; restore entries carry none. */
877
+ function batchesOf(entries) {
878
+ return entries.filter((entry) => entry.operations.length > 0).map(batchOf);
879
+ }
880
+ /** The batches from the original to the state after `entries`, through the last restore. */
881
+ function batchesFromOriginal(entries) {
882
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
883
+ const base = entries[index].base;
884
+ if (base)
885
+ return [...base.batches, ...batchesOf(entries.slice(index + 1))];
886
+ }
887
+ return batchesOf(entries);
888
+ }
889
+ /**
890
+ * The ids a move from the state after `current` to the state after `target`
891
+ * removes and creates: the entries past the common prefix are undone in
892
+ * reverse, then the target's are redone, each id netted out.
893
+ */
894
+ function entryDiff(current, target) {
895
+ let common = 0;
896
+ while (common < current.length &&
897
+ common < target.length &&
898
+ current[common].stateId === target[common].stateId)
899
+ common += 1;
900
+ const created = new Set();
901
+ const removed = new Set();
902
+ const create = (id) => {
903
+ if (removed.has(id))
904
+ removed.delete(id);
905
+ else
906
+ created.add(id);
907
+ };
908
+ const remove = (id) => {
909
+ if (created.has(id))
910
+ created.delete(id);
911
+ else
912
+ removed.add(id);
913
+ };
914
+ for (const entry of current.slice(common).reverse()) {
915
+ entry.createdIds.forEach(remove);
916
+ entry.removedIds.forEach(create);
917
+ }
918
+ for (const entry of target.slice(common)) {
919
+ entry.removedIds.forEach(remove);
920
+ entry.createdIds.forEach(create);
921
+ }
922
+ return { created: [...created], removed: [...removed] };
923
+ }
924
+ /** 128 random bits as 22 URL-safe base64 characters; unique across sessions and reloads. */
925
+ function randomId() {
684
926
  const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
685
927
  let binary = "";
686
928
  for (const byte of bytes)
@@ -1,4 +1,5 @@
1
1
  import type { ViewerWarning } from "../contracts.js";
2
+ import type { EditSessionReads } from "./ai/types.js";
2
3
  /** Formats an edit session can be started for. */
3
4
  export type EditableFormat = "pdf" | "pptx" | "docx";
4
5
  export interface EditOptions {
@@ -163,16 +164,27 @@ export interface EditState {
163
164
  readonly canRedo: boolean;
164
165
  readonly pageCount: number;
165
166
  }
167
+ /** How a batch is written: in place, or as tracked changes (DOCX) for a person to accept or reject. */
168
+ export type ChangeMode = "direct" | "tracked";
166
169
  export interface ApplyOptions {
167
170
  /** Rejects the call with `edit-conflict` unless `state.revision` equals this value. */
168
171
  readonly expectedRevision?: number;
172
+ /**
173
+ * Default `direct`. `tracked` writes Word revisions (`w:ins`, `w:del`,
174
+ * `w:rPrChange`, `w:pPrChange`) with `author` and `timestamp`; refused
175
+ * with `unsupported-change-mode` by formats and operations without a
176
+ * tracked form, and with `required` at `/author` without an author.
177
+ */
178
+ readonly changeMode?: ChangeMode;
179
+ /** The `w:author` of tracked changes; required with `changeMode: "tracked"`. */
180
+ readonly author?: string;
169
181
  /** Rejects with `edit-conflict` unless it equals `sessionId`; for callers that outlive a session. */
170
182
  readonly expectedSessionId?: string;
171
183
  /** Validates and simulates the batch without changing anything. */
172
184
  readonly dryRun?: boolean;
173
185
  /** Free text stored with the history entry, for host UIs and audit logs. */
174
186
  readonly label?: string;
175
- /** Reserved: ISO 8601 time a format records with the change; never generated by web-doc. */
187
+ /** ISO 8601 time a format records with the change (the `w:date` of a tracked change); never generated by web-doc. */
176
188
  readonly timestamp?: string;
177
189
  readonly signal?: AbortSignal;
178
190
  }
@@ -228,7 +240,7 @@ export interface OperationIssue {
228
240
  readonly code: string;
229
241
  readonly message: string;
230
242
  }
231
- export interface EditSessionBase<TOperation extends EditOperation, TElement extends EditElement> {
243
+ export interface EditSessionBase<TOperation extends EditOperation, TElement extends EditElement> extends EditSessionReads {
232
244
  readonly format: EditableFormat;
233
245
  /** Unique per session; stamped on state, receipts, read results and events. */
234
246
  readonly sessionId: string;
@@ -258,7 +270,9 @@ export interface EditStateChange extends EditState {
258
270
  readonly active: boolean;
259
271
  readonly format?: EditableFormat;
260
272
  }
261
- export type DocumentChangeReason = "apply" | "undo" | "redo" | "reset";
273
+ export type DocumentChangeReason = "apply" | "undo" | "redo" | "reset"
274
+ /** `restoreCheckpoint()`: the content of a named checkpoint, as one history entry. */
275
+ | "restore";
262
276
  export interface DocumentChange {
263
277
  readonly sessionId: string;
264
278
  readonly revision: number;
@@ -1,6 +1,6 @@
1
1
  import type { WorkerRpcClient } from "../worker-client.js";
2
2
  import type { EditWorkerOperation } from "../worker-protocol.js";
3
- import type { EditEngine, EditEngineContext, EngineBatch, EngineChange, MaterializedDocument, MaterializeOptions, RestoreTarget } from "./engine.js";
3
+ import type { EditEngine, EditEngineContext, BatchMode, EngineBatch, EngineChange, MaterializedDocument, MaterializeOptions, RestoreTarget } from "./engine.js";
4
4
  import type { EditElement, EditFindOptions, EditOperation, ElementQuery, OperationIssue, OperationSchemaSet, PagePoint, TextTarget } from "./types.js";
5
5
  export declare abstract class WorkerEngineClient implements EditEngine {
6
6
  #private;
@@ -8,7 +8,7 @@ export declare abstract class WorkerEngineClient implements EditEngine {
8
8
  protected readonly rpc: WorkerRpcClient;
9
9
  protected readonly context: EditEngineContext;
10
10
  constructor(rpc: WorkerRpcClient, context: EditEngineContext);
11
- validate(operations: readonly EditOperation[], signal: AbortSignal): Promise<readonly OperationIssue[]>;
11
+ validate(operations: readonly EditOperation[], signal: AbortSignal, mode?: BatchMode): Promise<readonly OperationIssue[]>;
12
12
  /** Plain operation arrays, as the unit tests pass them, become the next batch. */
13
13
  apply(input: EngineBatch | readonly EditOperation[], signal: AbortSignal): Promise<EngineChange>;
14
14
  /** A bare signal, as the unit tests pass it, asks for the shown form. */
@@ -12,8 +12,8 @@ export class WorkerEngineClient {
12
12
  this.rpc = rpc;
13
13
  this.context = context;
14
14
  }
15
- validate(operations, signal) {
16
- return this.request("edit-validate", { operations }, signal);
15
+ validate(operations, signal, mode = {}) {
16
+ return this.request("edit-validate", { operations, mode }, signal);
17
17
  }
18
18
  /** Plain operation arrays, as the unit tests pass them, become the next batch. */
19
19
  apply(input, signal) {
@@ -6,7 +6,14 @@
6
6
  * than a page × query traceback matrix. Equal-cost occurrences prefer the
7
7
  * earliest end, so an unmatched character after a passage cannot extend it.
8
8
  */
9
- export declare function alignFuzzyPassage(text: string, query: string, caseSensitive: boolean, maxScore: number): {
10
- start: number;
11
- end: number;
12
- } | undefined;
9
+ /** The best contiguous alignment of a query in a text, with its edit cost. */
10
+ export interface FuzzyPassage {
11
+ /** Offsets into the original text, in UTF-16 code units. */
12
+ readonly start: number;
13
+ readonly end: number;
14
+ /** Edits between the query and the passage; `cost / length` is the score Fuse bounds. */
15
+ readonly cost: number;
16
+ /** The normalized query's length. */
17
+ readonly length: number;
18
+ }
19
+ export declare function alignFuzzyPassage(text: string, query: string, caseSensitive: boolean, maxScore: number): FuzzyPassage | undefined;
@@ -1,12 +1,4 @@
1
1
  import { normalizeSearchText, normalizeWithMap } from "./search-text.js";
2
- /**
3
- * Fuse's indices describe character masks, not a contiguous edit alignment.
4
- * Refine an accepted page with semi-global Levenshtein alignment: consume the
5
- * whole query, allowing free text before and after one occurrence. Tracking
6
- * the start alongside each cost needs O(query length) working memory rather
7
- * than a page × query traceback matrix. Equal-cost occurrences prefer the
8
- * earliest end, so an unmatched character after a passage cannot extend it.
9
- */
10
2
  export function alignFuzzyPassage(text, query, caseSensitive, maxScore) {
11
3
  const source = normalizeWithMap(text, caseSensitive);
12
4
  const pattern = normalizeSearchText(query, caseSensitive);
@@ -59,5 +51,7 @@ export function alignFuzzyPassage(text, query, caseSensitive, maxScore) {
59
51
  return undefined;
60
52
  const start = source.starts[bestStart];
61
53
  const end = source.ends[bestEnd - 1];
62
- return start === undefined || end === undefined ? undefined : { start, end };
54
+ return start === undefined || end === undefined
55
+ ? undefined
56
+ : { start, end, cost: bestCost, length };
63
57
  }
@@ -6,7 +6,7 @@ export * from "./edit/pptx/types.js";
6
6
  export * from "./client.js";
7
7
  export * from "./viewer.js";
8
8
  export * from "./detect.js";
9
- export * from "./errors.js";
9
+ export { abortError, errorFromData, normalizeError, ViewerError, } from "./errors.js";
10
10
  export * from "./format.js";
11
11
  export * from "./limits.js";
12
12
  export * from "./interaction.js";
package/dist/headless.js CHANGED
@@ -6,7 +6,10 @@ export * from "./edit/pptx/types.js";
6
6
  export * from "./client.js";
7
7
  export * from "./viewer.js";
8
8
  export * from "./detect.js";
9
- export * from "./errors.js";
9
+ // Named on purpose: a bundler that inlines the lazily loaded edit engines
10
+ // (esbuild without code splitting) initialises this module lazily, and a
11
+ // star re-export would then hand a consumer an undefined class.
12
+ export { abortError, errorFromData, normalizeError, ViewerError, } from "./errors.js";
10
13
  export * from "./format.js";
11
14
  export * from "./limits.js";
12
15
  export * from "./interaction.js";
package/dist/index.d.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  export * from "./contracts.js";
2
2
  export * from "./edit/types.js";
3
+ export * from "./edit/ai/types.js";
4
+ export { describeReceipt } from "./edit/ai/tools.js";
3
5
  export * from "./edit/sessions.js";
4
6
  export * from "./edit/pdf/types.js";
5
7
  export * from "./edit/pptx/types.js";
6
8
  export * from "./edit/docx/types.js";
7
9
  export * from "./client.js";
8
10
  export * from "./detect.js";
9
- export * from "./errors.js";
11
+ export { abortError, errorFromData, normalizeError, ViewerError, } from "./errors.js";
10
12
  export * from "./format.js";
11
13
  export * from "./limits.js";
12
14
  export * from "./interaction.js";
@@ -23,7 +25,8 @@ export * from "./ui.js";
23
25
  export * from "./ui-styles.js";
24
26
  export * from "./registry.js";
25
27
  export * from "./viewer.js";
26
- export * from "./worker-client.js";
28
+ export { WorkerRpcClient } from "./worker-client.js";
29
+ export type { WorkerLike, WorkerRequestOptions } from "./worker-client.js";
27
30
  export * from "./worker-adapter.js";
28
31
  export * from "./worker-endpoint.js";
29
32
  export * from "./worker-protocol.js";
package/dist/index.js CHANGED
@@ -1,12 +1,17 @@
1
1
  export * from "./contracts.js";
2
2
  export * from "./edit/types.js";
3
+ export * from "./edit/ai/types.js";
4
+ export { describeReceipt } from "./edit/ai/tools.js";
3
5
  export * from "./edit/sessions.js";
4
6
  export * from "./edit/pdf/types.js";
5
7
  export * from "./edit/pptx/types.js";
6
8
  export * from "./edit/docx/types.js";
7
9
  export * from "./client.js";
8
10
  export * from "./detect.js";
9
- export * from "./errors.js";
11
+ // Named like the worker client below: the error class is reached from the
12
+ // lazily loaded engines too and must not come out undefined in a consumer
13
+ // bundle without code splitting.
14
+ export { abortError, errorFromData, normalizeError, ViewerError, } from "./errors.js";
10
15
  export * from "./format.js";
11
16
  export * from "./limits.js";
12
17
  export * from "./interaction.js";
@@ -23,7 +28,11 @@ export * from "./ui.js";
23
28
  export * from "./ui-styles.js";
24
29
  export * from "./registry.js";
25
30
  export * from "./viewer.js";
26
- export * from "./worker-client.js";
31
+ // Named on purpose: a bundler that inlines the lazily loaded edit engines
32
+ // (esbuild without code splitting) initialises this module lazily, and a
33
+ // star re-export would then hand a consumer an undefined class
34
+ // (`scripts/consumer-bundle.test.mjs` guards every class and constant).
35
+ export { WorkerRpcClient } from "./worker-client.js";
27
36
  export * from "./worker-adapter.js";
28
37
  export * from "./worker-endpoint.js";
29
38
  export * from "./worker-protocol.js";
package/dist/limits.js CHANGED
@@ -13,6 +13,9 @@ export const defaultResourceLimits = Object.freeze({
13
13
  maxEditOperations: 500,
14
14
  maxEditHistory: 200,
15
15
  maxEditCheckpointBytes: 64 * 1024 * 1024,
16
+ maxOutlineNodes: 5000,
17
+ maxDescribeChars: 200_000,
18
+ maxEditCheckpoints: 20,
16
19
  });
17
20
  export function resolveLimits(base = {}, override = {}) {
18
21
  const result = { ...defaultResourceLimits, ...base, ...override };
@@ -2,7 +2,7 @@ import type { DocumentFormat, DocumentInfo, ResourceLimits, TextRun, ViewerError
2
2
  import type { EditableFormat } from "./edit/types.js";
3
3
  export type DocumentWorkerOperation = "init" | "open" | "get-info" | "render" | "get-text-map" | "close" | "destroy";
4
4
  /** Operations of an edit engine worker; payloads mirror the engine interface. */
5
- export type EditWorkerOperation = "edit-init" | "edit-open" | "edit-validate" | "edit-apply" | "edit-materialize" | "edit-restore" | "edit-put-asset" | "edit-elements" | "edit-element" | "edit-elements-at" | "edit-find-text" | "edit-text-layout" | "edit-position-at" | "edit-range-rects" | "edit-render-without" | "edit-page-layout" | "edit-pptx-slides" | "edit-pptx-layouts" | "edit-dispose";
5
+ export type EditWorkerOperation = "edit-init" | "edit-open" | "edit-validate" | "edit-apply" | "edit-materialize" | "edit-restore" | "edit-put-asset" | "edit-elements" | "edit-element" | "edit-elements-at" | "edit-find-text" | "edit-text-layout" | "edit-position-at" | "edit-range-rects" | "edit-render-without" | "edit-page-layout" | "edit-pptx-slides" | "edit-pptx-layouts" | "edit-docx-revisions" | "edit-dispose";
6
6
  export type WorkerOperation = DocumentWorkerOperation | EditWorkerOperation;
7
7
  export interface WorkerRequest {
8
8
  readonly kind: "request";
@@ -1634,7 +1634,7 @@ function alignFuzzyPassage(text, query, caseSensitive, maxScore) {
1634
1634
  if (bestEnd <= bestStart || bestCost / length > maxScore) return void 0;
1635
1635
  const start = source.starts[bestStart];
1636
1636
  const end = source.ends[bestEnd - 1];
1637
- return start === void 0 || end === void 0 ? void 0 : { start, end };
1637
+ return start === void 0 || end === void 0 ? void 0 : { start, end, cost: bestCost, length };
1638
1638
  }
1639
1639
 
1640
1640
  // src/fuzzy-search.ts