@superdoc/sdk 2.10.0-next.7 → 2.10.0-next.9

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 (53) 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 +203 -223
  9. package/dist/agent/actions.d.ts +5 -0
  10. package/dist/agent/actions.js +203 -223
  11. package/dist/agent/catalog.cjs +15 -0
  12. package/dist/agent/catalog.js +15 -0
  13. package/dist/agent/doc-snapshot.cjs +22 -46
  14. package/dist/agent/doc-snapshot.js +22 -46
  15. package/dist/agent/execution-context.cjs +385 -0
  16. package/dist/agent/execution-context.d.ts +97 -0
  17. package/dist/agent/execution-context.js +376 -0
  18. package/dist/agent/runtime.cjs +116 -158
  19. package/dist/agent/runtime.d.ts +3 -0
  20. package/dist/agent/runtime.js +117 -159
  21. package/dist/agent/v2-preset-compat.cjs +5 -1
  22. package/dist/agent/v2-preset-compat.js +4 -1
  23. package/dist/embedded-tools.generated.cjs +5 -5
  24. package/dist/embedded-tools.generated.js +5 -5
  25. package/dist/generated/client.cjs +2 -0
  26. package/dist/generated/client.d.ts +47 -0
  27. package/dist/generated/client.js +2 -0
  28. package/dist/generated/contract.cjs +1605 -1297
  29. package/dist/generated/contract.js +1605 -1297
  30. package/dist/index.cjs +11 -0
  31. package/dist/index.d.ts +3 -1
  32. package/dist/index.js +11 -0
  33. package/dist/presets/core.cjs +1 -1
  34. package/dist/presets/core.js +1 -1
  35. package/dist/runtime/document-evidence.cjs +40 -0
  36. package/dist/runtime/document-evidence.d.ts +13 -0
  37. package/dist/runtime/document-evidence.js +30 -0
  38. package/dist/runtime/host.cjs +30 -2
  39. package/dist/runtime/host.d.ts +2 -0
  40. package/dist/runtime/host.js +30 -2
  41. package/dist/runtime/process.cjs +6 -0
  42. package/dist/runtime/process.d.ts +2 -0
  43. package/dist/runtime/process.js +6 -0
  44. package/dist/runtime/sdk-version.generated.cjs +1 -1
  45. package/dist/runtime/sdk-version.generated.d.ts +1 -1
  46. package/dist/runtime/sdk-version.generated.js +1 -1
  47. package/package.json +10 -9
  48. package/tools/catalog.json +35 -0
  49. package/tools/tools-policy.json +1 -1
  50. package/tools/tools.anthropic.json +35 -0
  51. package/tools/tools.generic.json +35 -0
  52. package/tools/tools.openai.json +35 -0
  53. package/tools/tools.vercel.json +35 -0
@@ -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');
@@ -336,172 +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) {
343
- return {
344
- status: 'failed',
345
- intent: plan.intent,
346
- preSnapshot: { revision: 'unknown', counts: emptyCounts() },
347
- selectedTargets: [],
348
- executedOperations: [],
349
- verification: [],
350
- errors: validation.errors.map((e) => ({ code: e.code, message: e.message })),
351
- };
352
- }
353
- let preSnapshot;
354
- try {
355
- preSnapshot = await docSnapshot.buildMutationSnapshot(doc);
356
- }
357
- catch (error) {
358
- if (!(error instanceof docSnapshot.MutationSnapshotError))
359
- throw error;
386
+ if (!validation.ok)
360
387
  return {
361
388
  status: 'failed',
362
389
  intent: plan.intent,
363
- preSnapshot: { revision: 'unknown', counts: emptyCounts() },
364
390
  selectedTargets: [],
365
391
  executedOperations: [],
366
392
  verification: [],
367
- errors: [{ code: error.code, message: error.message, recovery: { kind: 'retry' } }],
393
+ errors: validation.errors.map((error) => ({ code: error.code, message: error.message })),
368
394
  };
369
- }
370
- const selectedTargets = [];
371
- 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
+ });
372
424
  const bindings = new Map();
