@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
@@ -491,6 +491,19 @@ export type AddHyperlinkArgs = {
491
491
  url: string;
492
492
  tooltip?: string;
493
493
  };
494
+ export declare function typedMarkerPrefixOf(text: string): {
495
+ prefix: string;
496
+ marker: string;
497
+ } | null;
498
+ export type MarkerShape = {
499
+ wrapper: 'paren' | 'period' | 'close';
500
+ kind: 'digit' | 'lower-roman' | 'lower-alpha' | 'upper-roman' | 'upper-alpha';
501
+ };
502
+ export declare function markerShapeOf(text: string): MarkerShape | null;
503
+ export declare function typedCaptionSpanOf(text: string, delimiter?: string): {
504
+ start: number;
505
+ end: number;
506
+ } | null;
494
507
  export declare function superdocPerformAction(doc: BoundDocApi, args: unknown): Promise<AgentReceipt>;
495
508
  export declare const ACTION_NAMES_LIST: readonly ActionName[];
496
509
  export {};
@@ -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);
@@ -877,6 +875,7 @@ async function styleTableCells(doc, cells, changeMode) {
877
875
  await applyItemStyling(doc, nodeIds, specs, changeMode);
878
876
  return styled.length;
879
877
  }
878
+ const TYPED_SIBLING_WINDOW = 20;
880
879
  async function runInsertParagraphs(doc, args, formatArgs) {
881
880
  const context = new ExecutionContext(doc, { evidence: args.evidence, selectors: placementSelectors(args.placement) });
882
881
  try {
@@ -889,7 +888,7 @@ async function runInsertParagraphs(doc, args, formatArgs) {
889
888
  kind: 'placement',
890
889
  at: placement.kind === 'documentStart' ? 'document_start' : 'document_end',
891
890
  });
892
- const preRows = pre.complete ? await listBlockRows(doc) : anchor ? [anchor] : [];
891
+ let preRows = pre.complete ? await listBlockRows(doc) : anchor ? [anchor] : [];
893
892
  let currentPlacement = placement;
894
893
  const nodeIds = [];
895
894
  const specs = [];
@@ -929,6 +928,16 @@ async function runInsertParagraphs(doc, args, formatArgs) {
929
928
  if (!pre.complete) {
930
929
  const created = await post.page({ nodeIds, limit: Math.max(nodeIds.length, 1) });
931
930
  localRows = [...(anchor ? [anchor] : []), ...created].sort((a, b) => a.ordinal - b.ordinal);
931
+ const placed = anchor && texts.some((text) => typedMarkerPrefixOf(text)) ? await post.block(anchor.nodeId) : null;
932
+ if (placed) {
933
+ const span = TYPED_SIBLING_WINDOW + nodeIds.length;
934
+ const window = await post.page({ offset: Math.max(0, placed.ordinal - 1 - span), limit: 2 * span + 1 });
935
+ const createdIds = new Set(nodeIds);
936
+ if (nodeIds.every((id) => window.some((row) => row.nodeId === id))) {
937
+ preRows = window.filter((row) => !createdIds.has(row.nodeId));
938
+ localRows = window;
939
+ }
940
+ }
932
941
  }
933
942
  const count = context.executedOperations.length;
934
943
  formatted = await matchInsertedBlockFormatting(context.document, preRows, context.receipt('insert_paragraphs', []), formatArgs, localRows);
@@ -1278,6 +1287,172 @@ async function runDeleteText(doc, args) {
1278
1287
  return failedReceipt('delete_text', err, pre);
1279
1288
  }
1280
1289
  }
1290
+ const CONJUNCTION_TAIL = /[;,]\s*(and|or)\s*$/i;
1291
+ const ITEM_ENDING = /(?:[;,]\s*(?:and|or)|[.;,:])\s*$/i;
1292
+ const ITEM_CLOSERS = /["'”’)\]]+$/;
1293
+ function enumerationSlotOf(block) {
1294
+ const numbering = blockNumbering(block);
1295
+ const path = numbering?.path;
1296
+ if (Array.isArray(path) && path.length > 0) {
1297
+ return { key: `scheme:${path.slice(0, -1).join('.')}:${path.length}`, depth: path.length, typed: null };
1298
+ }
1299
+ const markerShape = numbering?.marker ? markerShapeOf(numbering.marker) : null;
1300
+ if (markerShape)
1301
+ return { key: `scheme-marker:${markerShape.wrapper}:${markerShape.kind}`, depth: null, typed: null };
1302
+ const text = block.text || block.textPreview || '';
1303
+ const typedMarker = typedMarkerPrefixOf(text)?.marker;
1304
+ const typedShape = typedMarker ? markerShapeOf(text) : null;
1305
+ if (!typedMarker || !typedShape)
1306
+ return null;
1307
+ const parent = typedShape.kind === 'digit' ? typedDigitParentOf(typedMarker) : '';
1308
+ return { key: `typed:${typedShape.wrapper}:${typedShape.kind}:${parent}`, depth: null, typed: typedShape };
1309
+ }
1310
+ function typedDigitParentOf(marker) {
1311
+ const segments = marker.replace(/^\(/, '').replace(/[.)]$/, '').split('.');
1312
+ return `${segments.length}:${segments.slice(0, -1).join('.')}`;
1313
+ }
1314
+ const ROMAN_DIGITS = { i: 1, v: 5, x: 10 };
1315
+ function markerValueOf(token, shape) {
1316
+ const core = token.replace(/^\(/, '').replace(/[.)]$/, '').toLowerCase();
1317
+ if (shape.kind === 'digit')
1318
+ return Number(core.split('.').pop());
1319
+ if (shape.kind === 'lower-roman' || shape.kind === 'upper-roman') {
1320
+ let total = 0;
1321
+ for (let i = 0; i < core.length; i += 1) {
1322
+ const value = ROMAN_DIGITS[core[i]] ?? 0;
1323
+ total += value < (ROMAN_DIGITS[core[i + 1] ?? ''] ?? 0) ? -value : value;
1324
+ }
1325
+ return total;
1326
+ }
1327
+ const letter = core.charCodeAt(0) - 96;
1328
+ return core.length === 1 ? letter : 26 * (core.length - 1) + letter;
1329
+ }
1330
+ function enumerationRunOf(blocks, index, slot, textOf) {
1331
+ const typed = slot.typed;
1332
+ const valueOf = (block) => {
1333
+ if (!typed) {
1334
+ const path = blockNumbering(block)?.path;
1335
+ return Array.isArray(path) && path.length > 0 ? Number(path[path.length - 1]) : null;
1336
+ }
1337
+ const token = typed ? typedMarkerPrefixOf(textOf(block))?.marker : null;
1338
+ return token && typed ? markerValueOf(token, typed) : null;
1339
+ };
1340
+ const walk = (step) => {
1341
+ const found = [];
1342
+ let edge = valueOf(blocks[index]);
1343
+ let crossed = false;
1344
+ for (let i = index + step; i >= 0 && i < blocks.length; i += step) {
1345
+ const block = blocks[i];
1346
+ if (textOf(block).length === 0)
1347
+ continue;
1348
+ const other = enumerationSlotOf(block);
1349
+ const continuesAsLetter = typed != null &&
1350
+ other?.typed != null &&
1351
+ markerShapeFits(typed, other.typed) &&
1352
+ edge != null &&
1353
+ valueOf(block) === edge + step;
1354
+ if (other?.key === slot.key || continuesAsLetter) {
1355
+ const value = valueOf(block);
1356
+ if (value != null && edge != null) {
1357
+ const advance = (value - edge) * step;
1358
+ if (advance <= 0 || ((crossed || !typed) && advance !== 1))
1359
+ break;
1360
+ }
1361
+ found.push(block);
1362
+ edge = value;
1363
+ crossed = false;
1364
+ continue;
1365
+ }
1366
+ if (typed && other?.typed) {
1367
+ if (step === -1 && edge != null) {
1368
+ const prefix = typedMarkerPrefixOf(textOf(block)).prefix;
1369
+ const inline = new RegExp(String.raw `(?:^|\s)(${TYPED_MARKER_TOKEN}) `).exec(textOf(block).slice(prefix.length));
1370
+ const inlineShape = inline ? markerShapeOf(inline[1]) : null;
1371
+ if (inline && sameMarkerShape(inlineShape, typed) && markerValueOf(inline[1], typed) === edge - 1) {
1372
+ found.push(block);
1373
+ break;
1374
+ }
1375
+ }
1376
+ crossed = true;
1377
+ continue;
1378
+ }
1379
+ if (slot.depth != null && other?.depth != null && other.depth > slot.depth)
1380
+ continue;
1381
+ break;
1382
+ }
1383
+ return found;
1384
+ };
1385
+ return [...walk(-1).reverse(), blocks[index], ...walk(1)];
1386
+ }
1387
+ function listPunctuationAfterDelete(blocks, deletedIds) {
1388
+ const textOf = (block) => (block.text || (block.textPreview ?? '').replace(/…\s*$/, '')).trimEnd();
1389
+ const endingOf = (block) => {
1390
+ const text = textOf(block);
1391
+ const closers = ITEM_CLOSERS.exec(text)?.[0] ?? '';
1392
+ const body = text.slice(0, text.length - closers.length).trimEnd();
1393
+ const ending = ITEM_ENDING.exec(body);
1394
+ return {
1395
+ core: ending?.[0].trim() ?? '',
1396
+ real: ending ? `${ending[0].trim()}${closers}` : closers,
1397
+ closers,
1398
+ conjunction: CONJUNCTION_TAIL.exec(body)?.[0].trim() ?? null,
1399
+ };
1400
+ };
1401
+ const quote = (block) => {
1402
+ const text = textOf(block);
1403
+ return JSON.stringify(text.length > 40 ? `...${text.slice(-40)}` : text);
1404
+ };
1405
+ const change = (block, to) => {
1406
+ const ending = endingOf(block);
1407
+ return `change the ending of ${quote(block)} (nodeId ${block.nodeId}) from "${ending.real}" to "${to}${ending.closers}"`;
1408
+ };
1409
+ const entries = [];
1410
+ const covered = new Set();
1411
+ blocks.forEach((block, index) => {
1412
+ if (!deletedIds.has(block.nodeId) || covered.has(block.nodeId))
1413
+ return;
1414
+ const slot = enumerationSlotOf(block);
1415
+ if (!slot)
1416
+ return;
1417
+ const run = enumerationRunOf(blocks, index, slot, textOf);
1418
+ for (const item of run)
1419
+ covered.add(item.nodeId);
1420
+ const survivors = run.filter((item) => !deletedIds.has(item.nodeId));
1421
+ if (survivors.length === 0)
1422
+ return;
1423
+ const last = survivors[survivors.length - 1];
1424
+ const penultimate = survivors.length >= 2 ? survivors[survivors.length - 2] : undefined;
1425
+ const originalPenultimate = run[run.length - 2];
1426
+ if (run.some((item) => item !== originalPenultimate && endingOf(item).conjunction))
1427
+ return;
1428
+ const runEnd = run[run.length - 1];
1429
+ if (!deletedIds.has(runEnd.nodeId))
1430
+ return;
1431
+ const tail = originalPenultimate ? endingOf(originalPenultimate).conjunction : null;
1432
+ const separator = tail ? tail.charAt(0) : ';';
1433
+ const closing = endingOf(runEnd).core || '.';
1434
+ const lastEnding = endingOf(last);
1435
+ if (lastEnding.core === ':')
1436
+ return;
1437
+ const needsClose = lastEnding.core !== closing && (lastEnding.conjunction != null || lastEnding.core === separator);
1438
+ const needsTail = penultimate != null && tail != null && endingOf(penultimate).core !== ':' && !endingOf(penultimate).conjunction;
1439
+ if (!needsClose && !(needsTail && lastEnding.core !== closing))
1440
+ return;
1441
+ const edits = [
1442
+ `end ${quote(last)} (nodeId ${last.nodeId}) with "${closing}${lastEnding.closers}" instead of "${lastEnding.real}"`,
1443
+ ];
1444
+ if (needsTail)
1445
+ edits.push(change(penultimate, tail));
1446
+ entries.push({
1447
+ deleted: runEnd.nodeId,
1448
+ lastItem: last.nodeId,
1449
+ lastItemEnds: lastEnding.real,
1450
+ ...(penultimate ? { penultimate: penultimate.nodeId, penultimateEnds: endingOf(penultimate).real } : {}),
1451
+ suggestion: `The run now ends at ${quote(last)}: ${edits.join(', and ')}, using replace_text.`,
1452
+ });
1453
+ });
1454
+ return entries;
1455
+ }
1281
1456
  /** Block node types `doc.blocks.delete` removes as a whole paragraph-shaped block. */
1282
1457
  const DELETABLE_BLOCK_NODE_TYPES = new Set(['paragraph', 'heading', 'listItem']);
1283
1458
  /**
@@ -1447,6 +1622,11 @@ async function runDeleteBlocks(doc, args) {
1447
1622
  // an error, so the `errors.length` term still catches a short request.
1448
1623
  const allApplied = deleted.length === targets.length && errors.length === 0;
1449
1624
  const verified = verification.every((v) => v.passed);
1625
+ let listPunctuation = [];
1626
+ try {
1627
+ listPunctuation = listPunctuationAfterDelete(pre.blocks, new Set(deleted.map((entry) => entry.nodeId)));
1628
+ }
1629
+ catch { }
1450
1630
  return {
1451
1631
  status: !verified || deleted.length === 0 ? 'failed' : allApplied ? 'ok' : 'partial',
1452
1632
  intent: `delete_blocks: ${deleted.length} block(s)`,
@@ -1456,6 +1636,7 @@ async function runDeleteBlocks(doc, args) {
1456
1636
  executedOperations,
1457
1637
  verification,
1458
1638
  deletedBlocks: deleted,
1639
+ ...(listPunctuation.length > 0 ? { listPunctuation } : {}),
1459
1640
  ...(errors.length ? { errors } : {}),
1460
1641
  ...(errors.length
1461
1642
  ? { nextStep: 'Re-inspect the document and retry the selectors that did not resolve to one block.' }
@@ -4400,8 +4581,9 @@ async function runScopedReplaceUnchecked(doc, args) {
4400
4581
  occurrences: spans.length,
4401
4582
  ...(edit.occurrence != null ? { occurrence: edit.occurrence } : {}),
4402
4583
  });
4403
- for (const span of spans)
4404
- allSpans.push({ ...span, replace: edit.replace, find: edit.find });
4584
+ for (const span of spans) {
4585
+ allSpans.push({ ...span, replace: edit.replace, find: edit.find, editIndex: perEdit.length - 1 });
4586
+ }
4405
4587
  }
4406
4588
  const appliedEdits = perEdit.filter((e) => e.occurrences > 0);
4407
4589
  const skippedEdits = perEdit.filter((e) => e.occurrences === 0);
@@ -4438,6 +4620,23 @@ async function runScopedReplaceUnchecked(doc, args) {
4438
4620
  planned.push(span);
4439
4621
  lastEnd = span.end;
4440
4622
  }
4623
+ planned.forEach((span, at) => {
4624
+ if (span.replace.trim().length > 0)
4625
+ return;
4626
+ const text = block.text;
4627
+ if (text.charAt(span.start - 1) !== ' ' || text.charAt(span.end) !== ' ')
4628
+ return;
4629
+ if (text.charAt(span.start - 2) === ' ' || text.charAt(span.end + 1) === ' ')
4630
+ return;
4631
+ if (/[.!?]["'\u201d\u2019)\]]*$/.test(text.slice(0, span.start - 1)))
4632
+ return;
4633
+ if ((planned[at + 1]?.start ?? Infinity) <= span.end || (planned[at - 1]?.end ?? -1) >= span.start - 1)
4634
+ return;
4635
+ if (span.replace.length > 0)
4636
+ span.start -= 1;
4637
+ span.end += 1;
4638
+ perEdit[span.editIndex].collapsedSpace = true;
4639
+ });
4441
4640
  // The block text once every planned span is replaced: the whole-block
4442
4641
  // rewrite writes it, and exact-target writes must produce it.
4443
4642
  const expected = planned.reduceRight((text, span) => text.slice(0, span.start) + span.replace + text.slice(span.end), block.text);
@@ -4724,6 +4923,8 @@ function leadInShapeOf(blockText, labelText, quote) {
4724
4923
  }
4725
4924
  /** The span of `text` that carries the label look, or null when it has none. */
4726
4925
  function leadInSpanOf(text, shape) {
4926
+ if (shape.kind === 'typed')
4927
+ return typedCaptionSpanOf(text, shape.delimiter);
4727
4928
  if (shape.kind === 'word') {
4728
4929
  // Only the SAME token inherits the look — "WHEREAS the Parties…" does,
4729
4930
  // "The Parties further agree…" does not (no guessing a label there).
@@ -4758,6 +4959,81 @@ function leadInSpanOf(text, shape) {
4758
4959
  const end = leadInEndOf(text, shape.delimiter);
4759
4960
  return end == null ? null : { start: 0, end };
4760
4961
  }
4962
+ const TYPED_MARKER_CORE = String.raw `(?:\d{1,3}(?:\.\d{1,3})*|[ivx]{1,6}|[IVX]{1,6}|[a-z]{1,2}|[A-Z])`;
4963
+ const TYPED_MARKER_TOKEN = String.raw `(?:\(${TYPED_MARKER_CORE}\)|${TYPED_MARKER_CORE}[.)])`;
4964
+ const TYPED_MARKER_PREFIX = new RegExp(String.raw `^[ \t]*(${TYPED_MARKER_TOKEN})\t[\t ]*`);
4965
+ const LEADING_MARKER_TOKEN = new RegExp(String.raw `^\s*(${TYPED_MARKER_TOKEN})(?=\s|$)`);
4966
+ export function typedMarkerPrefixOf(text) {
4967
+ const match = TYPED_MARKER_PREFIX.exec(text);
4968
+ return match ? { prefix: match[0], marker: match[1] } : null;
4969
+ }
4970
+ export function markerShapeOf(text) {
4971
+ const match = LEADING_MARKER_TOKEN.exec(text);
4972
+ if (!match)
4973
+ return null;
4974
+ const token = match[1];
4975
+ const wrapper = token.startsWith('(') ? 'paren' : token.endsWith('.') ? 'period' : 'close';
4976
+ const core = token.replace(/^\(/, '').replace(/[.)]$/, '');
4977
+ let kind;
4978
+ if (/^\d/.test(core))
4979
+ kind = 'digit';
4980
+ else if (/^[ivx]+$/.test(core))
4981
+ kind = 'lower-roman';
4982
+ else if (/^[IVX]+$/.test(core))
4983
+ kind = 'upper-roman';
4984
+ else if (/^[a-z]+$/.test(core))
4985
+ kind = 'lower-alpha';
4986
+ else if (/^[A-Z]+$/.test(core))
4987
+ kind = 'upper-alpha';
4988
+ else
4989
+ return null;
4990
+ return { wrapper, kind };
4991
+ }
4992
+ function sameMarkerShape(a, b) {
4993
+ return a != null && b != null && a.wrapper === b.wrapper && a.kind === b.kind;
4994
+ }
4995
+ function markerShapeFits(reference, created) {
4996
+ if (!reference || reference.wrapper !== created.wrapper)
4997
+ return false;
4998
+ if (reference.kind === created.kind)
4999
+ return true;
5000
+ return ((created.kind === 'lower-roman' && reference.kind === 'lower-alpha') ||
5001
+ (created.kind === 'upper-roman' && reference.kind === 'upper-alpha'));
5002
+ }
5003
+ function continuesAlphabet(referenceText, createdText, referenceBefore) {
5004
+ const letterOf = (text) => /^[a-zA-Z]$/.exec(LEADING_MARKER_TOKEN.exec(text)?.[1]?.replace(/[().]/g, '') ?? '')?.[0];
5005
+ const reference = letterOf(referenceText);
5006
+ const created = letterOf(createdText);
5007
+ return (reference != null &&
5008
+ created != null &&
5009
+ created.charCodeAt(0) - reference.charCodeAt(0) === (referenceBefore ? 1 : -1));
5010
+ }
5011
+ const TYPED_CAPTION_MAX_CHARS = 60;
5012
+ function isTitleLike(caption) {
5013
+ const words = caption.match(/\p{L}+/gu) ?? [];
5014
+ const capitalised = words.filter((word) => /^\p{Lu}/u.test(word)).length;
5015
+ return (words.length > 0 &&
5016
+ words.every((word) => word.length < 4 || /^\p{Lu}/u.test(word)) &&
5017
+ capitalised * 2 >= words.length);
5018
+ }
5019
+ export function typedCaptionSpanOf(text, delimiter = '.') {
5020
+ const typed = typedMarkerPrefixOf(text);
5021
+ const mark = delimiter.trim();
5022
+ if (!typed || !mark)
5023
+ return null;
5024
+ const start = typed.prefix.length;
5025
+ for (let at = text.indexOf(mark, start); at >= 0; at = text.indexOf(mark, at + 1)) {
5026
+ const end = at + mark.length;
5027
+ if (end - start > TYPED_CAPTION_MAX_CHARS)
5028
+ return null;
5029
+ if (at === start || !/\s/.test(text.charAt(end)))
5030
+ continue;
5031
+ if (!isTitleLike(text.slice(start, at)))
5032
+ return null;
5033
+ return text.slice(end).trim().length > 0 ? { start, end } : null;
5034
+ }
5035
+ return null;
5036
+ }
4761
5037
  /**
4762
5038
  * Cut `text` at `end` without splitting a surrogate pair: half an emoji is not
4763
5039
  * document text, and `query.match` finds nothing when handed a lone surrogate —
@@ -4834,7 +5110,10 @@ async function listItemRunPattern(doc, text, blockId) {
4834
5110
  // Window: through the label's delimiter plus enough of the sentence to sample
4835
5111
  // it, or a plain head when the item carries no label at all.
4836
5112
  const delimiterAt = leadInEndOf(text);
4837
- const windowEnd = delimiterAt != null ? delimiterAt + LEAD_IN_BODY_SAMPLE_CHARS : LEAD_IN_BODY_SAMPLE_CHARS;
5113
+ let windowEnd = delimiterAt != null ? delimiterAt + LEAD_IN_BODY_SAMPLE_CHARS : LEAD_IN_BODY_SAMPLE_CHARS;
5114
+ const typedCaption = typedCaptionSpanOf(text);
5115
+ if (typedCaption)
5116
+ windowEnd = Math.max(windowEnd, typedCaption.end + LEAD_IN_BODY_SAMPLE_CHARS);
4838
5117
  const pattern = sliceWholeChars(text, windowEnd);
4839
5118
  const result = asRecord(await queryFn({ select: { type: 'text', pattern } }));
4840
5119
  const items = Array.isArray(result?.items) ? result.items : [];
@@ -4848,6 +5127,9 @@ async function listItemRunPattern(doc, text, blockId) {
4848
5127
  const runs = (Array.isArray(rec?.runs) ? rec.runs : []).filter((r) => runTextOf(r).length > 0);
4849
5128
  if (runs.length === 0)
4850
5129
  continue;
5130
+ const typedLead = typedPrefixLabelOf(runs, runTextOf);
5131
+ if (typedLead)
5132
+ return typedLead;
4851
5133
  const blockText = asString(rec?.text, '') ?? '';
4852
5134
  // A quoted defined term keeps its opening quote in a run of its own —
4853
5135
  // `"` + **Deliverables** + `" means …` — so the label is the SECOND run
@@ -4883,6 +5165,59 @@ async function listItemRunPattern(doc, text, blockId) {
4883
5165
  }
4884
5166
  return null;
4885
5167
  }
5168
+ function typedPrefixLabelOf(runs, runTextOf) {
5169
+ const typed = typedMarkerPrefixOf(runs.map(runTextOf).join(''));
5170
+ if (!typed)
5171
+ return null;
5172
+ const markerEnd = typed.prefix.indexOf(typed.marker) + typed.marker.length;
5173
+ let covered = 0;
5174
+ let labelIndex = 0;
5175
+ while (labelIndex < runs.length && covered + runTextOf(runs[labelIndex]).length <= typed.prefix.length) {
5176
+ covered += runTextOf(runs[labelIndex]).length;
5177
+ labelIndex += 1;
5178
+ }
5179
+ if (covered < markerEnd || labelIndex + 1 >= runs.length)
5180
+ return null;
5181
+ const underlined = (run) => asRecord(asRecord(asRecord(run)?.styles)?.effective)?.underline === true;
5182
+ const emphasisOf = (run) => {
5183
+ const style = matchRunStyle(run);
5184
+ return `${style.bold}/${style.italic}/${underlined(run)}`;
5185
+ };
5186
+ const label = runs[labelIndex];
5187
+ if (emphasisOf(label) === 'false/false/false')
5188
+ return null;
5189
+ const isBlank = (run) => runTextOf(run).trim().length === 0;
5190
+ const continuesLabel = (index) => {
5191
+ if (emphasisOf(runs[index]) === emphasisOf(label))
5192
+ return true;
5193
+ let next = index;
5194
+ while (next < runs.length && isBlank(runs[next]))
5195
+ next += 1;
5196
+ return next > index && next < runs.length && emphasisOf(runs[next]) === emphasisOf(label);
5197
+ };
5198
+ let bodyIndex = labelIndex + 1;
5199
+ while (bodyIndex < runs.length && continuesLabel(bodyIndex))
5200
+ bodyIndex += 1;
5201
+ while (bodyIndex < runs.length - 1 && isBlank(runs[bodyIndex]))
5202
+ bodyIndex += 1;
5203
+ if (bodyIndex >= runs.length)
5204
+ return null;
5205
+ const labelText = runs.slice(labelIndex, bodyIndex).map(runTextOf).join('').trim();
5206
+ if (labelText.length === 0 || labelText.length > LEAD_IN_MAX_CHARS)
5207
+ return null;
5208
+ const delimiter = ['.', ...LEAD_IN_DELIMITERS.map((d) => d.trim())].find((d) => labelText.endsWith(d) && labelText.length > d.length);
5209
+ if (!delimiter)
5210
+ return null;
5211
+ const body = runs[bodyIndex];
5212
+ const leadStyle = matchRunStyle(label);
5213
+ const bodyStyle = matchRunStyle(body);
5214
+ const leadUnderline = underlined(label);
5215
+ const bodyUnderline = underlined(body);
5216
+ const look = inlineFromMatchStyle(leadStyle, bodyStyle);
5217
+ if (leadUnderline || bodyUnderline)
5218
+ look.underline = leadUnderline;
5219
+ return { body: inlineFromMatchStyle(bodyStyle), lead: { look, shape: { kind: 'typed', delimiter } } };
5220
+ }
4886
5221
  /**
4887
5222
  * Pull a paragraph's direct indentation out of a `getNodeById` payload and
4888
5223
  * convert it to the twips that `format.paragraph.setIndentation` expects.
@@ -5761,6 +6096,7 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
5761
6096
  }
5762
6097
  if (!reference)
5763
6098
  return null;
6099
+ reference = typedShapeReference(postBlocks, preIds, created, createdIndex, createdText, reference, compatibleHeading);
5764
6100
  const applied = {};
5765
6101
  // Numbering: when the reference participates in a numbering scheme and
5766
6102
  // the new block does not, attach it to the same sequence — this is what
@@ -5786,7 +6122,9 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
5786
6122
  }
5787
6123
  }
5788
6124
  if (reference.styleId && reference.styleId !== created.styleId) {
5789
- const setStyleFn = maybeMethod(doc, ['styles', 'paragraph', 'setStyle']);
6125
+ const keepRunMarks = !allowLeadPattern;
6126
+ const setStyleFn = (keepRunMarks ? maybeMethod(doc, ['styles', 'paragraph', 'setStyleRef']) : null) ??
6127
+ maybeMethod(doc, ['styles', 'paragraph', 'setStyle']);
5790
6128
  if (setStyleFn) {
5791
6129
  const target = { kind: 'block', nodeType: created.nodeType, nodeId: created.nodeId };
5792
6130
  let canCopyStyle = true;
@@ -5889,6 +6227,42 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
5889
6227
  return { matchedSibling: null, skipped: reasonOf(err) };
5890
6228
  }
5891
6229
  }
6230
+ function typedShapeReference(postBlocks, preIds, created, createdIndex, createdText, reference, compatible) {
6231
+ const typed = createdText ? typedMarkerPrefixOf(createdText) : null;
6232
+ if (!typed || createdText.slice(typed.prefix.length).trim().length === 0)
6233
+ return reference;
6234
+ const shape = markerShapeOf(createdText);
6235
+ const referenceShape = markerShapeOf(reference.textPreview ?? '');
6236
+ if (!shape || sameMarkerShape(referenceShape, shape))
6237
+ return reference;
6238
+ const usable = (row) => !!row && preIds.has(row.nodeId) && row.nodeType === created.nodeType && !row.isEmpty && compatible(row);
6239
+ const before = postBlocks.slice(0, createdIndex).reverse().find(usable);
6240
+ const after = postBlocks.slice(createdIndex + 1).find(usable);
6241
+ const continued = [reference, before, after].find((row) => !!row &&
6242
+ markerShapeFits(markerShapeOf(row.textPreview ?? ''), shape) &&
6243
+ continuesAlphabet(row.textPreview ?? '', createdText, postBlocks.findIndex((b) => b.nodeId === row.nodeId) < createdIndex));
6244
+ if (continued)
6245
+ return continued;
6246
+ const candidate = (row) => {
6247
+ if (!usable(row))
6248
+ return null;
6249
+ const preview = row.textPreview ?? '';
6250
+ return typedMarkerPrefixOf(preview) ? markerShapeOf(preview) : null;
6251
+ };
6252
+ const nearest = (accepts) => {
6253
+ for (let distance = 1; distance < postBlocks.length; distance += 1) {
6254
+ for (const index of [createdIndex - distance, createdIndex + distance]) {
6255
+ const row = postBlocks[index];
6256
+ if (index >= 0 && accepts(candidate(row)))
6257
+ return row;
6258
+ }
6259
+ }
6260
+ return null;
6261
+ };
6262
+ return (nearest((rowShape) => sameMarkerShape(rowShape, shape)) ??
6263
+ nearest((rowShape) => markerShapeFits(rowShape, shape)) ??
6264
+ reference);
6265
+ }
5892
6266
  /** A lead-in marker token like "(m)", "1.", "a)", "(iv)" at the start of clause text. */
5893
6267
  const LEAD_MARKER_PATTERN = /^\(?(?:[a-zA-Z]{1,3}|\d{1,3})[).:\]]/;
5894
6268
  /**
@@ -6019,7 +6393,8 @@ async function matchBlockRunPattern(doc, reference, created, createdText) {
6019
6393
  const span = leadInSpanOf(fullText, pattern.lead.shape);
6020
6394
  // A uniform reference has no body look and no delimiter of its own: its look
6021
6395
  // belongs to the whole created block, not to a label span inside it.
6022
- const range = span ?? (Object.keys(pattern.body).length === 0 ? { start: 0, end } : null);
6396
+ const range = span ??
6397
+ (Object.keys(pattern.body).length === 0 && pattern.lead.shape.kind !== 'typed' ? { start: 0, end } : null);
6023
6398
  if (range && (await applyInlineQuiet(formatFn, { blockId: created.nodeId, ...range, inline: pattern.lead.look }))) {
6024
6399
  applied.lead = { ...pattern.lead.look, range: [range.start, range.end] };
6025
6400
  }
@@ -6046,7 +6421,7 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
6046
6421
  // When the caller styled any entry explicitly (runs/marks), it took
6047
6422
  // ownership of intra-paragraph formatting — the automatic lead-marker
6048
6423
  // pattern pass must not fight it.
6049
- const rawTexts = Array.isArray(args?.texts) ? args.texts : [];
6424
+ const rawTexts = Array.isArray(args?.texts) ? args.texts : args?.text != null ? [args.text] : [];
6050
6425
  const hasExplicitStyling = rawTexts.some((entry) => isRecord(entry) && (Array.isArray(entry.runs) || entry.marks != null));
6051
6426
  const insertedTexts = rawTexts.map((entry) => {
6052
6427
  if (typeof entry === 'string')
@@ -6247,7 +6622,15 @@ async function runAttachNumbering(doc, args) {
6247
6622
  errors: [
6248
6623
  {
6249
6624
  code: 'TARGET_NOT_FOUND',
6250
- message: `no block renders marker ${JSON.stringify(likeMarker)}. Available: ${available.join(' ')}`,
6625
+ message: available.length > 0
6626
+ ? `no block renders marker ${JSON.stringify(likeMarker)}. Available: ${available.join(' ')}`
6627
+ : rows.some((r) => typedMarkerPrefixOf(r.textPreview ?? '') != null)
6628
+ ? `no block renders marker ${JSON.stringify(likeMarker)}: this document has no numbering scheme; its clause numbers are typed text. ` +
6629
+ `Copy the sibling's typed prefix (tab, next number, tab) into the paragraph text with insert_paragraphs or replace_text, ` +
6630
+ `then renumber the following typed markers with replace_text.`
6631
+ : `no block renders marker ${JSON.stringify(likeMarker)}: no block in this document carries numbering. ` +
6632
+ `To add a paragraph that matches its neighbours use insert_paragraphs next to them; ` +
6633
+ `to start a numbered list use append_list or convert_list.`,
6251
6634
  recovery: { kind: 'reinspect' },
6252
6635
  },
6253
6636
  ],
@@ -7,6 +7,7 @@ var errors = require('../runtime/errors.cjs');
7
7
  var ir = require('./ir.cjs');
8
8
  var docSnapshot = require('./doc-snapshot.cjs');
9
9
  var operationCatalog = require('./operation-catalog.cjs');
10
+ var guards = require('../guards.cjs');
10
11
 
11
12
  const RESERVED_ARG_KEYS = new Set(['sessionId', 'doc']);
12
13
  function ensureKnownOperation(operationId) {
@@ -30,11 +31,8 @@ function ensureClean(args) {
30
31
  }
31
32
  return args;
32
33
  }
33
- function isRecord(value) {
34
- return value != null && typeof value === 'object' && !Array.isArray(value);
35
- }
36
34
  function isRefToken(value) {
37
- return (isRecord(value) &&
35
+ return (guards.isRecord(value) &&
38
36
  typeof value.ref === 'string' &&
39
37
  (value.path == null || typeof value.path === 'string') &&
40
38
  Object.keys(value).every((key) => key === 'ref' || key === 'path'));
@@ -51,7 +49,7 @@ function readBindingPath(value, path) {
51
49
  cursor = cursor[index];
52
50
  continue;
53
51
  }
54
- if (!isRecord(cursor) || !(segment in cursor))
52
+ if (!guards.isRecord(cursor) || !(segment in cursor))
55
53
  return undefined;
56
54
  cursor = cursor[segment];
57
55
  }
@@ -80,7 +78,7 @@ function resolveBindingTokens(value, bindings) {
80
78
  if (Array.isArray(value)) {
81
79
  return value.map((entry) => resolveBindingTokens(entry, bindings));
82
80
  }
83
- if (!isRecord(value))
81
+ if (!guards.isRecord(value))
84
82
  return value;
85
83
  return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolveBindingTokens(entry, bindings)]));
86
84
  }
@@ -90,7 +88,7 @@ function extractBoundNodeIds(value) {
90
88
  if (Array.isArray(value)) {
91
89
  return [...new Set(value.flatMap((entry) => extractBoundNodeIds(entry)))];
92
90
  }
93
- if (!isRecord(value))
91
+ if (!guards.isRecord(value))
94
92
  return [];
95
93
  const direct = typeof value.nodeId === 'string' && value.nodeId.length > 0
96
94
  ? [value.nodeId]
@@ -364,7 +362,7 @@ function revisionOnlyParagraphStep(plan) {
364
362
  return undefined;
365
363
  const at = apply.args.at;
366
364
  if (at !== undefined) {
367
- if (!isRecord(at))
365
+ if (!guards.isRecord(at))
368
366
  return undefined;
369
367
  if (at.kind === 'documentStart' || at.kind === 'documentEnd') {
370
368
  if (Object.keys(at).some((key) => key !== 'kind'))
@@ -372,7 +370,7 @@ function revisionOnlyParagraphStep(plan) {
372
370
  }
373
371
  else if (at.kind === 'before' || at.kind === 'after') {
374
372
  if (Object.keys(at).some((key) => key !== 'kind' && key !== 'target') ||
375
- !isRecord(at.target) ||
373
+ !guards.isRecord(at.target) ||
376
374
  at.target.kind !== 'block' ||
377
375
  typeof at.target.nodeId !== 'string' ||
378
376
  typeof at.target.nodeType !== 'string' ||
@@ -5,6 +5,7 @@ import { SuperDocCliError } from '../runtime/errors.js';
5
5
  import { validatePlan } from './ir.js';
6
6
  import { buildDocumentSnapshot, buildMutationSnapshot, resolveSnapshotSelector, AmbiguousSelectorError, } from './doc-snapshot.js';
7
7
  import { getOperationCatalogEntry } from './operation-catalog.js';
8
+ import { isRecord } from '../guards.js';
8
9
  const RESERVED_ARG_KEYS = new Set(['sessionId', 'doc']);
9
10
  function ensureKnownOperation(operationId) {
10
11
  const entry = getOperationCatalogEntry(operationId);
@@ -27,9 +28,6 @@ function ensureClean(args) {
27
28
  }
28
29
  return args;
29
30
  }
30
- function isRecord(value) {
31
- return value != null && typeof value === 'object' && !Array.isArray(value);
32
- }
33
31
  function isRefToken(value) {
34
32
  return (isRecord(value) &&
35
33
  typeof value.ref === 'string' &&