@superdoc/sdk 2.23.0-next.1 → 2.23.0-next.11

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 (38) hide show
  1. package/dist/action-primitives/tools/structure-insert.cjs +3 -5
  2. package/dist/action-primitives/tools/structure-insert.js +3 -5
  3. package/dist/action-primitives/tools/text-transform.cjs +3 -5
  4. package/dist/action-primitives/tools/text-transform.js +3 -5
  5. package/dist/actions/define.cjs +15 -17
  6. package/dist/actions/define.js +1 -3
  7. package/dist/agent/actions.cjs +42 -44
  8. package/dist/agent/actions.js +1 -3
  9. package/dist/agent/operation-catalog.js +0 -4
  10. package/dist/agent/runtime.cjs +7 -9
  11. package/dist/agent/runtime.js +1 -3
  12. package/dist/agent/v2-preset-compat.cjs +5 -7
  13. package/dist/agent/v2-preset-compat.js +1 -3
  14. package/dist/embedded-tools.generated.cjs +5 -5
  15. package/dist/embedded-tools.generated.js +5 -5
  16. package/dist/generated/client.cjs +10 -0
  17. package/dist/generated/client.d.ts +16343 -919
  18. package/dist/generated/client.js +10 -0
  19. package/dist/generated/contract.cjs +1 -1
  20. package/dist/generated/contract.js +1 -1
  21. package/dist/guards.cjs +7 -0
  22. package/dist/guards.d.ts +1 -0
  23. package/dist/guards.js +3 -0
  24. package/dist/presets/core.cjs +10 -12
  25. package/dist/presets/core.js +1 -3
  26. package/dist/presets/legacy.cjs +6 -8
  27. package/dist/presets/legacy.js +1 -3
  28. package/dist/runtime/sdk-version.generated.cjs +1 -1
  29. package/dist/runtime/sdk-version.generated.d.ts +1 -1
  30. package/dist/runtime/sdk-version.generated.js +1 -1
  31. package/package.json +6 -6
  32. package/tools/catalog.json +77 -0
  33. package/tools/generation-receipt.json +17 -17
  34. package/tools/tools-policy.json +1 -1
  35. package/tools/tools.anthropic.json +77 -0
  36. package/tools/tools.generic.json +77 -0
  37. package/tools/tools.openai.json +77 -0
  38. package/tools/tools.vercel.json +77 -0
@@ -3,6 +3,7 @@
3
3
  var docIndex = require('../doc-index.cjs');
4
4
  var engine = require('../engine.cjs');
5
5
  var resolve = require('../resolve.cjs');
6
+ var guards = require('../../guards.cjs');
6
7
 