373
425
  try {
426
+ const pre = await context.start();
374
427
  for (const step of plan.steps) {
375
428
  if (step.kind === 'select') {
376
- const matched = resolveSelectorWithBindings(preSnapshot, step.selector, bindings);
377
- 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)
378
435
  throw new docSnapshot.AmbiguousSelectorError(`Selector did not resolve uniquely (matched ${matched.length}).`, matched.map((nodeId) => ({ nodeId, description: nodeId })));
379
- }
380
- selectedTargets.push({ selector: step.selector, matched });
381
- if (step.bind) {
436
+ context.selectedTargets.push({ selector: step.selector, matched });
437
+ if (step.bind)
382
438
  bindings.set(step.bind, matched.length === 1 ? matched[0] : [...matched]);
383
- }
384
- continue;
385
439
  }
386
- if (step.kind === 'inspect') {
387
- const inspectArgs = ensureClean(resolveBindingTokens(step.args, bindings));
388
- const method = resolveDocMethod(doc, step.operationId);
389
- const result = await method(inspectArgs);
390
- 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)
391
447
  bindings.set(step.bind, result);
392
- continue;
393
- }
394
- if (step.kind === 'apply') {
395
- ensureKnownOperation(step.operationId);
396
- const applyArgs = ensureClean(resolveBindingTokens(step.args, bindings));
397
- const method = resolveDocMethod(doc, step.operationId);
398
- const argsWithMode = step.changeMode != null && operationCatalog.getOperationCatalogEntry(step.operationId)?.supportsChangeMode
399
- ? { ...applyArgs, changeMode: step.changeMode }
400
- : applyArgs;
401
- const result = await method(argsWithMode);
402
- 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;
403
450
  }
404
451
  }
405
- }
406
- catch (err) {
407
- if (err instanceof docSnapshot.AmbiguousSelectorError) {
408
- return {
409
- status: 'aborted',
410
- intent: plan.intent,
411
- preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
412
- selectedTargets,
413
- executedOperations,
414
- verification: [],
415
- errors: [
416
- {
417
- code: err.code,
418
- message: err.message,
419
- },
420
- ],
421
- };
422
- }
423
- const message = err instanceof Error ? err.message : String(err);
424
- return {
425
- status: 'failed',
426
- intent: plan.intent,
427
- preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
428
- selectedTargets,
429
- executedOperations,
430
- verification: [],
431
- errors: [{ code: 'APPLY_FAILED', message }],
432
- };
433
- }
434
- let postSnapshot;
435
- try {
436
- postSnapshot = await docSnapshot.buildMutationSnapshot(doc);
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 });
437
463
  }
438
464
  catch (error) {
439
- if (!(error instanceof docSnapshot.MutationSnapshotError))
440
- throw error;
441
- return {
442
- status: 'failed',
443
- intent: plan.intent,
444
- preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
445
- selectedTargets,
446
- executedOperations,
447
- verification: [],
448
- errors: [{ code: error.code, message: error.message, recovery: { kind: 'reinspect' } }],
449
- };
450
- }
451
- const verifyStep = plan.steps.find((s) => s.kind === 'verify');
452
- let saveReopen;
453
- const shouldSaveReopen = (verifyStep?.kind === 'verify' && (verifyStep.saveReopen || verificationNeedsSaveReopen(verifyStep.checks))) ||
454
- planTouchesRiskyDomain(plan);
455
- if (shouldSaveReopen) {
456
- saveReopen = await trySaveReopen(doc, verifyStep?.kind === 'verify' ? verifyStep.checks : []);
465
+ return context.failure(plan.intent, error);
457
466
  }
458
- const verification = verifyStep?.kind === 'verify' ? computeDeltaChecks(preSnapshot, postSnapshot, verifyStep.checks, saveReopen) : [];
459
- const allVerified = verification.every((v) => v.passed);
460
- return {
461
- status: allVerified ? 'ok' : 'failed',
462
- intent: plan.intent,
463
- preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
464
- postSnapshot: { revision: postSnapshot.revision, counts: postSnapshot.counts },
465
- selectedTargets,
466
- executedOperations,
467
- verification,
468
- saveReopen,
469
- };
470
467
  }
471
468
  async function agentVerify(doc, args) {
472
- let snapshot;
469
+ const context = new executionContext.ExecutionContext(doc, { evidence: args.evidence, checks: args.checks });
473
470
  try {
474
- snapshot = await docSnapshot.buildMutationSnapshot(doc);
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 });
475
480
  }
