@superdoc/sdk 2.9.1-next.2 → 2.10.0-next.10

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 (57) hide show
  1. package/README.md +45 -0
  2. package/dist/action-primitives/doc-index.cjs +6 -4
  3. package/dist/action-primitives/doc-index.js +6 -4
  4. package/dist/action-primitives/engine.cjs +6 -2
  5. package/dist/action-primitives/engine.js +6 -2
  6. package/dist/action-primitives/receipt.d.ts +4 -0
  7. package/dist/action-primitives/tools/structure-insert.d.ts +1 -1
  8. package/dist/agent/actions.cjs +384 -373
  9. package/dist/agent/actions.d.ts +5 -0
  10. package/dist/agent/actions.js +385 -374
  11. package/dist/agent/catalog.cjs +15 -0
  12. package/dist/agent/catalog.js +15 -0
  13. package/dist/agent/doc-snapshot.cjs +205 -86
  14. package/dist/agent/doc-snapshot.d.ts +11 -0
  15. package/dist/agent/doc-snapshot.js +203 -86
  16. package/dist/agent/execution-context.cjs +385 -0
  17. package/dist/agent/execution-context.d.ts +97 -0
  18. package/dist/agent/execution-context.js +376 -0
  19. package/dist/agent/runtime.cjs +123 -117
  20. package/dist/agent/runtime.d.ts +3 -0
  21. package/dist/agent/runtime.js +124 -118
  22. package/dist/agent/v2-preset-compat.cjs +5 -1
  23. package/dist/agent/v2-preset-compat.js +4 -1
  24. package/dist/embedded-tools.generated.cjs +5 -5
  25. package/dist/embedded-tools.generated.js +5 -5
  26. package/dist/generated/client.cjs +2 -0
  27. package/dist/generated/client.d.ts +85 -0
  28. package/dist/generated/client.js +2 -0
  29. package/dist/generated/contract.cjs +1796 -1297
  30. package/dist/generated/contract.js +1797 -1297
  31. package/dist/index.cjs +23 -0
  32. package/dist/index.d.ts +7 -2
  33. package/dist/index.js +23 -0
  34. package/dist/presets/core.cjs +1 -1
  35. package/dist/presets/core.js +1 -1
  36. package/dist/runtime/document-evidence.cjs +40 -0
  37. package/dist/runtime/document-evidence.d.ts +13 -0
  38. package/dist/runtime/document-evidence.js +30 -0
  39. package/dist/runtime/document-rpc.cjs +27 -0
  40. package/dist/runtime/document-rpc.d.ts +2 -0
  41. package/dist/runtime/document-rpc.js +25 -0
  42. package/dist/runtime/host.cjs +65 -4
  43. package/dist/runtime/host.d.ts +3 -0
  44. package/dist/runtime/host.js +66 -5
  45. package/dist/runtime/process.cjs +38 -0
  46. package/dist/runtime/process.d.ts +16 -0
  47. package/dist/runtime/process.js +38 -0
  48. package/dist/runtime/sdk-version.generated.cjs +1 -1
  49. package/dist/runtime/sdk-version.generated.d.ts +1 -1
  50. package/dist/runtime/sdk-version.generated.js +1 -1
  51. package/package.json +10 -8
  52. package/tools/catalog.json +89 -0
  53. package/tools/tools-policy.json +1 -1
  54. package/tools/tools.anthropic.json +89 -0
  55. package/tools/tools.generic.json +89 -0
  56. package/tools/tools.openai.json +89 -0
  57. package/tools/tools.vercel.json +89 -0
