@superdoc/sdk 2.26.0 → 2.27.0-next.19

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 (45) hide show
  1. package/README.md +236 -29
  2. package/dist/agent/actions.cjs +62 -24
  3. package/dist/agent/actions.js +62 -24
  4. package/dist/agent/catalog.d.ts +0 -4
  5. package/dist/agent/catalog.js +0 -4
  6. package/dist/embedded-prompts.generated.cjs +1 -1
  7. package/dist/embedded-prompts.generated.js +1 -1
  8. package/dist/embedded-tools.generated.cjs +5 -5
  9. package/dist/embedded-tools.generated.js +5 -5
  10. package/dist/generated/client.cjs +18 -1
  11. package/dist/generated/client.d.ts +3250 -1019
  12. package/dist/generated/client.js +18 -1
  13. package/dist/generated/contract.cjs +1 -2
  14. package/dist/generated/contract.d.ts +1 -0
  15. package/dist/generated/contract.js +1 -2
  16. package/dist/index.cjs +3 -0
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.js +3 -0
  19. package/dist/presets/full.cjs +4 -0
  20. package/dist/presets/full.js +4 -0
  21. package/dist/prompts/full-addendum.md +1 -0
  22. package/dist/runtime/host.cjs +36 -3
  23. package/dist/runtime/host.d.ts +5 -0
  24. package/dist/runtime/host.js +36 -4
  25. package/dist/runtime/notifications.cjs +124 -0
  26. package/dist/runtime/notifications.d.ts +35 -0
  27. package/dist/runtime/notifications.js +116 -0
  28. package/dist/runtime/process.cjs +4 -1
  29. package/dist/runtime/process.d.ts +2 -0
  30. package/dist/runtime/process.js +4 -1
  31. package/dist/runtime/sdk-policy.d.ts +1 -0
  32. package/dist/runtime/sdk-version.generated.cjs +1 -1
  33. package/dist/runtime/sdk-version.generated.d.ts +1 -1
  34. package/dist/runtime/sdk-version.generated.js +1 -1
  35. package/dist/runtime/transport-common.cjs +9 -1
  36. package/dist/runtime/transport-common.d.ts +3 -0
  37. package/dist/runtime/transport-common.js +8 -1
  38. package/package.json +7 -7
  39. package/tools/catalog.json +42 -2
  40. package/tools/generation-receipt.json +60 -22
  41. package/tools/tools-policy.json +1 -1
  42. package/tools/tools.anthropic.json +42 -2
  43. package/tools/tools.generic.json +42 -2
  44. package/tools/tools.openai.json +42 -2
  45. package/tools/tools.vercel.json +42 -2
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # @superdoc/sdk
2
2
 