476
481
  catch (error) {
477
- if (!(error instanceof docSnapshot.MutationSnapshotError))
478
- throw error;
479
- return {
480
- status: 'failed',
481
- intent: 'verify',
482
- preSnapshot: { revision: 'unknown', counts: emptyCounts() },
483
- selectedTargets: [],
484
- executedOperations: [],
485
- verification: [],
486
- errors: [{ code: error.code, message: error.message, recovery: { kind: 'retry' } }],
487
- };
488
- }
489
- let saveReopen;
490
- if (args.saveReopen || verificationNeedsSaveReopen(args.checks)) {
491
- saveReopen = await trySaveReopen(doc, args.checks);
482
+ return context.failure('verify', error);
492
483
  }
493
- const verification = computeCurrentChecks(snapshot, args.checks, saveReopen);
494
- const allPassed = verification.every((v) => v.passed);
495
- return {
496
- status: allPassed ? 'ok' : 'failed',
497
- intent: 'verify',
498
- preSnapshot: { revision: snapshot.revision, counts: snapshot.counts },
499
- postSnapshot: { revision: snapshot.revision, counts: snapshot.counts },
500
- selectedTargets: [],
501
- executedOperations: [],
502
- verification,
503
- saveReopen,
504
- };
505
484
  }
506
485
  /**
507
486
  * Controlled escape hatch — dispatches a single generated operation by id.
@@ -529,27 +508,6 @@ async function agentOperation(doc, args) {
529
508
  const method = resolveDocMethod(doc, args.operationId);
530
509
  return method(callArgs);
531
510
  }
532
- function emptyCounts() {
533
- return {
534
- blocks: 0,
535
- paragraphs: 0,
536
- headings: 0,
537
- tables: 0,
538
- lists: 0,
539
- images: 0,
540
- comments: 0,
541
- trackedChanges: 0,
542
- sections: 0,
543
- fields: 0,
544
- hyperlinks: 0,
545
- bookmarks: 0,
546
- contentControls: 0,
547
- permissionRanges: 0,
548
- styles: 0,
549
- headers: 0,
550
- footers: 0,
551
- };
552
- }
553
511
 
554
512
  exports.agentApply = agentApply;
555
513
  exports.agentInspect = agentInspect;
@@ -1,3 +1,4 @@
1
+ import { type EvidencePolicy } from './execution-context.js';
1
2
  /**
2
3
  * Clean agent runtime.
3
4
  *
@@ -43,9 +44,11 @@ export type AgentInspectArgs = {
43
44
  blockRunsLimit?: number;
44
45
  };
45
46
  export type AgentApplyArgs = {
47
+ evidence?: EvidencePolicy;
46
48
  plan: AgentPlan;
47
49
  };
48
50
  export type AgentVerifyArgs = {
51
+ evidence?: EvidencePolicy;
49
52
  checks: readonly AgentVerificationCheck[];
50
53
  saveReopen?: boolean;
51
54
  };
@@ -1,7 +1,8 @@
1
+ import { ExecutionContext, evaluateFactChecks } from './execution-context.js';
1
2
  import { CONTRACT } from '../generated/contract.js';
2
3
  import { SuperDocCliError } from '../runtime/errors.js';
3
4
  import { validatePlan } from './ir.js';
4
- import { buildDocumentSnapshot, buildMutationSnapshot, resolveSnapshotSelector, AmbiguousSelectorError, MutationSnapshotError, } from './doc-snapshot.js';
5
+ import { buildDocumentSnapshot, buildMutationSnapshot, resolveSnapshotSelector, AmbiguousSelectorError, } from './doc-snapshot.js';
5
6
  import { getOperationCatalogEntry } from './operation-catalog.js';
6
7
  const RESERVED_ARG_KEYS = new Set(['sessionId', 'doc']);
7
8
  function ensureKnownOperation(operationId) {
@@ -333,172 +334,150 @@ async function trySaveReopen(doc, checks) {
333
334
  function verificationNeedsSaveReopen(checks) {
334
335
  return checks.some((check) => check.kind === 'document-saves-cleanly' || check.kind === 'save-reopen-text-contains');
335
336
  }
337
+ function revisionOnlyParagraphStep(plan) {
338
+ if (plan.atomic || plan.preconditions || plan.postconditions || plan.expectedDiff || plan.steps.length !== 2) {
339
+ return undefined;
340
+ }
341
+ const [apply, verify] = plan.steps;
342
+ if (apply?.kind !== 'apply' ||
343
+ apply.operationId !== 'doc.create.paragraph' ||
344
+ apply.atomic ||
345
+ verify?.kind !== 'verify' ||
346
+ verify.saveReopen ||
347
+ verify.checks.length === 0 ||
348
+ !verify.checks.every((check) => check.kind === 'revision-changed' || check.kind === 'revision-unchanged')) {
349
+ return undefined;
350
+ }
351
+ // Only literal body inputs are eligible. Binding tokens, story targets and
352
+ // additional operation options must retain the complete evidence path.
353
+ if (Object.keys(apply.args).some((key) => key !== 'text' && key !== 'at'))
354
+ return undefined;
355
+ if (apply.args.text !== undefined && typeof apply.args.text !== 'string')
356
+ return undefined;
357
+ const at = apply.args.at;
358
+ if (at !== undefined) {
359
+ if (!isRecord(at))
360
+ return undefined;
361
+ if (at.kind === 'documentStart' || at.kind === 'documentEnd') {
362
+ if (Object.keys(at).some((key) => key !== 'kind'))
363
+ return undefined;
364
+ }
365
+ else if (at.kind === 'before' || at.kind === 'after') {
366
+ if (Object.keys(at).some((key) => key !== 'kind' && key !== 'target') ||
367
+ !isRecord(at.target) ||
368
+ at.target.kind !== 'block' ||
369
+ typeof at.target.nodeId !== 'string' ||
370
+ typeof at.target.nodeType !== 'string' ||
371
+ Object.keys(at.target).some((key) => !['kind', 'nodeId', 'nodeType'].includes(key))) {
372
+ return undefined;
373
+ }
374
+ }
375
+ else
376
+ return undefined;
377
+ }
378
+ return apply;
379
+ }
336
380
  export async function agentApply(doc, args) {
337
381
  const plan = args.plan;
338
382
  const validation = validatePlan(plan);
339
- if (!validation.ok) {
340
- return {
341
- status: 'failed',
342
- intent: plan.intent,
343
- preSnapshot: { revision: 'unknown', counts: emptyCounts() },
344
- selectedTargets: [],
345
- executedOperations: [],
346
- verification: [],
347
- errors: validation.errors.map((e) => ({ code: e.code, message: e.message })),
348
- };
349
- }
350
- let preSnapshot;
351
- try {
352
- preSnapshot = await buildMutationSnapshot(doc);
353
- }
354
- catch (error) {
355
- if (!(error instanceof MutationSnapshotError))
356
- throw error;
383
+ if (!validation.ok)
357
384
  return {
358
385
  status: 'failed',
359
386
  intent: plan.intent,
360
- preSnapshot: { revision: 'unknown', counts: emptyCounts() },
361
387
  selectedTargets: [],
362
388
  executedOperations: [],
363
389
  verification: [],
364
- errors: [{ code: error.code, message: error.message, recovery: { kind: 'retry' } }],
390
+ errors: validation.errors.map((error) => ({ code: error.code, message: error.message })),
365
391
  };
366
- }
367
- const selectedTargets = [];
368
- const executedOperations = [];
392
+ const verifyStep = plan.steps.find((step) => step.kind === 'verify');
393
+ const checks = verifyStep?.kind === 'verify' ? verifyStep.checks : [];
394
+ let written = false;
395
+ const baselineAfterWrite = plan.steps.some((step) => {
396
+ if (step.kind === 'apply')
397
+ written = true;
398
+ return written && step.kind === 'select' && step.selector.kind !== 'ref';
399
+ });
400
+ const context = new ExecutionContext(doc, {
401
+ evidence: args.evidence,
402
+ revisionOnly: !!revisionOnlyParagraphStep(plan),
403
+ checks,
404
+ selectors: plan.steps.flatMap((step) => step.kind === 'select' && step.selector.kind !== 'ref' ? [step.selector] : []),
405
+ completeReason: plan.atomic ||
406
+ plan.preconditions ||
407
+ plan.postconditions ||
408
+ plan.expectedDiff ||
409
+ plan.steps.some((step) => step.kind === 'inspect') ||
410
+ plan.steps.filter((step) => step.kind === 'apply').length > 1
411
+ ? 'plan requires complete baseline evidence'
412
+ : baselineAfterWrite
413
+ ? 'plan selects baseline targets after a write'
414
+ : planTouchesRiskyDomain(plan)
415
+ ? 'plan requires document-wide save/reopen evidence'
416
+ : plan.steps.some((step) => step.kind === 'apply' &&
417
+ ['doc.history.undo', 'doc.history.redo', 'doc.plan.execute'].includes(step.operationId))
418
+ ? 'operation requires complete execution evidence'
419
+ : undefined,
420
+ });
369
421
  const bindings = new Map();
370
422
  try {
423
+ const pre = await context.start();
371
424
  for (const step of plan.steps) {
372
425
  if (step.kind === 'select') {
373
- const matched = resolveSelectorWithBindings(preSnapshot, step.selector, bindings);
374
- if (step.requireUnique && matched.length !== 1) {
426
+ const matched = pre.complete
427
+ ? resolveSelectorWithBindings(pre.complete, step.selector, bindings)
428
+ : step.selector.kind === 'ref'
429
+ ? extractBoundNodeIds(resolveBindingRef(bindings, step.selector.ref))
430
+ : await pre.select(step.selector);
431
+ if (step.requireUnique && matched.length !== 1)
375
432
  throw new AmbiguousSelectorError(`Selector did not resolve uniquely (matched ${matched.length}).`, matched.map((nodeId) => ({ nodeId, description: nodeId })));
376
- }
377
- selectedTargets.push({ selector: step.selector, matched });
378
- if (step.bind) {
433
+ context.selectedTargets.push({ selector: step.selector, matched });
434
+ if (step.bind)
379
435
  bindings.set(step.bind, matched.length === 1 ? matched[0] : [...matched]);
380
- }
381
- continue;
382
436
  }
383
- if (step.kind === 'inspect') {
384
- const inspectArgs = ensureClean(resolveBindingTokens(step.args, bindings));
385
- const method = resolveDocMethod(doc, step.operationId);
386
- const result = await method(inspectArgs);
387
- if (step.bind)
437
+ else if (step.kind === 'inspect' || step.kind === 'apply') {
438
+ const input = ensureClean(resolveBindingTokens(step.args, bindings));
439
+ const method = resolveDocMethod(context.document, step.operationId);
440
+ const result = await method(step.kind === 'apply' && step.changeMode && getOperationCatalogEntry(step.operationId)?.supportsChangeMode
441
+ ? { ...input, changeMode: step.changeMode }
442
+ : input);
443
+ if (step.kind === 'inspect' && step.bind)
388
444
  bindings.set(step.bind, result);
389
- continue;
390
- }
391
- if (step.kind === 'apply') {
392
- ensureKnownOperation(step.operationId);
393
- const applyArgs = ensureClean(resolveBindingTokens(step.args, bindings));
394
- const method = resolveDocMethod(doc, step.operationId);
395
- const argsWithMode = step.changeMode != null && getOperationCatalogEntry(step.operationId)?.supportsChangeMode
396
- ? { ...applyArgs, changeMode: step.changeMode }
397
- : applyArgs;
398
- const result = await method(argsWithMode);
399
- executedOperations.push({ operationId: step.operationId, rationale: step.rationale, result });
445
+ if (step.kind === 'apply' && context.executedOperations.length)
446
+ context.executedOperations.at(-1).rationale = step.rationale;
400
447
  }
401
448
  }
402
- }
403
- catch (err) {
404
- if (err instanceof AmbiguousSelectorError) {
405
- return {
406
- status: 'aborted',
407
- intent: plan.intent,
408
- preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
409
- selectedTargets,
410
- executedOperations,
411
- verification: [],
412
- errors: [
413
- {
414
- code: err.code,
415
- message: err.message,
416
- },
417
- ],
418
- };
419
- }
420
- const message = err instanceof Error ? err.message : String(err);
421
- return {
422
- status: 'failed',
423
- intent: plan.intent,
424
- preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
425
- selectedTargets,
426
- executedOperations,
427
- verification: [],
428
- errors: [{ code: 'APPLY_FAILED', message }],
429
- };
430
- }
431
- let postSnapshot;
432
- try {
433
- postSnapshot = await buildMutationSnapshot(doc);
449
+ const post = await context.finish();
450
+ const saveReopen = (verifyStep?.kind === 'verify' && (verifyStep.saveReopen || verificationNeedsSaveReopen(checks))) ||
451
+ planTouchesRiskyDomain(plan)
452
+ ? await trySaveReopen(doc, checks)
453
+ : undefined;
454
+ const verification = pre.complete && post.complete
455
+ ? computeDeltaChecks(pre.complete, post.complete, checks, saveReopen)
456
+ : await evaluateFactChecks(pre, post, checks);
457
+ if (!post.complete && checks.some((check) => !['revision-changed', 'revision-unchanged'].includes(check.kind)))
458
+ await post.fence();
459
+ return context.receipt(plan.intent, verification, { saveReopen });
434
460
  }
435
461
  catch (error) {
436
- if (!(error instanceof MutationSnapshotError))
437
- throw error;
438
- return {
439
- status: 'failed',
440
- intent: plan.intent,
441
- preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
442
- selectedTargets,
443
- executedOperations,
444
- verification: [],
445
- errors: [{ code: error.code, message: error.message, recovery: { kind: 'reinspect' } }],
446
- };
447
- }
448
- const verifyStep = plan.steps.find((s) => s.kind === 'verify');
449
- let saveReopen;
450
- const shouldSaveReopen = (verifyStep?.kind === 'verify' && (verifyStep.saveReopen || verificationNeedsSaveReopen(verifyStep.checks))) ||
451
- planTouchesRiskyDomain(plan);
452
- if (shouldSaveReopen) {
453
- saveReopen = await trySaveReopen(doc, verifyStep?.kind === 'verify' ? verifyStep.checks : []);
462
+ return context.failure(plan.intent, error);
454
463
  }
455
- const verification = verifyStep?.kind === 'verify' ? computeDeltaChecks(preSnapshot, postSnapshot, verifyStep.checks, saveReopen) : [];
456
- const allVerified = verification.every((v) => v.passed);
457
- return {
458
- status: allVerified ? 'ok' : 'failed',
459
- intent: plan.intent,
460
- preSnapshot: { revision: preSnapshot.revision, counts: preSnapshot.counts },
461
- postSnapshot: { revision: postSnapshot.revision, counts: postSnapshot.counts },
462
- selectedTargets,
463
- executedOperations,
464
- verification,
465
- saveReopen,
466
- };
467
464
  }
468
465
  export async function agentVerify(doc, args) {
469
- let snapshot;
466
+ const context = new ExecutionContext(doc, { evidence: args.evidence, checks: args.checks });
470
467
  try {
471
- snapshot = await buildMutationSnapshot(doc);
468
+ const current = await context.start();
469
+ context.post = current;
470
+ const saveReopen = args.saveReopen || verificationNeedsSaveReopen(args.checks) ? await trySaveReopen(doc, args.checks) : undefined;
471
+ const verification = current.complete
472
+ ? computeCurrentChecks(current.complete, args.checks, saveReopen)
473
+ : await evaluateFactChecks(undefined, current, args.checks);
474
+ if (!current.complete)
475
+ await current.fence();
476
+ return context.receipt('verify', verification, { saveReopen });
472
477
  }
473
478
  catch (error) {
474
- if (!(error instanceof MutationSnapshotError))
475
- throw error;
476
- return {
477
- status: 'failed',
478
- intent: 'verify',
479
- preSnapshot: { revision: 'unknown', counts: emptyCounts() },
480
- selectedTargets: [],
481
- executedOperations: [],
482
- verification: [],
483
- errors: [{ code: error.code, message: error.message, recovery: { kind: 'retry' } }],
484
- };
485
- }
486
- let saveReopen;
487
- if (args.saveReopen || verificationNeedsSaveReopen(args.checks)) {
488
- saveReopen = await trySaveReopen(doc, args.checks);
479
+ return context.failure('verify', error);
489
480
  }
490
- const verification = computeCurrentChecks(snapshot, args.checks, saveReopen);
491
- const allPassed = verification.every((v) => v.passed);
492
- return {
493
- status: allPassed ? 'ok' : 'failed',
494
- intent: 'verify',
495
- preSnapshot: { revision: snapshot.revision, counts: snapshot.counts },
496
- postSnapshot: { revision: snapshot.revision, counts: snapshot.counts },
497
- selectedTargets: [],
498
- executedOperations: [],
499
- verification,
500
- saveReopen,
501
- };
502
481
  }
503
482
  /**
504
483
  * Controlled escape hatch — dispatches a single generated operation by id.
@@ -526,24 +505,3 @@ export async function agentOperation(doc, args) {
526
505
  const method = resolveDocMethod(doc, args.operationId);
527
506
  return method(callArgs);
528
507
  }
529
- function emptyCounts() {
530
- return {
531
- blocks: 0,
532
- paragraphs: 0,
533
- headings: 0,
534
- tables: 0,
535
- lists: 0,
536
- images: 0,
537
- comments: 0,
538
- trackedChanges: 0,
539
- sections: 0,
540
- fields: 0,
541
- hyperlinks: 0,
542
- bookmarks: 0,
543
- contentControls: 0,
544
- permissionRanges: 0,
545
- styles: 0,
546
- headers: 0,
547
- footers: 0,
548
- };
549
- }