@@ -0,0 +1,376 @@
1
+ import { getDocumentRevisionReader, supportsDocumentFacts, forwardDocumentEvidence, getMutationRevision, } from '../runtime/document-evidence.js';
2
+ import { SuperDocCliError } from '../runtime/errors.js';
3
+ import { buildDocumentSnapshot, buildMutationSnapshot, resolveSnapshotSelector, } from './doc-snapshot.js';
4
+ import { getOperationCatalogEntry } from './operation-catalog.js';
5
+ export function supportsLocalSelector(selector) {
6
+ return (selector.kind === 'nodeId' ||
7
+ selector.kind === 'textSearch' ||
8
+ selector.kind === 'placement' ||
9
+ (selector.kind === 'ordinal' &&
10
+ ['blockOrdinal', 'headingOrdinal', 'tableOrdinal'].includes(selector.ordinalKind)) ||
11
+ (selector.kind === 'relative' && supportsLocalSelector(selector.target)));
12
+ }
13
+ function localCheck(check) {
14
+ return (['revision-changed', 'revision-unchanged', 'block-text-contains', 'block-text-equals', 'table-shape'].includes(check.kind) ||
15
+ (check.kind === 'block-count-delta' && ['paragraph', 'heading', 'listItem', 'table'].includes(check.nodeType)));
16
+ }
17
+ export async function readPublicRevision(doc) {
18
+ const revision = await getDocumentRevisionReader(doc)?.();
19
+ if (revision !== undefined)
20
+ return revision;
21
+ const page = await doc.blocks.list({ offset: Number.MAX_SAFE_INTEGER, limit: 1 });
22
+ if (!page.revision || page.revision === 'unknown')
23
+ throw new SuperDocCliError('Required document revision is unavailable.', { code: 'EVIDENCE_UNAVAILABLE' });
24
+ return page.revision;
25
+ }
26
+ export class DocumentFacts {
27
+ doc;
28
+ revision;
29
+ complete;
30
+ summaryCounts;
31
+ blocks = new Map();
32
+ counts = new Map();
33
+ selections = new Map();
34
+ constructor(doc, revision, complete, summaryCounts = complete?.counts) {
35
+ this.doc = doc;
36
+ this.revision = revision;
37
+ this.complete = complete;
38
+ this.summaryCounts = summaryCounts;
39
+ }
40
+ get summary() {
41
+ return { revision: this.revision, ...(this.summaryCounts ? { counts: this.summaryCounts } : {}) };
42
+ }
43
+ async fence() {
44
+ if ((await readPublicRevision(this.doc)) !== this.revision)
45
+ throw new SuperDocCliError('Document changed while acquiring execution evidence.', { code: 'REVISION_CONFLICT' });
46
+ }
47
+ async page(input) {
48
+ const result = await this.doc.blocks.list({ ...input, includeText: true });
49
+ if (result.revision !== this.revision)
50
+ throw new SuperDocCliError('Document changed while resolving a target.', { code: 'REVISION_CONFLICT' });
51
+ const blocks = result.blocks.map((block) => ({
52
+ ...block,
53
+ ordinal: block.ordinal + 1,
54
+ text: block.text ?? '',
55
+ textPreview: block.textPreview ?? null,
56
+ styleId: block.styleId ?? null,
57
+ }));
58
+ for (const block of blocks)
59
+ this.blocks.set(block.nodeId, block);
60
+ return blocks;
61
+ }
62
+ async count(nodeType) {
63
+ const key = nodeType ?? '*';
64
+ if (this.counts.has(key))
65
+ return this.counts.get(key);
66
+ if (this.complete)
67
+ return nodeType
68
+ ? this.complete.blocks.filter((block) => block.nodeType === nodeType).length
69
+ : this.complete.blocks.length;
70
+ const result = await this.doc.blocks.list({
71
+ offset: Number.MAX_SAFE_INTEGER,
72
+ limit: 1,
73
+ ...(nodeType ? { nodeTypes: [nodeType] } : {}),
74
+ });
75
+ if (result.revision !== this.revision || !Number.isSafeInteger(result.total))
76
+ throw new SuperDocCliError('Required count is unavailable at the execution revision.', {
77
+ code: 'EVIDENCE_UNAVAILABLE',
78
+ });
79
+ this.counts.set(key, result.total);
80
+ return result.total;
81
+ }
82
+ async block(nodeId) {
83
+ if (this.complete)
84
+ return this.complete.blocks.find((block) => block.nodeId === nodeId);
85
+ if (!this.blocks.has(nodeId))
86
+ await this.page({ nodeIds: [nodeId], limit: 1 });
87
+ return this.blocks.get(nodeId);
88
+ }
89
+ async select(selector) {
90
+ if (this.complete)
91
+ return resolveSnapshotSelector(this.complete, selector);
92
+ const key = JSON.stringify(selector);
93
+ if (this.selections.has(key))
94
+ return this.selections.get(key);
95
+ let blocks = [];
96
+ if (selector.kind === 'nodeId') {
97
+ const block = await this.block(selector.nodeId);
98
+ if (block)
99
+ blocks = [block];
100
+ }
101
+ else if (selector.kind === 'textSearch') {
102
+ const terms = selector.terms.filter((term) => term.trim());
103
+ if (terms.length && selector.nodeTypes?.length !== 0)
104
+ blocks = await this.page({
105
+ textSearch: { terms: [...terms], match: selector.match, caseSensitive: selector.caseSensitive },
106
+ nodeTypes: [...(selector.nodeTypes ?? ['paragraph', 'heading', 'listItem'])],
107
+ offset: (selector.occurrence ?? 1) - 1,
108
+ limit: 1,
109
+ });
110
+ }
111
+ else if (selector.kind === 'placement') {
112
+ const offset = selector.at === 'document_start' ? 0 : (await this.count()) - 1;
113
+ if (offset >= 0)
114
+ blocks = await this.page({ offset, limit: 1 });
115
+ }
116
+ else if (selector.kind === 'ordinal') {
117
+ blocks = await this.page({
118
+ offset: selector.value - 1,
119
+ limit: 1,
120
+ ...(selector.ordinalKind === 'headingOrdinal'
121
+ ? { nodeTypes: ['heading'] }
122
+ : selector.ordinalKind === 'tableOrdinal'
123
+ ? { nodeTypes: ['table'] }
124
+ : {}),
125
+ });
126
+ }
127
+ else if (selector.kind === 'relative') {
128
+ for (const id of await this.select(selector.target)) {
129
+ const target = await this.block(id);
130
+ const offset = target ? target.ordinal - 1 + (selector.position === 'before' ? -1 : 1) : -1;
131
+ if (offset >= 0)
132
+ blocks.push(...(await this.page({ offset, limit: 1 })));
133
+ }
134
+ }
135
+ else
136
+ throw new SuperDocCliError('Selector requires complete evidence.', { code: 'EVIDENCE_UNAVAILABLE' });
137
+ const ids = [...new Set(blocks.map((block) => block.nodeId))];
138
+ this.selections.set(key, ids);
139
+ return ids;
140
+ }
141
+ async target(selector) {
142
+ const ids = await this.select(selector);
143
+ if (this.complete && selector.kind === 'tableCell' && ids.length === 1) {
144
+ const cell = this.complete.tables.flatMap((table) => table.cells).find((cell) => cell.nodeId === ids[0]);
145
+ if (cell?.nodeId)
146
+ return {
147
+ nodeId: cell.nodeId,
148
+ nodeType: 'paragraph',
149
+ text: cell.text,
150
+ textPreview: cell.text,
151
+ styleId: null,
152
+ ordinal: 0,
153
+ };
154
+ }
155
+ return ids.length === 1 ? this.block(ids[0]) : undefined;
156
+ }
157
+ async tableShape(nodeId) {
158
+ if (this.complete)
159
+ return this.complete.tables.find((table) => table.nodeId === nodeId);
160
+ return this.doc.tables.get({ nodeId });
161
+ }
162
+ async completeIndex(reason, acquire) {
163
+ const value = await acquire();
164
+ await this.fence();
165
+ return { value, coverage: 'complete', reason };
166
+ }
167
+ async prepare(checks) {
168
+ for (const check of checks)
169
+ if (check.kind === 'block-count-delta')
170
+ await this.count(check.nodeType);
171
+ }
172
+ }
173
+ export class ExecutionContext {
174
+ source;
175
+ requirements;
176
+ document;
177
+ executedOperations = [];
178
+ selectedTargets = [];
179
+ pre;
180
+ post;
181
+ fallbackReason;
182
+ currentRevision;
183
+ legacyRevisionOnly = false;
184
+ constructor(source, requirements = {}) {
185
+ this.source = source;
186
+ this.requirements = requirements;
187
+ if (requirements.evidence !== undefined && !['required', 'full'].includes(requirements.evidence))
188
+ throw new SuperDocCliError('evidence must be required or full.', { code: 'INVALID_ARGUMENT' });
189
+ const wrap = (value, path) => new Proxy(value, {
190
+ get: (target, property, receiver) => {
191
+ const member = Reflect.get(target, property, receiver);
192
+ if (typeof property !== 'string')
193
+ return member;
194
+ const memberPath = [...path, property];
195
+ const operationId = `doc.${memberPath.join('.')}`;
196
+ if (typeof member === 'function' && getOperationCatalogEntry(operationId)?.isMutating) {
197
+ return async (args = {}, options) => {
198
+ if (operationId === 'doc.format.apply' &&
199
+ supportsDocumentFacts(this.source) &&
200
+ typeof args.blockId === 'string') {
201
+ const { blockId, start, end, ...rest } = args;
202
+ args = {
203
+ ...rest,
204
+ target: {
205
+ kind: 'selection',
206
+ start: { kind: 'text', blockId, offset: start },
207
+ end: { kind: 'text', blockId, offset: end },
208
+ },
209
+ };
210
+ }
211
+ if (this.legacyRevisionOnly && this.executedOperations.length)
212
+ throw new SuperDocCliError('This host cannot bind revisions across multiple writes.', {
213
+ code: 'EVIDENCE_UNAVAILABLE',
214
+ });
215
+ const guarded = (supportsDocumentFacts(this.source) || this.legacyRevisionOnly) &&
216
+ !['doc.history.undo', 'doc.history.redo', 'doc.plan.execute'].includes(operationId);
217
+ const revision = guarded ? (this.currentRevision ?? (await readPublicRevision(this.source))) : undefined;
218
+ const result = await Reflect.apply(member, target, guarded
219
+ ? [
220
+ { ...args, expectedRevision: args.expectedRevision ?? revision },
221
+ { ...options, expectedRevision: options?.expectedRevision ?? revision },
222
+ ]
223
+ : [args, options]);
224
+ this.executedOperations.push({ operationId, result });
225
+ if (result?.success === false)
226
+ throw new SuperDocCliError('Document operation did not apply.', {
227
+ code: result.failure?.code ?? 'APPLY_FAILED',
228
+ details: result,
229
+ });
230
+ if (supportsDocumentFacts(this.source)) {
231
+ const applied = getMutationRevision(result);
232
+ if (!applied ||
233
+ applied.before !== this.currentRevision ||
234
+ !applied.after ||
235
+ applied.after === 'unknown')
236
+ throw new SuperDocCliError('Committed operation revision is unavailable or changed.', {
237
+ code: 'EVIDENCE_UNAVAILABLE',
238
+ });
239
+ this.currentRevision = applied.after;
240
+ }
241
+ return result;
242
+ };
243
+ }
244
+ if (member && typeof member === 'object')
245
+ return wrap(member, memberPath);
246
+ return member;
247
+ },
248
+ });
249
+ this.document = wrap(source, []);
250
+ forwardDocumentEvidence(source, this.document);
251
+ }
252
+ async start() {
253
+ const { evidence, selectors = [], checks = [], completeReason } = this.requirements;
254
+ const compactRevision = this.requirements.revisionOnly && evidence !== 'full' && !completeReason
255
+ ? await getDocumentRevisionReader(this.source)?.()
256
+ : undefined;
257
+ this.legacyRevisionOnly = compactRevision !== undefined && !supportsDocumentFacts(this.source);
258
+ this.fallbackReason =
259
+ completeReason ??
260
+ (!supportsDocumentFacts(this.source) && !this.legacyRevisionOnly
261
+ ? 'host lacks typed execution facts'
262
+ : selectors.some((selector) => !supportsLocalSelector(selector))
263
+ ? 'selector requires complete index'
264
+ : checks.some((check) => !localCheck(check))
265
+ ? 'verification requires complete evidence'
266
+ : undefined);
267
+ const complete = evidence === 'full' || this.fallbackReason ? await buildMutationSnapshot(this.source) : undefined;
268
+ if (complete && (complete.revision === 'unknown' || complete.diagnostics.some((item) => item.section === 'info')))
269
+ throw new SuperDocCliError('Required complete evidence is unavailable.', { code: 'EVIDENCE_UNAVAILABLE' });
270
+ this.pre = new DocumentFacts(this.source, complete?.revision ?? compactRevision ?? (await readPublicRevision(this.source)), complete);
271
+ this.currentRevision = this.pre.revision;
272
+ await this.pre.prepare(checks);
273
+ return this.pre;
274
+ }
275
+ async finishRevision() {
276
+ if (this.requirements.evidence === 'full')
277
+ return this.finish();
278
+ const summary = this.pre?.complete ? await buildDocumentSnapshot(this.source, { countsOnly: true }) : undefined;
279
+ if (summary && (summary.revision === 'unknown' || summary.diagnostics.some((item) => item.section === 'info')))
280
+ throw new SuperDocCliError('Required summary is unavailable.', { code: 'EVIDENCE_UNAVAILABLE' });
281
+ const revision = summary?.revision ?? (await readPublicRevision(this.source));
282
+ this.assertExecutionRevision(revision);
283
+ this.post = new DocumentFacts(this.source, revision, undefined, summary?.counts);
284
+ this.currentRevision = this.post.revision;
285
+ return this.post;
286
+ }
287
+ async finish() {
288
+ const complete = this.pre?.complete ? await buildMutationSnapshot(this.source) : undefined;
289
+ if (complete && (complete.revision === 'unknown' || complete.diagnostics.some((item) => item.section === 'info')))
290
+ throw new SuperDocCliError('Required complete evidence is unavailable.', { code: 'EVIDENCE_UNAVAILABLE' });
291
+ const revision = complete?.revision ?? (await readPublicRevision(this.source));
292
+ this.assertExecutionRevision(revision);
293
+ this.post = new DocumentFacts(this.source, revision, complete);
294
+ this.currentRevision = this.post.revision;
295
+ return this.post;
296
+ }
297
+ assertExecutionRevision(revision) {
298
+ if (supportsDocumentFacts(this.source) && this.currentRevision !== revision)
299
+ throw new SuperDocCliError('Document changed outside this execution.', { code: 'REVISION_CONFLICT' });
300
+ }
301
+ receipt(intent, verification, extra = {}) {
302
+ return {
303
+ status: verification.every((check) => check.passed) ? 'ok' : 'failed',
304
+ intent,
305
+ preSnapshot: this.pre?.summary,
306
+ postSnapshot: this.post?.summary,
307
+ selectedTargets: this.selectedTargets,
308
+ executedOperations: this.executedOperations,
309
+ verification,
310
+ ...(this.fallbackReason ? { evidenceFallback: this.fallbackReason } : {}),
311
+ ...extra,
312
+ };
313
+ }
314
+ failure(intent, error) {
315
+ const err = error;
316
+ return this.receipt(intent, [], {
317
+ status: err?.code === 'AMBIGUOUS_SELECTOR' ? 'aborted' : 'failed',
318
+ errors: [
319
+ { code: err?.code ?? 'ACTION_FAILED', message: err?.message ?? String(error), recovery: { kind: 'reinspect' } },
320
+ ],
321
+ });
322
+ }
323
+ }
324
+ export async function evaluateFactChecks(pre, post, checks) {
325
+ const results = [];
326
+ for (const check of checks) {
327
+ if ([
328
+ 'revision-changed',
329
+ 'revision-unchanged',
330
+ 'block-count-delta',
331
+ 'comment-count-delta',
332
+ 'tracked-change-count-delta',
333
+ ].includes(check.kind) &&
334
+ !pre) {
335
+ results.push({ check, passed: false, detail: `${check.kind} requires a baseline snapshot` });
336
+ continue;
337
+ }
338
+ if (check.kind === 'revision-changed' || check.kind === 'revision-unchanged')
339
+ results.push({
340
+ check,
341
+ passed: check.kind === 'revision-changed' ? pre.revision !== post.revision : pre.revision === post.revision,
342
+ detail: `pre=${pre.revision} post=${post.revision}`,
343
+ });
344
+ else if (check.kind === 'block-count-delta') {
345
+ const before = await pre.count(check.nodeType), after = await post.count(check.nodeType);
346
+ results.push({
347
+ check,
348
+ passed: after - before === check.delta,
349
+ detail: `pre=${before} post=${after} delta=${after - before}`,
350
+ });
351
+ }
352
+ else if (check.kind === 'block-text-contains' || check.kind === 'block-text-equals') {
353
+ const block = await post.block(check.nodeId);
354
+ results.push({
355
+ check,
356
+ passed: !!block && (check.kind === 'block-text-equals' ? block.text === check.text : block.text.includes(check.text)),
357
+ });
358
+ }
359
+ else if (check.kind === 'table-shape') {
360
+ const table = await post.tableShape(check.nodeId);
361
+ results.push({ check, passed: !!table && table.rows === check.rows && table.columns === check.columns });
362
+ }
363
+ else if (check.kind === 'comment-count-delta' || check.kind === 'tracked-change-count-delta') {
364
+ const key = check.kind === 'comment-count-delta' ? 'comments' : 'trackedChanges';
365
+ const before = pre?.complete?.counts[key], after = post.complete?.counts[key];
366
+ results.push({
367
+ check,
368
+ passed: before !== undefined && after !== undefined && after - before === check.delta,
369
+ detail: `pre=${before} post=${after}`,
370
+ });
371
+ }
372
+ else
373
+ results.push({ check, passed: false, detail: 'Check requires domain-specific evidence.' });
374
+ }
375
+ return results;
376
+ }
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var executionContext = require('./execution-context.cjs');
3
4
  var contract = require('../generated/contract.cjs');