7
8
  const STRUCTURE_INSERT_ACTIONS = [
8
9
  'insert_toc',
@@ -22,9 +23,6 @@ const DEFAULT_TOC_CONFIG = {
22
23
  rightAlignPageNumbers: true,
23
24
  tabLeader: 'dot',
24
25
  };
25
- function isObjectRecord(value) {
26
- return value != null && typeof value === 'object' && !Array.isArray(value);
27
- }
28
26
  function parseAction(raw) {
29
27
  if (typeof raw !== 'string') {
30
28
  return undefined;
@@ -166,7 +164,7 @@ function resolvePlacement(context, action) {
166
164
  source: 'provided',
167
165
  });
168
166
  }
169
- if (!isObjectRecord(rawPlacement)) {
167
+ if (!guards.isRecord(rawPlacement)) {
170
168
  return engine.workflowStepFailure({
171
169
  status: 'failed',
172
170
  phase: 'resolve',
@@ -960,7 +958,7 @@ async function executeStep(context, _resolved, plan) {
960
958
  sectionId: 'section-break-fallback',
961
959
  });
962
960
  }
963
- const sectionId = isObjectRecord(sectionResult.section) && typeof sectionResult.section.sectionId === 'string'
961
+ const sectionId = guards.isRecord(sectionResult.section) && typeof sectionResult.section.sectionId === 'string'
964
962
  ? sectionResult.section.sectionId
965
963
  : undefined;
966
964
  const afterInfo = await context.documentHandle.info({}, context.invokeOptions);
@@ -1,6 +1,7 @@
1
1
  import { buildWorkflowDocIndex } from '../doc-index.js';
2
2
  import { runWorkflowEngine, workflowStepFailure, workflowStepSuccess, } from '../engine.js';
3
3
  import { resolveWorkflowTargetFromUnknown, } from '../resolve.js';
4
+ import { isRecord } from '../../guards.js';
4
5
  const STRUCTURE_INSERT_ACTIONS = [
5
6
  'insert_toc',
6
7
  'insert_section_break',
@@ -19,9 +20,6 @@ const DEFAULT_TOC_CONFIG = {
19
20
  rightAlignPageNumbers: true,
20
21
  tabLeader: 'dot',
21
22
  };
22
- function isObjectRecord(value) {
23
- return value != null && typeof value === 'object' && !Array.isArray(value);
24
- }
25
23
  function parseAction(raw) {
26
24
  if (typeof raw !== 'string') {
27
25
  return undefined;
@@ -163,7 +161,7 @@ function resolvePlacement(context, action) {
163
161
  source: 'provided',
164
162
  });
165
163
  }
166
- if (!isObjectRecord(rawPlacement)) {
164
+ if (!isRecord(rawPlacement)) {
167
165
  return workflowStepFailure({
168
166
  status: 'failed',
169
167
  phase: 'resolve',
@@ -957,7 +955,7 @@ async function executeStep(context, _resolved, plan) {
957
955
  sectionId: 'section-break-fallback',
958
956
  });
959
957
  }
960
- const sectionId = isObjectRecord(sectionResult.section) && typeof sectionResult.section.sectionId === 'string'
958
+ const sectionId = isRecord(sectionResult.section) && typeof sectionResult.section.sectionId === 'string'
961
959
  ? sectionResult.section.sectionId
962
960
  : undefined;
963
961
  const afterInfo = await context.documentHandle.info({}, context.invokeOptions);
@@ -2,13 +2,11 @@
2
2
 
3
3
  var engine = require('../engine.cjs');
4
4
  var resolve = require('../resolve.cjs');
5
+ var guards = require('../../guards.cjs');
5
6
 
6
7
  const TEXT_TRANSFORM_ACTIONS = ['replace_all', 'delete_all', 'rewrite_block', 'fill_placeholders'];
7
8
  const CHANGE_MODES = ['direct', 'tracked'];
8
9
  const REWRITE_BLOCK_SUPPORTED_NODE_TYPES = ['paragraph', 'heading', 'listItem'];
9
- function isObjectRecord(value) {
10
- return value != null && typeof value === 'object' && !Array.isArray(value);
11
- }
12
10
  function coerceBoolean(raw, fallback) {
13
11
  if (typeof raw === 'boolean') {
14
12
  return raw;
@@ -155,7 +153,7 @@ function parseEdits(raw) {
155
153
  }
156
154
  const edits = [];
157
155
  for (const entry of raw) {
158
- if (!isObjectRecord(entry)) {
156
+ if (!guards.isRecord(entry)) {
159
157
  return undefined;
160
158
  }
161
159
  const find = entry.find;
@@ -184,7 +182,7 @@ function parsePlaceholderValues(raw) {
184
182
  values.push(entry);
185
183
  continue;
186
184
  }
187
- if (isObjectRecord(entry) && typeof entry.value === 'string' && entry.value.length > 0) {
185
+ if (guards.isRecord(entry) && typeof entry.value === 'string' && entry.value.length > 0) {
188
186
  values.push(entry.value);
189
187
  continue;
190
188
  }
@@ -1,11 +1,9 @@
1
1
  import { runWorkflowEngine, workflowStepFailure, workflowStepSuccess, } from '../engine.js';
2
2
  import { resolveWorkflowTargetFromUnknown, } from '../resolve.js';
3
+ import { isRecord } from '../../guards.js';
3
4
  const TEXT_TRANSFORM_ACTIONS = ['replace_all', 'delete_all', 'rewrite_block', 'fill_placeholders'];
4
5
  const CHANGE_MODES = ['direct', 'tracked'];
5
6
  const REWRITE_BLOCK_SUPPORTED_NODE_TYPES = ['paragraph', 'heading', 'listItem'];
6
- function isObjectRecord(value) {
7
- return value != null && typeof value === 'object' && !Array.isArray(value);
8
- }
9
7
  function coerceBoolean(raw, fallback) {
10
8
  if (typeof raw === 'boolean') {
11
9
  return raw;
@@ -152,7 +150,7 @@ function parseEdits(raw) {
152
150
  }
153
151
  const edits = [];
154
152
  for (const entry of raw) {
155
- if (!isObjectRecord(entry)) {
153
+ if (!isRecord(entry)) {
156
154
  return undefined;
157
155
  }
158
156
  const find = entry.find;
@@ -181,7 +179,7 @@ function parsePlaceholderValues(raw) {
181
179
  values.push(entry);
182
180
  continue;
183
181
  }
184
- if (isObjectRecord(entry) && typeof entry.value === 'string' && entry.value.length > 0) {
182
+ if (isRecord(entry) && typeof entry.value === 'string' && entry.value.length > 0) {
185
183
  values.push(entry.value);
186
184
  continue;
187
185
  }
@@ -4,6 +4,7 @@ var errors = require('../runtime/errors.cjs');
4
4
  var presets = require('../presets.cjs');
5
5
  var actions = require('../agent/actions.cjs');
6
6
  var catalog = require('../agent/catalog.cjs');
7
+ var guards = require('../guards.cjs');
7
8
 
8
9
  /**
9
10
  * Customer-extensible **custom actions** for the SuperDoc LLM-tools SDK. The
@@ -71,7 +72,7 @@ function coerceInputSchema(input) {
71
72
  // `{ type: 'object', additionalProperties: true }`). The declared type makes
72
73
  // `properties` required and every consumer (and the Python mirror) assumes
73
74
  // it exists, so normalize a missing/non-object bag to {}.
74
- return isRecord(input.properties) ? input : { ...input, properties: {} };
75
+ return guards.isRecord(input.properties) ? input : { ...input, properties: {} };
75
76
  }
76
77
  // A bare properties bag — wrap it.
77
78
  return { type: 'object', properties: input, additionalProperties: true };
@@ -121,7 +122,7 @@ function defineAction(input) {
121
122
  if (!BUILTIN_ACTION_NAMES.has(step.action)) {
122
123
  throw new errors.SuperDocCliError(`defineAction "${input.name}": steps[${index}].action "${step.action}" is not a built-in core action. Steps compose built-in actions only; use the \`run\` tier for anything else.`, { code: 'INVALID_ARGUMENT', details: { name: input.name, index, action: step.action } });
123
124
  }
124
- if (step.args != null && !isRecord(step.args)) {
125
+ if (step.args != null && !guards.isRecord(step.args)) {
125
126
  throw new errors.SuperDocCliError(`defineAction "${input.name}": steps[${index}].args must be an object.`, {
126
127
  code: 'INVALID_ARGUMENT',
127
128
  details: { name: input.name, index },
@@ -240,9 +241,6 @@ function assertActionsValid(actions, presetId, standalone = false) {
240
241
  // ---------------------------------------------------------------------------
241
242
  // runCustomAction — validate, codegen, dispatch via superdoc_execute_code, map receipt
242
243
  // ---------------------------------------------------------------------------
243
- function isRecord(value) {
244
- return value != null && typeof value === 'object' && !Array.isArray(value);
245
- }
246
244
  /** Kit-level args every custom action accepts without declaring them. */
247
245
  const IMPLICIT_ACTION_ARGS = new Set(['changeMode', 'rationale']);
248
246
  function validateAgainstSchema(action, args) {
@@ -254,7 +252,7 @@ function validateAgainstSchema(action, args) {
254
252
  details: { action: action.name, missingKeys: missing },
255
253
  });
256
254
  }
257
- const properties = isRecord(action.inputSchema.properties) ? action.inputSchema.properties : {};
255
+ const properties = guards.isRecord(action.inputSchema.properties) ? action.inputSchema.properties : {};
258
256
  if (action.inputSchema.additionalProperties === false) {
259
257
  const unknown = Object.keys(args).filter((key) => !(key in properties) && !IMPLICIT_ACTION_ARGS.has(key));
260
258
  if (unknown.length > 0) {
@@ -266,7 +264,7 @@ function validateAgainstSchema(action, args) {
266
264
  }
267
265
  for (const [key, prop] of Object.entries(properties)) {
268
266
  const value = args[key];
269
- if (value !== undefined && isRecord(prop) && Array.isArray(prop.enum) && !prop.enum.includes(value)) {
267
+ if (value !== undefined && guards.isRecord(prop) && Array.isArray(prop.enum) && !prop.enum.includes(value)) {
270
268
  throw new errors.SuperDocCliError(`Invalid value for ${action.name}.${key}: ${JSON.stringify(value)} (allowed: ${prop.enum.map((entry) => JSON.stringify(entry)).join(', ')})`, { code: 'INVALID_ARGUMENT', details: { action: action.name, key, allowed: prop.enum } });
271
269
  }
272
270
  }
@@ -275,7 +273,7 @@ function validateAgainstSchema(action, args) {
275
273
  function applyInputDefaults(action, args) {
276
274
  const out = { ...args };
277
275
  for (const [key, prop] of Object.entries(action.inputSchema.properties ?? {})) {
278
- if (out[key] === undefined && isRecord(prop) && 'default' in prop)
276
+ if (out[key] === undefined && guards.isRecord(prop) && 'default' in prop)
279
277
  out[key] = prop.default;
280
278
  }
281
279
  return out;
@@ -309,7 +307,7 @@ function substituteTemplates(node, vars) {
309
307
  // would crash the transport).
310
308
  return node.map((item) => substituteTemplates(item, vars)).filter((item) => item !== undefined);
311
309
  }
312
- if (isRecord(node)) {
310
+ if (guards.isRecord(node)) {
313
311
  const out = {};
314
312
  for (const [key, value] of Object.entries(node)) {
315
313
  const substituted = substituteTemplates(value, vars);
@@ -341,7 +339,7 @@ async function runStepsAction(base, action, documentHandle, args, invokeOptions)
341
339
  let receipt;
342
340
  try {
343
341
  const dispatched = await base.dispatch(documentHandle, 'superdoc_perform_action', { action: step.action, ...stepArgs }, stepOptions);
344
- receipt = isRecord(dispatched) ? dispatched : { status: 'ok', result: dispatched };
342
+ receipt = guards.isRecord(dispatched) ? dispatched : { status: 'ok', result: dispatched };
345
343
  }
346
344
  catch (error) {
347
345
  // Validation errors THROW from dispatch; runtime failures come back as
@@ -458,13 +456,13 @@ function renormalizeAnthropicCacheMarker(tools, provider, cacheRequested) {
458
456
  if (provider !== 'anthropic' || !cacheRequested || tools.length === 0)
459
457
  return tools;
460
458
  const stripped = tools.map((tool) => {
461
- if (!isRecord(tool) || !('cache_control' in tool))
459
+ if (!guards.isRecord(tool) || !('cache_control' in tool))
462
460
  return tool;
463
461
  const { cache_control: _drop, ...rest } = tool;
464
462
  return rest;
465
463
  });
466
464
  const last = stripped[stripped.length - 1];
467
- stripped[stripped.length - 1] = isRecord(last) ? { ...last, cache_control: { type: 'ephemeral' } } : last;
465
+ stripped[stripped.length - 1] = guards.isRecord(last) ? { ...last, cache_control: { type: 'ephemeral' } } : last;
468
466
  return stripped;
469
467
  }
470
468
  function customActionsDescription(actions) {
@@ -535,16 +533,16 @@ function mergeIntoAgentAction(tools, actions) {
535
533
  // Provider dialects: openai nests under `function` (`parameters`);
536
534
  // anthropic is flat with `input_schema`; the core agent dialect for
537
535
  // vercel is flat with `inputSchema`; generic is flat with `parameters`.
538
- const fn = isRecord(t.function) ? t.function : null;
536
+ const fn = guards.isRecord(t.function) ? t.function : null;
539
537
  const schemaContainer = fn ?? t;
540
538
  const schemaKey = 'input_schema' in schemaContainer
541
539
  ? 'input_schema'
542
540
  : 'inputSchema' in schemaContainer
543
541
  ? 'inputSchema'
544
542
  : 'parameters';
545
- const schema = isRecord(schemaContainer[schemaKey]) ? schemaContainer[schemaKey] : {};
546
- const properties = isRecord(schema.properties) ? { ...schema.properties } : {};
547
- const actionProp = isRecord(properties.action) ? { ...properties.action } : {};
543
+ const schema = guards.isRecord(schemaContainer[schemaKey]) ? schemaContainer[schemaKey] : {};
544
+ const properties = guards.isRecord(schema.properties) ? { ...schema.properties } : {};
545
+ const actionProp = guards.isRecord(properties.action) ? { ...properties.action } : {};
548
546
  const enumValues = Array.isArray(actionProp.enum) ? [...actionProp.enum] : [];
549
547
  for (const action of actions) {
550
548
  if (!enumValues.includes(action.name))
@@ -748,7 +746,7 @@ function extendPreset(baseId, options) {
748
746
  // must be refused before it runs (the base can only refuse built-ins).
749
747
  const exclusionOptions = invokeOptions;
750
748
  const excluded = new Set(exclusionOptions?.excludeActions ?? []);
751
- if (toolName === 'superdoc_perform_action' && isRecord(args) && typeof args.action === 'string') {
749
+ if (toolName === 'superdoc_perform_action' && guards.isRecord(args) && typeof args.action === 'string') {
752
750
  // Advertised surface == dispatchable surface: in standalone mode custom
753
751
  // actions are advertised as their own tools, so the (unadvertised)
754
752
  // perform_action route must not execute them — it falls through to the
@@ -32,6 +32,7 @@ import { SuperDocCliError } from '../runtime/errors.js';
32
32
  import { getPreset, } from '../presets.js';
33
33
  import { ACTION_NAMES_LIST } from '../agent/actions.js';
34
34
  import { AGENT_TOOL_NAMES, buildPerformActionDefinition } from '../agent/catalog.js';
35
+ import { isRecord } from '../guards.js';
35
36
  /** Which execution tier a spec uses. */
36
37
  export function executionKindOf(spec) {
37
38
  return Array.isArray(spec.steps) ? 'steps' : 'run';
@@ -237,9 +238,6 @@ function assertActionsValid(actions, presetId, standalone = false) {
237
238
  // ---------------------------------------------------------------------------
238
239
  // runCustomAction — validate, codegen, dispatch via superdoc_execute_code, map receipt
239
240
  // ---------------------------------------------------------------------------
240
- function isRecord(value) {
241
- return value != null && typeof value === 'object' && !Array.isArray(value);
242
- }
243
241
  /** Kit-level args every custom action accepts without declaring them. */
244
242
  const IMPLICIT_ACTION_ARGS = new Set(['changeMode', 'rationale']);
245
243
  function validateAgainstSchema(action, args) {
@@ -9,6 +9,7 @@ var listTransform = require('../action-primitives/tools/list-transform.cjs');
9
9
  var structureInsert = require('../action-primitives/tools/structure-insert.cjs');
10
10
  var textTransform = require('../action-primitives/tools/text-transform.cjs');
11
11
  var docSnapshot = require('./doc-snapshot.cjs');
12
+ var guards = require('../guards.cjs');
12
13
 
13
14
  const ACTION_DEFINITIONS = actionDescriptors;
14
15
  function projectActionDefinitions(project) {
@@ -72,9 +73,6 @@ function getActionDispositions() {
72
73
  function isActionName(value) {
73
74
  return typeof value === 'string' && ACTION_NAMES.includes(value);
74
75
  }
75
- function isRecord(value) {
76
- return value != null && typeof value === 'object' && !Array.isArray(value);
77
- }
78
76
  /** The message to put in a receipt when something best-effort did not happen. */
79
77
  function reasonOf(err) {
80
78
  return err instanceof Error ? err.message : String(err);
@@ -100,7 +98,7 @@ function isNoOp(err) {
100
98
  return err?.code === 'NO_OP';
101
99
  }
102
100
  function asRecord(value) {
103
- return isRecord(value) ? value : null;
101
+ return guards.isRecord(value) ? value : null;
104
102
  }
105
103
  function asString(value, fallback) {
106
104
  return typeof value === 'string' ? value : fallback;
@@ -657,7 +655,7 @@ async function previewMutations(doc, steps, changeMode, expectedRevision) {
657
655
  });
658
656
  }
659
657
  const result = await fn({ atomic: true, changeMode: changeMode ?? 'direct', expectedRevision, steps });
660
- return isRecord(result) ? result : {};
658
+ return guards.isRecord(result) ? result : {};
661
659
  }
662
660
  async function executeCreateTable(doc, args, placement) {
663
661
  const fn = maybeMethod(doc, ['create', 'table']);
@@ -1052,7 +1050,7 @@ async function runReplaceTextUnchecked(doc, args) {
1052
1050
  const preview = await previewMutations(doc, candidates.map((candidate) => candidate.step), args.changeMode, pre.revision);
1053
1051
  if (preview.valid === true)
1054
1052
  break;
1055
- const failures = Array.isArray(preview.failures) ? preview.failures.filter(isRecord) : [];
1053
+ const failures = Array.isArray(preview.failures) ? preview.failures.filter(guards.isRecord) : [];
1056
1054
  const absent = failures.find((failure) => failure.code === 'MATCH_NOT_FOUND' && typeof failure.stepId === 'string');
1057
1055
  if (!absent) {
1058
1056
  throw new Error(typeof failures[0]?.message === 'string' ? failures[0].message : 'replacement plan could not be prepared');
@@ -2713,7 +2711,7 @@ async function listAllTrackedChanges(listFn) {
2713
2711
  const page = asRecord(await listFn({ offset, limit: PAGE, in: 'all' }));
2714
2712
  const items = Array.isArray(page?.items) ? page.items : [];
2715
2713
  for (const item of items) {
2716
- if (isRecord(item))
2714
+ if (guards.isRecord(item))
2717
2715
  out.push(item);
2718
2716
  }
2719
2717
  const total = asNumber(page?.total, out.length) ?? out.length;
@@ -4290,12 +4288,12 @@ function findTextTarget(snapshot, nodeId) {
4290
4288
  return null;
4291
4289
  }
4292
4290
  function compactOpResult(result) {
4293
- if (!isRecord(result))
4291
+ if (!guards.isRecord(result))
4294
4292
  return result;
4295
4293
  const compact = {};
4296
4294
  if (typeof result.success === 'boolean')
4297
4295
  compact.success = result.success;
4298
- if (isRecord(result.revision))
4296
+ if (guards.isRecord(result.revision))
4299
4297
  compact.revision = result.revision;
4300
4298
  if (Array.isArray(result.steps))
4301
4299
  compact.stepCount = result.steps.length;
@@ -4311,7 +4309,7 @@ function trackedChangeRefIds(result) {
4311
4309
  }));
4312
4310
  }
4313
4311
  function parseScopedReplaceArgs(args, edits, changeMode, expectedRevision) {
4314
- if (!isRecord(args.selector))
4312
+ if (!guards.isRecord(args.selector))
4315
4313
  return null;
4316
4314
  return {
4317
4315
  edits,
@@ -4341,9 +4339,9 @@ async function fieldResultRanges(doc, nodeId, blockText) {
4341
4339
  const limit = 500;
4342
4340
  for (let offset = 0; offset < 100_000; offset += limit) {
4343
4341
  const page = await fn({ limit, offset });
4344
- const items = isRecord(page) && Array.isArray(page.items) ? page.items.filter(isRecord) : [];
4342
+ const items = guards.isRecord(page) && Array.isArray(page.items) ? page.items.filter(guards.isRecord) : [];
4345
4343
  for (const item of items) {
4346
- const address = isRecord(item.address) ? item.address : null;
4344
+ const address = guards.isRecord(item.address) ? item.address : null;
4347
4345
  const inBody = typeof address?.storyId !== 'string' || address.storyId.startsWith('main:');
4348
4346
  if (address?.blockId !== nodeId || !inBody)
4349
4347
  continue;
@@ -4352,7 +4350,7 @@ async function fieldResultRanges(doc, nodeId, blockText) {
4352
4350
  }
4353
4351
  results.push(item.resolvedText);
4354
4352
  }
4355
- const total = isRecord(page) && typeof page.total === 'number' ? page.total : null;
4353
+ const total = guards.isRecord(page) && typeof page.total === 'number' ? page.total : null;
4356
4354
  if (items.length < limit || (total != null && offset + limit >= total))
4357
4355
  break;
4358
4356
  }
@@ -4493,11 +4491,11 @@ async function runScopedReplaceUnchecked(doc, args) {
4493
4491
  // that each one still covers the text matched in the snapshot, so a
4494
4492
  // projection mismatch never rewrites other text.
4495
4493
  const preview = await previewMutations(doc, steps, args.changeMode, pre.revision);
4496
- const previewSteps = Array.isArray(preview.steps) ? preview.steps.filter(isRecord) : [];
4494
+ const previewSteps = Array.isArray(preview.steps) ? preview.steps.filter(guards.isRecord) : [];
4497
4495
  const aligned = preview.valid === true &&
4498
4496
  steps.every((step, index) => {
4499
4497
  const resolutions = previewSteps.find((candidate) => candidate.stepId === step.id)?.resolutions;
4500
- const resolved = Array.isArray(resolutions) && isRecord(resolutions[0]) ? resolutions[0].text : undefined;
4498
+ const resolved = Array.isArray(resolutions) && guards.isRecord(resolutions[0]) ? resolutions[0].text : undefined;
4501
4499
  const span = targetedSpans[index];
4502
4500
  return resolved === block.text.slice(span.start, span.end);
4503
4501
  });
@@ -4530,7 +4528,7 @@ async function runScopedReplaceUnchecked(doc, args) {
4530
4528
  // atomic history step. Only do so while the document is still at the
4531
4529
  // revision this plan produced, so the undo cannot reach any other change.
4532
4530
  let rolledBack = false;
4533
- const appliedRevision = isRecord(result) && isRecord(result.revision) ? result.revision.after : undefined;
4531
+ const appliedRevision = guards.isRecord(result) && guards.isRecord(result.revision) ? result.revision.after : undefined;
4534
4532
  if (wroteExactTargets &&
4535
4533
  postBlock != null &&
4536
4534
  postBlock.text !== expected &&
@@ -4645,13 +4643,13 @@ function markRunPropsFromMarks(marks) {
4645
4643
  * paragraphIndentOf. Returns null when the paragraph mark carries nothing.
4646
4644
  */
4647
4645
  function paragraphMarkRunPropsOf(payload) {
4648
- const node = isRecord(payload) ? payload.node : null;
4649
- if (!isRecord(node))
4646
+ const node = guards.isRecord(payload) ? payload.node : null;
4647
+ if (!guards.isRecord(node))
4650
4648
  return null;
4651
- const candidates = [isRecord(node.paragraph) ? node.paragraph.props : null, node.props];
4649
+ const candidates = [guards.isRecord(node.paragraph) ? node.paragraph.props : null, node.props];
4652
4650
  for (const props of candidates) {
4653
- const mark = isRecord(props) ? props.markRunProps : null;
4654
- if (isRecord(mark) && Object.keys(mark).length > 0)
4651
+ const mark = guards.isRecord(props) ? props.markRunProps : null;
4652
+ if (guards.isRecord(mark) && Object.keys(mark).length > 0)
4655
4653
  return mark;
4656
4654
  }
4657
4655
  return null;
@@ -4896,13 +4894,13 @@ async function listItemRunPattern(doc, text, blockId) {
4896
4894
  * indent copied" rather than a thrown read.
4897
4895
  */
4898
4896
  function paragraphIndentOf(payload) {
4899
- const node = isRecord(payload) ? payload.node : null;
4900
- if (!isRecord(node))
4897
+ const node = guards.isRecord(payload) ? payload.node : null;
4898
+ if (!guards.isRecord(node))
4901
4899
  return null;
4902
- const candidates = [isRecord(node.paragraph) ? node.paragraph.props : null, node.props];
4900
+ const candidates = [guards.isRecord(node.paragraph) ? node.paragraph.props : null, node.props];
4903
4901
  for (const props of candidates) {
4904
- const indent = isRecord(props) ? props.indent : null;
4905
- if (!isRecord(indent))
4902
+ const indent = guards.isRecord(props) ? props.indent : null;
4903
+ if (!guards.isRecord(indent))
4906
4904
  continue;
4907
4905
  const twips = {};
4908
4906
  for (const key of ['left', 'right', 'firstLine', 'hanging']) {
@@ -4979,7 +4977,7 @@ function marksToInline(marks) {
4979
4977
  }
4980
4978
  /** Parse a raw agent mark bag, keeping only recognized keys with valid types. */
4981
4979
  function normalizeMarks(raw) {
4982
- if (!isRecord(raw))
4980
+ if (!guards.isRecord(raw))
4983
4981
  return undefined;
4984
4982
  const m = {};
4985
4983
  if (typeof raw.bold === 'boolean')
@@ -5009,7 +5007,7 @@ function normalizeMarks(raw) {
5009
5007
  function normalizeItemSpec(raw) {
5010
5008
  if (typeof raw === 'string')
5011
5009
  return { text: raw, level: 0 };
5012
- if (!isRecord(raw))
5010
+ if (!guards.isRecord(raw))
5013
5011
  return null;
5014
5012
  const spec = {};
5015
5013
  const lvl = asNumber(raw.level);
@@ -5023,7 +5021,7 @@ function normalizeItemSpec(raw) {
5023
5021
  if (typeof r === 'string') {
5024
5022
  runs.push({ text: r });
5025
5023
  }
5026
- else if (isRecord(r) && typeof r.text === 'string') {
5024
+ else if (guards.isRecord(r) && typeof r.text === 'string') {
5027
5025
  const rm = normalizeMarks(r.marks);
5028
5026
  runs.push(rm ? { text: r.text, marks: rm } : { text: r.text });
5029
5027
  }
@@ -5642,9 +5640,9 @@ async function appendListAtPlacement(doc, args) {
5642
5640
  const items = Array.isArray(args.items)
5643
5641
  ? args.items.filter((t) => typeof t === 'string' && t.length > 0)
5644
5642
  : [];
5645
- const placement = isRecord(args.placement) ? args.placement : null;
5643
+ const placement = guards.isRecord(args.placement) ? args.placement : null;
5646
5644
  const at = placement && typeof placement.at === 'string' ? placement.at : null;
5647
- const selector = placement && isRecord(placement.selector) ? placement.selector : null;
5645
+ const selector = placement && guards.isRecord(placement.selector) ? placement.selector : null;
5648
5646
  if (items.length === 0 || !placement || (at !== 'after' && at !== 'before') || !selector)
5649
5647
  return null;
5650
5648
  const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
@@ -6042,15 +6040,15 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
6042
6040
  return receipt;
6043
6041
  // The placement anchor, when the action targeted one, is the user's own
6044
6042
  // statement of "blend in with THIS" — prefer it over type-matched siblings.
6045
- const placement = isRecord(args?.placement) ? args.placement : null;
6046
- const anchorSelector = placement && isRecord(placement.selector) ? placement.selector : null;
6043
+ const placement = guards.isRecord(args?.placement) ? args.placement : null;
6044
+ const anchorSelector = placement && guards.isRecord(placement.selector) ? placement.selector : null;
6047
6045
  const anchorId = anchorSelector && typeof anchorSelector.nodeId === 'string' ? anchorSelector.nodeId : null;
6048
6046
  const anchorRow = anchorId ? (postBlocks.find((b) => b.nodeId === anchorId) ?? null) : null;
6049
6047
  // When the caller styled any entry explicitly (runs/marks), it took
6050
6048
  // ownership of intra-paragraph formatting — the automatic lead-marker
6051
6049
  // pattern pass must not fight it.
6052
6050
  const rawTexts = Array.isArray(args?.texts) ? args.texts : [];
6053
- const hasExplicitStyling = rawTexts.some((entry) => isRecord(entry) && (Array.isArray(entry.runs) || entry.marks != null));
6051
+ const hasExplicitStyling = rawTexts.some((entry) => guards.isRecord(entry) && (Array.isArray(entry.runs) || entry.marks != null));
6054
6052
  const insertedTexts = rawTexts.map((entry) => {
6055
6053
  if (typeof entry === 'string')
6056
6054
  return entry;
@@ -6075,7 +6073,7 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
6075
6073
  // the previous NAME line, not like the "Name:" line directly above it.
6076
6074
  // Only tried when matching against the neighbour found nothing, so this
6077
6075
  // cannot change a case that already had an answer.
6078
- const applied = isRecord(result?.applied) ? result.applied : null;
6076
+ const applied = guards.isRecord(result?.applied) ? result.applied : null;
6079
6077
  if (createdRows.length > 1 && (!applied || Object.keys(applied).length === 0)) {
6080
6078
  const parallel = postBlocks[createdIndex - createdRows.length];
6081
6079
  if (parallel && preIds.has(parallel.nodeId) && !parallel.isEmpty) {
@@ -6291,7 +6289,7 @@ async function runAttachNumbering(doc, args) {
6291
6289
  };
6292
6290
  }
6293
6291
  async function dispatchSuperdocPerformAction(doc, args) {
6294
- if (!isRecord(args)) {
6292
+ if (!guards.isRecord(args)) {
6295
6293
  throw new errors.SuperDocCliError('superdoc_perform_action arguments must be an object', {
6296
6294
  code: 'INVALID_ARGUMENT',
6297
6295
  });
@@ -6365,7 +6363,7 @@ async function dispatchSuperdocPerformAction(doc, args) {
6365
6363
  if (expectedRevision !== undefined && (typeof expectedRevision !== 'string' || expectedRevision.length === 0)) {
6366
6364
  throw new errors.SuperDocCliError('replace_text: expectedRevision must be a non-empty string from this live document handle', { code: 'INVALID_ARGUMENT' });
6367
6365
  }
6368
- if (edits.some((edit) => edit.occurrence != null) && !isRecord(args.selector)) {
6366
+ if (edits.some((edit) => edit.occurrence != null) && !guards.isRecord(args.selector)) {
6369
6367
  throw new errors.SuperDocCliError('replace_text: explicit occurrence requires a selector resolving to one block', {
6370
6368
  code: 'INVALID_ARGUMENT',
6371
6369
  });
@@ -6995,13 +6993,13 @@ function parseExactTrackedChangeIdList(value) {
6995
6993
  }
6996
6994
  async function superdocPerformAction(doc, args) {
6997
6995
  try {
6998
- const changeMode = isRecord(args) ? parseChangeMode(args.changeMode) : undefined;
6996
+ const changeMode = guards.isRecord(args) ? parseChangeMode(args.changeMode) : undefined;
6999
6997
  return await dispatchSuperdocPerformAction(changeMode ? actionChangeMode.bindActionChangeMode(doc, changeMode) : doc, args);
7000
6998
  }
7001
6999
  catch (error) {
7002
7000
  if (!(error instanceof docSnapshot.MutationSnapshotError))
7003
7001
  throw error;
7004
- const action = isRecord(args) && isActionName(args.action) ? args.action : 'superdoc_perform_action';
7002
+ const action = guards.isRecord(args) && isActionName(args.action) ? args.action : 'superdoc_perform_action';
7005
7003
  return {
7006
7004
  status: 'failed',
7007
7005
  intent: action,
@@ -7062,7 +7060,7 @@ function parseEdits(value) {
7062
7060
  return undefined;
7063
7061
  const result = [];
7064
7062
  for (const entry of value) {
7065
- if (!isRecord(entry))
7063
+ if (!guards.isRecord(entry))
7066
7064
  return undefined;
7067
7065
  const find = asString(entry.find);
7068
7066
  const replace = asString(entry.replace) ?? '';
@@ -7082,7 +7080,7 @@ function parseEdits(value) {
7082
7080
  function parseCellCandidate(cell) {
7083
7081
  if (typeof cell === 'string')
7084
7082
  return cell;
7085
- if (isRecord(cell)) {
7083
+ if (guards.isRecord(cell)) {
7086
7084
  const spec = normalizeItemSpec(cell);
7087
7085
  if (spec)
7088
7086
  return { text: spec.text, runs: spec.runs, marks: spec.marks };
@@ -7110,7 +7108,7 @@ function parsePlaceholderFields(value) {
7110
7108
  return undefined;
7111
7109
  const result = [];
7112
7110
  for (const entry of value) {
7113
- if (!isRecord(entry) || typeof entry.value !== 'string')
7111
+ if (!guards.isRecord(entry) || typeof entry.value !== 'string')
7114
7112
  return undefined;
7115
7113
  const label = asString(entry.label);
7116
7114
  result.push(label ? { label, value: entry.value } : { value: entry.value });
@@ -7118,7 +7116,7 @@ function parsePlaceholderFields(value) {
7118
7116
  return result;
7119
7117
  }
7120
7118
  function parsePlacement(value) {
7121
- if (!isRecord(value))
7119
+ if (!guards.isRecord(value))
7122
7120
  return undefined;
7123
7121
  const at = asString(value.at);
7124
7122
  if (at === 'document_end' || at === 'document_start') {
@@ -7133,7 +7131,7 @@ function parsePlacement(value) {
7133
7131
  return undefined;
7134
7132
  }
7135
7133
  function parseSelector(value) {
7136
- if (!isRecord(value))
7134
+ if (!guards.isRecord(value))
7137
7135
  return undefined;
7138
7136
  const kind = asString(value.kind);
7139
7137
  if (kind === 'nodeId') {
@@ -7,6 +7,7 @@ import { runSuperdocListTransformWorkflow } from '../action-primitives/tools/lis
7
7
  import { runSuperdocStructureInsertWorkflow } from '../action-primitives/tools/structure-insert.js';
8
8
  import { runSuperdocTextTransformWorkflow } from '../action-primitives/tools/text-transform.js';
9
9
  import { buildDocumentSnapshot, buildMutationSnapshot, matchRunsForBlock, MutationSnapshotError, resolveSnapshotSelector, } from './doc-snapshot.js';
10
+ import { isRecord } from '../guards.js';
10
11
  export const ACTION_DEFINITIONS = actionDescriptors;
11
12
  function projectActionDefinitions(project) {
12
13
  return Object.fromEntries(Object.entries(ACTION_DEFINITIONS).map(([name, definition]) => [name, project(definition)]));
@@ -69,9 +70,6 @@ export function getActionDispositions() {
69
70
  export function isActionName(value) {
70
71
  return typeof value === 'string' && ACTION_NAMES.includes(value);
71
72
  }
72
- function isRecord(value) {
73
- return value != null && typeof value === 'object' && !Array.isArray(value);
74
- }
75
73
  /** The message to put in a receipt when something best-effort did not happen. */
76
74
  function reasonOf(err) {
77
75
  return err instanceof Error ? err.message : String(err);
@@ -26,10 +26,6 @@ const BENCHMARK_ONLY_HINTS = [
26
26
  function tokensFromOperationId(operationId) {
27
27
  return operationId.split('.').slice(1);
28
28
  }
29
- function lastToken(operationId) {
30
- const tokens = tokensFromOperationId(operationId);
31
- return (tokens[tokens.length - 1] ?? '').toLowerCase();
32
- }
33
29
  function deriveDomain(operationId) {
34
30
  const tokens = tokensFromOperationId(operationId);
35
31
  const first = (tokens[0] ?? '').toLowerCase();