From ad9f99e188d5e40895fd3fb158ee2e758d7899c8 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Wed, 25 Feb 2026 14:27:00 -0800 Subject: [PATCH 1/2] chore: more document-api standardization and clean up --- .../reference/_generated-manifest.json | 2 +- .../reference/comments/create.mdx | 1 - .../reference/comments/delete.mdx | 1 - .../document-api/reference/comments/list.mdx | 2 +- .../document-api/reference/comments/patch.mdx | 1 - .../document-api/reference/create/heading.mdx | 2 - .../reference/create/paragraph.mdx | 2 - apps/docs/document-api/reference/delete.mdx | 1 - apps/docs/document-api/reference/find.mdx | 3 +- .../document-api/reference/format/apply.mdx | 2 - apps/docs/document-api/reference/insert.mdx | 1 - .../document-api/reference/lists/exit.mdx | 2 - .../document-api/reference/lists/indent.mdx | 2 - .../document-api/reference/lists/insert.mdx | 2 - .../document-api/reference/lists/list.mdx | 2 + .../document-api/reference/lists/outdent.mdx | 2 - .../document-api/reference/lists/restart.mdx | 2 - .../document-api/reference/lists/set-type.mdx | 2 - apps/docs/document-api/reference/replace.mdx | 1 - .../reference/track-changes/decide.mdx | 1 - .../reference/track-changes/list.mdx | 2 +- apps/mcp/src/tools/comments.ts | 4 +- apps/mcp/src/tools/format.ts | 4 +- apps/mcp/src/tools/mutation.ts | 12 +- apps/mcp/src/tools/query.ts | 2 +- .../scripts/check-overview-alignment.ts | 6 +- .../scripts/lib/contract-output-artifacts.ts | 2 - packages/document-api/src/README.md | 103 ++++---------- .../src/contract/contract.test.ts | 2 +- .../src/contract/metadata-types.ts | 2 - .../src/contract/operation-definitions.ts | 46 +++---- packages/document-api/src/create/create.ts | 28 +--- packages/document-api/src/index.test.ts | 2 +- packages/document-api/src/lists/lists.ts | 30 +---- .../src/overview-examples.test.ts | 10 +- .../src/types/mutation-plan.types.ts | 7 - packages/sdk/langs/node/README.md | 2 +- packages/sdk/langs/python/README.md | 2 +- .../plan-engine/comments-wrappers.ts | 12 +- .../compiler-ref-targeting.test.ts | 41 ------ .../plan-engine/compiler.ts | 127 ++---------------- .../plan-engine/executor-registry.types.ts | 2 +- .../plan-engine/query-match-adapter.test.ts | 2 +- 43 files changed, 103 insertions(+), 381 deletions(-) diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index 2bf94d869c..f80d87dda0 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -119,5 +119,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "71719d809aa97aaaf7d9200dedd1edf47dc16486b208b473cc009257667bed77" + "sourceHash": "ef0d931b0ccec18b0cc91efee5ed117a0d3328adc8406faaab4119e2f07b2ca5" } diff --git a/apps/docs/document-api/reference/comments/create.mdx b/apps/docs/document-api/reference/comments/create.mdx index bb4e8c8c8d..b57693dfcf 100644 --- a/apps/docs/document-api/reference/comments/create.mdx +++ b/apps/docs/document-api/reference/comments/create.mdx @@ -72,7 +72,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` diff --git a/apps/docs/document-api/reference/comments/delete.mdx b/apps/docs/document-api/reference/comments/delete.mdx index 0734f93adc..1d8353e0e0 100644 --- a/apps/docs/document-api/reference/comments/delete.mdx +++ b/apps/docs/document-api/reference/comments/delete.mdx @@ -61,7 +61,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` ## Non-applied failure codes diff --git a/apps/docs/document-api/reference/comments/list.mdx b/apps/docs/document-api/reference/comments/list.mdx index a41ca86637..f76146c13e 100644 --- a/apps/docs/document-api/reference/comments/list.mdx +++ b/apps/docs/document-api/reference/comments/list.mdx @@ -78,7 +78,7 @@ description: Reference for comments.list ## Pre-apply throws -- None +- `INVALID_INPUT` ## Non-applied failure codes diff --git a/apps/docs/document-api/reference/comments/patch.mdx b/apps/docs/document-api/reference/comments/patch.mdx index 1bf0ca17cb..81eb1cbf14 100644 --- a/apps/docs/document-api/reference/comments/patch.mdx +++ b/apps/docs/document-api/reference/comments/patch.mdx @@ -74,7 +74,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` - `INVALID_INPUT` diff --git a/apps/docs/document-api/reference/create/heading.mdx b/apps/docs/document-api/reference/create/heading.mdx index 04f5ce1540..d9f5168102 100644 --- a/apps/docs/document-api/reference/create/heading.mdx +++ b/apps/docs/document-api/reference/create/heading.mdx @@ -73,8 +73,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `COMMAND_UNAVAILABLE` -- `TRACK_CHANGE_COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` - `AMBIGUOUS_TARGET` diff --git a/apps/docs/document-api/reference/create/paragraph.mdx b/apps/docs/document-api/reference/create/paragraph.mdx index 2311b7f7bc..d92f76e5a4 100644 --- a/apps/docs/document-api/reference/create/paragraph.mdx +++ b/apps/docs/document-api/reference/create/paragraph.mdx @@ -71,8 +71,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `COMMAND_UNAVAILABLE` -- `TRACK_CHANGE_COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` - `AMBIGUOUS_TARGET` diff --git a/apps/docs/document-api/reference/delete.mdx b/apps/docs/document-api/reference/delete.mdx index 3b8d491599..4b6e50fa7d 100644 --- a/apps/docs/document-api/reference/delete.mdx +++ b/apps/docs/document-api/reference/delete.mdx @@ -91,7 +91,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `TRACK_CHANGE_COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` diff --git a/apps/docs/document-api/reference/find.mdx b/apps/docs/document-api/reference/find.mdx index 73812e7a24..2cb6564ffd 100644 --- a/apps/docs/document-api/reference/find.mdx +++ b/apps/docs/document-api/reference/find.mdx @@ -132,7 +132,8 @@ description: Reference for find ## Pre-apply throws -- None +- `CAPABILITY_UNAVAILABLE` +- `INVALID_INPUT` ## Non-applied failure codes diff --git a/apps/docs/document-api/reference/format/apply.mdx b/apps/docs/document-api/reference/format/apply.mdx index 7b7ede5012..09f12134f0 100644 --- a/apps/docs/document-api/reference/format/apply.mdx +++ b/apps/docs/document-api/reference/format/apply.mdx @@ -96,8 +96,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `COMMAND_UNAVAILABLE` -- `TRACK_CHANGE_COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` - `INVALID_INPUT` diff --git a/apps/docs/document-api/reference/insert.mdx b/apps/docs/document-api/reference/insert.mdx index 21413654f5..b4dbeb9ef8 100644 --- a/apps/docs/document-api/reference/insert.mdx +++ b/apps/docs/document-api/reference/insert.mdx @@ -93,7 +93,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `TRACK_CHANGE_COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` diff --git a/apps/docs/document-api/reference/lists/exit.mdx b/apps/docs/document-api/reference/lists/exit.mdx index b2c4472d6b..d006bb940b 100644 --- a/apps/docs/document-api/reference/lists/exit.mdx +++ b/apps/docs/document-api/reference/lists/exit.mdx @@ -56,8 +56,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `COMMAND_UNAVAILABLE` -- `TRACK_CHANGE_COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` diff --git a/apps/docs/document-api/reference/lists/indent.mdx b/apps/docs/document-api/reference/lists/indent.mdx index 99a70667c2..98fb66511a 100644 --- a/apps/docs/document-api/reference/lists/indent.mdx +++ b/apps/docs/document-api/reference/lists/indent.mdx @@ -56,8 +56,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `COMMAND_UNAVAILABLE` -- `TRACK_CHANGE_COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` diff --git a/apps/docs/document-api/reference/lists/insert.mdx b/apps/docs/document-api/reference/lists/insert.mdx index 1ffab9713e..2aa475918b 100644 --- a/apps/docs/document-api/reference/lists/insert.mdx +++ b/apps/docs/document-api/reference/lists/insert.mdx @@ -75,8 +75,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `COMMAND_UNAVAILABLE` -- `TRACK_CHANGE_COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` diff --git a/apps/docs/document-api/reference/lists/list.mdx b/apps/docs/document-api/reference/lists/list.mdx index b44231ac85..df6644a2e7 100644 --- a/apps/docs/document-api/reference/lists/list.mdx +++ b/apps/docs/document-api/reference/lists/list.mdx @@ -85,6 +85,8 @@ description: Reference for lists.list ## Pre-apply throws - `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` ## Non-applied failure codes diff --git a/apps/docs/document-api/reference/lists/outdent.mdx b/apps/docs/document-api/reference/lists/outdent.mdx index 2bc918e38b..1a3e89dea8 100644 --- a/apps/docs/document-api/reference/lists/outdent.mdx +++ b/apps/docs/document-api/reference/lists/outdent.mdx @@ -56,8 +56,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `COMMAND_UNAVAILABLE` -- `TRACK_CHANGE_COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` diff --git a/apps/docs/document-api/reference/lists/restart.mdx b/apps/docs/document-api/reference/lists/restart.mdx index 1977ab0752..9dd5b39073 100644 --- a/apps/docs/document-api/reference/lists/restart.mdx +++ b/apps/docs/document-api/reference/lists/restart.mdx @@ -56,8 +56,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `COMMAND_UNAVAILABLE` -- `TRACK_CHANGE_COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` diff --git a/apps/docs/document-api/reference/lists/set-type.mdx b/apps/docs/document-api/reference/lists/set-type.mdx index c2fcbaa633..ee67114987 100644 --- a/apps/docs/document-api/reference/lists/set-type.mdx +++ b/apps/docs/document-api/reference/lists/set-type.mdx @@ -58,8 +58,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `COMMAND_UNAVAILABLE` -- `TRACK_CHANGE_COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` diff --git a/apps/docs/document-api/reference/replace.mdx b/apps/docs/document-api/reference/replace.mdx index 9b8d954182..40a97a558c 100644 --- a/apps/docs/document-api/reference/replace.mdx +++ b/apps/docs/document-api/reference/replace.mdx @@ -93,7 +93,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `TRACK_CHANGE_COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_TARGET` diff --git a/apps/docs/document-api/reference/track-changes/decide.mdx b/apps/docs/document-api/reference/track-changes/decide.mdx index 1d1eeb36ff..944f4837ad 100644 --- a/apps/docs/document-api/reference/track-changes/decide.mdx +++ b/apps/docs/document-api/reference/track-changes/decide.mdx @@ -65,7 +65,6 @@ _No fields._ ## Pre-apply throws - `TARGET_NOT_FOUND` -- `COMMAND_UNAVAILABLE` - `CAPABILITY_UNAVAILABLE` - `INVALID_INPUT` - `INVALID_TARGET` diff --git a/apps/docs/document-api/reference/track-changes/list.mdx b/apps/docs/document-api/reference/track-changes/list.mdx index 763733295d..af2745fb90 100644 --- a/apps/docs/document-api/reference/track-changes/list.mdx +++ b/apps/docs/document-api/reference/track-changes/list.mdx @@ -78,7 +78,7 @@ description: Reference for trackChanges.list ## Pre-apply throws -- None +- `INVALID_INPUT` ## Non-applied failure codes diff --git a/apps/mcp/src/tools/comments.ts b/apps/mcp/src/tools/comments.ts index 6fb960d9c8..b0f038324f 100644 --- a/apps/mcp/src/tools/comments.ts +++ b/apps/mcp/src/tools/comments.ts @@ -8,14 +8,14 @@ export function registerCommentTools(server: McpServer, sessions: SessionManager { title: 'Add Comment', description: - 'Add a comment anchored to a text range in the document. Use superdoc_find with a text pattern first, then pass a TextAddress from context[].textRanges as the target.', + 'Add a comment anchored to a text range in the document. Use superdoc_find with a text pattern first, then pass a TextAddress from items[].context.textRanges as the target.', inputSchema: { session_id: z.string().describe('Session ID from superdoc_open.'), text: z.string().describe('The comment text (question, concern, or feedback).'), target: z .string() .describe( - 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find context[].textRanges, NOT from matches[].', + 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find items[].context.textRanges.', ), }, annotations: { readOnlyHint: false }, diff --git a/apps/mcp/src/tools/format.ts b/apps/mcp/src/tools/format.ts index 96227f8208..4a5b8ec6cc 100644 --- a/apps/mcp/src/tools/format.ts +++ b/apps/mcp/src/tools/format.ts @@ -16,14 +16,14 @@ export function registerFormatTools(server: McpServer, sessions: SessionManager) { title: 'Format Text', description: - "Apply formatting on a text range. Use superdoc_find with a text pattern first, then pass a TextAddress from the result's context[].textRanges as the target. Set suggest=true to format as a tracked change (suggestion).", + 'Apply formatting on a text range. Use superdoc_find with a text pattern first, then pass a TextAddress from items[].context.textRanges as the target. Set suggest=true to format as a tracked change (suggestion).', inputSchema: { session_id: z.string().describe('Session ID from superdoc_open.'), style: z.enum(STYLES).describe('The formatting style to apply.'), target: z .string() .describe( - 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find context[].textRanges, NOT from matches[].', + 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find items[].context.textRanges.', ), suggest: z .boolean() diff --git a/apps/mcp/src/tools/mutation.ts b/apps/mcp/src/tools/mutation.ts index 1ef4155599..fbd21bd867 100644 --- a/apps/mcp/src/tools/mutation.ts +++ b/apps/mcp/src/tools/mutation.ts @@ -12,14 +12,14 @@ export function registerMutationTools(server: McpServer, sessions: SessionManage { title: 'Insert Text', description: - "Insert text at a target position in the document. Use superdoc_find first, then pass a TextAddress from the result's context[].textRanges as the target. Set suggest=true to insert as a tracked change (suggestion) instead of a direct edit.", + 'Insert text at a target position in the document. Use superdoc_find first, then pass a TextAddress from items[].context.textRanges as the target. Set suggest=true to insert as a tracked change (suggestion) instead of a direct edit.', inputSchema: { session_id: z.string().describe('Session ID from superdoc_open.'), text: z.string().describe('The text content to insert.'), target: z .string() .describe( - 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find context[].textRanges, NOT from matches[].', + 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find items[].context.textRanges.', ), suggest: z .boolean() @@ -56,14 +56,14 @@ export function registerMutationTools(server: McpServer, sessions: SessionManage { title: 'Replace Text', description: - "Replace content at a target range with new text. Use superdoc_find with a text pattern first, then pass a TextAddress from the result's context[].textRanges as the target. Set suggest=true to make the replacement a tracked change (suggestion).", + 'Replace content at a target range with new text. Use superdoc_find with a text pattern first, then pass a TextAddress from items[].context.textRanges as the target. Set suggest=true to make the replacement a tracked change (suggestion).', inputSchema: { session_id: z.string().describe('Session ID from superdoc_open.'), text: z.string().describe('The replacement text.'), target: z .string() .describe( - 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find context[].textRanges, NOT from matches[].', + 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find items[].context.textRanges.', ), suggest: z .boolean() @@ -100,13 +100,13 @@ export function registerMutationTools(server: McpServer, sessions: SessionManage { title: 'Delete Content', description: - "Delete content at a target range. Use superdoc_find with a text pattern first, then pass a TextAddress from the result's context[].textRanges as the target. Set suggest=true to delete as a tracked change (suggestion).", + 'Delete content at a target range. Use superdoc_find with a text pattern first, then pass a TextAddress from items[].context.textRanges as the target. Set suggest=true to delete as a tracked change (suggestion).', inputSchema: { session_id: z.string().describe('Session ID from superdoc_open.'), target: z .string() .describe( - 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find context[].textRanges, NOT from matches[].', + 'JSON-encoded TextAddress: {"kind":"text","blockId":"...","range":{"start":N,"end":N}}. Get this from superdoc_find items[].context.textRanges.', ), suggest: z .boolean() diff --git a/apps/mcp/src/tools/query.ts b/apps/mcp/src/tools/query.ts index c7f8927b68..3196197c88 100644 --- a/apps/mcp/src/tools/query.ts +++ b/apps/mcp/src/tools/query.ts @@ -8,7 +8,7 @@ export function registerQueryTools(server: McpServer, sessions: SessionManager): { title: 'Find in Document', description: - 'Search the document for nodes matching a type, text pattern, or both. For text searches, the result includes context[].textRanges — these are the TextAddress objects you pass as "target" to replace/insert/delete/format tools. Do NOT use matches[] as mutation targets (those are block addresses).', + 'Search the document for nodes matching a type, text pattern, or both. For text searches, each item in items[] includes context.textRanges — these are the TextAddress objects you pass as "target" to replace/insert/delete/format tools.', inputSchema: { session_id: z.string().describe('Session ID from superdoc_open.'), type: z.string().optional().describe('Node type to filter by (e.g. "heading", "paragraph", "table", "image").'), diff --git a/packages/document-api/scripts/check-overview-alignment.ts b/packages/document-api/scripts/check-overview-alignment.ts index 15c6b1a2c9..2bb4efb37b 100644 --- a/packages/document-api/scripts/check-overview-alignment.ts +++ b/packages/document-api/scripts/check-overview-alignment.ts @@ -42,15 +42,15 @@ const REQUIRED_PATTERNS = [ const FORBIDDEN_PATTERNS = [ { - label: 'legacy placeholder query API', + label: 'removed placeholder query API', pattern: /\bdoc\.query\s*\(/, }, { - label: 'legacy placeholder table API', + label: 'removed placeholder table API', pattern: /\bdoc\.table\s*\(/, }, { - label: 'legacy field-annotation selector example', + label: 'removed field-annotation selector example', pattern: /field-annotation/i, }, { diff --git a/packages/document-api/scripts/lib/contract-output-artifacts.ts b/packages/document-api/scripts/lib/contract-output-artifacts.ts index 1b529a6414..6055677ebf 100644 --- a/packages/document-api/scripts/lib/contract-output-artifacts.ts +++ b/packages/document-api/scripts/lib/contract-output-artifacts.ts @@ -103,8 +103,6 @@ export function buildToolManifestArtifacts(): GeneratedFile[] { const DEFAULT_REMEDIATION_BY_CODE: Record = { TARGET_NOT_FOUND: 'Refresh targets via find/get operations and retry with a fresh address or ID.', - COMMAND_UNAVAILABLE: 'Call capabilities.get and branch to a fallback when operation availability is false.', - TRACK_CHANGE_COMMAND_UNAVAILABLE: 'Verify track-changes support via capabilities.get before requesting tracked mode.', CAPABILITY_UNAVAILABLE: 'Check runtime capabilities and switch to supported mode or operation.', INVALID_TARGET: 'Confirm the target shape and operation compatibility, then retry with a valid target.', NO_OP: 'Treat as idempotent no-op and avoid retry loops unless inputs change.', diff --git a/packages/document-api/src/README.md b/packages/document-api/src/README.md index 9599d28a3f..2dae5599a0 100644 --- a/packages/document-api/src/README.md +++ b/packages/document-api/src/README.md @@ -32,7 +32,7 @@ Do not hand-edit generated files; regenerate via script. ## Purpose This package defines the Document API surface and type contracts. Editor-specific behavior -lives in adapter layers that map engine behavior into `QueryResult` and other API outputs. +lives in adapter layers that map engine behavior into discovery envelopes and other API outputs. ## Selector Semantics @@ -41,11 +41,11 @@ lives in adapter layers that map engine behavior into `QueryResult` and other AP ## Find Result Contract -- `find` always returns `matches` as `NodeAddress[]`. -- For text selectors (`{ type: 'text', ... }`), `matches` are containing block addresses. -- Exact matched spans are returned in `context[*].textRanges` as `TextAddress`. -- Mutating operations should target `TextAddress` values from `context[*].textRanges`. -- `insert` supports three targeting modes: canonical `TextAddress`, block-relative (`blockId` + optional `offset`), or default insertion point when all target fields are omitted. +- `find` always returns `items` as discovery items. +- For text selectors (`{ type: 'text', ... }`), items include containing block addresses. +- Exact matched spans are returned in `items[*].context.textRanges` as `TextAddress`. +- Mutating operations should target `TextAddress` values from `items[*].context.textRanges`. +- `insert` supports canonical `TextAddress` targeting or default insertion point when target is omitted. - Structural creation is exposed under `create.*` (for example `create.paragraph`), separate from text mutations. ## Adapter Error Convention @@ -59,9 +59,9 @@ lives in adapter layers that map engine behavior into `QueryResult` and other AP ## Tracked-Change Semantics - Tracking is operation-scoped (`changeMode: 'direct' | 'tracked'`), not global editor-mode state. -- `insert`, `replace`, `delete`, `format.bold`, `format.italic`, `format.underline`, `format.strikethrough`, and `create.paragraph`, `create.heading` may run in tracked mode. -- `trackChanges.*` (`list`, `get`, `accept`, `reject`, `acceptAll`, `rejectAll`) is the review lifecycle namespace. -- `lists.insert` may run in tracked mode; `lists.setType|indent|outdent|restart|exit` are direct-only in v1. +- `insert`, `replace`, `delete`, `format.apply`, and `create.paragraph`, `create.heading` may run in tracked mode. +- `trackChanges.*` (`list`, `get`, `decide`) is the review lifecycle namespace. +- `lists.insert` may run in tracked mode; `lists.setType|indent|outdent|restart|exit` are direct-only. ## List Namespace Semantics @@ -89,25 +89,13 @@ The following examples show typical multi-step patterns using the Document API. Locate text in the document and replace it: ```ts -const result = editor.doc.find({ type: 'text', text: 'foo' }); +const result = editor.doc.find({ type: 'text', pattern: 'foo' }); const target = result.items?.[0]?.context?.textRanges?.[0]; if (target) { editor.doc.replace({ target, text: 'bar' }); } ``` -### Workflow: Block-Relative Insert - -Insert text at a specific position within a known block, without constructing a full `TextAddress`: - -```ts -// Insert at the start of a block -editor.doc.insert({ blockId: 'paragraph-1', text: 'Hello ' }); - -// Insert at a specific character offset within a block -editor.doc.insert({ blockId: 'paragraph-1', offset: 5, text: 'world' }); -``` - ### Workflow: Tracked-Mode Insert Insert text as a tracked change so reviewers can accept or reject it: @@ -155,8 +143,8 @@ Check what the editor supports before attempting mutations: ```ts const caps = editor.doc.capabilities(); -if (caps.operations['format.bold'].available) { - editor.doc.format.bold({ target }); +if (caps.operations['format.apply'].available) { + editor.doc.format.apply({ target, inline: { bold: true } }); } if (caps.global.trackChanges.enabled) { editor.doc.insert({ text: 'tracked' }, { changeMode: 'tracked' }); @@ -177,10 +165,10 @@ Each operation has a dedicated section below. Grouped by namespace. ### `find` -Search the document for nodes or text matching a selector. Returns `QueryResult` with `matches` as `NodeAddress[]`. Text selectors include `context[*].textRanges` for precise span targeting. +Search the document for nodes or text matching a selector. Returns discovery items via `items`. Text selectors include `items[*].context.textRanges` for precise span targeting. - **Input**: `Selector | Query` -- **Output**: `QueryResult` +- **Output**: `FindOutput` - **Mutates**: No - **Idempotency**: idempotent @@ -222,17 +210,11 @@ Return document summary metadata (block count, word count, character count). ### `insert` -Insert text at a target location. Supports three targeting modes: - -1. **Canonical target**: `{ target: TextAddress, text }` — full address with block ID and range. -2. **Block-relative**: `{ blockId, offset?, text }` — friendly shorthand. `offset` defaults to 0 when omitted. -3. **Default insertion point**: `{ text }` — no target; adapter resolves to first paragraph start. - -Exactly one targeting mode is allowed per call. Mixing `target` with `blockId`/`offset` throws `INVALID_TARGET`. `offset` without `blockId` throws `INVALID_TARGET`. `offset` must be a non-negative integer. +Insert text at a target location. When `target` is provided, inserts at that `TextAddress`. When omitted, the adapter resolves to the default insertion point (first paragraph start). Supports dry-run and tracked mode. -- **Input**: `InsertInput` (`{ target?, blockId?, offset?, text }`) +- **Input**: `InsertInput` (`{ target?, text }`) - **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) - **Output**: `TextMutationReceipt` - **Mutates**: Yes @@ -298,44 +280,11 @@ Insert a new heading node at a specified location with a given level (1-6). Retu ### Format -### `format.bold` - -Toggle bold formatting on a `TextAddress` range. Supports dry-run and tracked mode. Availability depends on the `bold` mark being registered in the editor schema. - -- **Input**: `FormatBoldInput` (`{ target }`) -- **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) -- **Output**: `TextMutationReceipt` -- **Mutates**: Yes -- **Idempotency**: conditional -- **Failure codes**: `INVALID_TARGET` - -### `format.italic` - -Toggle italic formatting on a `TextAddress` range. Supports dry-run and tracked mode. Availability depends on the `italic` mark being registered in the editor schema. - -- **Input**: `FormatItalicInput` (`{ target }`) -- **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) -- **Output**: `TextMutationReceipt` -- **Mutates**: Yes -- **Idempotency**: conditional -- **Failure codes**: `INVALID_TARGET` - -### `format.underline` - -Toggle underline formatting on a `TextAddress` range. Supports dry-run and tracked mode. Availability depends on the `underline` mark being registered in the editor schema. - -- **Input**: `FormatUnderlineInput` (`{ target }`) -- **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) -- **Output**: `TextMutationReceipt` -- **Mutates**: Yes -- **Idempotency**: conditional -- **Failure codes**: `INVALID_TARGET` - -### `format.strikethrough` +### `format.apply` -Toggle strikethrough formatting on a `TextAddress` range. Supports dry-run and tracked mode. Availability depends on the `strike` mark being registered in the editor schema. +Apply explicit inline style changes (bold, italic, underline, strike) to a `TextAddress` range using boolean patch semantics. Supports dry-run and tracked mode. Availability depends on the corresponding marks being registered in the editor schema. -- **Input**: `FormatStrikethroughInput` (`{ target }`) +- **Input**: `StyleApplyInput` (`{ target, inline: { bold?, italic?, underline?, strike? } }`) - **Options**: `MutationOptions` (`{ changeMode?, dryRun? }`) - **Output**: `TextMutationReceipt` - **Mutates**: Yes @@ -349,7 +298,7 @@ Toggle strikethrough formatting on a `TextAddress` range. Supports dry-run and t List all list items in the document, optionally filtered by `within`, `kind`, `level`, or `ordinal`. Supports pagination via `limit` and `offset`. - **Input**: `ListsListQuery | undefined` -- **Output**: `ListsListResult` (`{ matches, total, items }`) +- **Output**: `ListsListResult` (`{ items, total }`) - **Mutates**: No - **Idempotency**: idempotent @@ -375,7 +324,7 @@ Insert a new list item before or after a target item. Returns the new item's `Li ### `lists.setType` -Change a list item's kind (`ordered` or `bullet`). Returns `NO_OP` when the item already has the requested kind. Direct-only (no tracked mode in v1). Supports dry-run. +Change a list item's kind (`ordered` or `bullet`). Returns `NO_OP` when the item already has the requested kind. Direct-only. Supports dry-run. - **Input**: `ListSetTypeInput` (`{ target, kind }`) - **Options**: `MutationOptions` (`{ dryRun? }`) @@ -386,7 +335,7 @@ Change a list item's kind (`ordered` or `bullet`). Returns `NO_OP` when the item ### `lists.indent` -Increase the indent level of a list item. Returns `NO_OP` when already at maximum depth. Direct-only (no tracked mode in v1). Supports dry-run. +Increase the indent level of a list item. Returns `NO_OP` when already at maximum depth. Direct-only. Supports dry-run. - **Input**: `ListTargetInput` (`{ target }`) - **Options**: `MutationOptions` (`{ dryRun? }`) @@ -397,7 +346,7 @@ Increase the indent level of a list item. Returns `NO_OP` when already at maximu ### `lists.outdent` -Decrease the indent level of a list item. Returns `NO_OP` when already at top level. Direct-only (no tracked mode in v1). Supports dry-run. +Decrease the indent level of a list item. Returns `NO_OP` when already at top level. Direct-only. Supports dry-run. - **Input**: `ListTargetInput` (`{ target }`) - **Options**: `MutationOptions` (`{ dryRun? }`) @@ -474,7 +423,7 @@ Retrieve full information for a single comment by ID. Throws `TARGET_NOT_FOUND` List all comments in the document. Optionally include resolved comments. - **Input**: `CommentsListQuery | undefined` (`{ includeResolved? }`) -- **Output**: `CommentsListResult` (`{ matches, total }`) +- **Output**: `CommentsListResult` (`{ items, total }`) - **Mutates**: No - **Idempotency**: idempotent @@ -485,7 +434,7 @@ List all comments in the document. Optionally include resolved comments. List tracked changes in the document. Supports filtering by `type` and pagination via `limit`/`offset`. - **Input**: `TrackChangesListInput | undefined` (`{ limit?, offset?, type? }`) -- **Output**: `TrackChangesListResult` (`{ matches, total, changes? }`) +- **Output**: `TrackChangesListResult` (`{ items, total }`) - **Mutates**: No - **Idempotency**: idempotent @@ -506,4 +455,4 @@ Accept or reject a tracked change by ID, or accept/reject all changes with `{ sc - **Output**: `Receipt` - **Mutates**: Yes - **Idempotency**: conditional -- **Failure codes**: `NO_OP`, `TARGET_NOT_FOUND`, `COMMAND_UNAVAILABLE` +- **Failure codes**: `NO_OP`, `TARGET_NOT_FOUND` diff --git a/packages/document-api/src/contract/contract.test.ts b/packages/document-api/src/contract/contract.test.ts index d75a25c866..0617c40b0a 100644 --- a/packages/document-api/src/contract/contract.test.ts +++ b/packages/document-api/src/contract/contract.test.ts @@ -73,7 +73,7 @@ describe('document-api contract catalog', () => { } }); - it('uses simplified target-based insert input schema without legacy locator constraints', () => { + it('uses simplified target-based insert input schema without locator constraints', () => { const schemas = buildInternalContractSchemas(); const insertInputSchema = schemas.operations.insert.input as { type?: string; diff --git a/packages/document-api/src/contract/metadata-types.ts b/packages/document-api/src/contract/metadata-types.ts index a99ae7fb7d..1c95cc1c91 100644 --- a/packages/document-api/src/contract/metadata-types.ts +++ b/packages/document-api/src/contract/metadata-types.ts @@ -12,8 +12,6 @@ export type OperationIdempotency = (typeof OPERATION_IDEMPOTENCY_VALUES)[number] export const PRE_APPLY_THROW_CODES = [ 'TARGET_NOT_FOUND', - 'COMMAND_UNAVAILABLE', - 'TRACK_CHANGE_COMMAND_UNAVAILABLE', 'CAPABILITY_UNAVAILABLE', 'INVALID_TARGET', 'AMBIGUOUS_TARGET', diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index 63f125742c..c0006652c5 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -111,14 +111,7 @@ function mutationOperation(options: { // Throw-code shorthand arrays const T_NOT_FOUND = ['TARGET_NOT_FOUND'] as const; -const T_NOT_FOUND_COMMAND = ['TARGET_NOT_FOUND', 'COMMAND_UNAVAILABLE', 'CAPABILITY_UNAVAILABLE'] as const; -const T_NOT_FOUND_TRACKED = ['TARGET_NOT_FOUND', 'TRACK_CHANGE_COMMAND_UNAVAILABLE', 'CAPABILITY_UNAVAILABLE'] as const; -const T_NOT_FOUND_COMMAND_TRACKED = [ - 'TARGET_NOT_FOUND', - 'COMMAND_UNAVAILABLE', - 'TRACK_CHANGE_COMMAND_UNAVAILABLE', - 'CAPABILITY_UNAVAILABLE', -] as const; +const T_NOT_FOUND_CAPABLE = ['TARGET_NOT_FOUND', 'CAPABILITY_UNAVAILABLE'] as const; // Plan-engine throw-code arrays const T_PLAN_ENGINE = [ @@ -149,6 +142,7 @@ export const OPERATION_DEFINITIONS = { requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', + throws: ['CAPABILITY_UNAVAILABLE', 'INVALID_INPUT'], deterministicTargetResolution: false, }), referenceDocPath: 'find.mdx', @@ -202,7 +196,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: true, supportsTrackedMode: true, possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'], - throws: [...T_NOT_FOUND_TRACKED, 'INVALID_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET'], }), referenceDocPath: 'insert.mdx', referenceGroup: 'core', @@ -216,7 +210,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: true, supportsTrackedMode: true, possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'], - throws: [...T_NOT_FOUND_TRACKED, 'INVALID_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET'], }), referenceDocPath: 'replace.mdx', referenceGroup: 'core', @@ -230,7 +224,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: true, supportsTrackedMode: true, possibleFailureCodes: ['NO_OP'], - throws: [...T_NOT_FOUND_TRACKED, 'INVALID_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET'], }), referenceDocPath: 'delete.mdx', referenceGroup: 'core', @@ -246,7 +240,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: true, supportsTrackedMode: true, possibleFailureCodes: ['INVALID_TARGET'], - throws: [...T_NOT_FOUND_COMMAND_TRACKED, 'INVALID_TARGET', 'INVALID_INPUT'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'INVALID_INPUT'], }), referenceDocPath: 'format/apply.mdx', referenceGroup: 'format', @@ -261,7 +255,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: true, supportsTrackedMode: true, possibleFailureCodes: ['INVALID_TARGET'], - throws: [...T_NOT_FOUND_COMMAND_TRACKED, 'INVALID_TARGET', 'AMBIGUOUS_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'AMBIGUOUS_TARGET'], }), referenceDocPath: 'create/paragraph.mdx', referenceGroup: 'create', @@ -275,7 +269,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: true, supportsTrackedMode: true, possibleFailureCodes: ['INVALID_TARGET'], - throws: [...T_NOT_FOUND_COMMAND_TRACKED, 'INVALID_TARGET', 'AMBIGUOUS_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'AMBIGUOUS_TARGET'], }), referenceDocPath: 'create/heading.mdx', referenceGroup: 'create', @@ -287,7 +281,7 @@ export const OPERATION_DEFINITIONS = { requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', - throws: T_NOT_FOUND, + throws: ['TARGET_NOT_FOUND', 'INVALID_TARGET', 'INVALID_INPUT'], }), referenceDocPath: 'lists/list.mdx', referenceGroup: 'lists', @@ -312,7 +306,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: true, supportsTrackedMode: true, possibleFailureCodes: ['INVALID_TARGET'], - throws: [...T_NOT_FOUND_COMMAND_TRACKED, 'INVALID_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET'], }), referenceDocPath: 'lists/insert.mdx', referenceGroup: 'lists', @@ -326,7 +320,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: true, supportsTrackedMode: false, possibleFailureCodes: ['NO_OP', 'INVALID_TARGET'], - throws: [...T_NOT_FOUND_COMMAND_TRACKED, 'INVALID_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET'], }), referenceDocPath: 'lists/set-type.mdx', referenceGroup: 'lists', @@ -340,7 +334,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: true, supportsTrackedMode: false, possibleFailureCodes: ['NO_OP', 'INVALID_TARGET'], - throws: [...T_NOT_FOUND_COMMAND_TRACKED, 'INVALID_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET'], }), referenceDocPath: 'lists/indent.mdx', referenceGroup: 'lists', @@ -354,7 +348,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: true, supportsTrackedMode: false, possibleFailureCodes: ['NO_OP', 'INVALID_TARGET'], - throws: [...T_NOT_FOUND_COMMAND_TRACKED, 'INVALID_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET'], }), referenceDocPath: 'lists/outdent.mdx', referenceGroup: 'lists', @@ -368,7 +362,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: true, supportsTrackedMode: false, possibleFailureCodes: ['NO_OP', 'INVALID_TARGET'], - throws: [...T_NOT_FOUND_COMMAND_TRACKED, 'INVALID_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET'], }), referenceDocPath: 'lists/restart.mdx', referenceGroup: 'lists', @@ -382,7 +376,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: true, supportsTrackedMode: false, possibleFailureCodes: ['INVALID_TARGET'], - throws: [...T_NOT_FOUND_COMMAND_TRACKED, 'INVALID_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET'], }), referenceDocPath: 'lists/exit.mdx', referenceGroup: 'lists', @@ -397,7 +391,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: false, supportsTrackedMode: false, possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'], - throws: [...T_NOT_FOUND_COMMAND, 'INVALID_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET'], }), referenceDocPath: 'comments/create.mdx', referenceGroup: 'comments', @@ -411,7 +405,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: false, supportsTrackedMode: false, possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'], - throws: [...T_NOT_FOUND_COMMAND, 'INVALID_TARGET', 'INVALID_INPUT'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_TARGET', 'INVALID_INPUT'], }), referenceDocPath: 'comments/patch.mdx', referenceGroup: 'comments', @@ -425,7 +419,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: false, supportsTrackedMode: false, possibleFailureCodes: ['NO_OP'], - throws: T_NOT_FOUND_COMMAND, + throws: T_NOT_FOUND_CAPABLE, }), referenceDocPath: 'comments/delete.mdx', referenceGroup: 'comments', @@ -447,6 +441,7 @@ export const OPERATION_DEFINITIONS = { requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', + throws: ['INVALID_INPUT'], }), referenceDocPath: 'comments/list.mdx', referenceGroup: 'comments', @@ -458,6 +453,7 @@ export const OPERATION_DEFINITIONS = { requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', + throws: ['INVALID_INPUT'], }), referenceDocPath: 'track-changes/list.mdx', referenceGroup: 'trackChanges', @@ -482,7 +478,7 @@ export const OPERATION_DEFINITIONS = { supportsDryRun: false, supportsTrackedMode: false, possibleFailureCodes: ['NO_OP'], - throws: [...T_NOT_FOUND_COMMAND, 'INVALID_INPUT', 'INVALID_TARGET'], + throws: [...T_NOT_FOUND_CAPABLE, 'INVALID_INPUT', 'INVALID_TARGET'], }), referenceDocPath: 'track-changes/decide.mdx', referenceGroup: 'trackChanges', diff --git a/packages/document-api/src/create/create.ts b/packages/document-api/src/create/create.ts index 4130ae8e97..66231f3c78 100644 --- a/packages/document-api/src/create/create.ts +++ b/packages/document-api/src/create/create.ts @@ -19,37 +19,19 @@ export type CreateAdapter = CreateApi; /** * Validates the `at` location for create operations when `before`/`after` is used. - * Ensures either `target` or `nodeId` is provided, not both. + * Ensures `target` is provided. */ function validateCreateLocation(at: ParagraphCreateLocation | HeadingCreateLocation, operationName: string): void { if (at.kind !== 'before' && at.kind !== 'after') return; - const loc = at as { kind: string; target?: unknown; nodeId?: unknown }; - const hasTarget = loc.target !== undefined; - const hasNodeId = loc.nodeId !== undefined; - - if (hasTarget && hasNodeId) { - throw new DocumentApiValidationError( - 'INVALID_TARGET', - `Cannot combine at.target with at.nodeId on ${operationName} request. Use exactly one locator mode.`, - { fields: ['at.target', 'at.nodeId'] }, - ); - } - - if (!hasTarget && !hasNodeId) { + const loc = at as { kind: string; target?: unknown }; + if (loc.target === undefined) { throw new DocumentApiValidationError( 'INVALID_TARGET', - `${operationName} with at.kind="${at.kind}" requires either at.target or at.nodeId.`, - { fields: ['at.target', 'at.nodeId'] }, + `${operationName} with at.kind="${at.kind}" requires at.target.`, + { fields: ['at.target'] }, ); } - - if (hasNodeId && typeof loc.nodeId !== 'string') { - throw new DocumentApiValidationError('INVALID_TARGET', `at.nodeId must be a string, got ${typeof loc.nodeId}.`, { - field: 'at.nodeId', - value: loc.nodeId, - }); - } } function normalizeParagraphCreateLocation(location?: ParagraphCreateLocation): ParagraphCreateLocation { diff --git a/packages/document-api/src/index.test.ts b/packages/document-api/src/index.test.ts index 730738b5e5..d531d38e8d 100644 --- a/packages/document-api/src/index.test.ts +++ b/packages/document-api/src/index.test.ts @@ -1766,7 +1766,7 @@ describe('createDocumentApi', () => { const api = makeApi(); expectValidationError( () => api.create.paragraph({ at: { kind: 'before' } as any, text: 'Hello' }), - 'requires either at.target or at.nodeId', + 'requires at.target', ); }); diff --git a/packages/document-api/src/lists/lists.ts b/packages/document-api/src/lists/lists.ts index f4a369dc31..b5202e9024 100644 --- a/packages/document-api/src/lists/lists.ts +++ b/packages/document-api/src/lists/lists.ts @@ -27,33 +27,11 @@ export type { } from './lists.types.js'; /** - * Validates that a list operation input has exactly one target locator mode: - * either `target` (canonical ListItemAddress) or `nodeId` (shorthand). + * Validates that a list operation input has a target locator. */ -function validateListTarget(input: { target?: unknown; nodeId?: unknown }, operationName: string): void { - const hasTarget = input.target !== undefined; - const hasNodeId = input.nodeId !== undefined; - - if (hasTarget && hasNodeId) { - throw new DocumentApiValidationError( - 'INVALID_TARGET', - `Cannot combine target with nodeId on ${operationName} request. Use exactly one locator mode.`, - { fields: ['target', 'nodeId'] }, - ); - } - - if (!hasTarget && !hasNodeId) { - throw new DocumentApiValidationError( - 'INVALID_TARGET', - `${operationName} requires a target. Provide either target or nodeId.`, - ); - } - - if (hasNodeId && typeof input.nodeId !== 'string') { - throw new DocumentApiValidationError('INVALID_TARGET', `nodeId must be a string, got ${typeof input.nodeId}.`, { - field: 'nodeId', - value: input.nodeId, - }); +function validateListTarget(input: { target?: unknown }, operationName: string): void { + if (input.target === undefined) { + throw new DocumentApiValidationError('INVALID_TARGET', `${operationName} requires a target.`); } } diff --git a/packages/document-api/src/overview-examples.test.ts b/packages/document-api/src/overview-examples.test.ts index 98b1ecba0f..86010e1f8f 100644 --- a/packages/document-api/src/overview-examples.test.ts +++ b/packages/document-api/src/overview-examples.test.ts @@ -490,7 +490,7 @@ describe('src/README.md workflow examples', () => { it('find then replace', () => { const doc = makeApi(); - const result = doc.find({ type: 'text', text: 'foo' }); + const result = doc.find({ type: 'text', pattern: 'foo' }); const target = result.items[0]?.context?.textRanges?.[0]; if (target) { doc.replace({ target, text: 'bar' }); @@ -522,17 +522,17 @@ describe('src/README.md workflow examples', () => { const doc = makeApi(); // Simulate having a find result in scope (the example assumes `result` exists) - const result = doc.find({ type: 'text', text: 'something' }); + const result = doc.find({ type: 'text', pattern: 'something' }); const target = result.items[0]?.context?.textRanges?.[0]; const createReceipt = doc.comments.create({ target: target!, text: 'Review this section.' }); // Use the comment ID from the receipt to reply const comments = doc.comments.list(); const thread = comments.items[0]; - doc.comments.create({ parentCommentId: thread.commentId, text: 'Looks good.' }); - doc.comments.patch({ commentId: thread.commentId, status: 'resolved' }); + doc.comments.create({ parentCommentId: thread.id, text: 'Looks good.' }); + doc.comments.patch({ commentId: thread.id, status: 'resolved' }); expect(createReceipt.success).toBe(true); - expect(thread.commentId).toBeDefined(); + expect(thread.id).toBeDefined(); }); }); diff --git a/packages/document-api/src/types/mutation-plan.types.ts b/packages/document-api/src/types/mutation-plan.types.ts index cee116a87f..b1431c567d 100644 --- a/packages/document-api/src/types/mutation-plan.types.ts +++ b/packages/document-api/src/types/mutation-plan.types.ts @@ -25,13 +25,6 @@ export type RefWhere = { by: 'ref'; ref: string; within?: NodeAddress; - /** - * Legacy field — kept for backward compatibility during migration. - * Only `'exactlyOne'` is accepted; other values fail with INVALID_INPUT. - * A ref already identifies one logical target, so cardinality is implicit. - * @deprecated Will be removed in the next major contract version. - */ - require?: 'exactlyOne'; }; export type StepWhere = SelectWhere | RefWhere; diff --git a/packages/sdk/langs/node/README.md b/packages/sdk/langs/node/README.md index 68e89acc7b..6df6188668 100644 --- a/packages/sdk/langs/node/README.md +++ b/packages/sdk/langs/node/README.md @@ -34,7 +34,7 @@ console.log(info.counts); const results = await client.doc.find({ type: 'text', pattern: 'termination' }); await client.doc.replace({ - target: results.context[0].textRanges[0], + target: results.items[0].context.textRanges[0], text: 'expiration', }); diff --git a/packages/sdk/langs/python/README.md b/packages/sdk/langs/python/README.md index 087773398f..7ada58924e 100644 --- a/packages/sdk/langs/python/README.md +++ b/packages/sdk/langs/python/README.md @@ -33,7 +33,7 @@ async def main(): print(info["counts"]) results = await client.doc.find({"type": "text", "pattern": "termination"}) - target = results["context"][0]["textRanges"][0] + target = results["items"][0]["context"]["textRanges"][0] await client.doc.replace({"target": target, "text": "expiration"}) await client.doc.save({"inPlace": True}) diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/comments-wrappers.ts b/packages/super-editor/src/document-api-adapters/plan-engine/comments-wrappers.ts index e2551d3754..951928e5b6 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/comments-wrappers.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/comments-wrappers.ts @@ -229,7 +229,7 @@ function buildCommentInfos(editor: Editor): CommentInfo[] { // --------------------------------------------------------------------------- function addCommentHandler(editor: Editor, input: AddCommentInput, options?: RevisionGuardOptions): Receipt { - requireEditorCommand(editor.commands?.addComment, 'comments.add (addComment)'); + requireEditorCommand(editor.commands?.addComment, 'comments.create (addComment)'); if (input.target.range.start === input.target.range.end) { return { @@ -273,7 +273,7 @@ function addCommentHandler(editor: Editor, input: AddCommentInput, options?: Rev const receipt = executeDomainCommand( editor, () => { - const addComment = requireEditorCommand(editor.commands?.addComment, 'comments.add (addComment)'); + const addComment = requireEditorCommand(editor.commands?.addComment, 'comments.create (addComment)'); const didInsert = addComment({ content: input.text, isInternal: false, commentId }) === true; if (didInsert) { clearIndexCache(editor); @@ -311,7 +311,7 @@ function addCommentHandler(editor: Editor, input: AddCommentInput, options?: Rev } function editCommentHandler(editor: Editor, input: EditCommentInput, options?: RevisionGuardOptions): Receipt { - const editComment = requireEditorCommand(editor.commands?.editComment, 'comments.edit (editComment)'); + const editComment = requireEditorCommand(editor.commands?.editComment, 'comments.patch (editComment)'); const store = getCommentEntityStore(editor); const identity = resolveCommentIdentity(editor, input.commentId); @@ -355,7 +355,7 @@ function editCommentHandler(editor: Editor, input: EditCommentInput, options?: R } function replyToCommentHandler(editor: Editor, input: ReplyToCommentInput, options?: RevisionGuardOptions): Receipt { - const addCommentReply = requireEditorCommand(editor.commands?.addCommentReply, 'comments.reply (addCommentReply)'); + const addCommentReply = requireEditorCommand(editor.commands?.addCommentReply, 'comments.create (addCommentReply)'); if (!input.parentCommentId) { return { @@ -413,7 +413,7 @@ function replyToCommentHandler(editor: Editor, input: ReplyToCommentInput, optio } function moveCommentHandler(editor: Editor, input: MoveCommentInput, options?: RevisionGuardOptions): Receipt { - const moveComment = requireEditorCommand(editor.commands?.moveComment, 'comments.move (moveComment)'); + const moveComment = requireEditorCommand(editor.commands?.moveComment, 'comments.patch (moveComment)'); if (input.target.range.start === input.target.range.end) { return { @@ -476,7 +476,7 @@ function moveCommentHandler(editor: Editor, input: MoveCommentInput, options?: R } function resolveCommentHandler(editor: Editor, input: ResolveCommentInput, options?: RevisionGuardOptions): Receipt { - const resolveComment = requireEditorCommand(editor.commands?.resolveComment, 'comments.resolve (resolveComment)'); + const resolveComment = requireEditorCommand(editor.commands?.resolveComment, 'comments.patch (resolveComment)'); const store = getCommentEntityStore(editor); const identity = resolveCommentIdentity(editor, input.commentId); diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/compiler-ref-targeting.test.ts b/packages/super-editor/src/document-api-adapters/plan-engine/compiler-ref-targeting.test.ts index 29a7a55792..8cbaa36717 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/compiler-ref-targeting.test.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/compiler-ref-targeting.test.ts @@ -95,47 +95,6 @@ describe('compilePlan ref-targeting semantics', () => { throw new Error('expected compilePlan to throw MATCH_NOT_FOUND'); }); - - it('throws INVALID_INPUT when v2 span ref segments are not contiguous in block order', () => { - mockedDeps.getBlockIndex.mockReturnValue({ - candidates: [ - { nodeId: 'p1', pos: 0, end: 12, node: {} }, - { nodeId: 'p2', pos: 20, end: 32, node: {} }, - { nodeId: 'p3', pos: 40, end: 52, node: {} }, - ], - }); - - const ref = encodeTextRefPayload({ - v: 2, - rev: '0', - matchId: 'm:0', - segments: [ - { blockId: 'p1', start: 0, end: 3 }, - { blockId: 'p3', start: 0, end: 3 }, - ], - }); - - const editor = makeEditor(); - const steps: MutationStep[] = [ - { - id: 'span-delete', - op: 'text.delete', - where: { by: 'ref', ref }, - args: {}, - }, - ]; - - try { - compilePlan(editor, steps); - } catch (error) { - expect(error).toBeInstanceOf(PlanError); - expect((error as PlanError).code).toBe('INVALID_INPUT'); - expect((error as PlanError).message).toContain('not contiguous'); - return; - } - - throw new Error('expected compilePlan to throw INVALID_INPUT for non-contiguous span'); - }); }); // --------------------------------------------------------------------------- diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/compiler.ts b/packages/super-editor/src/document-api-adapters/plan-engine/compiler.ts index 7c2856803f..69f4ee181b 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/compiler.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/compiler.ts @@ -59,21 +59,6 @@ function isRefWhere(where: MutationStep['where']): where is RefWhere { // Text ref payload versions // --------------------------------------------------------------------------- -/** V1: single-block text ref (legacy). */ // TODO(v1v2-sunset) -interface TextRefV1 { - rev: string; - addr: unknown; - ranges?: TextAddress[]; -} - -/** V2: multi-block span ref with explicit version tag. */ // TODO(v1v2-sunset) -interface TextRefV2 { - v: 2; - rev: string; - matchId: string; - segments: Array<{ blockId: string; start: number; end: number }>; -} - /** V3: scope-aware ref with match/block/run targeting (D6). */ interface TextRefV3 { v: 3; @@ -85,14 +70,10 @@ interface TextRefV3 { runIndex?: number; } -type TextRefPayload = TextRefV1 | TextRefV2 | TextRefV3; - -function isV3Ref(payload: TextRefPayload): payload is TextRefV3 { - return 'v' in payload && payload.v === 3; -} - -function isV2Ref(payload: TextRefPayload): payload is TextRefV2 { - return 'v' in payload && payload.v === 2; +function isV3Ref(payload: unknown): payload is TextRefV3 { + return ( + typeof payload === 'object' && payload !== null && 'v' in payload && (payload as Record).v === 3 + ); } // --------------------------------------------------------------------------- @@ -127,15 +108,12 @@ function resolveAbsoluteRange( } // --------------------------------------------------------------------------- -// Single-block range normalizer (legacy compatibility wrapper per D12) +// Single-block range normalizer — delegates to normalizeMatchSpan (D12) // --------------------------------------------------------------------------- /** * Coalesces text ranges from a single logical match into one contiguous range. * All ranges must belong to the same block. - * - * @deprecated Use `normalizeMatchSpan` for new code paths; this wrapper - * delegates to it and extracts the single-block result. */ export function normalizeMatchRanges( stepId: string, @@ -462,7 +440,7 @@ function getBlockText(editor: Editor, candidate: { pos: number; end: number }): // Ref resolution // --------------------------------------------------------------------------- -function decodeTextRefPayload(encoded: string, stepId: string): TextRefPayload { +function decodeTextRefPayload(encoded: string, stepId: string): unknown { try { return JSON.parse(atob(encoded)); } catch { @@ -470,80 +448,6 @@ function decodeTextRefPayload(encoded: string, stepId: string): TextRefPayload { } } -function resolveV1TextRef(editor: Editor, index: BlockIndex, step: MutationStep, refData: TextRefV1): CompiledTarget[] { - const currentRevision = getRevision(editor); - if (refData.rev !== currentRevision) { - throw planError( - 'REVISION_MISMATCH', - `text ref was created at revision "${refData.rev}" but document is at "${currentRevision}"`, - step.id, - { refRevision: refData.rev, currentRevision }, - ); - } - - if (!refData.ranges?.length) return []; - - const coalesced = normalizeMatchRanges(step.id, refData.ranges); - const candidate = index.candidates.find((c) => c.nodeId === coalesced.blockId); - if (!candidate) return []; - - const blockText = getBlockText(editor, candidate); - const matchText = blockText.slice(coalesced.from, coalesced.to); - - const addr: ResolvedAddress = { - blockId: coalesced.blockId, - from: coalesced.from, - to: coalesced.to, - text: matchText, - marks: [], - blockPos: candidate.pos, - }; - - return [buildRangeTarget(editor, step, addr, candidate)]; -} - -function resolveV2TextRef(editor: Editor, index: BlockIndex, step: MutationStep, refData: TextRefV2): CompiledTarget[] { - const currentRevision = getRevision(editor); - if (refData.rev !== currentRevision) { - throw planError( - 'REVISION_MISMATCH', - `text ref was created at revision "${refData.rev}" but document is at "${currentRevision}"`, - step.id, - { refRevision: refData.rev, currentRevision }, - ); - } - - if (!refData.segments?.length) return []; - - const segments = refData.segments.map((s) => ({ blockId: s.blockId, from: s.start, to: s.end })); - - // D2/Phase 3: single-segment V2 refs downcast to range target - if (segments.length === 1) { - const seg = segments[0]; - const candidate = index.candidates.find((c) => c.nodeId === seg.blockId); - if (!candidate) return []; - - const blockText = getBlockText(editor, candidate); - const matchText = blockText.slice(seg.from, seg.to); - - const addr: ResolvedAddress = { - blockId: seg.blockId, - from: seg.from, - to: seg.to, - text: matchText, - marks: [], - blockPos: candidate.pos, - }; - - const target = buildRangeTarget(editor, step, addr, candidate); - target.matchId = refData.matchId; - return [target]; - } - - // Multi-segment: build span target - return [buildSpanTarget(editor, index, step, segments, refData.matchId)]; -} - /** * Resolves a V3 text ref into compiled targets. * @@ -598,15 +502,11 @@ function resolveTextRef(editor: Editor, index: BlockIndex, step: MutationStep, r const encoded = ref.slice(5); // strip 'text:' prefix const payload = decodeTextRefPayload(encoded, step.id); - if (isV3Ref(payload)) { - return resolveV3TextRef(editor, index, step, payload); + if (!isV3Ref(payload)) { + throw planError('INVALID_INPUT', 'only V3 text refs are supported', step.id); } - if (isV2Ref(payload)) { - return resolveV2TextRef(editor, index, step, payload); // TODO(v1v2-sunset) - } - - return resolveV1TextRef(editor, index, step, payload); // TODO(v1v2-sunset) + return resolveV3TextRef(editor, index, step, payload); } function resolveBlockRef(editor: Editor, index: BlockIndex, step: MutationStep, ref: string): CompiledTarget[] { @@ -674,15 +574,6 @@ function dispatchRefHandler(editor: Editor, index: BlockIndex, step: MutationSte } function resolveRefTargets(editor: Editor, index: BlockIndex, step: MutationStep, where: RefWhere): CompiledTarget[] { - // D7: validate legacy require field - if (where.require !== undefined && where.require !== 'exactlyOne') { - throw planError( - 'INVALID_INPUT', - `ref-based targeting only accepts require: 'exactlyOne' (received '${where.require}')`, - step.id, - ); - } - return dispatchRefHandler(editor, index, step, where.ref); } diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/executor-registry.types.ts b/packages/super-editor/src/document-api-adapters/plan-engine/executor-registry.types.ts index b0d097cee0..2e0fb27ab6 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/executor-registry.types.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/executor-registry.types.ts @@ -24,7 +24,7 @@ export interface CompiledSegment { absTo: number; } -/** Single-block range target — used for all legacy single-block operations. */ +/** Single-block range target — used for all single-block operations. */ export interface CompiledRangeTarget { kind: 'range'; stepId: string; diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/query-match-adapter.test.ts b/packages/super-editor/src/document-api-adapters/plan-engine/query-match-adapter.test.ts index a4ca99a509..730bae3c89 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/query-match-adapter.test.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/query-match-adapter.test.ts @@ -130,7 +130,7 @@ function makeEditorWithBlocks( // --------------------------------------------------------------------------- /** - * Builds a FindOutput-shaped mock from legacy-style matches/context arrays. + * Builds a FindOutput-shaped mock from matches/context arrays. * Merges parallel arrays into per-item discovery items as the real findAdapter does. */ function setupFindResult(options: { matches: any[]; context?: any[]; total: number }) { From 6ad3e183d2130acd995d45298cb999038567af64 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Wed, 25 Feb 2026 14:32:31 -0800 Subject: [PATCH 2/2] chore: fix test --- apps/mcp/src/__tests__/protocol.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/mcp/src/__tests__/protocol.test.ts b/apps/mcp/src/__tests__/protocol.test.ts index 0d6100e722..3652a843b6 100644 --- a/apps/mcp/src/__tests__/protocol.test.ts +++ b/apps/mcp/src/__tests__/protocol.test.ts @@ -155,7 +155,7 @@ describe('MCP protocol integration', () => { expect(textContent(result)).toContain('No open session'); }); - it('returns isError for invalid file path', async () => { + it('creates a blank document when file does not exist', async () => { await ready; const result = await client.callTool({ @@ -163,7 +163,8 @@ describe('MCP protocol integration', () => { arguments: { path: '/nonexistent/file.docx' }, }); - expect(result).toHaveProperty('isError', true); - expect(textContent(result)).toContain('Failed to open document'); + expect(result).not.toHaveProperty('isError'); + const body = JSON.parse(textContent(result)); + expect(body).toHaveProperty('session_id'); }); });