4
5
  var errors = require('../runtime/errors.cjs');
5
6
  var ir = require('./ir.cjs');
@@ -227,8 +228,8 @@ function computeDeltaChecks(pre, post, checks, saveReopen) {
227
228
  else if (check.kind === 'comment-count-delta') {
228
229
  results.push({
229
230
  check,
230
- passed: post.comments.length - pre.comments.length === check.delta,
231
- detail: `pre=${pre.comments.length} post=${post.comments.length}`,
231
+ passed: post.counts.comments - pre.counts.comments === check.delta,
232
+ detail: `pre=${pre.counts.comments} post=${post.counts.comments}`,
232
233
  });
233
234
  }
234
235
  else if (check.kind === 'tracked-change-count-delta') {
@@ -313,7 +314,7 @@ async function trySaveReopen(doc, checks) {
313
314
  await saveAny.call(doc, {});
314
315
  // Rebuild a fresh snapshot after save. Host-level true reopen still needs
315
316
  // a new document handle, which this runtime cannot force on its own.
316
- const fresh = await docSnapshot.buildDocumentSnapshot(doc);
317
+ const fresh = await docSnapshot.buildMutationSnapshot(doc);
317
318
  for (const check of checks) {
318
319
  if (check.kind === 'save-reopen-text-contains') {
319
320
  const found = fresh.blocks.some((b) => b.text.includes(check.text));
@@ -336,124 +337,150 @@ async function trySaveReopen(doc, checks) {
336
337
  function verificationNeedsSaveReopen(checks) {
337
338
  return checks.some((check) => check.kind === 'document-saves-cleanly' || check.kind === 'save-reopen-text-contains');
338
339
  }
340
+ function revisionOnlyParagraphStep(plan) {
341
+ if (plan.atomic || plan.preconditions || plan.postconditions || plan.expectedDiff || plan.steps.length !== 2) {
342
+ return undefined;
343
+ }
344
+ const [apply, verify] = plan.steps;
345
+ if (apply?.kind !== 'apply' ||
346
+ apply.operationId !== 'doc.create.paragraph' ||
347
+ apply.atomic ||
348
+ verify?.kind !== 'verify' ||
349
+ verify.saveReopen ||
350
+ verify.checks.length === 0 ||
351
+ !verify.checks.every((check) => check.kind === 'revision-changed' || check.kind === 'revision-unchanged')) {
352
+ return undefined;
353
+ }
354
+ // Only literal body inputs are eligible. Binding tokens, story targets and
355
+ // additional operation options must retain the complete evidence path.
356
+ if (Object.keys(apply.args).some((key) => key !== 'text' && key !== 'at'))
357
+ return undefined;
358
+ if (apply.args.text !== undefined && typeof apply.args.text !== 'string')
359
+ return undefined;
360
+ const at = apply.args.at;
361
+ if (at !== undefined) {
362
+ if (!isRecord(at))
363
+ return undefined;
364
+ if (at.kind === 'documentStart' || at.kind === 'documentEnd') {
365
+ if (Object.keys(at).some((key) => key !== 'kind'))
366
+ return undefined;
367
+ }
368
+ else if (at.kind === 'before' || at.kind === 'after') {
369
+ if (Object.keys(at).some((key) => key !== 'kind' && key !== 'target') ||
370
+ !isRecord(at.target) ||
371
+ at.target.kind !== 'block' ||
372
+ typeof at.target.nodeId !== 'string' ||
373
+ typeof at.target.nodeType !== 'string' ||
374
+ Object.keys(at.target).some((key) => !['kind', 'nodeId', 'nodeType'].includes(key))) {
375
+ return undefined;
376
+ }
377
+ }
378
+ else
379
+ return undefined;
380
+ }
381
+ return apply;
382
+ }
339
383
  async function agentApply(doc, args) {
340
384
  const plan = args.plan;
341
385
  const validation = ir.validatePlan(plan);
342
- if (!validation.ok) {
386
+ if (!validation.ok)
343
387
  return {
344
388
  status: 'failed',
345
389
  intent: plan.intent,
346
- preSnapshot: { revision: 'unknown', counts: emptyCounts() },
347
390
  selectedTargets: [],
348
391
  executedOperations: [],
349
392
  verification: [],
350
- errors: validation.errors.map((e) => ({ code: e.code, message: e.message })),
393
+ errors: validation.errors.map((error) => ({ code: error.code, message: error.message })),
351
394
  };
352
- }
353
- const preSnapshot = await docSnapshot.buildDocumentSnapshot(doc);
354
- const selectedTargets = [];
355
- const executedOperations = [];
395
+ const verifyStep = plan.steps.find((step) => step.kind === 'verify');
396
+ const checks = verifyStep?.kind === 'verify' ? verifyStep.checks : [];
397
+ let written = false;
398
+ const baselineAfterWrite = plan.steps.some((step) => {
399
+ if (step.kind === 'apply')
400
+ written = true;
401
+ return written && step.kind === 'select' && step.selector.kind !== 'ref';
402
+ });
403
+ const context = new executionContext.ExecutionContext(doc, {
404
+ evidence: args.evidence,
405
+ revisionOnly: !!revisionOnlyParagraphStep(plan),
406
+ checks,
407
+ selectors: plan.steps.flatMap((step) => step.kind === 'select' && step.selector.kind !== 'ref' ? [step.selector] : []),
408
+ completeReason: plan.atomic ||
409
+ plan.preconditions ||
410
+ plan.postconditions ||
411
+ plan.expectedDiff ||
412
+ plan.steps.some((step) => step.kind === 'inspect') ||
413
+ plan.steps.filter((step) => step.kind === 'apply').length > 1
414
+ ? 'plan requires complete baseline evidence'
415
+ : baselineAfterWrite
416
+ ? 'plan selects baseline targets after a write'
417
+ : planTouchesRiskyDomain(plan)
418
+ ? 'plan requires document-wide save/reopen evidence'
419
+ : plan.steps.some((step) => step.kind === 'apply' &&
420
+ ['doc.history.undo', 'doc.history.redo', 'doc.plan.execute'].includes(step.operationId))
421
+ ? 'operation requires complete execution evidence'
422
+ : undefined,
423
+ });
356
424
  const bindings = new Map();
357
425
  try {
426
+ const pre = await context.start();
358
427
  for (const step of plan.steps) {
359
428
  if (step.kind === 'select') {
360
- const matched = resolveSelectorWithBindings(preSnapshot, step.selector, bindings);
361
- if (step.requireUnique && matched.length !== 1) {
429
+ const matched = pre.complete
430
+ ? resolveSelectorWithBindings(pre.complete, step.selector, bindings)
431
+ : step.selector.kind === 'ref'
432
+ ? extractBoundNodeIds(resolveBindingRef(bindings, step.selector.ref))
433
+ : await pre.select(step.selector);
434
+ if (step.requireUnique && matched.length !== 1)
362
435
  throw new docSnapshot.AmbiguousSelectorError(`Selector did not resolve uniquely (matched ${matched.length}).`, matched.map((nodeId) => ({ nodeId, description: nodeId })));
363
- }
364
- selectedTargets.push({ selector: step.selector, matched });
365
- if (step.bind) {
436
+ context.selectedTargets.push({ selector: step.selector, matched });
437
+ if (step.bind)
366
438
  bindings.set(step.bind, matched.length === 1 ? matched[0] : [...matched]);
367
- }
368
- continue;
369
439
  }
370
- if (step.kind === 'inspect') {
371
- const inspectArgs = ensureClean(resolveBindingTokens(step.args, bindings));
372
- const method = resolveDocMethod(doc, step.operationId);
373
- const result = await method(inspectArgs);
374
- if (step.bind)
440
+ else if (step.kind === 'inspect' || step.kind === 'apply') {
441
+ const input = ensureClean(resolveBindingTokens(step.args, bindings));
442
+ const method = resolveDocMethod(context.document, step.operationId);
443
+ const result = await method(step.kind === 'apply' && step.changeMode && operationCatalog.getOperationCatalogEntry(step.operationId)?.supportsChangeMode
444
+ ? { ...input, changeMode: step.changeMode }
445
+ : input);
446
+ if (step.kind === 'inspect' && step.bind)
375
447
  bindings.set(step.bind, result);
376
- continue;
377
- }
378
- if (step.kind === 'apply') {
379
- ensureKnownOperation(step.operationId);
380
- const applyArgs = ensureClean(resolveBindingTokens(step.args, bindings));
381
- const method = resolveDocMethod(doc, step.operationId);
382
- const argsWithMode = step.changeMode != null && operationCatalog.getOperationCatalogEntry(step.operationId)?.supportsChangeMode
383
- ? { ...applyArgs, changeMode: step.changeMode }
384
- : applyArgs;
385
- const result = await method(argsWithMode);
386
- executedOperations.push({ operationId: step.operationId, rationale: step.rationale, result });
448
+ if (step.kind === 'apply' && context.executedOperations.length)
449
+ context.executedOperations.at(-1).rationale = step.rationale;
387
450
  }
388
451
  }
452
+ const post = await context.finish();
453
+ const saveReopen = (verifyStep?.kind === 'verify' && (verifyStep.saveReopen || verificationNeedsSaveReopen(checks))) ||
454
+ planTouchesRiskyDomain(plan)
455
+ ? await trySaveReopen(doc, checks)
456
+ : undefined;
457
+ const verification = pre.complete && post.complete
458
+ ? computeDeltaChecks(pre.complete, post.complete, checks, saveReopen)
459
+ : await executionContext.evaluateFactChecks(pre, post, checks);
460
+ if (!post.complete && checks.some((check) => !['revision-changed', 'revision-unchanged'].includes(check.kind)))
461
+ await post.fence();
462
+ return context.receipt(plan.intent, verification, { saveReopen });
389
463
  }
390
- catch (err) {
391
- if (err instanceof docSnapshot.AmbiguousSelectorError) {
392
- return {
393
- status: 'aborted',
394
- intent: plan.intent,
395
- preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
396
- selectedTargets,
397
- executedOperations,
398
- verification: [],
399
- errors: [
400
- {
401
- code: err.code,
402
- message: err.message,
403
- },
404
- ],
405
- };
406
- }
407
- const message = err instanceof Error ? err.message : String(err);
408
- return {
409
- status: 'failed',
410
- intent: plan.intent,
411
- preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
412
- selectedTargets,
413
- executedOperations,
414
- verification: [],
415
- errors: [{ code: 'APPLY_FAILED', message }],
416
- };
464
+ catch (error) {
465
+ return context.failure(plan.intent, error);
417
466
  }
418
- const postSnapshot = await docSnapshot.buildDocumentSnapshot(doc);
419
- const verifyStep = plan.steps.find((s) => s.kind === 'verify');
420
- let saveReopen;
421
- const shouldSaveReopen = (verifyStep?.kind === 'verify' && (verifyStep.saveReopen || verificationNeedsSaveReopen(verifyStep.checks))) ||
422
- planTouchesRiskyDomain(plan);
423
- if (shouldSaveReopen) {
424
- saveReopen = await trySaveReopen(doc, verifyStep?.kind === 'verify' ? verifyStep.checks : []);
425
- }
426
- const verification = verifyStep?.kind === 'verify' ? computeDeltaChecks(preSnapshot, postSnapshot, verifyStep.checks, saveReopen) : [];
427
- const allVerified = verification.every((v) => v.passed);
428
- return {
429
- status: allVerified ? 'ok' : 'failed',
430
- intent: plan.intent,
431
- preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
432
- postSnapshot: { revision: postSnapshot.revision, counts: postSnapshot.counts },
433
- selectedTargets,
434
- executedOperations,
435
- verification,
436
- saveReopen,
437
- };
438
467
  }
439
468
  async function agentVerify(doc, args) {
440
- const snapshot = await docSnapshot.buildDocumentSnapshot(doc);
441
- let saveReopen;
442
- if (args.saveReopen || verificationNeedsSaveReopen(args.checks)) {
443
- saveReopen = await trySaveReopen(doc, args.checks);
469
+ const context = new executionContext.ExecutionContext(doc, { evidence: args.evidence, checks: args.checks });
470
+ try {
471
+ const current = await context.start();
472
+ context.post = current;
473
+ const saveReopen = args.saveReopen || verificationNeedsSaveReopen(args.checks) ? await trySaveReopen(doc, args.checks) : undefined;
474
+ const verification = current.complete
475
+ ? computeCurrentChecks(current.complete, args.checks, saveReopen)
476
+ : await executionContext.evaluateFactChecks(undefined, current, args.checks);
477
+ if (!current.complete)
478
+ await current.fence();
479
+ return context.receipt('verify', verification, { saveReopen });
480
+ }
481
+ catch (error) {
482
+ return context.failure('verify', error);
444
483
  }
445
- const verification = computeCurrentChecks(snapshot, args.checks, saveReopen);
446
- const allPassed = verification.every((v) => v.passed);
447
- return {
448
- status: allPassed ? 'ok' : 'failed',
449
- intent: 'verify',
450
- preSnapshot: { revision: snapshot.revision, counts: snapshot.counts },
451
- postSnapshot: { revision: snapshot.revision, counts: snapshot.counts },
452
- selectedTargets: [],
453
- executedOperations: [],
454
- verification,
455
- saveReopen,
456
- };
457
484
  }
458
485
  /**
459
486
  * Controlled escape hatch — dispatches a single generated operation by id.
@@ -481,27 +508,6 @@ async function agentOperation(doc, args) {
481
508
  const method = resolveDocMethod(doc, args.operationId);
482
509
  return method(callArgs);
483
510
  }
484
- function emptyCounts() {
485
- return {
486
- blocks: 0,
487
- paragraphs: 0,
488
- headings: 0,
489
- tables: 0,
490
- lists: 0,
491
- images: 0,
492
- comments: 0,
493
- trackedChanges: 0,
494
- sections: 0,
495
- fields: 0,
496
- hyperlinks: 0,
497
- bookmarks: 0,
498
- contentControls: 0,
499
- permissionRanges: 0,
500
- styles: 0,
501
- headers: 0,
502
- footers: 0,
503
- };
504
- }
505
511
 
506
512
  exports.agentApply = agentApply;
507
513
  exports.agentInspect = agentInspect;