3
- Node.js SDK for reading and editing DOCX files with SuperDoc.
3
+ Node.js SDK for reading and editing DOCX files with SuperDoc, without a
4
+ browser. Query, edit, comment, and review tracked changes from your server,
5
+ or give an LLM SuperDoc tools with the agent toolkit. Edits write back to
6
+ OOXML, so the output opens in Word. For an interactive editor, use
7
+ [`superdoc`](https://www.npmjs.com/package/superdoc).
8
+
9
+ [Documentation](https://docs.superdoc.dev/agents/automation/node-sdk) · [GitHub](https://github.com/superdoc/docx-editor)
4
10
 
5
11
  ## Install
6
12
 
@@ -8,23 +14,37 @@ Node.js SDK for reading and editing DOCX files with SuperDoc.
8
14
  npm install @superdoc/sdk
9
15
  ```
10
16
 
17
+ The SDK embeds the SuperDoc runtime for your platform. You do not need a
18
+ separate CLI installation.
19
+
11
20
  ## Quick start
12
21
 
13
22
  ```js
14
23
  import { SuperDocClient } from '@superdoc/sdk';
15
24
 
16
- const client = new SuperDocClient();
17
- await client.connect();
25
+ const client = new SuperDocClient({ user: { name: 'Contract bot', email: 'bot@example.com' } });
18
26
 
19
- const document = await client.open({ doc: './contract.docx' });
20
- const markdown = await document.getMarkdown();
27
+ try {
28
+ await client.connect();
29
+ const doc = await client.open({ doc: './contract.docx' });
21
30
 
22
- console.log(markdown);
31
+ try {
32
+ const receipt = await doc.trackChanges.decide({ decision: 'accept', target: { kind: 'all' } });
33
+ if (!receipt.success) throw new Error('Accepting tracked changes failed.');
23
34
 
24
- await document.close();
25
- await client.dispose();
35
+ await doc.save({ out: './contract.accepted.docx', force: true });
36
+ } finally {
37
+ await doc.close({ discard: true });
38
+ }
39
+ } finally {
40
+ await client.dispose();
41
+ }
26
42
  ```
27
43
 
44
+ Save to a new path so the source stays unchanged. `force: true` lets the save
45
+ replace an earlier output. `close({ discard: true })` then closes the session
46
+ without writing over the source.
47
+
28
48
  ## Client lifetime
29
49
 
30
50
  One client owns one SuperDoc host process. `connect()` starts it once; every
@@ -37,6 +57,131 @@ room sync on every call. To cut round trips further, batch related edits with
37
57
  `document.mutations.apply()` or arbitrary operations with
38
58
  `document.plan.execute()`.
39
59
 
60
+ ### Batch timeouts and recovery
61
+
62
+ Use `mutations.apply({ atomic: true, steps, changeMode: 'tracked' })` for related
63
+ structured rewrites instead of a separate search and edit request for every
64
+ replacement. Resolve targets from the current document and inspect the batch
65
+ receipt. Choose batch sizes from your workload's measurements; there is no
66
+ recommended fixed cap.
67
+
68
+ `requestTimeoutMs` sets the host's default request budget. A per-call
69
+ `{ timeoutMs }` overrides it on hosts that support request timeout metadata.
70
+ The SDK watchdog waits longer than that budget so the host can return a
71
+ structured `TIMEOUT`. A timeout is a response deadline, not mutation rollback.
72
+
73
+ When the host's response deadline expires, `TIMEOUT` details describe execution and retry safety:
74
+
75
+ - `executionState: 'not-started'`, `retrySafe: true`: the request expired in
76
+ the host queue and will not run. Retrying cannot duplicate that request's edits.
77
+ - `executionState: 'unknown'`, `retrySafe: false`: invocation started and may
78
+ still finish. Do not replay the batch. Read the same document's text and tracked
79
+ changes with a sufficient budget; the host serializes those reads after the
80
+ timed-out invocation settles. Compare the actual result with the intended
81
+ changes, then build a fresh plan only for work that remains.
82
+ - `executionState: 'unknown'`, `retrySafe: true`: a recognized change-list long
83
+ poll already started a read. Retrying this read-only poll cannot duplicate edits,
84
+ even when its final re-query expires in the queue.
85
+
86
+ Treat absent recovery details as an unknown outcome, including errors from
87
+ older hosts or an operation's own timeout handling.
88
+ `HOST_TIMEOUT` is the separate SDK watchdog error: the host did not respond and
89
+ the SDK disconnects it. It does not prove that edits were rolled back. Preserve
90
+ the session state and inspect the recoverable working document before creating
91
+ another session or retrying edits. A permanently stalled invocation can also
92
+ block later reads; another timeout is not evidence that nothing applied.
93
+
94
+ ## Common tasks
95
+
96
+ ### Read the document
97
+
98
+ ```js
99
+ const markdown = await doc.getMarkdown();
100
+ const text = await doc.getText({});
101
+ ```
102
+
103
+ ### Find and replace text as a tracked change
104
+
105
+ `query.match` returns a complete target. Pass it to the mutation instead of
106
+ building one yourself:
107
+
108
+ ```js
109
+ const match = await doc.query.match({
110
+ select: { type: 'text', pattern: 'termination' },
111
+ require: 'first',
112
+ });
113
+ const item = match.items[0];
114
+ if (!item || item.matchKind !== 'text') throw new Error('Text not found.');
115
+
116
+ const operation = await doc.replace({
117
+ target: item.target,
118
+ text: 'cancellation',
119
+ changeMode: 'tracked',
120
+ });
121
+ const receipt = 'receipt' in operation ? operation.receipt : operation;
122
+ if (!receipt.success) throw new Error('Creating the tracked change failed.');
123
+ ```
124
+
125
+ ### Give an LLM SuperDoc tools
126
+
127
+ `createAgentToolkit` returns tool definitions, a matching system prompt, and a
128
+ `dispatch` function, so the three cannot disagree:
129
+
130
+ ```js
131
+ import { createAgentToolkit } from '@superdoc/sdk';
132
+
133
+ const { tools, systemPrompt, dispatch } = await createAgentToolkit({
134
+ provider: 'openai', // 'openai' | 'anthropic' | 'vercel' | 'generic'
135
+ preset: 'core',
136
+ enforceChangeMode: 'tracked',
137
+ });
138
+
139
+ // For each tool call in an OpenAI Chat Completions reply:
140
+ const result = await dispatch(doc, call.function.name, JSON.parse(call.function.arguments));
141
+ ```
142
+
143
+ OpenAI sends tool arguments as a JSON string, so parse them before calling
144
+ `dispatch`. Anthropic and Vercel pass an object already.
145
+
146
+ `enforceChangeMode: 'tracked'` hides actions that cannot record a tracked
147
+ change and records every other edit as tracked. See
148
+ [build an agent](https://docs.superdoc.dev/agents/build/build-an-agent) for the
149
+ full loop.
150
+
151
+ ## Common mistakes
152
+
153
+ | Wrong | Right |
154
+ | --- | --- |
155
+ | Creating a client per operation | Create one per service and keep document handles open |
156
+ | Passing OpenAI's JSON-string arguments to `dispatch` | `JSON.parse` them first. `dispatch` takes an object |
157
+ | Omitting `preset` in `createAgentToolkit` | Pass `preset: 'core'`. Omitting it selects the older `legacy` tools |
158
+ | Omitting `user` | Name the workflow so tracked changes show who proposed them |
159
+ | Reusing a `query.match` target after an edit | Query again. Targets are bound to the revision that produced them |
160
+ | Building targets from OOXML IDs such as `w14:paraId` | Use the address or target the API returns |
161
+ | Saving over the source while testing | Save to a new path with `out` |
162
+ | Retrying a failed receipt automatically | Inspect `executedOperations` first. Part of the edit may have committed |
163
+ | Importing `superdoc` in Node.js | Use this package. `superdoc` is the browser editor |
164
+
165
+ ## Related packages
166
+
167
+ - [`superdoc`](https://www.npmjs.com/package/superdoc): browser DOCX editor for reviewing the SDK's output.
168
+ - [`@superdoc/mcp`](https://www.npmjs.com/package/@superdoc/mcp): the same tools for an existing MCP client, without writing an agent loop.
169
+ - [`@superdoc/cli`](https://www.npmjs.com/package/@superdoc/cli): the same operations from a terminal or script.
170
+ - [`superdoc-sdk`](https://pypi.org/project/superdoc-sdk/) on PyPI and [`superdoc-sdk`](https://crates.io/crates/superdoc-sdk) on crates.io: Python and Rust SDKs.
171
+
172
+ ## Documentation
173
+
174
+ - [Node.js SDK guide](https://docs.superdoc.dev/agents/automation/node-sdk)
175
+ - [Build an agent](https://docs.superdoc.dev/agents/build/build-an-agent)
176
+ - [Document API mental model](https://docs.superdoc.dev/document-api/mental-model)
177
+ - [Document comparison](https://docs.superdoc.dev/document-api/document-comparison)
178
+ - [Agent safety](https://docs.superdoc.dev/agents/operate/safety)
179
+ - [`llms.txt`](https://docs.superdoc.dev/llms.txt): machine-readable index of the documentation for coding agents.
180
+
181
+ ## Advanced
182
+
183
+ ### State directory
184
+
40
185
  For read-only hosts and containers, configure a writable state directory:
41
186
 
42
187
  ```js
@@ -73,6 +218,8 @@ the SDK parent can leave temporary state behind; the operating system or host
73
218
  operator must reclaim it. A new client gets a separate private root and does
74
219
  not sweep another client's state.
75
220
 
221
+ ### Authenticated collaboration rooms
222
+
76
223
  For token-authenticated Hocuspocus or y-websocket rooms, pass the credential as
77
224
  an option on that document's open request:
78
225
 
@@ -94,9 +241,41 @@ The credential applies only to that open request. It is not part of the
94
241
  collaboration profile or command-line arguments. Per-open token authentication
95
242
  is not supported for Liveblocks.
96
243
 
97
- See the [Node.js SDK guide](https://docs.superdoc.dev/agents/automation/node-sdk) for a complete workflow.
244
+ ### Attributed change records
98
245
 
99
- ## Discover footer paragraphs
246
+ Give each session a stable actor id with `userId`. Pass an application-signed
247
+ claim, if you have one, as the `identityClaim` open option; like the
248
+ collaboration token, it is sent only with that open request and is never
249
+ written to arguments or state. SuperDoc records the claim but does not verify
250
+ it.
251
+
252
+ ```js
253
+ const document = await client.open(
254
+ { doc: './contract.docx', userId: 'user-42', collaboration },
255
+ { collaborationAuth: { type: 'token', token: userToken }, identityClaim: signedClaim },
256
+ );
257
+ ```
258
+
259
+ Every local and remote edit produces a change record. Read them with
260
+ `document.changes.list({ after, waitMs })`; `waitMs` waits on the host without
261
+ blocking other requests. To be told about new records, subscribe to host
262
+ notifications:
263
+
264
+ ```js
265
+ const notifications = client.subscribe();
266
+ for await (const notification of notifications) {
267
+ if (notification.method === 'event.change') handle(notification.params.records);
268
+ }
269
+ ```
270
+
271
+ Each subscription holds up to `capacity` notifications (default 64). If you
272
+ fall behind, the next read rejects with `NOTIFICATIONS_LAGGED`; resynchronize
273
+ with `changes.list({ after })` from the last cursor you processed, then keep
274
+ iterating. A host disconnect rejects one read with `HOST_DISCONNECTED`.
275
+ `notifications.close()` unsubscribes and `client.dispose()` ends every
276
+ subscription.
277
+
278
+ ### Discover footer paragraphs
100
279
 
101
280
  `blocks.list({ in: story })` lists top-level blocks. To discover paragraphs
102
281
  inside footer tables, including tables wrapped in block content controls, use
@@ -132,7 +311,47 @@ if (matches.total === 1) {
132
311
  }
133
312
  ```
134
313
 
135
- ## Tracked comparison with direct style definitions
314
+ ### Inspecting textbox sections
315
+
316
+ `superdoc_inspect` includes discovered text-bearing textbox stories in `textboxes` when
317
+ block inspection is enabled. Each entry contains its explicit `story` locator,
318
+ its story-local `total`, and normalized `blocks` with text, identities, and list
319
+ markers. The top-level `blocks` array and its ordinals continue to refer to the
320
+ body; textbox block ordinals belong to their containing story.
321
+
322
+ The block offset, limit, text cap, and block filters also apply within each
323
+ textbox. Count-only inspections and inspections excluding the blocks domain do
324
+ not collect textbox text. Check `diagnostics` for incomplete story discovery or
325
+ reads. To edit a discovered heading, use `query.match` with its story locator
326
+ and pass the returned reference to `replace`; a textbox ordinal is not a body
327
+ action selector.
328
+
329
+ ### Rejectable comparison styles
330
+
331
+ For standalone V2 comparisons, use the tracked style policy when reviewers need to
332
+ reject proposed styling as well as text:
333
+
334
+ ```js
335
+ const diff = await older.diff.compare({ targetSnapshot, styleChangeMode: 'tracked' });
336
+ const result = await older.diff.apply({ diff, changeMode: 'tracked', styleChangeMode: 'tracked' });
337
+ await older.save({ out: 'comparison.docx' });
338
+ ```
339
+
340
+ The saved file contains separate proposed style definitions and tracked references
341
+ that Word can accept or reject. Rejection restores the original text and effective
342
+ styling. Unused definitions may remain after a decision, and proposed styles retain
343
+ their distinct names. Inspect `result.styleChanges` and `result.diagnostics`.
344
+
345
+ This policy supports non-default paragraph and character styles in the main body,
346
+ including inherited styles and outline-based TOCs. TOCs that select an affected style
347
+ by identity (`\o`) or name (`\t`) are refused. Defaults, table styles, affected styles in other stories,
348
+ combined table topology changes, pre-existing revisions, and definitions that activate already-used unresolved style
349
+ references are refused. Numbering
350
+ and settings retain their restrictions. Versioned suggest-only review sessions have
351
+ a separate policy and refuse this option. Read eligibility before applying and
352
+ handle apply-time refusals without saving a result.
353
+
354
+ ### Tracked comparison with direct style definitions
136
355
 
137
356
  For standalone V2 comparisons, `styleChangeMode: 'direct'` lets you explicitly
138
357
  carry the target style definitions while applying body edits as tracked changes:
@@ -147,12 +366,12 @@ await older.save({ out: 'comparison.docx' });
147
366
  Set the policy on both calls: compare reports eligibility for that policy;
148
367
  apply requires its own explicit consent. Style definitions are applied directly
149
368
  and remain if the body revisions are rejected in Word. A style-only comparison
150
- creates no body revisions. Without this option, changed style definitions still
369
+ creates no body revisions. Without an explicit style policy, changed definitions still
151
370
  block tracked apply. Unsupported numbering, settings, and other package changes
152
371
  retain their existing restrictions. Suggest-only versioned review sessions
153
372
  refuse this direct-style option.
154
373
 
155
- ## Tracked comparison with exact-file review
374
+ ### Tracked comparison with exact-file review
156
375
 
157
376
  The SDK's `diff.capture` and `diff.compare` describe a snapshot comparison. A
158
377
  blocked tracked `diff.apply` cannot turn that snapshot into a complete review
@@ -216,7 +435,7 @@ endpoints; other supported changes become one whole-document proposal. A stale
216
435
  base or invalid DOCX returns a refusal without a partial apply. The host owns
217
436
  authorization, durable storage, reviewer presentation, and decisions.
218
437
 
219
- ## Agent execution evidence
438
+ ### Agent execution evidence
220
439
 
221
440
  The core preset's `agent_apply`, `agent_verify`, and supported named actions
222
441
  collect the facts their targets and checks need. CLI-backed sessions
@@ -262,19 +481,7 @@ automatically replay the write.
262
481
 
263
482
  ## License
264
483
 
265
- AGPL-3.0. Commercial licenses are available from [SuperDoc](https://www.superdoc.dev).
266
-
267
- ## Inspecting textbox sections
484
+ AGPL-3.0. [Commercial licenses](https://www.superdoc.dev/#pricing) are
485
+ available for proprietary deployments.
268
486
 
269
- `superdoc_inspect` includes discovered text-bearing textbox stories in `textboxes` when
270
- block inspection is enabled. Each entry contains its explicit `story` locator,
271
- its story-local `total`, and normalized `blocks` with text, identities, and list
272
- markers. The top-level `blocks` array and its ordinals continue to refer to the
273
- body; textbox block ordinals belong to their containing story.
274
-
275
- The block offset, limit, text cap, and block filters also apply within each
276
- textbox. Count-only inspections and inspections excluding the blocks domain do
277
- not collect textbox text. Check `diagnostics` for incomplete story discovery or
278
- reads. To edit a discovered heading, use `query.match` with its story locator
279
- and pass the returned reference to `replace`; a textbox ordinal is not a body
280
- action selector.
487
+ The retained legacy comparison adapter refuses tracked style policy with `CAPABILITY_UNSUPPORTED`; use the current typed engine through the standalone SDK.
@@ -5398,6 +5398,9 @@ function matchRunStyle(run) {
5398
5398
  ...(color ? { color } : {}),
5399
5399
  };
5400
5400
  }
5401
+ function sameMatchRunStyle(a, b) {
5402
+ return ['bold', 'italic', 'underline', 'fontSize', 'fontFamily', 'color'].every((key) => a[key] === b[key]);
5403
+ }
5401
5404
  /**
5402
5405
  * A sampled style as a `format.apply` `inline` payload.
5403
5406
  *
@@ -5466,36 +5469,71 @@ async function listItemRunPattern(doc, text, blockId) {
5466
5469
  if (asString(rec?.blockId) !== blockId)
5467
5470
  continue;
5468
5471
  const runTextOf = (r) => asString(asRecord(r)?.text, '') ?? '';
5469
- const runs = (Array.isArray(rec?.runs) ? rec.runs : []).filter((r) => runTextOf(r).length > 0);
5470
- if (runs.length === 0)
5472
+ const sourceRuns = (Array.isArray(rec?.runs) ? rec.runs : []).filter((run) => runTextOf(run));
5473
+ if (sourceRuns.length === 0)
5471
5474
  continue;
5472
- const typedLead = typedPrefixLabelOf(runs, runTextOf);
5473
- if (typedLead)
5474
- return typedLead;
5475
5475
  const blockText = asString(rec?.text, '') ?? '';
5476
- // A quoted defined term keeps its opening quote in a run of its own —
5477
- // `"` + **Deliverables** + `" means …` — so the label is the SECOND run
5478
- // and the quotes stay unemphasised.
5479
- const opener = runTextOf(runs[0]).trim();
5480
- const quote = LEAD_IN_QUOTES.find((q) => opener === q);
5481
- const labelIndex = quote && runs.length > 2 ? 1 : 0;
5482
- const label = runs[labelIndex];
5483
- const labelText = runTextOf(label);
5484
- const leadStyle = matchRunStyle(label);
5485
- if (runs.length === 1)
5486
- return runPatternOf(leadStyle);
5487
- // The body is the run the label INTRODUCES, not the longest run in the
5488
- // block: a later emphasised phrase is part of the prose, and sampling it
5489
- // would stamp its emphasis across the whole new block.
5490
- const bodyStyle = matchRunStyle(runs[labelIndex + 1]);
5491
- const differs = ['bold', 'italic', 'underline', 'fontSize', 'fontFamily', 'color'].some((k) => leadStyle[k] !== bodyStyle[k]);
5492
- const shape = leadInShapeOf(blockText, labelText, labelIndex > 0 ? quote : undefined);
5493
- const isLabel = differs && shape != null && labelText.length > 0 && labelText.length <= LEAD_IN_MAX_CHARS;
5494
- return runPatternOf(bodyStyle, isLabel && shape ? { style: leadStyle, shape } : undefined);
5476
+ const rawPattern = runPatternFromRuns(sourceRuns, blockText, runTextOf);
5477
+ if (rawPattern.lead)
5478
+ return rawPattern;
5479
+ const runs = [];
5480
+ for (const run of sourceRuns) {
5481
+ const current = asRecord(run);
5482
+ const text = runTextOf(current);
5483
+ if (!current || !text)
5484
+ continue;
5485
+ const previous = runs.at(-1);
5486
+ const previousRange = asRecord(previous?.range);
5487
+ const currentRange = asRecord(current.range);
5488
+ const contiguous = previousRange || currentRange
5489
+ ? typeof previousRange?.start === 'number' &&
5490
+ typeof previousRange.end === 'number' &&
5491
+ typeof currentRange?.start === 'number' &&
5492
+ typeof currentRange?.end === 'number' &&
5493
+ previousRange.end === currentRange.start
5494
+ : true;
5495
+ const previousStyle = matchRunStyle(previous);
5496
+ const currentStyle = matchRunStyle(current);
5497
+ const equalStyle = sameMatchRunStyle(previousStyle, currentStyle);
5498
+ if (previous && contiguous && equalStyle) {
5499
+ previous.text = runTextOf(previous) + text;
5500
+ if (previousRange && currentRange)
5501
+ previous.range = { ...previousRange, end: currentRange.end };
5502
+ }
5503
+ else {
5504
+ runs.push({ ...current });
5505
+ }
5506
+ }
5507
+ const normalizedPattern = runPatternFromRuns(runs, blockText, runTextOf);
5508
+ return normalizedPattern.lead ? normalizedPattern : rawPattern;
5495
5509
  }
5496
5510
  }
5497
5511
  return null;
5498
5512
  }
5513
+ function runPatternFromRuns(runs, blockText, runTextOf) {
5514
+ const typedLead = typedPrefixLabelOf(runs, runTextOf);
5515
+ if (typedLead)
5516
+ return typedLead;
5517
+ // A quoted defined term keeps its opening quote in a run of its own —
5518
+ // `"` + **Deliverables** + `" means …` — so the label is the SECOND run
5519
+ // and the quotes stay unemphasised.
5520
+ const opener = runTextOf(runs[0]).trim();
5521
+ const quote = LEAD_IN_QUOTES.find((q) => opener === q);
5522
+ const labelIndex = quote && runs.length > 2 ? 1 : 0;
5523
+ const label = runs[labelIndex];
5524
+ const labelText = runTextOf(label);
5525
+ const leadStyle = matchRunStyle(label);
5526
+ if (runs.length === 1)
5527
+ return runPatternOf(leadStyle);
5528
+ // The body is the run the label INTRODUCES, not the longest run in the
5529
+ // block: a later emphasised phrase is part of the prose, and sampling it
5530
+ // would stamp its emphasis across the whole new block.
5531
+ const bodyStyle = matchRunStyle(runs[labelIndex + 1]);
5532
+ const differs = !sameMatchRunStyle(leadStyle, bodyStyle);
5533
+ const shape = leadInShapeOf(blockText, labelText, labelIndex > 0 ? quote : undefined);
5534
+ const isLabel = differs && shape != null && labelText.length > 0 && labelText.length <= LEAD_IN_MAX_CHARS;
5535
+ return runPatternOf(bodyStyle, isLabel && shape ? { style: leadStyle, shape } : undefined);
5536
+ }
5499
5537
  function typedPrefixLabelOf(runs, runTextOf) {
5500
5538
  const typed = typedMarkerPrefixOf(runs.map(runTextOf).join(''));
5501
5539
  if (!typed)
@@ -5395,6 +5395,9 @@ function matchRunStyle(run) {
5395
5395
  ...(color ? { color } : {}),
5396
5396
  };
5397
5397
  }
5398
+ function sameMatchRunStyle(a, b) {
5399
+ return ['bold', 'italic', 'underline', 'fontSize', 'fontFamily', 'color'].every((key) => a[key] === b[key]);
5400
+ }
5398
5401
  /**
5399
5402
  * A sampled style as a `format.apply` `inline` payload.
5400
5403
  *
@@ -5463,36 +5466,71 @@ async function listItemRunPattern(doc, text, blockId) {
5463
5466
  if (asString(rec?.blockId) !== blockId)
5464
5467
  continue;
5465
5468
  const runTextOf = (r) => asString(asRecord(r)?.text, '') ?? '';
5466
- const runs = (Array.isArray(rec?.runs) ? rec.runs : []).filter((r) => runTextOf(r).length > 0);
5467
- if (runs.length === 0)
5469
+ const sourceRuns = (Array.isArray(rec?.runs) ? rec.runs : []).filter((run) => runTextOf(run));
5470
+ if (sourceRuns.length === 0)
5468
5471
  continue;
5469
- const typedLead = typedPrefixLabelOf(runs, runTextOf);
5470
- if (typedLead)
5471
- return typedLead;
5472
5472
  const blockText = asString(rec?.text, '') ?? '';
5473
- // A quoted defined term keeps its opening quote in a run of its own —
5474
- // `"` + **Deliverables** + `" means …` — so the label is the SECOND run
5475
- // and the quotes stay unemphasised.
5476
- const opener = runTextOf(runs[0]).trim();
5477
- const quote = LEAD_IN_QUOTES.find((q) => opener === q);
5478
- const labelIndex = quote && runs.length > 2 ? 1 : 0;
5479
- const label = runs[labelIndex];
5480
- const labelText = runTextOf(label);
5481
- const leadStyle = matchRunStyle(label);
5482
- if (runs.length === 1)
5483
- return runPatternOf(leadStyle);
5484
- // The body is the run the label INTRODUCES, not the longest run in the
5485
- // block: a later emphasised phrase is part of the prose, and sampling it
5486
- // would stamp its emphasis across the whole new block.
5487
- const bodyStyle = matchRunStyle(runs[labelIndex + 1]);
5488
- const differs = ['bold', 'italic', 'underline', 'fontSize', 'fontFamily', 'color'].some((k) => leadStyle[k] !== bodyStyle[k]);
5489
- const shape = leadInShapeOf(blockText, labelText, labelIndex > 0 ? quote : undefined);
5490
- const isLabel = differs && shape != null && labelText.length > 0 && labelText.length <= LEAD_IN_MAX_CHARS;
5491
- return runPatternOf(bodyStyle, isLabel && shape ? { style: leadStyle, shape } : undefined);
5473
+ const rawPattern = runPatternFromRuns(sourceRuns, blockText, runTextOf);
5474
+ if (rawPattern.lead)
5475
+ return rawPattern;
5476
+ const runs = [];
5477
+ for (const run of sourceRuns) {
5478
+ const current = asRecord(run);
5479
+ const text = runTextOf(current);
5480
+ if (!current || !text)
5481
+ continue;
5482
+ const previous = runs.at(-1);
5483
+ const previousRange = asRecord(previous?.range);
5484
+ const currentRange = asRecord(current.range);
5485
+ const contiguous = previousRange || currentRange
5486
+ ? typeof previousRange?.start === 'number' &&
5487
+ typeof previousRange.end === 'number' &&
5488
+ typeof currentRange?.start === 'number' &&
5489
+ typeof currentRange?.end === 'number' &&
5490
+ previousRange.end === currentRange.start
5491
+ : true;
5492
+ const previousStyle = matchRunStyle(previous);
5493
+ const currentStyle = matchRunStyle(current);
5494
+ const equalStyle = sameMatchRunStyle(previousStyle, currentStyle);
5495
+ if (previous && contiguous && equalStyle) {
5496
+ previous.text = runTextOf(previous) + text;
5497
+ if (previousRange && currentRange)
5498
+ previous.range = { ...previousRange, end: currentRange.end };
5499
+ }
5500
+ else {
5501
+ runs.push({ ...current });
5502
+ }
5503
+ }
5504
+ const normalizedPattern = runPatternFromRuns(runs, blockText, runTextOf);
5505
+ return normalizedPattern.lead ? normalizedPattern : rawPattern;
5492
5506
  }
5493
5507
  }
5494
5508
  return null;
5495
5509
  }
5510
+ function runPatternFromRuns(runs, blockText, runTextOf) {
5511
+ const typedLead = typedPrefixLabelOf(runs, runTextOf);
5512
+ if (typedLead)
5513
+ return typedLead;
5514
+ // A quoted defined term keeps its opening quote in a run of its own —
5515
+ // `"` + **Deliverables** + `" means …` — so the label is the SECOND run
5516
+ // and the quotes stay unemphasised.
5517
+ const opener = runTextOf(runs[0]).trim();
5518
+ const quote = LEAD_IN_QUOTES.find((q) => opener === q);
5519
+ const labelIndex = quote && runs.length > 2 ? 1 : 0;
5520
+ const label = runs[labelIndex];
5521
+ const labelText = runTextOf(label);
5522
+ const leadStyle = matchRunStyle(label);
5523
+ if (runs.length === 1)
5524
+ return runPatternOf(leadStyle);
5525
+ // The body is the run the label INTRODUCES, not the longest run in the
5526
+ // block: a later emphasised phrase is part of the prose, and sampling it
5527
+ // would stamp its emphasis across the whole new block.
5528
+ const bodyStyle = matchRunStyle(runs[labelIndex + 1]);
5529
+ const differs = !sameMatchRunStyle(leadStyle, bodyStyle);
5530
+ const shape = leadInShapeOf(blockText, labelText, labelIndex > 0 ? quote : undefined);
5531
+ const isLabel = differs && shape != null && labelText.length > 0 && labelText.length <= LEAD_IN_MAX_CHARS;
5532
+ return runPatternOf(bodyStyle, isLabel && shape ? { style: leadStyle, shape } : undefined);
5533
+ }
5496
5534
  function typedPrefixLabelOf(runs, runTextOf) {
5497
5535
  const typed = typedMarkerPrefixOf(runs.map(runTextOf).join(''));
5498
5536
  if (!typed)
@@ -388,10 +388,6 @@ export interface ResolveActionPolicyOptions {
388
388
  ignoreUnknownActions?: boolean;
389
389
  }
390
390
  export declare function resolveActionPolicy(options?: ListAgentToolsOptions, resolveOptions?: ResolveActionPolicyOptions): ResolvedActionPolicy;
391
- /** @deprecated Use {@link resolveActionPolicy}; kept for callers that only need the union. */
392
- export declare function validateExclusions(options?: ListAgentToolsOptions): {
393
- excludedActions: Set<string>;
394
- };
395
391
  /**
396
392
  * Tools advertised to the LLM. Only PUBLIC_AGENT_TOOL_NAMES (currently
397
393
  * superdoc_inspect + superdoc_perform_action) are returned; the remaining
@@ -614,10 +614,6 @@ export function resolveActionPolicy(options, resolveOptions) {
614
614
  allowDirectActions,
615
615
  };
616
616
  }
617
- /** @deprecated Use {@link resolveActionPolicy}; kept for callers that only need the union. */
618
- export function validateExclusions(options) {
619
- return { excludedActions: resolveActionPolicy(options).excludedActions };
620
- }
621
617
  /**
622
618
  * Tools advertised to the LLM. Only PUBLIC_AGENT_TOOL_NAMES (currently
623
619
  * superdoc_inspect + superdoc_perform_action) are returned; the remaining