@superdoc/sdk 2.23.0-next.8 → 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) 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 +438 -52
  8. package/dist/agent/actions.d.ts +13 -0
  9. package/dist/agent/actions.js +394 -11
  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-prompts.generated.cjs +1 -1
  15. package/dist/embedded-prompts.generated.js +1 -1
  16. package/dist/embedded-tools.generated.cjs +5 -5
  17. package/dist/embedded-tools.generated.js +5 -5
  18. package/dist/generated/client.d.ts +193 -177
  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/prompts/system-prompt.md +6 -6
  29. package/dist/runtime/sdk-version.generated.cjs +1 -1
  30. package/dist/runtime/sdk-version.generated.d.ts +1 -1
  31. package/dist/runtime/sdk-version.generated.js +1 -1
  32. package/package.json +6 -6
  33. package/tools/catalog.json +8 -0
  34. package/tools/generation-receipt.json +18 -18
  35. package/tools/tools-policy.json +1 -1
  36. package/tools/tools.anthropic.json +8 -0
  37. package/tools/tools.generic.json +8 -0
  38. package/tools/tools.openai.json +8 -0
  39. package/tools/tools.vercel.json +8 -0
@@ -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']);
@@ -880,6 +878,7 @@ async function styleTableCells(doc, cells, changeMode) {
880
878
  await applyItemStyling(doc, nodeIds, specs, changeMode);
881
879
  return styled.length;
882
880
  }
881
+ const TYPED_SIBLING_WINDOW = 20;
883
882
  async function runInsertParagraphs(doc, args, formatArgs) {
884
883
  const context = new executionContext.ExecutionContext(doc, { evidence: args.evidence, selectors: placementSelectors(args.placement) });
885
884
  try {
@@ -892,7 +891,7 @@ async function runInsertParagraphs(doc, args, formatArgs) {
892
891
  kind: 'placement',
893
892
  at: placement.kind === 'documentStart' ? 'document_start' : 'document_end',
894
893
  });
895
- const preRows = pre.complete ? await listBlockRows(doc) : anchor ? [anchor] : [];
894
+ let preRows = pre.complete ? await listBlockRows(doc) : anchor ? [anchor] : [];
896
895
  let currentPlacement = placement;
897
896
  const nodeIds = [];
898
897
  const specs = [];
@@ -932,6 +931,16 @@ async function runInsertParagraphs(doc, args, formatArgs) {
932
931
  if (!pre.complete) {
933
932
  const created = await post.page({ nodeIds, limit: Math.max(nodeIds.length, 1) });
934
933
  localRows = [...(anchor ? [anchor] : []), ...created].sort((a, b) => a.ordinal - b.ordinal);
934
+ const placed = anchor && texts.some((text) => typedMarkerPrefixOf(text)) ? await post.block(anchor.nodeId) : null;
935
+ if (placed) {
936
+ const span = TYPED_SIBLING_WINDOW + nodeIds.length;
937
+ const window = await post.page({ offset: Math.max(0, placed.ordinal - 1 - span), limit: 2 * span + 1 });
938
+ const createdIds = new Set(nodeIds);
939
+ if (nodeIds.every((id) => window.some((row) => row.nodeId === id))) {
940
+ preRows = window.filter((row) => !createdIds.has(row.nodeId));
941
+ localRows = window;
942
+ }
943
+ }
935
944
  }
936
945
  const count = context.executedOperations.length;
937
946
  formatted = await matchInsertedBlockFormatting(context.document, preRows, context.receipt('insert_paragraphs', []), formatArgs, localRows);
@@ -1052,7 +1061,7 @@ async function runReplaceTextUnchecked(doc, args) {
1052
1061
  const preview = await previewMutations(doc, candidates.map((candidate) => candidate.step), args.changeMode, pre.revision);
1053
1062
  if (preview.valid === true)
1054
1063
  break;
1055
- const failures = Array.isArray(preview.failures) ? preview.failures.filter(isRecord) : [];
1064
+ const failures = Array.isArray(preview.failures) ? preview.failures.filter(guards.isRecord) : [];
1056
1065
  const absent = failures.find((failure) => failure.code === 'MATCH_NOT_FOUND' && typeof failure.stepId === 'string');
1057
1066
  if (!absent) {
1058
1067
  throw new Error(typeof failures[0]?.message === 'string' ? failures[0].message : 'replacement plan could not be prepared');
@@ -1281,6 +1290,172 @@ async function runDeleteText(doc, args) {
1281
1290
  return failedReceipt('delete_text', err, pre);
1282
1291
  }
1283
1292
  }
1293
+ const CONJUNCTION_TAIL = /[;,]\s*(and|or)\s*$/i;
1294
+ const ITEM_ENDING = /(?:[;,]\s*(?:and|or)|[.;,:])\s*$/i;
1295
+ const ITEM_CLOSERS = /["'”’)\]]+$/;
1296
+ function enumerationSlotOf(block) {
1297
+ const numbering = blockNumbering(block);
1298
+ const path = numbering?.path;
1299
+ if (Array.isArray(path) && path.length > 0) {
1300
+ return { key: `scheme:${path.slice(0, -1).join('.')}:${path.length}`, depth: path.length, typed: null };
1301
+ }
1302
+ const markerShape = numbering?.marker ? markerShapeOf(numbering.marker) : null;
1303
+ if (markerShape)
1304
+ return { key: `scheme-marker:${markerShape.wrapper}:${markerShape.kind}`, depth: null, typed: null };
1305
+ const text = block.text || block.textPreview || '';
1306
+ const typedMarker = typedMarkerPrefixOf(text)?.marker;
1307
+ const typedShape = typedMarker ? markerShapeOf(text) : null;
1308
+ if (!typedMarker || !typedShape)
1309
+ return null;
1310
+ const parent = typedShape.kind === 'digit' ? typedDigitParentOf(typedMarker) : '';
1311
+ return { key: `typed:${typedShape.wrapper}:${typedShape.kind}:${parent}`, depth: null, typed: typedShape };
1312
+ }
1313
+ function typedDigitParentOf(marker) {
1314
+ const segments = marker.replace(/^\(/, '').replace(/[.)]$/, '').split('.');
1315
+ return `${segments.length}:${segments.slice(0, -1).join('.')}`;
1316
+ }
1317
+ const ROMAN_DIGITS = { i: 1, v: 5, x: 10 };
1318
+ function markerValueOf(token, shape) {
1319
+ const core = token.replace(/^\(/, '').replace(/[.)]$/, '').toLowerCase();
1320
+ if (shape.kind === 'digit')
1321
+ return Number(core.split('.').pop());
1322
+ if (shape.kind === 'lower-roman' || shape.kind === 'upper-roman') {
1323
+ let total = 0;
1324
+ for (let i = 0; i < core.length; i += 1) {
1325
+ const value = ROMAN_DIGITS[core[i]] ?? 0;
1326
+ total += value < (ROMAN_DIGITS[core[i + 1] ?? ''] ?? 0) ? -value : value;
1327
+ }
1328
+ return total;
1329
+ }
1330
+ const letter = core.charCodeAt(0) - 96;
1331
+ return core.length === 1 ? letter : 26 * (core.length - 1) + letter;
1332
+ }
1333
+ function enumerationRunOf(blocks, index, slot, textOf) {
1334
+ const typed = slot.typed;
1335
+ const valueOf = (block) => {
1336
+ if (!typed) {
1337
+ const path = blockNumbering(block)?.path;
1338
+ return Array.isArray(path) && path.length > 0 ? Number(path[path.length - 1]) : null;
1339
+ }
1340
+ const token = typed ? typedMarkerPrefixOf(textOf(block))?.marker : null;
1341
+ return token && typed ? markerValueOf(token, typed) : null;
1342
+ };
1343
+ const walk = (step) => {
1344
+ const found = [];
1345
+ let edge = valueOf(blocks[index]);
1346
+ let crossed = false;
1347
+ for (let i = index + step; i >= 0 && i < blocks.length; i += step) {
1348
+ const block = blocks[i];
1349
+ if (textOf(block).length === 0)
1350
+ continue;
1351
+ const other = enumerationSlotOf(block);
1352
+ const continuesAsLetter = typed != null &&
1353
+ other?.typed != null &&
1354
+ markerShapeFits(typed, other.typed) &&
1355
+ edge != null &&
1356
+ valueOf(block) === edge + step;
1357
+ if (other?.key === slot.key || continuesAsLetter) {
1358
+ const value = valueOf(block);
1359
+ if (value != null && edge != null) {
1360
+ const advance = (value - edge) * step;
1361
+ if (advance <= 0 || ((crossed || !typed) && advance !== 1))
1362
+ break;
1363
+ }
1364
+ found.push(block);
1365
+ edge = value;
1366
+ crossed = false;
1367
+ continue;
1368
+ }
1369
+ if (typed && other?.typed) {
1370
+ if (step === -1 && edge != null) {
1371
+ const prefix = typedMarkerPrefixOf(textOf(block)).prefix;
1372
+ const inline = new RegExp(String.raw `(?:^|\s)(${TYPED_MARKER_TOKEN}) `).exec(textOf(block).slice(prefix.length));
1373
+ const inlineShape = inline ? markerShapeOf(inline[1]) : null;
1374
+ if (inline && sameMarkerShape(inlineShape, typed) && markerValueOf(inline[1], typed) === edge - 1) {
1375
+ found.push(block);
1376
+ break;
1377
+ }
1378
+ }
1379
+ crossed = true;
1380
+ continue;
1381
+ }
1382
+ if (slot.depth != null && other?.depth != null && other.depth > slot.depth)
1383
+ continue;
1384
+ break;
1385
+ }
1386
+ return found;
1387
+ };
1388
+ return [...walk(-1).reverse(), blocks[index], ...walk(1)];
1389
+ }
1390
+ function listPunctuationAfterDelete(blocks, deletedIds) {
1391
+ const textOf = (block) => (block.text || (block.textPreview ?? '').replace(/…\s*$/, '')).trimEnd();
1392
+ const endingOf = (block) => {
1393
+ const text = textOf(block);
1394
+ const closers = ITEM_CLOSERS.exec(text)?.[0] ?? '';
1395
+ const body = text.slice(0, text.length - closers.length).trimEnd();
1396
+ const ending = ITEM_ENDING.exec(body);
1397
+ return {
1398
+ core: ending?.[0].trim() ?? '',
1399
+ real: ending ? `${ending[0].trim()}${closers}` : closers,
1400
+ closers,
1401
+ conjunction: CONJUNCTION_TAIL.exec(body)?.[0].trim() ?? null,
1402
+ };
1403
+ };
1404
+ const quote = (block) => {
1405
+ const text = textOf(block);
1406
+ return JSON.stringify(text.length > 40 ? `...${text.slice(-40)}` : text);
1407
+ };
1408
+ const change = (block, to) => {
1409
+ const ending = endingOf(block);
1410
+ return `change the ending of ${quote(block)} (nodeId ${block.nodeId}) from "${ending.real}" to "${to}${ending.closers}"`;
1411
+ };
1412
+ const entries = [];
1413
+ const covered = new Set();
1414
+ blocks.forEach((block, index) => {
1415
+ if (!deletedIds.has(block.nodeId) || covered.has(block.nodeId))
1416
+ return;
1417
+ const slot = enumerationSlotOf(block);
1418
+ if (!slot)
1419
+ return;
1420
+ const run = enumerationRunOf(blocks, index, slot, textOf);
1421
+ for (const item of run)
1422
+ covered.add(item.nodeId);
1423
+ const survivors = run.filter((item) => !deletedIds.has(item.nodeId));
1424
+ if (survivors.length === 0)
1425
+ return;
1426
+ const last = survivors[survivors.length - 1];
1427
+ const penultimate = survivors.length >= 2 ? survivors[survivors.length - 2] : undefined;
1428
+ const originalPenultimate = run[run.length - 2];
1429
+ if (run.some((item) => item !== originalPenultimate && endingOf(item).conjunction))
1430
+ return;
1431
+ const runEnd = run[run.length - 1];
1432
+ if (!deletedIds.has(runEnd.nodeId))
1433
+ return;
1434
+ const tail = originalPenultimate ? endingOf(originalPenultimate).conjunction : null;
1435
+ const separator = tail ? tail.charAt(0) : ';';
1436
+ const closing = endingOf(runEnd).core || '.';
1437
+ const lastEnding = endingOf(last);
1438
+ if (lastEnding.core === ':')
1439
+ return;
1440
+ const needsClose = lastEnding.core !== closing && (lastEnding.conjunction != null || lastEnding.core === separator);
1441
+ const needsTail = penultimate != null && tail != null && endingOf(penultimate).core !== ':' && !endingOf(penultimate).conjunction;
1442
+ if (!needsClose && !(needsTail && lastEnding.core !== closing))
1443
+ return;
1444
+ const edits = [
1445
+ `end ${quote(last)} (nodeId ${last.nodeId}) with "${closing}${lastEnding.closers}" instead of "${lastEnding.real}"`,
1446
+ ];
1447
+ if (needsTail)
1448
+ edits.push(change(penultimate, tail));
1449
+ entries.push({
1450
+ deleted: runEnd.nodeId,
1451
+ lastItem: last.nodeId,
1452
+ lastItemEnds: lastEnding.real,
1453
+ ...(penultimate ? { penultimate: penultimate.nodeId, penultimateEnds: endingOf(penultimate).real } : {}),
1454
+ suggestion: `The run now ends at ${quote(last)}: ${edits.join(', and ')}, using replace_text.`,
1455
+ });
1456
+ });
1457
+ return entries;
1458
+ }
1284
1459
  /** Block node types `doc.blocks.delete` removes as a whole paragraph-shaped block. */
1285
1460
  const DELETABLE_BLOCK_NODE_TYPES = new Set(['paragraph', 'heading', 'listItem']);
1286
1461
  /**
@@ -1450,6 +1625,11 @@ async function runDeleteBlocks(doc, args) {
1450
1625
  // an error, so the `errors.length` term still catches a short request.
1451
1626
  const allApplied = deleted.length === targets.length && errors$1.length === 0;
1452
1627
  const verified = verification.every((v) => v.passed);
1628
+ let listPunctuation = [];
1629
+ try {
1630
+ listPunctuation = listPunctuationAfterDelete(pre.blocks, new Set(deleted.map((entry) => entry.nodeId)));
1631
+ }
1632
+ catch { }
1453
1633
  return {
1454
1634
  status: !verified || deleted.length === 0 ? 'failed' : allApplied ? 'ok' : 'partial',
1455
1635
  intent: `delete_blocks: ${deleted.length} block(s)`,
@@ -1459,6 +1639,7 @@ async function runDeleteBlocks(doc, args) {
1459
1639
  executedOperations,
1460
1640
  verification,
1461
1641
  deletedBlocks: deleted,
1642
+ ...(listPunctuation.length > 0 ? { listPunctuation } : {}),
1462
1643
  ...(errors$1.length ? { errors: errors$1 } : {}),
1463
1644
  ...(errors$1.length
1464
1645
  ? { nextStep: 'Re-inspect the document and retry the selectors that did not resolve to one block.' }
@@ -2713,7 +2894,7 @@ async function listAllTrackedChanges(listFn) {
2713
2894
  const page = asRecord(await listFn({ offset, limit: PAGE, in: 'all' }));
2714
2895
  const items = Array.isArray(page?.items) ? page.items : [];
2715
2896
  for (const item of items) {
2716
- if (isRecord(item))
2897
+ if (guards.isRecord(item))
2717
2898
  out.push(item);
2718
2899
  }
2719
2900
  const total = asNumber(page?.total, out.length) ?? out.length;
@@ -4290,12 +4471,12 @@ function findTextTarget(snapshot, nodeId) {
4290
4471
  return null;
4291
4472
  }
4292
4473
  function compactOpResult(result) {
4293
- if (!isRecord(result))
4474
+ if (!guards.isRecord(result))
4294
4475
  return result;
4295
4476
  const compact = {};
4296
4477
  if (typeof result.success === 'boolean')
4297
4478
  compact.success = result.success;
4298
- if (isRecord(result.revision))
4479
+ if (guards.isRecord(result.revision))
4299
4480
  compact.revision = result.revision;
4300
4481
  if (Array.isArray(result.steps))
4301
4482
  compact.stepCount = result.steps.length;
@@ -4311,7 +4492,7 @@ function trackedChangeRefIds(result) {
4311
4492
  }));
4312
4493
  }
4313
4494
  function parseScopedReplaceArgs(args, edits, changeMode, expectedRevision) {
4314
- if (!isRecord(args.selector))
4495
+ if (!guards.isRecord(args.selector))
4315
4496
  return null;
4316
4497
  return {
4317
4498
  edits,
@@ -4341,9 +4522,9 @@ async function fieldResultRanges(doc, nodeId, blockText) {
4341
4522
  const limit = 500;
4342
4523
  for (let offset = 0; offset < 100_000; offset += limit) {
4343
4524
  const page = await fn({ limit, offset });
4344
- const items = isRecord(page) && Array.isArray(page.items) ? page.items.filter(isRecord) : [];
4525
+ const items = guards.isRecord(page) && Array.isArray(page.items) ? page.items.filter(guards.isRecord) : [];
4345
4526
  for (const item of items) {
4346
- const address = isRecord(item.address) ? item.address : null;
4527
+ const address = guards.isRecord(item.address) ? item.address : null;
4347
4528
  const inBody = typeof address?.storyId !== 'string' || address.storyId.startsWith('main:');
4348
4529
  if (address?.blockId !== nodeId || !inBody)
4349
4530
  continue;
@@ -4352,7 +4533,7 @@ async function fieldResultRanges(doc, nodeId, blockText) {
4352
4533
  }
4353
4534
  results.push(item.resolvedText);
4354
4535
  }
4355
- const total = isRecord(page) && typeof page.total === 'number' ? page.total : null;
4536
+ const total = guards.isRecord(page) && typeof page.total === 'number' ? page.total : null;
4356
4537
  if (items.length < limit || (total != null && offset + limit >= total))
4357
4538
  break;
4358
4539
  }
@@ -4403,8 +4584,9 @@ async function runScopedReplaceUnchecked(doc, args) {
4403
4584
  occurrences: spans.length,
4404
4585
  ...(edit.occurrence != null ? { occurrence: edit.occurrence } : {}),
4405
4586
  });
4406
- for (const span of spans)
4407
- allSpans.push({ ...span, replace: edit.replace, find: edit.find });
4587
+ for (const span of spans) {
4588
+ allSpans.push({ ...span, replace: edit.replace, find: edit.find, editIndex: perEdit.length - 1 });
4589
+ }
4408
4590
  }
4409
4591
  const appliedEdits = perEdit.filter((e) => e.occurrences > 0);
4410
4592
  const skippedEdits = perEdit.filter((e) => e.occurrences === 0);
@@ -4441,6 +4623,23 @@ async function runScopedReplaceUnchecked(doc, args) {
4441
4623
  planned.push(span);
4442
4624
  lastEnd = span.end;
4443
4625
  }
4626
+ planned.forEach((span, at) => {
4627
+ if (span.replace.trim().length > 0)
4628
+ return;
4629
+ const text = block.text;
4630
+ if (text.charAt(span.start - 1) !== ' ' || text.charAt(span.end) !== ' ')
4631
+ return;
4632
+ if (text.charAt(span.start - 2) === ' ' || text.charAt(span.end + 1) === ' ')
4633
+ return;
4634
+ if (/[.!?]["'\u201d\u2019)\]]*$/.test(text.slice(0, span.start - 1)))
4635
+ return;
4636
+ if ((planned[at + 1]?.start ?? Infinity) <= span.end || (planned[at - 1]?.end ?? -1) >= span.start - 1)
4637
+ return;
4638
+ if (span.replace.length > 0)
4639
+ span.start -= 1;
4640
+ span.end += 1;
4641
+ perEdit[span.editIndex].collapsedSpace = true;
4642
+ });
4444
4643
  // The block text once every planned span is replaced: the whole-block
4445
4644
  // rewrite writes it, and exact-target writes must produce it.
4446
4645
  const expected = planned.reduceRight((text, span) => text.slice(0, span.start) + span.replace + text.slice(span.end), block.text);
@@ -4493,11 +4692,11 @@ async function runScopedReplaceUnchecked(doc, args) {
4493
4692
  // that each one still covers the text matched in the snapshot, so a
4494
4693
  // projection mismatch never rewrites other text.
4495
4694
  const preview = await previewMutations(doc, steps, args.changeMode, pre.revision);
4496
- const previewSteps = Array.isArray(preview.steps) ? preview.steps.filter(isRecord) : [];
4695
+ const previewSteps = Array.isArray(preview.steps) ? preview.steps.filter(guards.isRecord) : [];
4497
4696
  const aligned = preview.valid === true &&
4498
4697
  steps.every((step, index) => {
4499
4698
  const resolutions = previewSteps.find((candidate) => candidate.stepId === step.id)?.resolutions;
4500
- const resolved = Array.isArray(resolutions) && isRecord(resolutions[0]) ? resolutions[0].text : undefined;
4699
+ const resolved = Array.isArray(resolutions) && guards.isRecord(resolutions[0]) ? resolutions[0].text : undefined;
4501
4700
  const span = targetedSpans[index];
4502
4701
  return resolved === block.text.slice(span.start, span.end);
4503
4702
  });
@@ -4530,7 +4729,7 @@ async function runScopedReplaceUnchecked(doc, args) {
4530
4729
  // atomic history step. Only do so while the document is still at the
4531
4730
  // revision this plan produced, so the undo cannot reach any other change.
4532
4731
  let rolledBack = false;
4533
- const appliedRevision = isRecord(result) && isRecord(result.revision) ? result.revision.after : undefined;
4732
+ const appliedRevision = guards.isRecord(result) && guards.isRecord(result.revision) ? result.revision.after : undefined;
4534
4733
  if (wroteExactTargets &&
4535
4734
  postBlock != null &&
4536
4735
  postBlock.text !== expected &&
@@ -4645,13 +4844,13 @@ function markRunPropsFromMarks(marks) {
4645
4844
  * paragraphIndentOf. Returns null when the paragraph mark carries nothing.
4646
4845
  */
4647
4846
  function paragraphMarkRunPropsOf(payload) {
4648
- const node = isRecord(payload) ? payload.node : null;
4649
- if (!isRecord(node))
4847
+ const node = guards.isRecord(payload) ? payload.node : null;
4848
+ if (!guards.isRecord(node))
4650
4849
  return null;
4651
- const candidates = [isRecord(node.paragraph) ? node.paragraph.props : null, node.props];
4850
+ const candidates = [guards.isRecord(node.paragraph) ? node.paragraph.props : null, node.props];
4652
4851
  for (const props of candidates) {
4653
- const mark = isRecord(props) ? props.markRunProps : null;
4654
- if (isRecord(mark) && Object.keys(mark).length > 0)
4852
+ const mark = guards.isRecord(props) ? props.markRunProps : null;
4853
+ if (guards.isRecord(mark) && Object.keys(mark).length > 0)
4655
4854
  return mark;
4656
4855
  }
4657
4856
  return null;
@@ -4727,6 +4926,8 @@ function leadInShapeOf(blockText, labelText, quote) {
4727
4926
  }
4728
4927
  /** The span of `text` that carries the label look, or null when it has none. */
4729
4928
  function leadInSpanOf(text, shape) {
4929
+ if (shape.kind === 'typed')
4930
+ return typedCaptionSpanOf(text, shape.delimiter);
4730
4931
  if (shape.kind === 'word') {
4731
4932
  // Only the SAME token inherits the look — "WHEREAS the Parties…" does,
4732
4933
  // "The Parties further agree…" does not (no guessing a label there).
@@ -4761,6 +4962,81 @@ function leadInSpanOf(text, shape) {
4761
4962
  const end = leadInEndOf(text, shape.delimiter);
4762
4963
  return end == null ? null : { start: 0, end };
4763
4964
  }
4965
+ const TYPED_MARKER_CORE = String.raw `(?:\d{1,3}(?:\.\d{1,3})*|[ivx]{1,6}|[IVX]{1,6}|[a-z]{1,2}|[A-Z])`;
4966
+ const TYPED_MARKER_TOKEN = String.raw `(?:\(${TYPED_MARKER_CORE}\)|${TYPED_MARKER_CORE}[.)])`;
4967
+ const TYPED_MARKER_PREFIX = new RegExp(String.raw `^[ \t]*(${TYPED_MARKER_TOKEN})\t[\t ]*`);
4968
+ const LEADING_MARKER_TOKEN = new RegExp(String.raw `^\s*(${TYPED_MARKER_TOKEN})(?=\s|$)`);
4969
+ function typedMarkerPrefixOf(text) {
4970
+ const match = TYPED_MARKER_PREFIX.exec(text);
4971
+ return match ? { prefix: match[0], marker: match[1] } : null;
4972
+ }
4973
+ function markerShapeOf(text) {
4974
+ const match = LEADING_MARKER_TOKEN.exec(text);
4975
+ if (!match)
4976
+ return null;
4977
+ const token = match[1];
4978
+ const wrapper = token.startsWith('(') ? 'paren' : token.endsWith('.') ? 'period' : 'close';
4979
+ const core = token.replace(/^\(/, '').replace(/[.)]$/, '');
4980
+ let kind;
4981
+ if (/^\d/.test(core))
4982
+ kind = 'digit';
4983
+ else if (/^[ivx]+$/.test(core))
4984
+ kind = 'lower-roman';
4985
+ else if (/^[IVX]+$/.test(core))
4986
+ kind = 'upper-roman';
4987
+ else if (/^[a-z]+$/.test(core))
4988
+ kind = 'lower-alpha';
4989
+ else if (/^[A-Z]+$/.test(core))
4990
+ kind = 'upper-alpha';
4991
+ else
4992
+ return null;
4993
+ return { wrapper, kind };
4994
+ }
4995
+ function sameMarkerShape(a, b) {
4996
+ return a != null && b != null && a.wrapper === b.wrapper && a.kind === b.kind;
4997
+ }
4998
+ function markerShapeFits(reference, created) {
4999
+ if (!reference || reference.wrapper !== created.wrapper)
5000
+ return false;
5001
+ if (reference.kind === created.kind)
5002
+ return true;
5003
+ return ((created.kind === 'lower-roman' && reference.kind === 'lower-alpha') ||
5004
+ (created.kind === 'upper-roman' && reference.kind === 'upper-alpha'));
5005
+ }
5006
+ function continuesAlphabet(referenceText, createdText, referenceBefore) {
5007
+ const letterOf = (text) => /^[a-zA-Z]$/.exec(LEADING_MARKER_TOKEN.exec(text)?.[1]?.replace(/[().]/g, '') ?? '')?.[0];
5008
+ const reference = letterOf(referenceText);
5009
+ const created = letterOf(createdText);
5010
+ return (reference != null &&
5011
+ created != null &&
5012
+ created.charCodeAt(0) - reference.charCodeAt(0) === (referenceBefore ? 1 : -1));
5013
+ }
5014
+ const TYPED_CAPTION_MAX_CHARS = 60;
5015
+ function isTitleLike(caption) {
5016
+ const words = caption.match(/\p{L}+/gu) ?? [];
5017
+ const capitalised = words.filter((word) => /^\p{Lu}/u.test(word)).length;
5018
+ return (words.length > 0 &&
5019
+ words.every((word) => word.length < 4 || /^\p{Lu}/u.test(word)) &&
5020
+ capitalised * 2 >= words.length);
5021
+ }
5022
+ function typedCaptionSpanOf(text, delimiter = '.') {
5023
+ const typed = typedMarkerPrefixOf(text);
5024
+ const mark = delimiter.trim();
5025
+ if (!typed || !mark)
5026
+ return null;
5027
+ const start = typed.prefix.length;
5028
+ for (let at = text.indexOf(mark, start); at >= 0; at = text.indexOf(mark, at + 1)) {
5029
+ const end = at + mark.length;
5030
+ if (end - start > TYPED_CAPTION_MAX_CHARS)
5031
+ return null;
5032
+ if (at === start || !/\s/.test(text.charAt(end)))
5033
+ continue;
5034
+ if (!isTitleLike(text.slice(start, at)))
5035
+ return null;
5036
+ return text.slice(end).trim().length > 0 ? { start, end } : null;
5037
+ }
5038
+ return null;
5039
+ }
4764
5040
  /**
4765
5041
  * Cut `text` at `end` without splitting a surrogate pair: half an emoji is not
4766
5042
  * document text, and `query.match` finds nothing when handed a lone surrogate —
@@ -4837,7 +5113,10 @@ async function listItemRunPattern(doc, text, blockId) {
4837
5113
  // Window: through the label's delimiter plus enough of the sentence to sample
4838
5114
  // it, or a plain head when the item carries no label at all.
4839
5115
  const delimiterAt = leadInEndOf(text);
4840
- const windowEnd = delimiterAt != null ? delimiterAt + LEAD_IN_BODY_SAMPLE_CHARS : LEAD_IN_BODY_SAMPLE_CHARS;
5116
+ let windowEnd = delimiterAt != null ? delimiterAt + LEAD_IN_BODY_SAMPLE_CHARS : LEAD_IN_BODY_SAMPLE_CHARS;
5117
+ const typedCaption = typedCaptionSpanOf(text);
5118
+ if (typedCaption)
5119
+ windowEnd = Math.max(windowEnd, typedCaption.end + LEAD_IN_BODY_SAMPLE_CHARS);
4841
5120
  const pattern = sliceWholeChars(text, windowEnd);
4842
5121
  const result = asRecord(await queryFn({ select: { type: 'text', pattern } }));
4843
5122
  const items = Array.isArray(result?.items) ? result.items : [];
@@ -4851,6 +5130,9 @@ async function listItemRunPattern(doc, text, blockId) {
4851
5130
  const runs = (Array.isArray(rec?.runs) ? rec.runs : []).filter((r) => runTextOf(r).length > 0);
4852
5131
  if (runs.length === 0)
4853
5132
  continue;
5133
+ const typedLead = typedPrefixLabelOf(runs, runTextOf);
5134
+ if (typedLead)
5135
+ return typedLead;
4854
5136
  const blockText = asString(rec?.text, '') ?? '';
4855
5137
  // A quoted defined term keeps its opening quote in a run of its own —
4856
5138
  // `"` + **Deliverables** + `" means …` — so the label is the SECOND run
@@ -4886,6 +5168,59 @@ async function listItemRunPattern(doc, text, blockId) {
4886
5168
  }
4887
5169
  return null;
4888
5170
  }
5171
+ function typedPrefixLabelOf(runs, runTextOf) {
5172
+ const typed = typedMarkerPrefixOf(runs.map(runTextOf).join(''));
5173
+ if (!typed)
5174
+ return null;
5175
+ const markerEnd = typed.prefix.indexOf(typed.marker) + typed.marker.length;
5176
+ let covered = 0;
5177
+ let labelIndex = 0;
5178
+ while (labelIndex < runs.length && covered + runTextOf(runs[labelIndex]).length <= typed.prefix.length) {
5179
+ covered += runTextOf(runs[labelIndex]).length;
5180
+ labelIndex += 1;
5181
+ }
5182
+ if (covered < markerEnd || labelIndex + 1 >= runs.length)
5183
+ return null;
5184
+ const underlined = (run) => asRecord(asRecord(asRecord(run)?.styles)?.effective)?.underline === true;
5185
+ const emphasisOf = (run) => {
5186
+ const style = matchRunStyle(run);
5187
+ return `${style.bold}/${style.italic}/${underlined(run)}`;
5188
+ };
5189
+ const label = runs[labelIndex];
5190
+ if (emphasisOf(label) === 'false/false/false')
5191
+ return null;
5192
+ const isBlank = (run) => runTextOf(run).trim().length === 0;
5193
+ const continuesLabel = (index) => {
5194
+ if (emphasisOf(runs[index]) === emphasisOf(label))
5195
+ return true;
5196
+ let next = index;
5197
+ while (next < runs.length && isBlank(runs[next]))
5198
+ next += 1;
5199
+ return next > index && next < runs.length && emphasisOf(runs[next]) === emphasisOf(label);
5200
+ };
5201
+ let bodyIndex = labelIndex + 1;
5202
+ while (bodyIndex < runs.length && continuesLabel(bodyIndex))
5203
+ bodyIndex += 1;
5204
+ while (bodyIndex < runs.length - 1 && isBlank(runs[bodyIndex]))
5205
+ bodyIndex += 1;
5206
+ if (bodyIndex >= runs.length)
5207
+ return null;
5208
+ const labelText = runs.slice(labelIndex, bodyIndex).map(runTextOf).join('').trim();
5209
+ if (labelText.length === 0 || labelText.length > LEAD_IN_MAX_CHARS)
5210
+ return null;
5211
+ const delimiter = ['.', ...LEAD_IN_DELIMITERS.map((d) => d.trim())].find((d) => labelText.endsWith(d) && labelText.length > d.length);
5212
+ if (!delimiter)
5213
+ return null;
5214
+ const body = runs[bodyIndex];
5215
+ const leadStyle = matchRunStyle(label);
5216
+ const bodyStyle = matchRunStyle(body);
5217
+ const leadUnderline = underlined(label);
5218
+ const bodyUnderline = underlined(body);
5219
+ const look = inlineFromMatchStyle(leadStyle, bodyStyle);
5220
+ if (leadUnderline || bodyUnderline)
5221
+ look.underline = leadUnderline;
5222
+ return { body: inlineFromMatchStyle(bodyStyle), lead: { look, shape: { kind: 'typed', delimiter } } };
5223
+ }
4889
5224
  /**
4890
5225
  * Pull a paragraph's direct indentation out of a `getNodeById` payload and
4891
5226
  * convert it to the twips that `format.paragraph.setIndentation` expects.
@@ -4896,13 +5231,13 @@ async function listItemRunPattern(doc, text, blockId) {
4896
5231
  * indent copied" rather than a thrown read.
4897
5232
  */
4898
5233
  function paragraphIndentOf(payload) {
4899
- const node = isRecord(payload) ? payload.node : null;
4900
- if (!isRecord(node))
5234
+ const node = guards.isRecord(payload) ? payload.node : null;
5235
+ if (!guards.isRecord(node))
4901
5236
  return null;
4902
- const candidates = [isRecord(node.paragraph) ? node.paragraph.props : null, node.props];
5237
+ const candidates = [guards.isRecord(node.paragraph) ? node.paragraph.props : null, node.props];
4903
5238
  for (const props of candidates) {
4904
- const indent = isRecord(props) ? props.indent : null;
4905
- if (!isRecord(indent))
5239
+ const indent = guards.isRecord(props) ? props.indent : null;
5240
+ if (!guards.isRecord(indent))
4906
5241
  continue;
4907
5242
  const twips = {};
4908
5243
  for (const key of ['left', 'right', 'firstLine', 'hanging']) {
@@ -4979,7 +5314,7 @@ function marksToInline(marks) {
4979
5314
  }
4980
5315
  /** Parse a raw agent mark bag, keeping only recognized keys with valid types. */
4981
5316
  function normalizeMarks(raw) {
4982
- if (!isRecord(raw))
5317
+ if (!guards.isRecord(raw))
4983
5318
  return undefined;
4984
5319
  const m = {};
4985
5320
  if (typeof raw.bold === 'boolean')
@@ -5009,7 +5344,7 @@ function normalizeMarks(raw) {
5009
5344
  function normalizeItemSpec(raw) {
5010
5345
  if (typeof raw === 'string')
5011
5346
  return { text: raw, level: 0 };
5012
- if (!isRecord(raw))
5347
+ if (!guards.isRecord(raw))
5013
5348
  return null;
5014
5349
  const spec = {};
5015
5350
  const lvl = asNumber(raw.level);
@@ -5023,7 +5358,7 @@ function normalizeItemSpec(raw) {
5023
5358
  if (typeof r === 'string') {
5024
5359
  runs.push({ text: r });
5025
5360
  }
5026
- else if (isRecord(r) && typeof r.text === 'string') {
5361
+ else if (guards.isRecord(r) && typeof r.text === 'string') {
5027
5362
  const rm = normalizeMarks(r.marks);
5028
5363
  runs.push(rm ? { text: r.text, marks: rm } : { text: r.text });
5029
5364
  }
@@ -5642,9 +5977,9 @@ async function appendListAtPlacement(doc, args) {
5642
5977
  const items = Array.isArray(args.items)
5643
5978
  ? args.items.filter((t) => typeof t === 'string' && t.length > 0)
5644
5979
  : [];
5645
- const placement = isRecord(args.placement) ? args.placement : null;
5980
+ const placement = guards.isRecord(args.placement) ? args.placement : null;
5646
5981
  const at = placement && typeof placement.at === 'string' ? placement.at : null;
5647
- const selector = placement && isRecord(placement.selector) ? placement.selector : null;
5982
+ const selector = placement && guards.isRecord(placement.selector) ? placement.selector : null;
5648
5983
  if (items.length === 0 || !placement || (at !== 'after' && at !== 'before') || !selector)
5649
5984
  return null;
5650
5985
  const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
@@ -5764,6 +6099,7 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
5764
6099
  }
5765
6100
  if (!reference)
5766
6101
  return null;
6102
+ reference = typedShapeReference(postBlocks, preIds, created, createdIndex, createdText, reference, compatibleHeading);
5767
6103
  const applied = {};
5768
6104
  // Numbering: when the reference participates in a numbering scheme and
5769
6105
  // the new block does not, attach it to the same sequence — this is what
@@ -5789,7 +6125,9 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
5789
6125
  }
5790
6126
  }
5791
6127
  if (reference.styleId && reference.styleId !== created.styleId) {
5792
- const setStyleFn = maybeMethod(doc, ['styles', 'paragraph', 'setStyle']);
6128
+ const keepRunMarks = !allowLeadPattern;
6129
+ const setStyleFn = (keepRunMarks ? maybeMethod(doc, ['styles', 'paragraph', 'setStyleRef']) : null) ??
6130
+ maybeMethod(doc, ['styles', 'paragraph', 'setStyle']);
5793
6131
  if (setStyleFn) {
5794
6132
  const target = { kind: 'block', nodeType: created.nodeType, nodeId: created.nodeId };
5795
6133
  let canCopyStyle = true;
@@ -5892,6 +6230,42 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
5892
6230
  return { matchedSibling: null, skipped: reasonOf(err) };
5893
6231
  }
5894
6232
  }
6233
+ function typedShapeReference(postBlocks, preIds, created, createdIndex, createdText, reference, compatible) {
6234
+ const typed = createdText ? typedMarkerPrefixOf(createdText) : null;
6235
+ if (!typed || createdText.slice(typed.prefix.length).trim().length === 0)
6236
+ return reference;
6237
+ const shape = markerShapeOf(createdText);
6238
+ const referenceShape = markerShapeOf(reference.textPreview ?? '');
6239
+ if (!shape || sameMarkerShape(referenceShape, shape))
6240
+ return reference;
6241
+ const usable = (row) => !!row && preIds.has(row.nodeId) && row.nodeType === created.nodeType && !row.isEmpty && compatible(row);
6242
+ const before = postBlocks.slice(0, createdIndex).reverse().find(usable);
6243
+ const after = postBlocks.slice(createdIndex + 1).find(usable);
6244
+ const continued = [reference, before, after].find((row) => !!row &&
6245
+ markerShapeFits(markerShapeOf(row.textPreview ?? ''), shape) &&
6246
+ continuesAlphabet(row.textPreview ?? '', createdText, postBlocks.findIndex((b) => b.nodeId === row.nodeId) < createdIndex));
6247
+ if (continued)
6248
+ return continued;
6249
+ const candidate = (row) => {
6250
+ if (!usable(row))
6251
+ return null;
6252
+ const preview = row.textPreview ?? '';
6253
+ return typedMarkerPrefixOf(preview) ? markerShapeOf(preview) : null;
6254
+ };
6255
+ const nearest = (accepts) => {
6256
+ for (let distance = 1; distance < postBlocks.length; distance += 1) {
6257
+ for (const index of [createdIndex - distance, createdIndex + distance]) {
6258
+ const row = postBlocks[index];
6259
+ if (index >= 0 && accepts(candidate(row)))
6260
+ return row;
6261
+ }
6262
+ }
6263
+ return null;
6264
+ };
6265
+ return (nearest((rowShape) => sameMarkerShape(rowShape, shape)) ??
6266
+ nearest((rowShape) => markerShapeFits(rowShape, shape)) ??
6267
+ reference);
6268
+ }
5895
6269
  /** A lead-in marker token like "(m)", "1.", "a)", "(iv)" at the start of clause text. */
5896
6270
  const LEAD_MARKER_PATTERN = /^\(?(?:[a-zA-Z]{1,3}|\d{1,3})[).:\]]/;
5897
6271
  /**
@@ -6022,7 +6396,8 @@ async function matchBlockRunPattern(doc, reference, created, createdText) {
6022
6396
  const span = leadInSpanOf(fullText, pattern.lead.shape);
6023
6397
  // A uniform reference has no body look and no delimiter of its own: its look
6024
6398
  // belongs to the whole created block, not to a label span inside it.
6025
- const range = span ?? (Object.keys(pattern.body).length === 0 ? { start: 0, end } : null);
6399
+ const range = span ??
6400
+ (Object.keys(pattern.body).length === 0 && pattern.lead.shape.kind !== 'typed' ? { start: 0, end } : null);
6026
6401
  if (range && (await applyInlineQuiet(formatFn, { blockId: created.nodeId, ...range, inline: pattern.lead.look }))) {
6027
6402
  applied.lead = { ...pattern.lead.look, range: [range.start, range.end] };
6028
6403
  }
@@ -6042,15 +6417,15 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
6042
6417
  return receipt;
6043
6418
  // The placement anchor, when the action targeted one, is the user's own
6044
6419
  // 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;
6420
+ const placement = guards.isRecord(args?.placement) ? args.placement : null;
6421
+ const anchorSelector = placement && guards.isRecord(placement.selector) ? placement.selector : null;
6047
6422
  const anchorId = anchorSelector && typeof anchorSelector.nodeId === 'string' ? anchorSelector.nodeId : null;
6048
6423
  const anchorRow = anchorId ? (postBlocks.find((b) => b.nodeId === anchorId) ?? null) : null;
6049
6424
  // When the caller styled any entry explicitly (runs/marks), it took
6050
6425
  // ownership of intra-paragraph formatting — the automatic lead-marker
6051
6426
  // pattern pass must not fight it.
6052
- const rawTexts = Array.isArray(args?.texts) ? args.texts : [];
6053
- const hasExplicitStyling = rawTexts.some((entry) => isRecord(entry) && (Array.isArray(entry.runs) || entry.marks != null));
6427
+ const rawTexts = Array.isArray(args?.texts) ? args.texts : args?.text != null ? [args.text] : [];
6428
+ const hasExplicitStyling = rawTexts.some((entry) => guards.isRecord(entry) && (Array.isArray(entry.runs) || entry.marks != null));
6054
6429
  const insertedTexts = rawTexts.map((entry) => {
6055
6430
  if (typeof entry === 'string')
6056
6431
  return entry;
@@ -6075,7 +6450,7 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
6075
6450
  // the previous NAME line, not like the "Name:" line directly above it.
6076
6451
  // Only tried when matching against the neighbour found nothing, so this
6077
6452
  // cannot change a case that already had an answer.
6078
- const applied = isRecord(result?.applied) ? result.applied : null;
6453
+ const applied = guards.isRecord(result?.applied) ? result.applied : null;
6079
6454
  if (createdRows.length > 1 && (!applied || Object.keys(applied).length === 0)) {
6080
6455
  const parallel = postBlocks[createdIndex - createdRows.length];
6081
6456
  if (parallel && preIds.has(parallel.nodeId) && !parallel.isEmpty) {
@@ -6250,7 +6625,15 @@ async function runAttachNumbering(doc, args) {
6250
6625
  errors: [
6251
6626
  {
6252
6627
  code: 'TARGET_NOT_FOUND',
6253
- message: `no block renders marker ${JSON.stringify(likeMarker)}. Available: ${available.join(' ')}`,
6628
+ message: available.length > 0
6629
+ ? `no block renders marker ${JSON.stringify(likeMarker)}. Available: ${available.join(' ')}`
6630
+ : rows.some((r) => typedMarkerPrefixOf(r.textPreview ?? '') != null)
6631
+ ? `no block renders marker ${JSON.stringify(likeMarker)}: this document has no numbering scheme; its clause numbers are typed text. ` +
6632
+ `Copy the sibling's typed prefix (tab, next number, tab) into the paragraph text with insert_paragraphs or replace_text, ` +
6633
+ `then renumber the following typed markers with replace_text.`
6634
+ : `no block renders marker ${JSON.stringify(likeMarker)}: no block in this document carries numbering. ` +
6635
+ `To add a paragraph that matches its neighbours use insert_paragraphs next to them; ` +
6636
+ `to start a numbered list use append_list or convert_list.`,
6254
6637
  recovery: { kind: 'reinspect' },
6255
6638
  },
6256
6639
  ],
@@ -6291,7 +6674,7 @@ async function runAttachNumbering(doc, args) {
6291
6674
  };
6292
6675
  }
6293
6676
  async function dispatchSuperdocPerformAction(doc, args) {
6294
- if (!isRecord(args)) {
6677
+ if (!guards.isRecord(args)) {
6295
6678
  throw new errors.SuperDocCliError('superdoc_perform_action arguments must be an object', {
6296
6679
  code: 'INVALID_ARGUMENT',
6297
6680
  });
@@ -6365,7 +6748,7 @@ async function dispatchSuperdocPerformAction(doc, args) {
6365
6748
  if (expectedRevision !== undefined && (typeof expectedRevision !== 'string' || expectedRevision.length === 0)) {
6366
6749
  throw new errors.SuperDocCliError('replace_text: expectedRevision must be a non-empty string from this live document handle', { code: 'INVALID_ARGUMENT' });
6367
6750
  }
6368
- if (edits.some((edit) => edit.occurrence != null) && !isRecord(args.selector)) {
6751
+ if (edits.some((edit) => edit.occurrence != null) && !guards.isRecord(args.selector)) {
6369
6752
  throw new errors.SuperDocCliError('replace_text: explicit occurrence requires a selector resolving to one block', {
6370
6753
  code: 'INVALID_ARGUMENT',
6371
6754
  });
@@ -6995,13 +7378,13 @@ function parseExactTrackedChangeIdList(value) {
6995
7378
  }
6996
7379
  async function superdocPerformAction(doc, args) {
6997
7380
  try {
6998
- const changeMode = isRecord(args) ? parseChangeMode(args.changeMode) : undefined;
7381
+ const changeMode = guards.isRecord(args) ? parseChangeMode(args.changeMode) : undefined;
6999
7382
  return await dispatchSuperdocPerformAction(changeMode ? actionChangeMode.bindActionChangeMode(doc, changeMode) : doc, args);
7000
7383
  }
7001
7384
  catch (error) {
7002
7385
  if (!(error instanceof docSnapshot.MutationSnapshotError))
7003
7386
  throw error;
7004
- const action = isRecord(args) && isActionName(args.action) ? args.action : 'superdoc_perform_action';
7387
+ const action = guards.isRecord(args) && isActionName(args.action) ? args.action : 'superdoc_perform_action';
7005
7388
  return {
7006
7389
  status: 'failed',
7007
7390
  intent: action,
@@ -7062,7 +7445,7 @@ function parseEdits(value) {
7062
7445
  return undefined;
7063
7446
  const result = [];
7064
7447
  for (const entry of value) {
7065
- if (!isRecord(entry))
7448
+ if (!guards.isRecord(entry))
7066
7449
  return undefined;
7067
7450
  const find = asString(entry.find);
7068
7451
  const replace = asString(entry.replace) ?? '';
@@ -7082,7 +7465,7 @@ function parseEdits(value) {
7082
7465
  function parseCellCandidate(cell) {
7083
7466
  if (typeof cell === 'string')
7084
7467
  return cell;
7085
- if (isRecord(cell)) {
7468
+ if (guards.isRecord(cell)) {
7086
7469
  const spec = normalizeItemSpec(cell);
7087
7470
  if (spec)
7088
7471
  return { text: spec.text, runs: spec.runs, marks: spec.marks };
@@ -7110,7 +7493,7 @@ function parsePlaceholderFields(value) {
7110
7493
  return undefined;
7111
7494
  const result = [];
7112
7495
  for (const entry of value) {
7113
- if (!isRecord(entry) || typeof entry.value !== 'string')
7496
+ if (!guards.isRecord(entry) || typeof entry.value !== 'string')
7114
7497
  return undefined;
7115
7498
  const label = asString(entry.label);
7116
7499
  result.push(label ? { label, value: entry.value } : { value: entry.value });
@@ -7118,7 +7501,7 @@ function parsePlaceholderFields(value) {
7118
7501
  return result;
7119
7502
  }
7120
7503
  function parsePlacement(value) {
7121
- if (!isRecord(value))
7504
+ if (!guards.isRecord(value))
7122
7505
  return undefined;
7123
7506
  const at = asString(value.at);
7124
7507
  if (at === 'document_end' || at === 'document_start') {
@@ -7133,7 +7516,7 @@ function parsePlacement(value) {
7133
7516
  return undefined;
7134
7517
  }
7135
7518
  function parseSelector(value) {
7136
- if (!isRecord(value))
7519
+ if (!guards.isRecord(value))
7137
7520
  return undefined;
7138
7521
  const kind = asString(value.kind);
7139
7522
  if (kind === 'nodeId') {
@@ -7260,4 +7643,7 @@ exports.ACTION_NAMES_LIST = ACTION_NAMES_LIST;
7260
7643
  exports.getActionDispositions = getActionDispositions;
7261
7644
  exports.getUnavailableTrackedActions = getUnavailableTrackedActions;
7262
7645
  exports.isActionName = isActionName;
7646
+ exports.markerShapeOf = markerShapeOf;
7263
7647
  exports.superdocPerformAction = superdocPerformAction;
7648
+ exports.typedCaptionSpanOf = typedCaptionSpanOf;
7649
+ exports.typedMarkerPrefixOf = typedMarkerPrefixOf;