diff --git a/.github/workflows/ci-document-api.yml b/.github/workflows/ci-document-api.yml
new file mode 100644
index 0000000000..e2d1f6cefd
--- /dev/null
+++ b/.github/workflows/ci-document-api.yml
@@ -0,0 +1,36 @@
+name: CI Document API
+
+permissions:
+ contents: read
+
+on:
+ pull_request:
+ paths:
+ - 'packages/document-api/**'
+ - 'apps/docs/document-api/**'
+ - 'package.json'
+ - '.github/workflows/ci-document-api.yml'
+ workflow_dispatch:
+
+concurrency:
+ group: ci-document-api-${{ github.event.pull_request.number || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: .nvmrc
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Check contract parity and generated outputs
+ run: pnpm run docapi:check
diff --git a/CLAUDE.md b/CLAUDE.md
index f6d065eb18..2cd6d0e91b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -64,6 +64,8 @@ tests/visual/ Visual regression tests (Playwright + R2 baselines)
| Style resolution | `layout-engine/style-engine/` |
| Main entry point (Vue) | `superdoc/src/SuperDoc.vue` |
| Visual regression tests | `tests/visual/` (see its CLAUDE.md) |
+| Document API contract | `packages/document-api/src/contract/operation-definitions.ts` |
+| Adding a doc-api operation | See `packages/document-api/README.md` § "Adding a new operation" |
## Style Resolution Boundary
@@ -82,6 +84,18 @@ tests/visual/ Visual regression tests (Playwright + R2 baselines)
- **Editing commands/behavior**: Modify `super-editor/src/extensions/`
- **State bridging**: Modify `PresentationEditor.ts`
+## Document API Contract
+
+The `packages/document-api/` package uses a contract-first pattern with a single source of truth.
+
+- **`operation-definitions.ts`** — canonical object defining every operation's key, metadata, member path, reference doc path, and group. All downstream maps are projected from this file automatically.
+- **`operation-registry.ts`** — type-level registry mapping each operation to its `input`, `options`, and `output` types.
+- **`invoke.ts`** — `TypedDispatchTable` validates dispatch wiring against the registry at compile time.
+
+Adding a new operation touches 4 files: `operation-definitions.ts`, `operation-registry.ts`, `invoke.ts` (dispatch table), and the implementation. See `packages/document-api/README.md` for the full guide.
+
+Do NOT hand-edit `COMMAND_CATALOG`, `OPERATION_MEMBER_PATH_MAP`, `OPERATION_REFERENCE_DOC_PATH_MAP`, or `REFERENCE_OPERATION_GROUPS` — they are derived from `OPERATION_DEFINITIONS`.
+
## JSDoc types
Many packages use `.js` files with JSDoc `@typedef` for type definitions (e.g., `packages/superdoc/src/core/types/index.js`). These typedefs ARE the published type declarations — `vite-plugin-dts` generates `.d.ts` files from them.
diff --git a/apps/docs/CLAUDE.md b/apps/docs/CLAUDE.md
index 97deaf8683..29e3d3d39e 100644
--- a/apps/docs/CLAUDE.md
+++ b/apps/docs/CLAUDE.md
@@ -15,6 +15,23 @@ When moving or renaming a page, always add a redirect in `docs.json`:
}
```
+## Document API generation boundary
+
+Document API docs have mixed manual/generated ownership. Treat these paths as authoritative:
+
+- `apps/docs/document-api/reference/*`: generated, commit to git, do not hand-edit.
+- `packages/document-api/generated/*`: generated, commit to git, do not hand-edit.
+- `apps/docs/document-api/overview.mdx`: manual except for the block between:
+ - `/* DOC_API_GENERATED_API_SURFACE_START */`
+ - `/* DOC_API_GENERATED_API_SURFACE_END */`
+
+To refresh generated content:
+
+```bash
+pnpm exec tsx packages/document-api/scripts/generate-contract-outputs.ts
+pnpm exec tsx packages/document-api/scripts/check-contract-outputs.ts
+```
+
## Brand voice
One personality, two registers. SuperDoc is the same person in every conversation — warm, clear, technically confident. It adjusts **what it emphasizes** based on who's listening. Developers hear about the how. Leaders hear about the why.
diff --git a/apps/docs/docs.json b/apps/docs/docs.json
index 1945507d18..9525d0568d 100644
--- a/apps/docs/docs.json
+++ b/apps/docs/docs.json
@@ -91,7 +91,7 @@
},
{
"group": "Document API",
- "tag": "SOON",
+ "tag": "ALPHA",
"pages": ["document-api/overview"]
},
{
diff --git a/apps/docs/document-api/overview.mdx b/apps/docs/document-api/overview.mdx
index f786ba50d4..d1d4f26bda 100644
--- a/apps/docs/document-api/overview.mdx
+++ b/apps/docs/document-api/overview.mdx
@@ -5,122 +5,74 @@ description: A stable, engine-agnostic interface for programmatic document acces
keywords: "document api, programmatic access, query documents, document manipulation, headless docx"
---
-The Document API is a new way to interact with documents programmatically. Query content, make changes, and build automations — all without touching editor internals.
+Document API gives you a consistent way to read and edit documents without relying on editor internals.
-**Coming Soon** — The Document API is currently in development. This page previews what's coming.
+Document API is in alpha and subject to breaking changes while the contract and adapters continue to evolve. The current API is not yet comprehensive, and more commands and namespaces are being added on an ongoing basis.
-## Why Document API?
-
-Today, programmatic access requires using internal editor methods:
-
-```javascript
-// Current approach - uses internal APIs
-editor.commands.insertContent(content);
-editor.state.doc.descendants((node) => { ... });
-```
-
-This works, but:
-- Internal APIs can change between versions
-- Requires understanding ProseMirror internals
-- Tightly coupled to the editor implementation
-
-## What's coming
-
-The Document API provides a stable, high-level interface:
-
-```javascript
-// Document API - stable public interface
-const paragraphs = doc.query({ type: 'paragraph' });
-const tables = doc.query({ type: 'table', contains: 'Revenue' });
-
-doc.replace(paragraphs[0], { text: 'New content' });
-```
-
-
-
- Find content by type, attributes, or text. Filter tables, paragraphs, lists — anything in the document.
-
-
- Public API that won't break between versions. Build with confidence.
-
-
- Works the same whether you're in the browser, Node.js, or headless mode.
-
-
- Full TypeScript support with autocomplete and type checking.
-
-
-
-## Feature preview
-
-### Querying content
-
-Find any content in your document:
-
-```javascript
-// Find all paragraphs
-const paragraphs = doc.query({ type: 'paragraph' });
-
-// Find tables containing specific text
-const tables = doc.query({
- type: 'table',
- contains: 'Q4 Revenue'
-});
-
-// Find content by attributes
-const signatures = doc.query({
- type: 'field-annotation',
- attrs: { fieldType: 'signature' }
-});
-```
-
-### Making changes
-
-Modify documents with a clean API:
-
-```javascript
-// Replace content
-doc.replace(address, { text: 'Updated text' });
-
-// Insert at position
-doc.insert(address, { type: 'paragraph', text: 'New paragraph' });
-
-// Delete content
-doc.delete(address);
-```
-
-### Working with tables
-
-First-class table operations:
-
-```javascript
-// Add a row
-doc.table(tableAddress).addRow({ after: 2 });
-
-// Update a cell
-doc.table(tableAddress).cell(1, 2).replace({ text: 'New value' });
-```
-
-## Timeline
-
-
-
- Query DSL for finding and reading document content
-
-
- Insert, replace, and delete operations
-
-
- Table operations, list manipulation, track changes integration
-
-
-
-## Stay updated
-
-Join Discord to get notified when Document API launches:
-
-
- Get early access and share feedback
-
+## Why use Document API
+
+- Build automations without editor-specific code.
+- Work with predictable inputs and outputs defined per operation.
+- Check capabilities up front and branch safely when features are unavailable.
+
+## Reference
+
+- Full operation reference: [/document-api/reference/index](/document-api/reference/index)
+- Machine-readable files are available for automation use (contract schema, tool manifest, and agent compatibility artifacts).
+
+{/* DOC_API_OPERATIONS_START */}
+### Available operations
+
+Use the tables below to see what operations are available and where each one is documented.
+
+| Namespace | Operations | Reference |
+| --- | --- | --- |
+| Core | 8 | [Reference](/document-api/reference/core/index) |
+| Capabilities | 1 | [Reference](/document-api/reference/capabilities/index) |
+| Create | 1 | [Reference](/document-api/reference/create/index) |
+| Format | 1 | [Reference](/document-api/reference/format/index) |
+| Lists | 8 | [Reference](/document-api/reference/lists/index) |
+| Comments | 11 | [Reference](/document-api/reference/comments/index) |
+| Track Changes | 6 | [Reference](/document-api/reference/track-changes/index) |
+
+| Editor method | Operation ID |
+| --- | --- |
+| `editor.doc.find(...)` | [`find`](/document-api/reference/find) |
+| `editor.doc.getNode(...)` | [`getNode`](/document-api/reference/get-node) |
+| `editor.doc.getNodeById(...)` | [`getNodeById`](/document-api/reference/get-node-by-id) |
+| `editor.doc.getText(...)` | [`getText`](/document-api/reference/get-text) |
+| `editor.doc.info(...)` | [`info`](/document-api/reference/info) |
+| `editor.doc.insert(...)` | [`insert`](/document-api/reference/insert) |
+| `editor.doc.replace(...)` | [`replace`](/document-api/reference/replace) |
+| `editor.doc.delete(...)` | [`delete`](/document-api/reference/delete) |
+| `editor.doc.format.bold(...)` | [`format.bold`](/document-api/reference/format/bold) |
+| `editor.doc.create.paragraph(...)` | [`create.paragraph`](/document-api/reference/create/paragraph) |
+| `editor.doc.lists.list(...)` | [`lists.list`](/document-api/reference/lists/list) |
+| `editor.doc.lists.get(...)` | [`lists.get`](/document-api/reference/lists/get) |
+| `editor.doc.lists.insert(...)` | [`lists.insert`](/document-api/reference/lists/insert) |
+| `editor.doc.lists.setType(...)` | [`lists.setType`](/document-api/reference/lists/set-type) |
+| `editor.doc.lists.indent(...)` | [`lists.indent`](/document-api/reference/lists/indent) |
+| `editor.doc.lists.outdent(...)` | [`lists.outdent`](/document-api/reference/lists/outdent) |
+| `editor.doc.lists.restart(...)` | [`lists.restart`](/document-api/reference/lists/restart) |
+| `editor.doc.lists.exit(...)` | [`lists.exit`](/document-api/reference/lists/exit) |
+| `editor.doc.comments.add(...)` | [`comments.add`](/document-api/reference/comments/add) |
+| `editor.doc.comments.edit(...)` | [`comments.edit`](/document-api/reference/comments/edit) |
+| `editor.doc.comments.reply(...)` | [`comments.reply`](/document-api/reference/comments/reply) |
+| `editor.doc.comments.move(...)` | [`comments.move`](/document-api/reference/comments/move) |
+| `editor.doc.comments.resolve(...)` | [`comments.resolve`](/document-api/reference/comments/resolve) |
+| `editor.doc.comments.remove(...)` | [`comments.remove`](/document-api/reference/comments/remove) |
+| `editor.doc.comments.setInternal(...)` | [`comments.setInternal`](/document-api/reference/comments/set-internal) |
+| `editor.doc.comments.setActive(...)` | [`comments.setActive`](/document-api/reference/comments/set-active) |
+| `editor.doc.comments.goTo(...)` | [`comments.goTo`](/document-api/reference/comments/go-to) |
+| `editor.doc.comments.get(...)` | [`comments.get`](/document-api/reference/comments/get) |
+| `editor.doc.comments.list(...)` | [`comments.list`](/document-api/reference/comments/list) |
+| `editor.doc.trackChanges.list(...)` | [`trackChanges.list`](/document-api/reference/track-changes/list) |
+| `editor.doc.trackChanges.get(...)` | [`trackChanges.get`](/document-api/reference/track-changes/get) |
+| `editor.doc.trackChanges.accept(...)` | [`trackChanges.accept`](/document-api/reference/track-changes/accept) |
+| `editor.doc.trackChanges.reject(...)` | [`trackChanges.reject`](/document-api/reference/track-changes/reject) |
+| `editor.doc.trackChanges.acceptAll(...)` | [`trackChanges.acceptAll`](/document-api/reference/track-changes/accept-all) |
+| `editor.doc.trackChanges.rejectAll(...)` | [`trackChanges.rejectAll`](/document-api/reference/track-changes/reject-all) |
+| `editor.doc.capabilities()` | [`capabilities.get`](/document-api/reference/capabilities/get) |
+{/* DOC_API_OPERATIONS_END */}
diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json
new file mode 100644
index 0000000000..66211da539
--- /dev/null
+++ b/apps/docs/document-api/reference/_generated-manifest.json
@@ -0,0 +1,124 @@
+{
+ "contractVersion": "0.1.0",
+ "files": [
+ "apps/docs/document-api/reference/capabilities/get.mdx",
+ "apps/docs/document-api/reference/capabilities/index.mdx",
+ "apps/docs/document-api/reference/comments/add.mdx",
+ "apps/docs/document-api/reference/comments/edit.mdx",
+ "apps/docs/document-api/reference/comments/get.mdx",
+ "apps/docs/document-api/reference/comments/go-to.mdx",
+ "apps/docs/document-api/reference/comments/index.mdx",
+ "apps/docs/document-api/reference/comments/list.mdx",
+ "apps/docs/document-api/reference/comments/move.mdx",
+ "apps/docs/document-api/reference/comments/remove.mdx",
+ "apps/docs/document-api/reference/comments/reply.mdx",
+ "apps/docs/document-api/reference/comments/resolve.mdx",
+ "apps/docs/document-api/reference/comments/set-active.mdx",
+ "apps/docs/document-api/reference/comments/set-internal.mdx",
+ "apps/docs/document-api/reference/core/index.mdx",
+ "apps/docs/document-api/reference/create/index.mdx",
+ "apps/docs/document-api/reference/create/paragraph.mdx",
+ "apps/docs/document-api/reference/delete.mdx",
+ "apps/docs/document-api/reference/find.mdx",
+ "apps/docs/document-api/reference/format/bold.mdx",
+ "apps/docs/document-api/reference/format/index.mdx",
+ "apps/docs/document-api/reference/get-node-by-id.mdx",
+ "apps/docs/document-api/reference/get-node.mdx",
+ "apps/docs/document-api/reference/get-text.mdx",
+ "apps/docs/document-api/reference/index.mdx",
+ "apps/docs/document-api/reference/info.mdx",
+ "apps/docs/document-api/reference/insert.mdx",
+ "apps/docs/document-api/reference/lists/exit.mdx",
+ "apps/docs/document-api/reference/lists/get.mdx",
+ "apps/docs/document-api/reference/lists/indent.mdx",
+ "apps/docs/document-api/reference/lists/index.mdx",
+ "apps/docs/document-api/reference/lists/insert.mdx",
+ "apps/docs/document-api/reference/lists/list.mdx",
+ "apps/docs/document-api/reference/lists/outdent.mdx",
+ "apps/docs/document-api/reference/lists/restart.mdx",
+ "apps/docs/document-api/reference/lists/set-type.mdx",
+ "apps/docs/document-api/reference/replace.mdx",
+ "apps/docs/document-api/reference/track-changes/accept-all.mdx",
+ "apps/docs/document-api/reference/track-changes/accept.mdx",
+ "apps/docs/document-api/reference/track-changes/get.mdx",
+ "apps/docs/document-api/reference/track-changes/index.mdx",
+ "apps/docs/document-api/reference/track-changes/list.mdx",
+ "apps/docs/document-api/reference/track-changes/reject-all.mdx",
+ "apps/docs/document-api/reference/track-changes/reject.mdx"
+ ],
+ "generatedBy": "packages/document-api/scripts/generate-reference-docs.ts",
+ "groups": [
+ {
+ "key": "core",
+ "operationIds": ["find", "getNode", "getNodeById", "getText", "info", "insert", "replace", "delete"],
+ "pagePath": "apps/docs/document-api/reference/core/index.mdx",
+ "title": "Core"
+ },
+ {
+ "key": "capabilities",
+ "operationIds": ["capabilities.get"],
+ "pagePath": "apps/docs/document-api/reference/capabilities/index.mdx",
+ "title": "Capabilities"
+ },
+ {
+ "key": "create",
+ "operationIds": ["create.paragraph"],
+ "pagePath": "apps/docs/document-api/reference/create/index.mdx",
+ "title": "Create"
+ },
+ {
+ "key": "format",
+ "operationIds": ["format.bold"],
+ "pagePath": "apps/docs/document-api/reference/format/index.mdx",
+ "title": "Format"
+ },
+ {
+ "key": "lists",
+ "operationIds": [
+ "lists.list",
+ "lists.get",
+ "lists.insert",
+ "lists.setType",
+ "lists.indent",
+ "lists.outdent",
+ "lists.restart",
+ "lists.exit"
+ ],
+ "pagePath": "apps/docs/document-api/reference/lists/index.mdx",
+ "title": "Lists"
+ },
+ {
+ "key": "comments",
+ "operationIds": [
+ "comments.add",
+ "comments.edit",
+ "comments.reply",
+ "comments.move",
+ "comments.resolve",
+ "comments.remove",
+ "comments.setInternal",
+ "comments.setActive",
+ "comments.goTo",
+ "comments.get",
+ "comments.list"
+ ],
+ "pagePath": "apps/docs/document-api/reference/comments/index.mdx",
+ "title": "Comments"
+ },
+ {
+ "key": "trackChanges",
+ "operationIds": [
+ "trackChanges.list",
+ "trackChanges.get",
+ "trackChanges.accept",
+ "trackChanges.reject",
+ "trackChanges.acceptAll",
+ "trackChanges.rejectAll"
+ ],
+ "pagePath": "apps/docs/document-api/reference/track-changes/index.mdx",
+ "title": "Track Changes"
+ }
+ ],
+ "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
+ "sourceHash": "8d875ceb279d213c1e779882f103d39fa2106545ce94c6b1553ae06b6c321e03"
+}
diff --git a/apps/docs/document-api/reference/capabilities/get.mdx b/apps/docs/document-api/reference/capabilities/get.mdx
new file mode 100644
index 0000000000..5185167890
--- /dev/null
+++ b/apps/docs/document-api/reference/capabilities/get.mdx
@@ -0,0 +1,1356 @@
+---
+title: capabilities.get
+sidebarTitle: capabilities.get
+description: Generated reference for capabilities.get
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `capabilities.get`
+- API member path: `editor.doc.capabilities()`
+- Mutates document: `no`
+- Idempotency: `idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- None
+
+## Non-applied failure codes
+
+- None
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "global": {
+ "additionalProperties": false,
+ "properties": {
+ "comments": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "enabled"
+ ],
+ "type": "object"
+ },
+ "dryRun": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "enabled"
+ ],
+ "type": "object"
+ },
+ "lists": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "enabled"
+ ],
+ "type": "object"
+ },
+ "trackChanges": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "enabled"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "trackChanges",
+ "comments",
+ "lists",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "operations": {
+ "additionalProperties": false,
+ "properties": {
+ "capabilities.get": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "comments.add": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "comments.edit": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "comments.get": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "comments.goTo": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "comments.list": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "comments.move": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "comments.remove": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "comments.reply": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "comments.resolve": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "comments.setActive": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "comments.setInternal": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "create.paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "delete": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "find": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "format.bold": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "getNode": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "getNodeById": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "getText": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "info": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "insert": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "lists.exit": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "lists.get": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "lists.indent": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "lists.insert": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "lists.list": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "lists.outdent": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "lists.restart": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "lists.setType": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "replace": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "trackChanges.accept": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "trackChanges.acceptAll": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "trackChanges.get": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "trackChanges.list": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "trackChanges.reject": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ },
+ "trackChanges.rejectAll": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "available",
+ "tracked",
+ "dryRun"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "find",
+ "getNode",
+ "getNodeById",
+ "getText",
+ "info",
+ "insert",
+ "replace",
+ "delete",
+ "format.bold",
+ "create.paragraph",
+ "lists.list",
+ "lists.get",
+ "lists.insert",
+ "lists.setType",
+ "lists.indent",
+ "lists.outdent",
+ "lists.restart",
+ "lists.exit",
+ "comments.add",
+ "comments.edit",
+ "comments.reply",
+ "comments.move",
+ "comments.resolve",
+ "comments.remove",
+ "comments.setInternal",
+ "comments.setActive",
+ "comments.goTo",
+ "comments.get",
+ "comments.list",
+ "trackChanges.list",
+ "trackChanges.get",
+ "trackChanges.accept",
+ "trackChanges.reject",
+ "trackChanges.acceptAll",
+ "trackChanges.rejectAll",
+ "capabilities.get"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "global",
+ "operations"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/capabilities/index.mdx b/apps/docs/document-api/reference/capabilities/index.mdx
new file mode 100644
index 0000000000..0047ee55c7
--- /dev/null
+++ b/apps/docs/document-api/reference/capabilities/index.mdx
@@ -0,0 +1,17 @@
+---
+title: Capabilities operations
+sidebarTitle: Capabilities
+description: Generated Capabilities operation reference from the canonical Document API contract.
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+[Back to full reference](../index)
+
+Runtime support discovery for capability-aware branching.
+
+| Operation | Member path | Mutates | Idempotency | Tracked | Dry run |
+| --- | --- | --- | --- | --- | --- |
+| [`capabilities.get`](./get) | `capabilities` | No | `idempotent` | No | No |
diff --git a/apps/docs/document-api/reference/comments/add.mdx b/apps/docs/document-api/reference/comments/add.mdx
new file mode 100644
index 0000000000..b1a51c07c0
--- /dev/null
+++ b/apps/docs/document-api/reference/comments/add.mdx
@@ -0,0 +1,472 @@
+---
+title: comments.add
+sidebarTitle: comments.add
+description: Generated reference for comments.add
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `comments.add`
+- API member path: `editor.doc.comments.add(...)`
+- Mutates document: `yes`
+- Idempotency: `non-idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `INVALID_TARGET`
+- `NO_OP`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "text"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET",
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET",
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/comments/edit.mdx b/apps/docs/document-api/reference/comments/edit.mdx
new file mode 100644
index 0000000000..527c7892d8
--- /dev/null
+++ b/apps/docs/document-api/reference/comments/edit.mdx
@@ -0,0 +1,439 @@
+---
+title: comments.edit
+sidebarTitle: comments.edit
+description: Generated reference for comments.edit
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `comments.edit`
+- API member path: `editor.doc.comments.edit(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "commentId",
+ "text"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/comments/get.mdx b/apps/docs/document-api/reference/comments/get.mdx
new file mode 100644
index 0000000000..a9253266d8
--- /dev/null
+++ b/apps/docs/document-api/reference/comments/get.mdx
@@ -0,0 +1,143 @@
+---
+title: comments.get
+sidebarTitle: comments.get
+description: Generated reference for comments.get
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `comments.get`
+- API member path: `editor.doc.comments.get(...)`
+- Mutates document: `no`
+- Idempotency: `idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+
+## Non-applied failure codes
+
+- None
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "commentId"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ "commentId": {
+ "type": "string"
+ },
+ "createdTime": {
+ "type": "number"
+ },
+ "creatorEmail": {
+ "type": "string"
+ },
+ "creatorName": {
+ "type": "string"
+ },
+ "importedId": {
+ "type": "string"
+ },
+ "isInternal": {
+ "type": "boolean"
+ },
+ "parentCommentId": {
+ "type": "string"
+ },
+ "status": {
+ "enum": [
+ "open",
+ "resolved"
+ ]
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "address",
+ "commentId",
+ "status"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/comments/go-to.mdx b/apps/docs/document-api/reference/comments/go-to.mdx
new file mode 100644
index 0000000000..2a1745655d
--- /dev/null
+++ b/apps/docs/document-api/reference/comments/go-to.mdx
@@ -0,0 +1,204 @@
+---
+title: comments.goTo
+sidebarTitle: comments.goTo
+description: Generated reference for comments.goTo
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `comments.goTo`
+- API member path: `editor.doc.comments.goTo(...)`
+- Mutates document: `no`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- None
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "commentId"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/comments/index.mdx b/apps/docs/document-api/reference/comments/index.mdx
new file mode 100644
index 0000000000..ba7588835c
--- /dev/null
+++ b/apps/docs/document-api/reference/comments/index.mdx
@@ -0,0 +1,27 @@
+---
+title: Comments operations
+sidebarTitle: Comments
+description: Generated Comments operation reference from the canonical Document API contract.
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+[Back to full reference](../index)
+
+Comment authoring and thread lifecycle operations.
+
+| Operation | Member path | Mutates | Idempotency | Tracked | Dry run |
+| --- | --- | --- | --- | --- | --- |
+| [`comments.add`](./add) | `comments.add` | Yes | `non-idempotent` | No | No |
+| [`comments.edit`](./edit) | `comments.edit` | Yes | `conditional` | No | No |
+| [`comments.reply`](./reply) | `comments.reply` | Yes | `non-idempotent` | No | No |
+| [`comments.move`](./move) | `comments.move` | Yes | `conditional` | No | No |
+| [`comments.resolve`](./resolve) | `comments.resolve` | Yes | `conditional` | No | No |
+| [`comments.remove`](./remove) | `comments.remove` | Yes | `conditional` | No | No |
+| [`comments.setInternal`](./set-internal) | `comments.setInternal` | Yes | `conditional` | No | No |
+| [`comments.setActive`](./set-active) | `comments.setActive` | Yes | `conditional` | No | No |
+| [`comments.goTo`](./go-to) | `comments.goTo` | No | `conditional` | No | No |
+| [`comments.get`](./get) | `comments.get` | No | `idempotent` | No | No |
+| [`comments.list`](./list) | `comments.list` | No | `idempotent` | No | No |
diff --git a/apps/docs/document-api/reference/comments/list.mdx b/apps/docs/document-api/reference/comments/list.mdx
new file mode 100644
index 0000000000..277e159460
--- /dev/null
+++ b/apps/docs/document-api/reference/comments/list.mdx
@@ -0,0 +1,156 @@
+---
+title: comments.list
+sidebarTitle: comments.list
+description: Generated reference for comments.list
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `comments.list`
+- API member path: `editor.doc.comments.list(...)`
+- Mutates document: `no`
+- Idempotency: `idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- None
+
+## Non-applied failure codes
+
+- None
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "includeResolved": {
+ "type": "boolean"
+ }
+ },
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "matches": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ "commentId": {
+ "type": "string"
+ },
+ "createdTime": {
+ "type": "number"
+ },
+ "creatorEmail": {
+ "type": "string"
+ },
+ "creatorName": {
+ "type": "string"
+ },
+ "importedId": {
+ "type": "string"
+ },
+ "isInternal": {
+ "type": "boolean"
+ },
+ "parentCommentId": {
+ "type": "string"
+ },
+ "status": {
+ "enum": [
+ "open",
+ "resolved"
+ ]
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "address",
+ "commentId",
+ "status"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "matches",
+ "total"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/comments/move.mdx b/apps/docs/document-api/reference/comments/move.mdx
new file mode 100644
index 0000000000..69980badf2
--- /dev/null
+++ b/apps/docs/document-api/reference/comments/move.mdx
@@ -0,0 +1,472 @@
+---
+title: comments.move
+sidebarTitle: comments.move
+description: Generated reference for comments.move
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `comments.move`
+- API member path: `editor.doc.comments.move(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `INVALID_TARGET`
+- `NO_OP`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "commentId",
+ "target"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET",
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET",
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/comments/remove.mdx b/apps/docs/document-api/reference/comments/remove.mdx
new file mode 100644
index 0000000000..ff5ad99731
--- /dev/null
+++ b/apps/docs/document-api/reference/comments/remove.mdx
@@ -0,0 +1,435 @@
+---
+title: comments.remove
+sidebarTitle: comments.remove
+description: Generated reference for comments.remove
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `comments.remove`
+- API member path: `editor.doc.comments.remove(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "commentId"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/comments/reply.mdx b/apps/docs/document-api/reference/comments/reply.mdx
new file mode 100644
index 0000000000..bd2336b4f5
--- /dev/null
+++ b/apps/docs/document-api/reference/comments/reply.mdx
@@ -0,0 +1,439 @@
+---
+title: comments.reply
+sidebarTitle: comments.reply
+description: Generated reference for comments.reply
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `comments.reply`
+- API member path: `editor.doc.comments.reply(...)`
+- Mutates document: `yes`
+- Idempotency: `non-idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `INVALID_TARGET`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "parentCommentId": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "parentCommentId",
+ "text"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/comments/resolve.mdx b/apps/docs/document-api/reference/comments/resolve.mdx
new file mode 100644
index 0000000000..c757af1ca8
--- /dev/null
+++ b/apps/docs/document-api/reference/comments/resolve.mdx
@@ -0,0 +1,435 @@
+---
+title: comments.resolve
+sidebarTitle: comments.resolve
+description: Generated reference for comments.resolve
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `comments.resolve`
+- API member path: `editor.doc.comments.resolve(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "commentId"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/comments/set-active.mdx b/apps/docs/document-api/reference/comments/set-active.mdx
new file mode 100644
index 0000000000..bb9d5ecd64
--- /dev/null
+++ b/apps/docs/document-api/reference/comments/set-active.mdx
@@ -0,0 +1,438 @@
+---
+title: comments.setActive
+sidebarTitle: comments.setActive
+description: Generated reference for comments.setActive
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `comments.setActive`
+- API member path: `editor.doc.comments.setActive(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `INVALID_TARGET`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": [
+ "commentId"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/comments/set-internal.mdx b/apps/docs/document-api/reference/comments/set-internal.mdx
new file mode 100644
index 0000000000..1a39ffec2d
--- /dev/null
+++ b/apps/docs/document-api/reference/comments/set-internal.mdx
@@ -0,0 +1,442 @@
+---
+title: comments.setInternal
+sidebarTitle: comments.setInternal
+description: Generated reference for comments.setInternal
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `comments.setInternal`
+- API member path: `editor.doc.comments.setInternal(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+- `INVALID_TARGET`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ },
+ "isInternal": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "commentId",
+ "isInternal"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/core/index.mdx b/apps/docs/document-api/reference/core/index.mdx
new file mode 100644
index 0000000000..87e0aea003
--- /dev/null
+++ b/apps/docs/document-api/reference/core/index.mdx
@@ -0,0 +1,24 @@
+---
+title: Core operations
+sidebarTitle: Core
+description: Generated Core operation reference from the canonical Document API contract.
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+[Back to full reference](../index)
+
+Primary read and write operations.
+
+| Operation | Member path | Mutates | Idempotency | Tracked | Dry run |
+| --- | --- | --- | --- | --- | --- |
+| [`find`](../find) | `find` | No | `idempotent` | No | No |
+| [`getNode`](../get-node) | `getNode` | No | `idempotent` | No | No |
+| [`getNodeById`](../get-node-by-id) | `getNodeById` | No | `idempotent` | No | No |
+| [`getText`](../get-text) | `getText` | No | `idempotent` | No | No |
+| [`info`](../info) | `info` | No | `idempotent` | No | No |
+| [`insert`](../insert) | `insert` | Yes | `non-idempotent` | Yes | Yes |
+| [`replace`](../replace) | `replace` | Yes | `conditional` | Yes | Yes |
+| [`delete`](../delete) | `delete` | Yes | `conditional` | Yes | Yes |
diff --git a/apps/docs/document-api/reference/create/index.mdx b/apps/docs/document-api/reference/create/index.mdx
new file mode 100644
index 0000000000..e933913838
--- /dev/null
+++ b/apps/docs/document-api/reference/create/index.mdx
@@ -0,0 +1,17 @@
+---
+title: Create operations
+sidebarTitle: Create
+description: Generated Create operation reference from the canonical Document API contract.
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+[Back to full reference](../index)
+
+Structured creation helpers.
+
+| Operation | Member path | Mutates | Idempotency | Tracked | Dry run |
+| --- | --- | --- | --- | --- | --- |
+| [`create.paragraph`](./paragraph) | `create.paragraph` | Yes | `non-idempotent` | Yes | Yes |
diff --git a/apps/docs/document-api/reference/create/paragraph.mdx b/apps/docs/document-api/reference/create/paragraph.mdx
new file mode 100644
index 0000000000..edbc13208b
--- /dev/null
+++ b/apps/docs/document-api/reference/create/paragraph.mdx
@@ -0,0 +1,419 @@
+---
+title: create.paragraph
+sidebarTitle: create.paragraph
+description: Generated reference for create.paragraph
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `create.paragraph`
+- API member path: `editor.doc.create.paragraph(...)`
+- Mutates document: `yes`
+- Idempotency: `non-idempotent`
+- Supports tracked mode: `yes`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `TRACK_CHANGE_COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `INVALID_TARGET`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "at": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "documentStart"
+ }
+ },
+ "required": [
+ "kind"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "documentEnd"
+ }
+ },
+ "required": [
+ "kind"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "before"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "target"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "after"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "target"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "insertionPoint": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "paragraph"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success",
+ "paragraph",
+ "insertionPoint"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "insertionPoint": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "paragraph"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success",
+ "paragraph",
+ "insertionPoint"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/delete.mdx b/apps/docs/document-api/reference/delete.mdx
new file mode 100644
index 0000000000..3904bca91e
--- /dev/null
+++ b/apps/docs/document-api/reference/delete.mdx
@@ -0,0 +1,853 @@
+---
+title: delete
+sidebarTitle: delete
+description: Generated reference for delete
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `delete`
+- API member path: `editor.doc.delete(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `yes`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `TRACK_CHANGE_COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "target"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success",
+ "resolution"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure",
+ "resolution"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success",
+ "resolution"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure",
+ "resolution"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/find.mdx b/apps/docs/document-api/reference/find.mdx
new file mode 100644
index 0000000000..e1a3fca4f6
--- /dev/null
+++ b/apps/docs/document-api/reference/find.mdx
@@ -0,0 +1,734 @@
+---
+title: find
+sidebarTitle: find
+description: Generated reference for find
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `find`
+- API member path: `editor.doc.find(...)`
+- Mutates document: `no`
+- Idempotency: `idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `no`
+
+## Pre-apply throws
+
+- None
+
+## Non-applied failure codes
+
+- None
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "includeNodes": {
+ "type": "boolean"
+ },
+ "includeUnknown": {
+ "type": "boolean"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "select": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "caseSensitive": {
+ "type": "boolean"
+ },
+ "mode": {
+ "enum": [
+ "contains",
+ "regex"
+ ]
+ },
+ "pattern": {
+ "type": "string"
+ },
+ "type": {
+ "const": "text"
+ }
+ },
+ "required": [
+ "type",
+ "pattern"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "enum": [
+ "block",
+ "inline"
+ ]
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ },
+ "type": {
+ "const": "node"
+ }
+ },
+ "required": [
+ "type"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": [
+ "nodeType"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "within": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "blockId",
+ "offset"
+ ],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "blockId",
+ "offset"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": [
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "sdt",
+ "image",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "anchor"
+ ],
+ "type": "object"
+ }
+ ]
+ }
+ },
+ "required": [
+ "select"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "context": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "blockId",
+ "offset"
+ ],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "blockId",
+ "offset"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": [
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "sdt",
+ "image",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "anchor"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "highlightRange": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ },
+ "snippet": {
+ "type": "string"
+ },
+ "textRanges": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "address",
+ "snippet",
+ "highlightRange"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "diagnostics": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "blockId",
+ "offset"
+ ],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "blockId",
+ "offset"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": [
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "sdt",
+ "image",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "anchor"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "hint": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "message"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "matches": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "blockId",
+ "offset"
+ ],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "blockId",
+ "offset"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": [
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "sdt",
+ "image",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "anchor"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "nodes": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "bodyNodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bodyText": {
+ "type": "string"
+ },
+ "kind": {
+ "enum": [
+ "block",
+ "inline"
+ ]
+ },
+ "nodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ },
+ "properties": {
+ "type": "object"
+ },
+ "summary": {
+ "additionalProperties": false,
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "nodeType",
+ "kind"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "matches",
+ "total"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/format/bold.mdx b/apps/docs/document-api/reference/format/bold.mdx
new file mode 100644
index 0000000000..cd42e62140
--- /dev/null
+++ b/apps/docs/document-api/reference/format/bold.mdx
@@ -0,0 +1,854 @@
+---
+title: format.bold
+sidebarTitle: format.bold
+description: Generated reference for format.bold
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `format.bold`
+- API member path: `editor.doc.format.bold(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `yes`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `TRACK_CHANGE_COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `INVALID_TARGET`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "target"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success",
+ "resolution"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure",
+ "resolution"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success",
+ "resolution"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure",
+ "resolution"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/format/index.mdx b/apps/docs/document-api/reference/format/index.mdx
new file mode 100644
index 0000000000..d472ae1092
--- /dev/null
+++ b/apps/docs/document-api/reference/format/index.mdx
@@ -0,0 +1,17 @@
+---
+title: Format operations
+sidebarTitle: Format
+description: Generated Format operation reference from the canonical Document API contract.
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+[Back to full reference](../index)
+
+Formatting mutations.
+
+| Operation | Member path | Mutates | Idempotency | Tracked | Dry run |
+| --- | --- | --- | --- | --- | --- |
+| [`format.bold`](./bold) | `format.bold` | Yes | `conditional` | Yes | Yes |
diff --git a/apps/docs/document-api/reference/get-node-by-id.mdx b/apps/docs/document-api/reference/get-node-by-id.mdx
new file mode 100644
index 0000000000..7d6d43ca22
--- /dev/null
+++ b/apps/docs/document-api/reference/get-node-by-id.mdx
@@ -0,0 +1,129 @@
+---
+title: getNodeById
+sidebarTitle: getNodeById
+description: Generated reference for getNodeById
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `getNodeById`
+- API member path: `editor.doc.getNodeById(...)`
+- Mutates document: `no`
+- Idempotency: `idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+
+## Non-applied failure codes
+
+- None
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt"
+ ]
+ }
+ },
+ "required": [
+ "nodeId"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "bodyNodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bodyText": {
+ "type": "string"
+ },
+ "kind": {
+ "enum": [
+ "block",
+ "inline"
+ ]
+ },
+ "nodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ },
+ "properties": {
+ "type": "object"
+ },
+ "summary": {
+ "additionalProperties": false,
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "nodeType",
+ "kind"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/get-node.mdx b/apps/docs/document-api/reference/get-node.mdx
new file mode 100644
index 0000000000..2258060aa4
--- /dev/null
+++ b/apps/docs/document-api/reference/get-node.mdx
@@ -0,0 +1,207 @@
+---
+title: getNode
+sidebarTitle: getNode
+description: Generated reference for getNode
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `getNode`
+- API member path: `editor.doc.getNode(...)`
+- Mutates document: `no`
+- Idempotency: `idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+
+## Non-applied failure codes
+
+- None
+
+## Input schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "blockId",
+ "offset"
+ ],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "blockId",
+ "offset"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": [
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "sdt",
+ "image",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "anchor"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Output schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "bodyNodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bodyText": {
+ "type": "string"
+ },
+ "kind": {
+ "enum": [
+ "block",
+ "inline"
+ ]
+ },
+ "nodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ },
+ "properties": {
+ "type": "object"
+ },
+ "summary": {
+ "additionalProperties": false,
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "nodeType",
+ "kind"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/get-text.mdx b/apps/docs/document-api/reference/get-text.mdx
new file mode 100644
index 0000000000..6aeaa7de4c
--- /dev/null
+++ b/apps/docs/document-api/reference/get-text.mdx
@@ -0,0 +1,45 @@
+---
+title: getText
+sidebarTitle: getText
+description: Generated reference for getText
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `getText`
+- API member path: `editor.doc.getText(...)`
+- Mutates document: `no`
+- Idempotency: `idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- None
+
+## Non-applied failure codes
+
+- None
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "type": "string"
+}
+```
diff --git a/apps/docs/document-api/reference/index.mdx b/apps/docs/document-api/reference/index.mdx
new file mode 100644
index 0000000000..9648b8135f
--- /dev/null
+++ b/apps/docs/document-api/reference/index.mdx
@@ -0,0 +1,63 @@
+---
+title: Document API reference
+sidebarTitle: Reference
+description: Generated operation reference from the canonical Document API contract.
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+This reference is generated from `packages/document-api/src/contract/*`.
+Document API is currently alpha and subject to breaking changes.
+
+## Browse by namespace
+
+| Namespace | Operations | Reference |
+| --- | --- | --- |
+| Core | 8 | [Open](./core/index) |
+| Capabilities | 1 | [Open](./capabilities/index) |
+| Create | 1 | [Open](./create/index) |
+| Format | 1 | [Open](./format/index) |
+| Lists | 8 | [Open](./lists/index) |
+| Comments | 11 | [Open](./comments/index) |
+| Track Changes | 6 | [Open](./track-changes/index) |
+
+## All operations
+
+| Operation | Namespace | Member path | Mutates | Idempotency | Tracked | Dry run |
+| --- | --- | --- | --- | --- | --- | --- |
+| [`find`](./find) | Core | `find` | No | `idempotent` | No | No |
+| [`getNode`](./get-node) | Core | `getNode` | No | `idempotent` | No | No |
+| [`getNodeById`](./get-node-by-id) | Core | `getNodeById` | No | `idempotent` | No | No |
+| [`getText`](./get-text) | Core | `getText` | No | `idempotent` | No | No |
+| [`info`](./info) | Core | `info` | No | `idempotent` | No | No |
+| [`insert`](./insert) | Core | `insert` | Yes | `non-idempotent` | Yes | Yes |
+| [`replace`](./replace) | Core | `replace` | Yes | `conditional` | Yes | Yes |
+| [`delete`](./delete) | Core | `delete` | Yes | `conditional` | Yes | Yes |
+| [`format.bold`](./format/bold) | Format | `format.bold` | Yes | `conditional` | Yes | Yes |
+| [`create.paragraph`](./create/paragraph) | Create | `create.paragraph` | Yes | `non-idempotent` | Yes | Yes |
+| [`lists.list`](./lists/list) | Lists | `lists.list` | No | `idempotent` | No | No |
+| [`lists.get`](./lists/get) | Lists | `lists.get` | No | `idempotent` | No | No |
+| [`lists.insert`](./lists/insert) | Lists | `lists.insert` | Yes | `non-idempotent` | Yes | Yes |
+| [`lists.setType`](./lists/set-type) | Lists | `lists.setType` | Yes | `conditional` | No | Yes |
+| [`lists.indent`](./lists/indent) | Lists | `lists.indent` | Yes | `conditional` | No | Yes |
+| [`lists.outdent`](./lists/outdent) | Lists | `lists.outdent` | Yes | `conditional` | No | Yes |
+| [`lists.restart`](./lists/restart) | Lists | `lists.restart` | Yes | `conditional` | No | Yes |
+| [`lists.exit`](./lists/exit) | Lists | `lists.exit` | Yes | `conditional` | No | Yes |
+| [`comments.add`](./comments/add) | Comments | `comments.add` | Yes | `non-idempotent` | No | No |
+| [`comments.edit`](./comments/edit) | Comments | `comments.edit` | Yes | `conditional` | No | No |
+| [`comments.reply`](./comments/reply) | Comments | `comments.reply` | Yes | `non-idempotent` | No | No |
+| [`comments.move`](./comments/move) | Comments | `comments.move` | Yes | `conditional` | No | No |
+| [`comments.resolve`](./comments/resolve) | Comments | `comments.resolve` | Yes | `conditional` | No | No |
+| [`comments.remove`](./comments/remove) | Comments | `comments.remove` | Yes | `conditional` | No | No |
+| [`comments.setInternal`](./comments/set-internal) | Comments | `comments.setInternal` | Yes | `conditional` | No | No |
+| [`comments.setActive`](./comments/set-active) | Comments | `comments.setActive` | Yes | `conditional` | No | No |
+| [`comments.goTo`](./comments/go-to) | Comments | `comments.goTo` | No | `conditional` | No | No |
+| [`comments.get`](./comments/get) | Comments | `comments.get` | No | `idempotent` | No | No |
+| [`comments.list`](./comments/list) | Comments | `comments.list` | No | `idempotent` | No | No |
+| [`trackChanges.list`](./track-changes/list) | Track Changes | `trackChanges.list` | No | `idempotent` | No | No |
+| [`trackChanges.get`](./track-changes/get) | Track Changes | `trackChanges.get` | No | `idempotent` | No | No |
+| [`trackChanges.accept`](./track-changes/accept) | Track Changes | `trackChanges.accept` | Yes | `conditional` | No | No |
+| [`trackChanges.reject`](./track-changes/reject) | Track Changes | `trackChanges.reject` | Yes | `conditional` | No | No |
+| [`trackChanges.acceptAll`](./track-changes/accept-all) | Track Changes | `trackChanges.acceptAll` | Yes | `conditional` | No | No |
+| [`trackChanges.rejectAll`](./track-changes/reject-all) | Track Changes | `trackChanges.rejectAll` | Yes | `conditional` | No | No |
+| [`capabilities.get`](./capabilities/get) | Capabilities | `capabilities` | No | `idempotent` | No | No |
diff --git a/apps/docs/document-api/reference/info.mdx b/apps/docs/document-api/reference/info.mdx
new file mode 100644
index 0000000000..af48f17154
--- /dev/null
+++ b/apps/docs/document-api/reference/info.mdx
@@ -0,0 +1,132 @@
+---
+title: info
+sidebarTitle: info
+description: Generated reference for info
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `info`
+- API member path: `editor.doc.info(...)`
+- Mutates document: `no`
+- Idempotency: `idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- None
+
+## Non-applied failure codes
+
+- None
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "capabilities": {
+ "additionalProperties": false,
+ "properties": {
+ "canComment": {
+ "type": "boolean"
+ },
+ "canFind": {
+ "type": "boolean"
+ },
+ "canGetNode": {
+ "type": "boolean"
+ },
+ "canReplace": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "canFind",
+ "canGetNode",
+ "canComment",
+ "canReplace"
+ ],
+ "type": "object"
+ },
+ "counts": {
+ "additionalProperties": false,
+ "properties": {
+ "comments": {
+ "type": "integer"
+ },
+ "headings": {
+ "type": "integer"
+ },
+ "images": {
+ "type": "integer"
+ },
+ "paragraphs": {
+ "type": "integer"
+ },
+ "tables": {
+ "type": "integer"
+ },
+ "words": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "words",
+ "paragraphs",
+ "headings",
+ "tables",
+ "images",
+ "comments"
+ ],
+ "type": "object"
+ },
+ "outline": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "level": {
+ "type": "integer"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "level",
+ "text",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "counts",
+ "outline",
+ "capabilities"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/insert.mdx b/apps/docs/document-api/reference/insert.mdx
new file mode 100644
index 0000000000..ae409fbf2b
--- /dev/null
+++ b/apps/docs/document-api/reference/insert.mdx
@@ -0,0 +1,859 @@
+---
+title: insert
+sidebarTitle: insert
+description: Generated reference for insert
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `insert`
+- API member path: `editor.doc.insert(...)`
+- Mutates document: `yes`
+- Idempotency: `non-idempotent`
+- Supports tracked mode: `yes`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `TRACK_CHANGE_COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `INVALID_TARGET`
+- `NO_OP`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "text"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success",
+ "resolution"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET",
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure",
+ "resolution"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success",
+ "resolution"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET",
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure",
+ "resolution"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/lists/exit.mdx b/apps/docs/document-api/reference/lists/exit.mdx
new file mode 100644
index 0000000000..e01820da04
--- /dev/null
+++ b/apps/docs/document-api/reference/lists/exit.mdx
@@ -0,0 +1,213 @@
+---
+title: lists.exit
+sidebarTitle: lists.exit
+description: Generated reference for lists.exit
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `lists.exit`
+- API member path: `editor.doc.lists.exit(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `TRACK_CHANGE_COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `INVALID_TARGET`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "target"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "paragraph"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": [
+ "success",
+ "paragraph"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "paragraph"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": [
+ "success",
+ "paragraph"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/lists/get.mdx b/apps/docs/document-api/reference/lists/get.mdx
new file mode 100644
index 0000000000..2b7bf55275
--- /dev/null
+++ b/apps/docs/document-api/reference/lists/get.mdx
@@ -0,0 +1,119 @@
+---
+title: lists.get
+sidebarTitle: lists.get
+description: Generated reference for lists.get
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `lists.get`
+- API member path: `editor.doc.lists.get(...)`
+- Mutates document: `no`
+- Idempotency: `idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+
+## Non-applied failure codes
+
+- None
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "address"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "enum": [
+ "ordered",
+ "bullet"
+ ]
+ },
+ "level": {
+ "type": "integer"
+ },
+ "marker": {
+ "type": "string"
+ },
+ "ordinal": {
+ "type": "integer"
+ },
+ "path": {
+ "items": {
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "address"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/lists/indent.mdx b/apps/docs/document-api/reference/lists/indent.mdx
new file mode 100644
index 0000000000..5bc2f3463c
--- /dev/null
+++ b/apps/docs/document-api/reference/lists/indent.mdx
@@ -0,0 +1,216 @@
+---
+title: lists.indent
+sidebarTitle: lists.indent
+description: Generated reference for lists.indent
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `lists.indent`
+- API member path: `editor.doc.lists.indent(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `TRACK_CHANGE_COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+- `INVALID_TARGET`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "target"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": [
+ "success",
+ "item"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": [
+ "success",
+ "item"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/lists/index.mdx b/apps/docs/document-api/reference/lists/index.mdx
new file mode 100644
index 0000000000..46b5679de6
--- /dev/null
+++ b/apps/docs/document-api/reference/lists/index.mdx
@@ -0,0 +1,24 @@
+---
+title: Lists operations
+sidebarTitle: Lists
+description: Generated Lists operation reference from the canonical Document API contract.
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+[Back to full reference](../index)
+
+List inspection and list mutations.
+
+| Operation | Member path | Mutates | Idempotency | Tracked | Dry run |
+| --- | --- | --- | --- | --- | --- |
+| [`lists.list`](./list) | `lists.list` | No | `idempotent` | No | No |
+| [`lists.get`](./get) | `lists.get` | No | `idempotent` | No | No |
+| [`lists.insert`](./insert) | `lists.insert` | Yes | `non-idempotent` | Yes | Yes |
+| [`lists.setType`](./set-type) | `lists.setType` | Yes | `conditional` | No | Yes |
+| [`lists.indent`](./indent) | `lists.indent` | Yes | `conditional` | No | Yes |
+| [`lists.outdent`](./outdent) | `lists.outdent` | Yes | `conditional` | No | Yes |
+| [`lists.restart`](./restart) | `lists.restart` | Yes | `conditional` | No | Yes |
+| [`lists.exit`](./exit) | `lists.exit` | Yes | `conditional` | No | Yes |
diff --git a/apps/docs/document-api/reference/lists/insert.mdx b/apps/docs/document-api/reference/lists/insert.mdx
new file mode 100644
index 0000000000..c7e2d462bb
--- /dev/null
+++ b/apps/docs/document-api/reference/lists/insert.mdx
@@ -0,0 +1,337 @@
+---
+title: lists.insert
+sidebarTitle: lists.insert
+description: Generated reference for lists.insert
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `lists.insert`
+- API member path: `editor.doc.lists.insert(...)`
+- Mutates document: `yes`
+- Idempotency: `non-idempotent`
+- Supports tracked mode: `yes`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `TRACK_CHANGE_COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `INVALID_TARGET`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "position": {
+ "enum": [
+ "before",
+ "after"
+ ]
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "position"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "insertionPoint": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success",
+ "item",
+ "insertionPoint"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "insertionPoint": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success",
+ "item",
+ "insertionPoint"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/lists/list.mdx b/apps/docs/document-api/reference/lists/list.mdx
new file mode 100644
index 0000000000..ba2bc07b87
--- /dev/null
+++ b/apps/docs/document-api/reference/lists/list.mdx
@@ -0,0 +1,183 @@
+---
+title: lists.list
+sidebarTitle: lists.list
+description: Generated reference for lists.list
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `lists.list`
+- API member path: `editor.doc.lists.list(...)`
+- Mutates document: `no`
+- Idempotency: `idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+
+## Non-applied failure codes
+
+- None
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "enum": [
+ "ordered",
+ "bullet"
+ ]
+ },
+ "level": {
+ "type": "integer"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "ordinal": {
+ "type": "integer"
+ },
+ "within": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt"
+ ]
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ }
+ },
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "items": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "enum": [
+ "ordered",
+ "bullet"
+ ]
+ },
+ "level": {
+ "type": "integer"
+ },
+ "marker": {
+ "type": "string"
+ },
+ "ordinal": {
+ "type": "integer"
+ },
+ "path": {
+ "items": {
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "address"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "matches": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "matches",
+ "total",
+ "items"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/lists/outdent.mdx b/apps/docs/document-api/reference/lists/outdent.mdx
new file mode 100644
index 0000000000..aebde40d10
--- /dev/null
+++ b/apps/docs/document-api/reference/lists/outdent.mdx
@@ -0,0 +1,216 @@
+---
+title: lists.outdent
+sidebarTitle: lists.outdent
+description: Generated reference for lists.outdent
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `lists.outdent`
+- API member path: `editor.doc.lists.outdent(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `TRACK_CHANGE_COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+- `INVALID_TARGET`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "target"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": [
+ "success",
+ "item"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": [
+ "success",
+ "item"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/lists/restart.mdx b/apps/docs/document-api/reference/lists/restart.mdx
new file mode 100644
index 0000000000..3cc6d5e308
--- /dev/null
+++ b/apps/docs/document-api/reference/lists/restart.mdx
@@ -0,0 +1,216 @@
+---
+title: lists.restart
+sidebarTitle: lists.restart
+description: Generated reference for lists.restart
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `lists.restart`
+- API member path: `editor.doc.lists.restart(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `TRACK_CHANGE_COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+- `INVALID_TARGET`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "target"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": [
+ "success",
+ "item"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": [
+ "success",
+ "item"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/lists/set-type.mdx b/apps/docs/document-api/reference/lists/set-type.mdx
new file mode 100644
index 0000000000..29bc7054a0
--- /dev/null
+++ b/apps/docs/document-api/reference/lists/set-type.mdx
@@ -0,0 +1,223 @@
+---
+title: lists.setType
+sidebarTitle: lists.setType
+description: Generated reference for lists.setType
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `lists.setType`
+- API member path: `editor.doc.lists.setType(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `TRACK_CHANGE_COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+- `INVALID_TARGET`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "enum": [
+ "ordered",
+ "bullet"
+ ]
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "target",
+ "kind"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": [
+ "success",
+ "item"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": [
+ "kind",
+ "nodeType",
+ "nodeId"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": [
+ "success",
+ "item"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP",
+ "INVALID_TARGET"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/replace.mdx b/apps/docs/document-api/reference/replace.mdx
new file mode 100644
index 0000000000..6393a5a5e9
--- /dev/null
+++ b/apps/docs/document-api/reference/replace.mdx
@@ -0,0 +1,860 @@
+---
+title: replace
+sidebarTitle: replace
+description: Generated reference for replace
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `replace`
+- API member path: `editor.doc.replace(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `yes`
+- Supports dry run: `yes`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `TRACK_CHANGE_COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `INVALID_TARGET`
+- `NO_OP`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "text"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success",
+ "resolution"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET",
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure",
+ "resolution"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success",
+ "resolution"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "INVALID_TARGET",
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "from",
+ "to"
+ ],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "start",
+ "end"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "kind",
+ "blockId",
+ "range"
+ ],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target",
+ "range",
+ "text"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure",
+ "resolution"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/track-changes/accept-all.mdx b/apps/docs/document-api/reference/track-changes/accept-all.mdx
new file mode 100644
index 0000000000..d995e2d106
--- /dev/null
+++ b/apps/docs/document-api/reference/track-changes/accept-all.mdx
@@ -0,0 +1,427 @@
+---
+title: trackChanges.acceptAll
+sidebarTitle: trackChanges.acceptAll
+description: Generated reference for trackChanges.acceptAll
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `trackChanges.acceptAll`
+- API member path: `editor.doc.trackChanges.acceptAll(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/track-changes/accept.mdx b/apps/docs/document-api/reference/track-changes/accept.mdx
new file mode 100644
index 0000000000..06369d160a
--- /dev/null
+++ b/apps/docs/document-api/reference/track-changes/accept.mdx
@@ -0,0 +1,435 @@
+---
+title: trackChanges.accept
+sidebarTitle: trackChanges.accept
+description: Generated reference for trackChanges.accept
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `trackChanges.accept`
+- API member path: `editor.doc.trackChanges.accept(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/track-changes/get.mdx b/apps/docs/document-api/reference/track-changes/get.mdx
new file mode 100644
index 0000000000..48a5440054
--- /dev/null
+++ b/apps/docs/document-api/reference/track-changes/get.mdx
@@ -0,0 +1,105 @@
+---
+title: trackChanges.get
+sidebarTitle: trackChanges.get
+description: Generated reference for trackChanges.get
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `trackChanges.get`
+- API member path: `editor.doc.trackChanges.get(...)`
+- Mutates document: `no`
+- Idempotency: `idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+
+## Non-applied failure codes
+
+- None
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ "author": {
+ "type": "string"
+ },
+ "authorEmail": {
+ "type": "string"
+ },
+ "authorImage": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "excerpt": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ "insert",
+ "delete",
+ "format"
+ ]
+ }
+ },
+ "required": [
+ "address",
+ "id",
+ "type"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/track-changes/index.mdx b/apps/docs/document-api/reference/track-changes/index.mdx
new file mode 100644
index 0000000000..ff64d0a388
--- /dev/null
+++ b/apps/docs/document-api/reference/track-changes/index.mdx
@@ -0,0 +1,22 @@
+---
+title: Track Changes operations
+sidebarTitle: Track Changes
+description: Generated Track Changes operation reference from the canonical Document API contract.
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+[Back to full reference](../index)
+
+Tracked-change inspection and review operations.
+
+| Operation | Member path | Mutates | Idempotency | Tracked | Dry run |
+| --- | --- | --- | --- | --- | --- |
+| [`trackChanges.list`](./list) | `trackChanges.list` | No | `idempotent` | No | No |
+| [`trackChanges.get`](./get) | `trackChanges.get` | No | `idempotent` | No | No |
+| [`trackChanges.accept`](./accept) | `trackChanges.accept` | Yes | `conditional` | No | No |
+| [`trackChanges.reject`](./reject) | `trackChanges.reject` | Yes | `conditional` | No | No |
+| [`trackChanges.acceptAll`](./accept-all) | `trackChanges.acceptAll` | Yes | `conditional` | No | No |
+| [`trackChanges.rejectAll`](./reject-all) | `trackChanges.rejectAll` | Yes | `conditional` | No | No |
diff --git a/apps/docs/document-api/reference/track-changes/list.mdx b/apps/docs/document-api/reference/track-changes/list.mdx
new file mode 100644
index 0000000000..24cd808c26
--- /dev/null
+++ b/apps/docs/document-api/reference/track-changes/list.mdx
@@ -0,0 +1,151 @@
+---
+title: trackChanges.list
+sidebarTitle: trackChanges.list
+description: Generated reference for trackChanges.list
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `trackChanges.list`
+- API member path: `editor.doc.trackChanges.list(...)`
+- Mutates document: `no`
+- Idempotency: `idempotent`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- None
+
+## Non-applied failure codes
+
+- None
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "type": {
+ "enum": [
+ "insert",
+ "delete",
+ "format"
+ ]
+ }
+ },
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "changes": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ "author": {
+ "type": "string"
+ },
+ "authorEmail": {
+ "type": "string"
+ },
+ "authorImage": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "excerpt": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "enum": [
+ "insert",
+ "delete",
+ "format"
+ ]
+ }
+ },
+ "required": [
+ "address",
+ "id",
+ "type"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "matches": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "matches",
+ "total"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/track-changes/reject-all.mdx b/apps/docs/document-api/reference/track-changes/reject-all.mdx
new file mode 100644
index 0000000000..f631dbde52
--- /dev/null
+++ b/apps/docs/document-api/reference/track-changes/reject-all.mdx
@@ -0,0 +1,427 @@
+---
+title: trackChanges.rejectAll
+sidebarTitle: trackChanges.rejectAll
+description: Generated reference for trackChanges.rejectAll
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `trackChanges.rejectAll`
+- API member path: `editor.doc.trackChanges.rejectAll(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/apps/docs/document-api/reference/track-changes/reject.mdx b/apps/docs/document-api/reference/track-changes/reject.mdx
new file mode 100644
index 0000000000..3599a457db
--- /dev/null
+++ b/apps/docs/document-api/reference/track-changes/reject.mdx
@@ -0,0 +1,435 @@
+---
+title: trackChanges.reject
+sidebarTitle: trackChanges.reject
+description: Generated reference for trackChanges.reject
+---
+
+{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: `trackChanges.reject`
+- API member path: `editor.doc.trackChanges.reject(...)`
+- Mutates document: `yes`
+- Idempotency: `conditional`
+- Supports tracked mode: `no`
+- Supports dry run: `no`
+- Deterministic target resolution: `yes`
+
+## Pre-apply throws
+
+- `TARGET_NOT_FOUND`
+- `COMMAND_UNAVAILABLE`
+- `CAPABILITY_UNAVAILABLE`
+
+## Non-applied failure codes
+
+- `NO_OP`
+
+## Input schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id"
+ ],
+ "type": "object"
+}
+```
+
+## Output schema
+
+```json
+{
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+## Success schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": [
+ "kind",
+ "entityType",
+ "entityId"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "success"
+ ],
+ "type": "object"
+}
+```
+
+## Failure schema
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "NO_OP"
+ ]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "failure"
+ ],
+ "type": "object"
+}
+```
diff --git a/lefthook.yml b/lefthook.yml
index 08eda888b5..067d565772 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -39,6 +39,9 @@ pre-commit:
root: "apps/docs/"
glob: "apps/docs/**/*.mdx"
run: pnpm run test:examples
+ docapi-sync:
+ glob: "{packages/document-api,apps/docs/document-api}/**"
+ run: pnpm run docapi:sync && git add packages/document-api/generated apps/docs/document-api/reference apps/docs/document-api/overview.mdx
commit-msg:
commands:
diff --git a/package.json b/package.json
index a0784bb07b..1fdceb3c62 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,10 @@
"local:publish": "pnpm --prefix packages/superdoc version prerelease --preid=local && pnpm --prefix packages/superdoc publish --registry http://localhost:4873",
"update-preset-geometry": "ROOT=$(pwd) && cd ../superdoc-devtools/preset-geometry && pnpm run build && cp ./dist/index.js ./dist/index.js.map ./dist/index.d.ts \"$ROOT/packages/preset-geometry/\"",
"manual-tag": "bash scripts/manual-tag.sh",
- "manual-clean-tag": "bash scripts/manual-clean-tag.sh"
+ "manual-clean-tag": "bash scripts/manual-clean-tag.sh",
+ "docapi:sync": "pnpm exec tsx packages/document-api/scripts/generate-contract-outputs.ts",
+ "docapi:check": "pnpm exec tsx packages/document-api/scripts/check-contract-parity.ts && pnpm exec tsx packages/document-api/scripts/check-contract-outputs.ts",
+ "docapi:sync:check": "pnpm run docapi:sync && pnpm run docapi:check"
},
"devDependencies": {
"@commitlint/cli": "catalog:",
@@ -83,6 +86,7 @@
"semantic-release-commit-filter": "catalog:",
"semantic-release-linear-app": "catalog:",
"semantic-release-pnpm": "^1.0.2",
+ "tsx": "catalog:",
"typescript": "catalog:",
"typescript-eslint": "catalog:",
"verdaccio": "catalog:",
diff --git a/packages/document-api/README.md b/packages/document-api/README.md
new file mode 100644
index 0000000000..4bf0e42cc0
--- /dev/null
+++ b/packages/document-api/README.md
@@ -0,0 +1,66 @@
+# @superdoc/document-api
+
+Contract-first Document API package (internal workspace package).
+
+## Generated vs manual files
+
+This package intentionally checks generated artifacts into git. Use this boundary when editing:
+
+| Path | Source of truth | Edit directly? |
+| --- | --- | --- |
+| `packages/document-api/src/contract/*` | Hand-authored contract source | Yes |
+| `packages/document-api/src/index.ts` and other `src/**` runtime/types | Hand-authored source | Yes |
+| `packages/document-api/scripts/**` | Hand-authored generation/check tooling | Yes |
+| `packages/document-api/generated/**` | Generated from contract + scripts | No (regenerate) |
+| `apps/docs/document-api/reference/**` | Generated docs from contract + scripts | No (regenerate) |
+| `apps/docs/document-api/overview.mdx` | Mixed: manual page + generated section between markers | Yes, but do not hand-edit inside generated marker block |
+
+Generated marker block in overview:
+
+- `/* DOC_API_GENERATED_API_SURFACE_START */`
+- `/* DOC_API_GENERATED_API_SURFACE_END */`
+
+## Regeneration commands
+
+From repo root:
+
+```bash
+pnpm run docapi:sync # regenerate all generated outputs
+pnpm run docapi:check # verify parity + output drift (CI runs this)
+pnpm run docapi:sync:check # sync then check in one step
+```
+
+These are also enforced automatically:
+- **Pre-commit hook** runs `docapi:sync` when document-api sources change and restages generated files.
+- **CI workflow** (`ci-document-api.yml`) runs `docapi:check` on every PR touching relevant paths.
+
+## Adding a new operation
+
+The contract uses a single-source-of-truth pattern. Adding a new operation touches 4 files:
+
+1. **`src/contract/operation-definitions.ts`** — add an entry to `OPERATION_DEFINITIONS` with `memberPath`, `metadata` (use `readOperation()` or `mutationOperation()`), `referenceDocPath`, and `referenceGroup`.
+2. **`src/contract/operation-registry.ts`** — add a type entry (`input`, `options`, `output`). The bidirectional `Assert` checks will fail until this is done.
+3. **`src/invoke/invoke.ts`** (`buildDispatchTable`) — add a one-line dispatch entry calling the API method. The `TypedDispatchTable` mapped type will fail until this is done.
+4. **Implement** — the API method on `DocumentApi` in `src/index.ts` + its adapter.
+
+The catalog (`COMMAND_CATALOG`), member-path map (`OPERATION_MEMBER_PATH_MAP`), and reference-doc map (`OPERATION_REFERENCE_DOC_PATH_MAP`) are all derived automatically from `OPERATION_DEFINITIONS` — do not edit them by hand.
+
+## Contract architecture
+
+```
+metadata-types.ts (leaf — CommandStaticMetadata, throw codes, idempotency)
+ ↑ ↑
+operation-definitions.ts types.ts (re-exports + CommandCatalog, guards)
+ ↑ ↑
+ +--- command-catalog.ts, operation-map.ts, reference-doc-map.ts,
+ operation-registry.ts, schemas.ts
+```
+
+- `operation-definitions.ts` is the single source of truth for operation keys, metadata, paths, and grouping.
+- `operation-registry.ts` is the single source of truth for type signatures (input/options/output per operation).
+- `TypedDispatchTable` (in `invoke.ts`) validates at compile time that dispatch wiring conforms to the registry.
+
+## Related docs
+
+- `packages/document-api/src/README.md` for contract semantics and invariants
+- `packages/document-api/scripts/README.md` for script catalog and behavior
diff --git a/packages/document-api/generated/agent/compatibility-hints.json b/packages/document-api/generated/agent/compatibility-hints.json
new file mode 100644
index 0000000000..55fc4d83d2
--- /dev/null
+++ b/packages/document-api/generated/agent/compatibility-hints.json
@@ -0,0 +1,330 @@
+{
+ "contractVersion": "0.1.0",
+ "operations": {
+ "capabilities.get": {
+ "deterministicTargetResolution": true,
+ "memberPath": "capabilities",
+ "mutates": false,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": false,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "comments.add": {
+ "deterministicTargetResolution": true,
+ "memberPath": "comments.add",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "comments.edit": {
+ "deterministicTargetResolution": true,
+ "memberPath": "comments.edit",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "comments.get": {
+ "deterministicTargetResolution": true,
+ "memberPath": "comments.get",
+ "mutates": false,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": false,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "comments.goTo": {
+ "deterministicTargetResolution": true,
+ "memberPath": "comments.goTo",
+ "mutates": false,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": false,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "comments.list": {
+ "deterministicTargetResolution": true,
+ "memberPath": "comments.list",
+ "mutates": false,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": false,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "comments.move": {
+ "deterministicTargetResolution": true,
+ "memberPath": "comments.move",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "comments.remove": {
+ "deterministicTargetResolution": true,
+ "memberPath": "comments.remove",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "comments.reply": {
+ "deterministicTargetResolution": true,
+ "memberPath": "comments.reply",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "comments.resolve": {
+ "deterministicTargetResolution": true,
+ "memberPath": "comments.resolve",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "comments.setActive": {
+ "deterministicTargetResolution": true,
+ "memberPath": "comments.setActive",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "comments.setInternal": {
+ "deterministicTargetResolution": true,
+ "memberPath": "comments.setInternal",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "create.paragraph": {
+ "deterministicTargetResolution": true,
+ "memberPath": "create.paragraph",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": true,
+ "supportsTrackedMode": true
+ },
+ "delete": {
+ "deterministicTargetResolution": true,
+ "memberPath": "delete",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": true,
+ "supportsTrackedMode": true
+ },
+ "find": {
+ "deterministicTargetResolution": false,
+ "memberPath": "find",
+ "mutates": false,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": false,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "format.bold": {
+ "deterministicTargetResolution": true,
+ "memberPath": "format.bold",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": true,
+ "supportsTrackedMode": true
+ },
+ "getNode": {
+ "deterministicTargetResolution": true,
+ "memberPath": "getNode",
+ "mutates": false,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": false,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "getNodeById": {
+ "deterministicTargetResolution": true,
+ "memberPath": "getNodeById",
+ "mutates": false,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": false,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "getText": {
+ "deterministicTargetResolution": true,
+ "memberPath": "getText",
+ "mutates": false,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": false,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "info": {
+ "deterministicTargetResolution": true,
+ "memberPath": "info",
+ "mutates": false,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": false,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "insert": {
+ "deterministicTargetResolution": true,
+ "memberPath": "insert",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": true,
+ "supportsTrackedMode": true
+ },
+ "lists.exit": {
+ "deterministicTargetResolution": true,
+ "memberPath": "lists.exit",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": true,
+ "supportsTrackedMode": false
+ },
+ "lists.get": {
+ "deterministicTargetResolution": true,
+ "memberPath": "lists.get",
+ "mutates": false,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": false,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "lists.indent": {
+ "deterministicTargetResolution": true,
+ "memberPath": "lists.indent",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": true,
+ "supportsTrackedMode": false
+ },
+ "lists.insert": {
+ "deterministicTargetResolution": true,
+ "memberPath": "lists.insert",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": true,
+ "supportsTrackedMode": true
+ },
+ "lists.list": {
+ "deterministicTargetResolution": true,
+ "memberPath": "lists.list",
+ "mutates": false,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": false,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "lists.outdent": {
+ "deterministicTargetResolution": true,
+ "memberPath": "lists.outdent",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": true,
+ "supportsTrackedMode": false
+ },
+ "lists.restart": {
+ "deterministicTargetResolution": true,
+ "memberPath": "lists.restart",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": true,
+ "supportsTrackedMode": false
+ },
+ "lists.setType": {
+ "deterministicTargetResolution": true,
+ "memberPath": "lists.setType",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": true,
+ "supportsTrackedMode": false
+ },
+ "replace": {
+ "deterministicTargetResolution": true,
+ "memberPath": "replace",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": true,
+ "supportsTrackedMode": true
+ },
+ "trackChanges.accept": {
+ "deterministicTargetResolution": true,
+ "memberPath": "trackChanges.accept",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "trackChanges.acceptAll": {
+ "deterministicTargetResolution": true,
+ "memberPath": "trackChanges.acceptAll",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "trackChanges.get": {
+ "deterministicTargetResolution": true,
+ "memberPath": "trackChanges.get",
+ "mutates": false,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": false,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "trackChanges.list": {
+ "deterministicTargetResolution": true,
+ "memberPath": "trackChanges.list",
+ "mutates": false,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": false,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "trackChanges.reject": {
+ "deterministicTargetResolution": true,
+ "memberPath": "trackChanges.reject",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ "trackChanges.rejectAll": {
+ "deterministicTargetResolution": true,
+ "memberPath": "trackChanges.rejectAll",
+ "mutates": true,
+ "postApplyThrowForbidden": true,
+ "requiresPreflightCapabilitiesCheck": true,
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ }
+ },
+ "sourceHash": "8d875ceb279d213c1e779882f103d39fa2106545ce94c6b1553ae06b6c321e03"
+}
diff --git a/packages/document-api/generated/agent/remediation-map.json b/packages/document-api/generated/agent/remediation-map.json
new file mode 100644
index 0000000000..38fda4f4bd
--- /dev/null
+++ b/packages/document-api/generated/agent/remediation-map.json
@@ -0,0 +1,292 @@
+{
+ "contractVersion": "0.1.0",
+ "entries": [
+ {
+ "code": "CAPABILITY_UNAVAILABLE",
+ "message": "Check runtime capabilities and switch to supported mode or operation.",
+ "nonAppliedOperations": [],
+ "operations": [
+ "comments.add",
+ "comments.edit",
+ "comments.goTo",
+ "comments.move",
+ "comments.remove",
+ "comments.reply",
+ "comments.resolve",
+ "comments.setActive",
+ "comments.setInternal",
+ "create.paragraph",
+ "delete",
+ "format.bold",
+ "insert",
+ "lists.exit",
+ "lists.indent",
+ "lists.insert",
+ "lists.outdent",
+ "lists.restart",
+ "lists.setType",
+ "replace",
+ "trackChanges.accept",
+ "trackChanges.acceptAll",
+ "trackChanges.reject",
+ "trackChanges.rejectAll"
+ ],
+ "preApplyOperations": [
+ "comments.add",
+ "comments.edit",
+ "comments.goTo",
+ "comments.move",
+ "comments.remove",
+ "comments.reply",
+ "comments.resolve",
+ "comments.setActive",
+ "comments.setInternal",
+ "create.paragraph",
+ "delete",
+ "format.bold",
+ "insert",
+ "lists.exit",
+ "lists.indent",
+ "lists.insert",
+ "lists.outdent",
+ "lists.restart",
+ "lists.setType",
+ "replace",
+ "trackChanges.accept",
+ "trackChanges.acceptAll",
+ "trackChanges.reject",
+ "trackChanges.rejectAll"
+ ]
+ },
+ {
+ "code": "COMMAND_UNAVAILABLE",
+ "message": "Call capabilities.get and branch to a fallback when operation availability is false.",
+ "nonAppliedOperations": [],
+ "operations": [
+ "comments.add",
+ "comments.edit",
+ "comments.goTo",
+ "comments.move",
+ "comments.remove",
+ "comments.reply",
+ "comments.resolve",
+ "comments.setActive",
+ "comments.setInternal",
+ "create.paragraph",
+ "format.bold",
+ "lists.exit",
+ "lists.indent",
+ "lists.insert",
+ "lists.outdent",
+ "lists.restart",
+ "lists.setType",
+ "trackChanges.accept",
+ "trackChanges.acceptAll",
+ "trackChanges.reject",
+ "trackChanges.rejectAll"
+ ],
+ "preApplyOperations": [
+ "comments.add",
+ "comments.edit",
+ "comments.goTo",
+ "comments.move",
+ "comments.remove",
+ "comments.reply",
+ "comments.resolve",
+ "comments.setActive",
+ "comments.setInternal",
+ "create.paragraph",
+ "format.bold",
+ "lists.exit",
+ "lists.indent",
+ "lists.insert",
+ "lists.outdent",
+ "lists.restart",
+ "lists.setType",
+ "trackChanges.accept",
+ "trackChanges.acceptAll",
+ "trackChanges.reject",
+ "trackChanges.rejectAll"
+ ]
+ },
+ {
+ "code": "INVALID_TARGET",
+ "message": "Confirm the target shape and operation compatibility, then retry with a valid target.",
+ "nonAppliedOperations": [
+ "comments.add",
+ "comments.move",
+ "comments.reply",
+ "comments.setActive",
+ "comments.setInternal",
+ "create.paragraph",
+ "format.bold",
+ "insert",
+ "lists.exit",
+ "lists.indent",
+ "lists.insert",
+ "lists.outdent",
+ "lists.restart",
+ "lists.setType",
+ "replace"
+ ],
+ "operations": [
+ "comments.add",
+ "comments.move",
+ "comments.reply",
+ "comments.setActive",
+ "comments.setInternal",
+ "create.paragraph",
+ "format.bold",
+ "insert",
+ "lists.exit",
+ "lists.indent",
+ "lists.insert",
+ "lists.outdent",
+ "lists.restart",
+ "lists.setType",
+ "replace"
+ ],
+ "preApplyOperations": []
+ },
+ {
+ "code": "NO_OP",
+ "message": "Treat as idempotent no-op and avoid retry loops unless inputs change.",
+ "nonAppliedOperations": [
+ "comments.add",
+ "comments.edit",
+ "comments.move",
+ "comments.remove",
+ "comments.resolve",
+ "comments.setInternal",
+ "delete",
+ "insert",
+ "lists.indent",
+ "lists.outdent",
+ "lists.restart",
+ "lists.setType",
+ "replace",
+ "trackChanges.accept",
+ "trackChanges.acceptAll",
+ "trackChanges.reject",
+ "trackChanges.rejectAll"
+ ],
+ "operations": [
+ "comments.add",
+ "comments.edit",
+ "comments.move",
+ "comments.remove",
+ "comments.resolve",
+ "comments.setInternal",
+ "delete",
+ "insert",
+ "lists.indent",
+ "lists.outdent",
+ "lists.restart",
+ "lists.setType",
+ "replace",
+ "trackChanges.accept",
+ "trackChanges.acceptAll",
+ "trackChanges.reject",
+ "trackChanges.rejectAll"
+ ],
+ "preApplyOperations": []
+ },
+ {
+ "code": "TARGET_NOT_FOUND",
+ "message": "Refresh targets via find/get operations and retry with a fresh address or ID.",
+ "nonAppliedOperations": [],
+ "operations": [
+ "comments.add",
+ "comments.edit",
+ "comments.get",
+ "comments.goTo",
+ "comments.move",
+ "comments.remove",
+ "comments.reply",
+ "comments.resolve",
+ "comments.setActive",
+ "comments.setInternal",
+ "create.paragraph",
+ "delete",
+ "format.bold",
+ "getNode",
+ "getNodeById",
+ "insert",
+ "lists.exit",
+ "lists.get",
+ "lists.indent",
+ "lists.insert",
+ "lists.list",
+ "lists.outdent",
+ "lists.restart",
+ "lists.setType",
+ "replace",
+ "trackChanges.accept",
+ "trackChanges.get",
+ "trackChanges.reject"
+ ],
+ "preApplyOperations": [
+ "comments.add",
+ "comments.edit",
+ "comments.get",
+ "comments.goTo",
+ "comments.move",
+ "comments.remove",
+ "comments.reply",
+ "comments.resolve",
+ "comments.setActive",
+ "comments.setInternal",
+ "create.paragraph",
+ "delete",
+ "format.bold",
+ "getNode",
+ "getNodeById",
+ "insert",
+ "lists.exit",
+ "lists.get",
+ "lists.indent",
+ "lists.insert",
+ "lists.list",
+ "lists.outdent",
+ "lists.restart",
+ "lists.setType",
+ "replace",
+ "trackChanges.accept",
+ "trackChanges.get",
+ "trackChanges.reject"
+ ]
+ },
+ {
+ "code": "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "message": "Verify track-changes support via capabilities.get before requesting tracked mode.",
+ "nonAppliedOperations": [],
+ "operations": [
+ "create.paragraph",
+ "delete",
+ "format.bold",
+ "insert",
+ "lists.exit",
+ "lists.indent",
+ "lists.insert",
+ "lists.outdent",
+ "lists.restart",
+ "lists.setType",
+ "replace"
+ ],
+ "preApplyOperations": [
+ "create.paragraph",
+ "delete",
+ "format.bold",
+ "insert",
+ "lists.exit",
+ "lists.indent",
+ "lists.insert",
+ "lists.outdent",
+ "lists.restart",
+ "lists.setType",
+ "replace"
+ ]
+ }
+ ],
+ "sourceHash": "8d875ceb279d213c1e779882f103d39fa2106545ce94c6b1553ae06b6c321e03"
+}
diff --git a/packages/document-api/generated/agent/workflow-playbooks.json b/packages/document-api/generated/agent/workflow-playbooks.json
new file mode 100644
index 0000000000..2b6f01785f
--- /dev/null
+++ b/packages/document-api/generated/agent/workflow-playbooks.json
@@ -0,0 +1,36 @@
+{
+ "contractVersion": "0.1.0",
+ "sourceHash": "8d875ceb279d213c1e779882f103d39fa2106545ce94c6b1553ae06b6c321e03",
+ "workflows": [
+ {
+ "id": "find-mutate",
+ "operations": ["find", "replace"],
+ "title": "Find + mutate workflow"
+ },
+ {
+ "id": "tracked-insert",
+ "operations": ["capabilities.get", "insert"],
+ "title": "Tracked insert workflow"
+ },
+ {
+ "id": "comment-thread-lifecycle",
+ "operations": ["comments.add", "comments.reply", "comments.resolve"],
+ "title": "Comment lifecycle workflow"
+ },
+ {
+ "id": "list-manipulation",
+ "operations": ["lists.insert", "lists.setType", "lists.indent", "lists.outdent", "lists.exit"],
+ "title": "List manipulation workflow"
+ },
+ {
+ "id": "capabilities-aware-branching",
+ "operations": ["capabilities.get", "replace", "insert"],
+ "title": "Capabilities-aware branching workflow"
+ },
+ {
+ "id": "track-change-review",
+ "operations": ["trackChanges.list", "trackChanges.accept", "trackChanges.reject"],
+ "title": "Track-change review workflow"
+ }
+ ]
+}
diff --git a/packages/document-api/generated/manifests/document-api-tools.json b/packages/document-api/generated/manifests/document-api-tools.json
new file mode 100644
index 0000000000..f372d928c4
--- /dev/null
+++ b/packages/document-api/generated/manifests/document-api-tools.json
@@ -0,0 +1,10705 @@
+{
+ "contractVersion": "0.1.0",
+ "generatedAt": null,
+ "sourceCommit": null,
+ "sourceHash": "8d875ceb279d213c1e779882f103d39fa2106545ce94c6b1553ae06b6c321e03",
+ "tools": [
+ {
+ "description": "Read Document API data via `find`.",
+ "deterministicTargetResolution": false,
+ "idempotency": "idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "includeNodes": {
+ "type": "boolean"
+ },
+ "includeUnknown": {
+ "type": "boolean"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "select": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "caseSensitive": {
+ "type": "boolean"
+ },
+ "mode": {
+ "enum": ["contains", "regex"]
+ },
+ "pattern": {
+ "type": "string"
+ },
+ "type": {
+ "const": "text"
+ }
+ },
+ "required": ["type", "pattern"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "enum": ["block", "inline"]
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ },
+ "type": {
+ "const": "node"
+ }
+ },
+ "required": ["type"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": ["nodeType"],
+ "type": "object"
+ }
+ ]
+ },
+ "within": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": [
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "sdt",
+ "image",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": ["kind", "nodeType", "anchor"],
+ "type": "object"
+ }
+ ]
+ }
+ },
+ "required": ["select"],
+ "type": "object"
+ },
+ "memberPath": "find",
+ "mutates": false,
+ "name": "find",
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "context": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": [
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "sdt",
+ "image",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": ["kind", "nodeType", "anchor"],
+ "type": "object"
+ }
+ ]
+ },
+ "highlightRange": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ },
+ "snippet": {
+ "type": "string"
+ },
+ "textRanges": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["address", "snippet", "highlightRange"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "diagnostics": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": [
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "sdt",
+ "image",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": ["kind", "nodeType", "anchor"],
+ "type": "object"
+ }
+ ]
+ },
+ "hint": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["message"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "matches": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": [
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "sdt",
+ "image",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": ["kind", "nodeType", "anchor"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "nodes": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "bodyNodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bodyText": {
+ "type": "string"
+ },
+ "kind": {
+ "enum": ["block", "inline"]
+ },
+ "nodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ },
+ "properties": {
+ "type": "object"
+ },
+ "summary": {
+ "additionalProperties": false,
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["nodeType", "kind"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ }
+ },
+ "required": ["matches", "total"],
+ "type": "object"
+ },
+ "possibleFailureCodes": [],
+ "preApplyThrows": [],
+ "remediationHints": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Read Document API data via `getNode`.",
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "inputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": ["run", "bookmark", "comment", "hyperlink", "sdt", "image", "footnoteRef", "tab", "lineBreak"]
+ }
+ },
+ "required": ["kind", "nodeType", "anchor"],
+ "type": "object"
+ }
+ ]
+ },
+ "memberPath": "getNode",
+ "mutates": false,
+ "name": "getNode",
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "bodyNodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bodyText": {
+ "type": "string"
+ },
+ "kind": {
+ "enum": ["block", "inline"]
+ },
+ "nodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ },
+ "properties": {
+ "type": "object"
+ },
+ "summary": {
+ "additionalProperties": false,
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["nodeType", "kind"],
+ "type": "object"
+ },
+ "possibleFailureCodes": [],
+ "preApplyThrows": ["TARGET_NOT_FOUND"],
+ "remediationHints": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Read Document API data via `getNodeById`.",
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["nodeId"],
+ "type": "object"
+ },
+ "memberPath": "getNodeById",
+ "mutates": false,
+ "name": "getNodeById",
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "bodyNodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bodyText": {
+ "type": "string"
+ },
+ "kind": {
+ "enum": ["block", "inline"]
+ },
+ "nodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ },
+ "properties": {
+ "type": "object"
+ },
+ "summary": {
+ "additionalProperties": false,
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["nodeType", "kind"],
+ "type": "object"
+ },
+ "possibleFailureCodes": [],
+ "preApplyThrows": ["TARGET_NOT_FOUND"],
+ "remediationHints": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Read Document API data via `getText`.",
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "memberPath": "getText",
+ "mutates": false,
+ "name": "getText",
+ "outputSchema": {
+ "type": "string"
+ },
+ "possibleFailureCodes": [],
+ "preApplyThrows": [],
+ "remediationHints": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Read Document API data via `info`.",
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "memberPath": "info",
+ "mutates": false,
+ "name": "info",
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "capabilities": {
+ "additionalProperties": false,
+ "properties": {
+ "canComment": {
+ "type": "boolean"
+ },
+ "canFind": {
+ "type": "boolean"
+ },
+ "canGetNode": {
+ "type": "boolean"
+ },
+ "canReplace": {
+ "type": "boolean"
+ }
+ },
+ "required": ["canFind", "canGetNode", "canComment", "canReplace"],
+ "type": "object"
+ },
+ "counts": {
+ "additionalProperties": false,
+ "properties": {
+ "comments": {
+ "type": "integer"
+ },
+ "headings": {
+ "type": "integer"
+ },
+ "images": {
+ "type": "integer"
+ },
+ "paragraphs": {
+ "type": "integer"
+ },
+ "tables": {
+ "type": "integer"
+ },
+ "words": {
+ "type": "integer"
+ }
+ },
+ "required": ["words", "paragraphs", "headings", "tables", "images", "comments"],
+ "type": "object"
+ },
+ "outline": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "level": {
+ "type": "integer"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["level", "text", "nodeId"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["counts", "outline", "capabilities"],
+ "type": "object"
+ },
+ "possibleFailureCodes": [],
+ "preApplyThrows": [],
+ "remediationHints": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `insert`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ },
+ "idempotency": "non-idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["text"],
+ "type": "object"
+ },
+ "memberPath": "insert",
+ "mutates": true,
+ "name": "insert",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["INVALID_TARGET", "NO_OP"],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "TRACK_CHANGE_COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ },
+ "supportsDryRun": true,
+ "supportsTrackedMode": true
+ },
+ {
+ "description": "Apply Document API mutation `replace`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "text"],
+ "type": "object"
+ },
+ "memberPath": "replace",
+ "mutates": true,
+ "name": "replace",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["INVALID_TARGET", "NO_OP"],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "TRACK_CHANGE_COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ },
+ "supportsDryRun": true,
+ "supportsTrackedMode": true
+ },
+ {
+ "description": "Apply Document API mutation `delete`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ }
+ },
+ "required": ["target"],
+ "type": "object"
+ },
+ "memberPath": "delete",
+ "mutates": true,
+ "name": "delete",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["NO_OP"],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "TRACK_CHANGE_COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ },
+ "supportsDryRun": true,
+ "supportsTrackedMode": true
+ },
+ {
+ "description": "Apply Document API mutation `format.bold`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ }
+ },
+ "required": ["target"],
+ "type": "object"
+ },
+ "memberPath": "format.bold",
+ "mutates": true,
+ "name": "format.bold",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["INVALID_TARGET"],
+ "preApplyThrows": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ },
+ "supportsDryRun": true,
+ "supportsTrackedMode": true
+ },
+ {
+ "description": "Apply Document API mutation `create.paragraph`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "non-idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "at": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "documentStart"
+ }
+ },
+ "required": ["kind"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "documentEnd"
+ }
+ },
+ "required": ["kind"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "before"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "target"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "after"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "target"],
+ "type": "object"
+ }
+ ]
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "memberPath": "create.paragraph",
+ "mutates": true,
+ "name": "create.paragraph",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "insertionPoint": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "paragraph"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "paragraph", "insertionPoint"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["INVALID_TARGET"],
+ "preApplyThrows": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "insertionPoint": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "paragraph"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "paragraph", "insertionPoint"],
+ "type": "object"
+ },
+ "supportsDryRun": true,
+ "supportsTrackedMode": true
+ },
+ {
+ "description": "Read Document API data via `lists.list`.",
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "enum": ["ordered", "bullet"]
+ },
+ "level": {
+ "type": "integer"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "ordinal": {
+ "type": "integer"
+ },
+ "within": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "memberPath": "lists.list",
+ "mutates": false,
+ "name": "lists.list",
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "items": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "kind": {
+ "enum": ["ordered", "bullet"]
+ },
+ "level": {
+ "type": "integer"
+ },
+ "marker": {
+ "type": "string"
+ },
+ "ordinal": {
+ "type": "integer"
+ },
+ "path": {
+ "items": {
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["address"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "matches": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ }
+ },
+ "required": ["matches", "total", "items"],
+ "type": "object"
+ },
+ "possibleFailureCodes": [],
+ "preApplyThrows": ["TARGET_NOT_FOUND"],
+ "remediationHints": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Read Document API data via `lists.get`.",
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["address"],
+ "type": "object"
+ },
+ "memberPath": "lists.get",
+ "mutates": false,
+ "name": "lists.get",
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "kind": {
+ "enum": ["ordered", "bullet"]
+ },
+ "level": {
+ "type": "integer"
+ },
+ "marker": {
+ "type": "string"
+ },
+ "ordinal": {
+ "type": "integer"
+ },
+ "path": {
+ "items": {
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["address"],
+ "type": "object"
+ },
+ "possibleFailureCodes": [],
+ "preApplyThrows": ["TARGET_NOT_FOUND"],
+ "remediationHints": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `lists.insert`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "non-idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "position": {
+ "enum": ["before", "after"]
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "position"],
+ "type": "object"
+ },
+ "memberPath": "lists.insert",
+ "mutates": true,
+ "name": "lists.insert",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "insertionPoint": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "item", "insertionPoint"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["INVALID_TARGET"],
+ "preApplyThrows": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "insertionPoint": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "item", "insertionPoint"],
+ "type": "object"
+ },
+ "supportsDryRun": true,
+ "supportsTrackedMode": true
+ },
+ {
+ "description": "Apply Document API mutation `lists.setType`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "enum": ["ordered", "bullet"]
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["target", "kind"],
+ "type": "object"
+ },
+ "memberPath": "lists.setType",
+ "mutates": true,
+ "name": "lists.setType",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["NO_OP", "INVALID_TARGET"],
+ "preApplyThrows": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ },
+ "supportsDryRun": true,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `lists.indent`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["target"],
+ "type": "object"
+ },
+ "memberPath": "lists.indent",
+ "mutates": true,
+ "name": "lists.indent",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["NO_OP", "INVALID_TARGET"],
+ "preApplyThrows": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ },
+ "supportsDryRun": true,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `lists.outdent`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["target"],
+ "type": "object"
+ },
+ "memberPath": "lists.outdent",
+ "mutates": true,
+ "name": "lists.outdent",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["NO_OP", "INVALID_TARGET"],
+ "preApplyThrows": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ },
+ "supportsDryRun": true,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `lists.restart`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["target"],
+ "type": "object"
+ },
+ "memberPath": "lists.restart",
+ "mutates": true,
+ "name": "lists.restart",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["NO_OP", "INVALID_TARGET"],
+ "preApplyThrows": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ },
+ "supportsDryRun": true,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `lists.exit`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["target"],
+ "type": "object"
+ },
+ "memberPath": "lists.exit",
+ "mutates": true,
+ "name": "lists.exit",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "paragraph"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "paragraph"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["INVALID_TARGET"],
+ "preApplyThrows": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "paragraph"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "paragraph"],
+ "type": "object"
+ },
+ "supportsDryRun": true,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `comments.add`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "non-idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "text"],
+ "type": "object"
+ },
+ "memberPath": "comments.add",
+ "mutates": true,
+ "name": "comments.add",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["INVALID_TARGET", "NO_OP"],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `comments.edit`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["commentId", "text"],
+ "type": "object"
+ },
+ "memberPath": "comments.edit",
+ "mutates": true,
+ "name": "comments.edit",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["NO_OP"],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `comments.reply`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "non-idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "parentCommentId": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["parentCommentId", "text"],
+ "type": "object"
+ },
+ "memberPath": "comments.reply",
+ "mutates": true,
+ "name": "comments.reply",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["INVALID_TARGET"],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `comments.move`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ }
+ },
+ "required": ["commentId", "target"],
+ "type": "object"
+ },
+ "memberPath": "comments.move",
+ "mutates": true,
+ "name": "comments.move",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["INVALID_TARGET", "NO_OP"],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `comments.resolve`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ }
+ },
+ "required": ["commentId"],
+ "type": "object"
+ },
+ "memberPath": "comments.resolve",
+ "mutates": true,
+ "name": "comments.resolve",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["NO_OP"],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `comments.remove`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ }
+ },
+ "required": ["commentId"],
+ "type": "object"
+ },
+ "memberPath": "comments.remove",
+ "mutates": true,
+ "name": "comments.remove",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["NO_OP"],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `comments.setInternal`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ },
+ "isInternal": {
+ "type": "boolean"
+ }
+ },
+ "required": ["commentId", "isInternal"],
+ "type": "object"
+ },
+ "memberPath": "comments.setInternal",
+ "mutates": true,
+ "name": "comments.setInternal",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["NO_OP", "INVALID_TARGET"],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `comments.setActive`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": ["string", "null"]
+ }
+ },
+ "required": ["commentId"],
+ "type": "object"
+ },
+ "memberPath": "comments.setActive",
+ "mutates": true,
+ "name": "comments.setActive",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["INVALID_TARGET"],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Read Document API data via `comments.goTo`.",
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ }
+ },
+ "required": ["commentId"],
+ "type": "object"
+ },
+ "memberPath": "comments.goTo",
+ "mutates": false,
+ "name": "comments.goTo",
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ "possibleFailureCodes": [],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Read Document API data via `comments.get`.",
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ }
+ },
+ "required": ["commentId"],
+ "type": "object"
+ },
+ "memberPath": "comments.get",
+ "mutates": false,
+ "name": "comments.get",
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "commentId": {
+ "type": "string"
+ },
+ "createdTime": {
+ "type": "number"
+ },
+ "creatorEmail": {
+ "type": "string"
+ },
+ "creatorName": {
+ "type": "string"
+ },
+ "importedId": {
+ "type": "string"
+ },
+ "isInternal": {
+ "type": "boolean"
+ },
+ "parentCommentId": {
+ "type": "string"
+ },
+ "status": {
+ "enum": ["open", "resolved"]
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["address", "commentId", "status"],
+ "type": "object"
+ },
+ "possibleFailureCodes": [],
+ "preApplyThrows": ["TARGET_NOT_FOUND"],
+ "remediationHints": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Read Document API data via `comments.list`.",
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "includeResolved": {
+ "type": "boolean"
+ }
+ },
+ "type": "object"
+ },
+ "memberPath": "comments.list",
+ "mutates": false,
+ "name": "comments.list",
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "matches": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "commentId": {
+ "type": "string"
+ },
+ "createdTime": {
+ "type": "number"
+ },
+ "creatorEmail": {
+ "type": "string"
+ },
+ "creatorName": {
+ "type": "string"
+ },
+ "importedId": {
+ "type": "string"
+ },
+ "isInternal": {
+ "type": "boolean"
+ },
+ "parentCommentId": {
+ "type": "string"
+ },
+ "status": {
+ "enum": ["open", "resolved"]
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["address", "commentId", "status"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ }
+ },
+ "required": ["matches", "total"],
+ "type": "object"
+ },
+ "possibleFailureCodes": [],
+ "preApplyThrows": [],
+ "remediationHints": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Read Document API data via `trackChanges.list`.",
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "type": {
+ "enum": ["insert", "delete", "format"]
+ }
+ },
+ "type": "object"
+ },
+ "memberPath": "trackChanges.list",
+ "mutates": false,
+ "name": "trackChanges.list",
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "changes": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "author": {
+ "type": "string"
+ },
+ "authorEmail": {
+ "type": "string"
+ },
+ "authorImage": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "excerpt": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "enum": ["insert", "delete", "format"]
+ }
+ },
+ "required": ["address", "id", "type"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "matches": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ }
+ },
+ "required": ["matches", "total"],
+ "type": "object"
+ },
+ "possibleFailureCodes": [],
+ "preApplyThrows": [],
+ "remediationHints": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Read Document API data via `trackChanges.get`.",
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ },
+ "required": ["id"],
+ "type": "object"
+ },
+ "memberPath": "trackChanges.get",
+ "mutates": false,
+ "name": "trackChanges.get",
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "author": {
+ "type": "string"
+ },
+ "authorEmail": {
+ "type": "string"
+ },
+ "authorImage": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "excerpt": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "enum": ["insert", "delete", "format"]
+ }
+ },
+ "required": ["address", "id", "type"],
+ "type": "object"
+ },
+ "possibleFailureCodes": [],
+ "preApplyThrows": ["TARGET_NOT_FOUND"],
+ "remediationHints": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `trackChanges.accept`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ },
+ "required": ["id"],
+ "type": "object"
+ },
+ "memberPath": "trackChanges.accept",
+ "mutates": true,
+ "name": "trackChanges.accept",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["NO_OP"],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `trackChanges.reject`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ },
+ "required": ["id"],
+ "type": "object"
+ },
+ "memberPath": "trackChanges.reject",
+ "mutates": true,
+ "name": "trackChanges.reject",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["NO_OP"],
+ "preApplyThrows": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `trackChanges.acceptAll`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "memberPath": "trackChanges.acceptAll",
+ "mutates": true,
+ "name": "trackChanges.acceptAll",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["NO_OP"],
+ "preApplyThrows": ["COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Apply Document API mutation `trackChanges.rejectAll`.",
+ "deterministicTargetResolution": true,
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "idempotency": "conditional",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "memberPath": "trackChanges.rejectAll",
+ "mutates": true,
+ "name": "trackChanges.rejectAll",
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "possibleFailureCodes": ["NO_OP"],
+ "preApplyThrows": ["COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"],
+ "remediationHints": [],
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ },
+ {
+ "description": "Read Document API data via `capabilities.get`.",
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "memberPath": "capabilities",
+ "mutates": false,
+ "name": "capabilities.get",
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "global": {
+ "additionalProperties": false,
+ "properties": {
+ "comments": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["enabled"],
+ "type": "object"
+ },
+ "dryRun": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["enabled"],
+ "type": "object"
+ },
+ "lists": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["enabled"],
+ "type": "object"
+ },
+ "trackChanges": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["enabled"],
+ "type": "object"
+ }
+ },
+ "required": ["trackChanges", "comments", "lists", "dryRun"],
+ "type": "object"
+ },
+ "operations": {
+ "additionalProperties": false,
+ "properties": {
+ "capabilities.get": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.add": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.edit": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.get": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.goTo": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.list": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.move": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.remove": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.reply": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.resolve": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.setActive": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.setInternal": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "create.paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "delete": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "find": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "format.bold": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "getNode": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "getNodeById": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "getText": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "info": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "insert": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.exit": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.get": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.indent": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.insert": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.list": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.outdent": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.restart": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.setType": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "replace": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "trackChanges.accept": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "trackChanges.acceptAll": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "trackChanges.get": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "trackChanges.list": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "trackChanges.reject": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "trackChanges.rejectAll": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ }
+ },
+ "required": [
+ "find",
+ "getNode",
+ "getNodeById",
+ "getText",
+ "info",
+ "insert",
+ "replace",
+ "delete",
+ "format.bold",
+ "create.paragraph",
+ "lists.list",
+ "lists.get",
+ "lists.insert",
+ "lists.setType",
+ "lists.indent",
+ "lists.outdent",
+ "lists.restart",
+ "lists.exit",
+ "comments.add",
+ "comments.edit",
+ "comments.reply",
+ "comments.move",
+ "comments.resolve",
+ "comments.remove",
+ "comments.setInternal",
+ "comments.setActive",
+ "comments.goTo",
+ "comments.get",
+ "comments.list",
+ "trackChanges.list",
+ "trackChanges.get",
+ "trackChanges.accept",
+ "trackChanges.reject",
+ "trackChanges.acceptAll",
+ "trackChanges.rejectAll",
+ "capabilities.get"
+ ],
+ "type": "object"
+ }
+ },
+ "required": ["global", "operations"],
+ "type": "object"
+ },
+ "possibleFailureCodes": [],
+ "preApplyThrows": [],
+ "remediationHints": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false
+ }
+ ]
+}
diff --git a/packages/document-api/generated/schemas/README.md b/packages/document-api/generated/schemas/README.md
new file mode 100644
index 0000000000..f8de1f16cb
--- /dev/null
+++ b/packages/document-api/generated/schemas/README.md
@@ -0,0 +1,4 @@
+# Generated Document API schemas
+
+GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`.
+This directory is generated from `packages/document-api/src/contract/*`.
diff --git a/packages/document-api/generated/schemas/document-api-contract.json b/packages/document-api/generated/schemas/document-api-contract.json
new file mode 100644
index 0000000000..346cd1b8bc
--- /dev/null
+++ b/packages/document-api/generated/schemas/document-api-contract.json
@@ -0,0 +1,10778 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "contractVersion": "0.1.0",
+ "generatedAt": null,
+ "operations": {
+ "capabilities.get": {
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "memberPath": "capabilities",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "mutates": false,
+ "possibleFailureCodes": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": []
+ }
+ },
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "global": {
+ "additionalProperties": false,
+ "properties": {
+ "comments": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["enabled"],
+ "type": "object"
+ },
+ "dryRun": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["enabled"],
+ "type": "object"
+ },
+ "lists": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["enabled"],
+ "type": "object"
+ },
+ "trackChanges": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["enabled"],
+ "type": "object"
+ }
+ },
+ "required": ["trackChanges", "comments", "lists", "dryRun"],
+ "type": "object"
+ },
+ "operations": {
+ "additionalProperties": false,
+ "properties": {
+ "capabilities.get": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.add": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.edit": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.get": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.goTo": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.list": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.move": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.remove": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.reply": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.resolve": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.setActive": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "comments.setInternal": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "create.paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "delete": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "find": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "format.bold": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "getNode": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "getNodeById": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "getText": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "info": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "insert": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.exit": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.get": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.indent": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.insert": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.list": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.outdent": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.restart": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "lists.setType": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "replace": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "trackChanges.accept": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "trackChanges.acceptAll": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "trackChanges.get": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "trackChanges.list": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "trackChanges.reject": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ },
+ "trackChanges.rejectAll": {
+ "additionalProperties": false,
+ "properties": {
+ "available": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "reasons": {
+ "items": {
+ "enum": [
+ "COMMAND_UNAVAILABLE",
+ "OPERATION_UNAVAILABLE",
+ "TRACKED_MODE_UNAVAILABLE",
+ "DRY_RUN_UNAVAILABLE",
+ "NAMESPACE_UNAVAILABLE"
+ ]
+ },
+ "type": "array"
+ },
+ "tracked": {
+ "type": "boolean"
+ }
+ },
+ "required": ["available", "tracked", "dryRun"],
+ "type": "object"
+ }
+ },
+ "required": [
+ "find",
+ "getNode",
+ "getNodeById",
+ "getText",
+ "info",
+ "insert",
+ "replace",
+ "delete",
+ "format.bold",
+ "create.paragraph",
+ "lists.list",
+ "lists.get",
+ "lists.insert",
+ "lists.setType",
+ "lists.indent",
+ "lists.outdent",
+ "lists.restart",
+ "lists.exit",
+ "comments.add",
+ "comments.edit",
+ "comments.reply",
+ "comments.move",
+ "comments.resolve",
+ "comments.remove",
+ "comments.setInternal",
+ "comments.setActive",
+ "comments.goTo",
+ "comments.get",
+ "comments.list",
+ "trackChanges.list",
+ "trackChanges.get",
+ "trackChanges.accept",
+ "trackChanges.reject",
+ "trackChanges.acceptAll",
+ "trackChanges.rejectAll",
+ "capabilities.get"
+ ],
+ "type": "object"
+ }
+ },
+ "required": ["global", "operations"],
+ "type": "object"
+ }
+ },
+ "comments.add": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "text"],
+ "type": "object"
+ },
+ "memberPath": "comments.add",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "non-idempotent",
+ "mutates": true,
+ "possibleFailureCodes": ["INVALID_TARGET", "NO_OP"],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ }
+ },
+ "comments.edit": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["commentId", "text"],
+ "type": "object"
+ },
+ "memberPath": "comments.edit",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["NO_OP"],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ }
+ },
+ "comments.get": {
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ }
+ },
+ "required": ["commentId"],
+ "type": "object"
+ },
+ "memberPath": "comments.get",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "mutates": false,
+ "possibleFailureCodes": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND"]
+ }
+ },
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "commentId": {
+ "type": "string"
+ },
+ "createdTime": {
+ "type": "number"
+ },
+ "creatorEmail": {
+ "type": "string"
+ },
+ "creatorName": {
+ "type": "string"
+ },
+ "importedId": {
+ "type": "string"
+ },
+ "isInternal": {
+ "type": "boolean"
+ },
+ "parentCommentId": {
+ "type": "string"
+ },
+ "status": {
+ "enum": ["open", "resolved"]
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["address", "commentId", "status"],
+ "type": "object"
+ }
+ },
+ "comments.goTo": {
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ }
+ },
+ "required": ["commentId"],
+ "type": "object"
+ },
+ "memberPath": "comments.goTo",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": false,
+ "possibleFailureCodes": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ }
+ },
+ "comments.list": {
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "includeResolved": {
+ "type": "boolean"
+ }
+ },
+ "type": "object"
+ },
+ "memberPath": "comments.list",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "mutates": false,
+ "possibleFailureCodes": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": []
+ }
+ },
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "matches": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "commentId": {
+ "type": "string"
+ },
+ "createdTime": {
+ "type": "number"
+ },
+ "creatorEmail": {
+ "type": "string"
+ },
+ "creatorName": {
+ "type": "string"
+ },
+ "importedId": {
+ "type": "string"
+ },
+ "isInternal": {
+ "type": "boolean"
+ },
+ "parentCommentId": {
+ "type": "string"
+ },
+ "status": {
+ "enum": ["open", "resolved"]
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["address", "commentId", "status"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ }
+ },
+ "required": ["matches", "total"],
+ "type": "object"
+ }
+ },
+ "comments.move": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ }
+ },
+ "required": ["commentId", "target"],
+ "type": "object"
+ },
+ "memberPath": "comments.move",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["INVALID_TARGET", "NO_OP"],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ }
+ },
+ "comments.remove": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ }
+ },
+ "required": ["commentId"],
+ "type": "object"
+ },
+ "memberPath": "comments.remove",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["NO_OP"],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ }
+ },
+ "comments.reply": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "parentCommentId": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["parentCommentId", "text"],
+ "type": "object"
+ },
+ "memberPath": "comments.reply",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "non-idempotent",
+ "mutates": true,
+ "possibleFailureCodes": ["INVALID_TARGET"],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ }
+ },
+ "comments.resolve": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ }
+ },
+ "required": ["commentId"],
+ "type": "object"
+ },
+ "memberPath": "comments.resolve",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["NO_OP"],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ }
+ },
+ "comments.setActive": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": ["string", "null"]
+ }
+ },
+ "required": ["commentId"],
+ "type": "object"
+ },
+ "memberPath": "comments.setActive",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["INVALID_TARGET"],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ }
+ },
+ "comments.setInternal": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "commentId": {
+ "type": "string"
+ },
+ "isInternal": {
+ "type": "boolean"
+ }
+ },
+ "required": ["commentId", "isInternal"],
+ "type": "object"
+ },
+ "memberPath": "comments.setInternal",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["NO_OP", "INVALID_TARGET"],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ }
+ },
+ "create.paragraph": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "at": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "documentStart"
+ }
+ },
+ "required": ["kind"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "documentEnd"
+ }
+ },
+ "required": ["kind"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "before"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "target"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "after"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "target"],
+ "type": "object"
+ }
+ ]
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "memberPath": "create.paragraph",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "non-idempotent",
+ "mutates": true,
+ "possibleFailureCodes": ["INVALID_TARGET"],
+ "supportsDryRun": true,
+ "supportsTrackedMode": true,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "insertionPoint": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "paragraph"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "paragraph", "insertionPoint"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "insertionPoint": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "paragraph"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "paragraph", "insertionPoint"],
+ "type": "object"
+ }
+ },
+ "delete": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ }
+ },
+ "required": ["target"],
+ "type": "object"
+ },
+ "memberPath": "delete",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["NO_OP"],
+ "supportsDryRun": true,
+ "supportsTrackedMode": true,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "TRACK_CHANGE_COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ }
+ },
+ "find": {
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "includeNodes": {
+ "type": "boolean"
+ },
+ "includeUnknown": {
+ "type": "boolean"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "select": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "caseSensitive": {
+ "type": "boolean"
+ },
+ "mode": {
+ "enum": ["contains", "regex"]
+ },
+ "pattern": {
+ "type": "string"
+ },
+ "type": {
+ "const": "text"
+ }
+ },
+ "required": ["type", "pattern"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "enum": ["block", "inline"]
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ },
+ "type": {
+ "const": "node"
+ }
+ },
+ "required": ["type"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": ["nodeType"],
+ "type": "object"
+ }
+ ]
+ },
+ "within": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": [
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "sdt",
+ "image",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": ["kind", "nodeType", "anchor"],
+ "type": "object"
+ }
+ ]
+ }
+ },
+ "required": ["select"],
+ "type": "object"
+ },
+ "memberPath": "find",
+ "metadata": {
+ "deterministicTargetResolution": false,
+ "idempotency": "idempotent",
+ "mutates": false,
+ "possibleFailureCodes": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": []
+ }
+ },
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "context": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": [
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "sdt",
+ "image",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": ["kind", "nodeType", "anchor"],
+ "type": "object"
+ }
+ ]
+ },
+ "highlightRange": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ },
+ "snippet": {
+ "type": "string"
+ },
+ "textRanges": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["address", "snippet", "highlightRange"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "diagnostics": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": [
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "sdt",
+ "image",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": ["kind", "nodeType", "anchor"],
+ "type": "object"
+ }
+ ]
+ },
+ "hint": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["message"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "matches": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": [
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "sdt",
+ "image",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ }
+ },
+ "required": ["kind", "nodeType", "anchor"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "nodes": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "bodyNodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bodyText": {
+ "type": "string"
+ },
+ "kind": {
+ "enum": ["block", "inline"]
+ },
+ "nodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ },
+ "properties": {
+ "type": "object"
+ },
+ "summary": {
+ "additionalProperties": false,
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["nodeType", "kind"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ }
+ },
+ "required": ["matches", "total"],
+ "type": "object"
+ }
+ },
+ "format.bold": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ }
+ },
+ "required": ["target"],
+ "type": "object"
+ },
+ "memberPath": "format.bold",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["INVALID_TARGET"],
+ "supportsDryRun": true,
+ "supportsTrackedMode": true,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ }
+ },
+ "getNode": {
+ "inputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ },
+ "start": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "offset": {
+ "type": "integer"
+ }
+ },
+ "required": ["blockId", "offset"],
+ "type": "object"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ },
+ "kind": {
+ "const": "inline"
+ },
+ "nodeType": {
+ "enum": ["run", "bookmark", "comment", "hyperlink", "sdt", "image", "footnoteRef", "tab", "lineBreak"]
+ }
+ },
+ "required": ["kind", "nodeType", "anchor"],
+ "type": "object"
+ }
+ ]
+ },
+ "memberPath": "getNode",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "mutates": false,
+ "possibleFailureCodes": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND"]
+ }
+ },
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "bodyNodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bodyText": {
+ "type": "string"
+ },
+ "kind": {
+ "enum": ["block", "inline"]
+ },
+ "nodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ },
+ "properties": {
+ "type": "object"
+ },
+ "summary": {
+ "additionalProperties": false,
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["nodeType", "kind"],
+ "type": "object"
+ }
+ },
+ "getNodeById": {
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["nodeId"],
+ "type": "object"
+ },
+ "memberPath": "getNodeById",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "mutates": false,
+ "possibleFailureCodes": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND"]
+ }
+ },
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "bodyNodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "bodyText": {
+ "type": "string"
+ },
+ "kind": {
+ "enum": ["block", "inline"]
+ },
+ "nodes": {
+ "items": {
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "nodeType": {
+ "enum": [
+ "paragraph",
+ "heading",
+ "listItem",
+ "table",
+ "tableRow",
+ "tableCell",
+ "image",
+ "sdt",
+ "run",
+ "bookmark",
+ "comment",
+ "hyperlink",
+ "footnoteRef",
+ "tab",
+ "lineBreak"
+ ]
+ },
+ "properties": {
+ "type": "object"
+ },
+ "summary": {
+ "additionalProperties": false,
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["nodeType", "kind"],
+ "type": "object"
+ }
+ },
+ "getText": {
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "memberPath": "getText",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "mutates": false,
+ "possibleFailureCodes": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": []
+ }
+ },
+ "outputSchema": {
+ "type": "string"
+ }
+ },
+ "info": {
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "memberPath": "info",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "mutates": false,
+ "possibleFailureCodes": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": []
+ }
+ },
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "capabilities": {
+ "additionalProperties": false,
+ "properties": {
+ "canComment": {
+ "type": "boolean"
+ },
+ "canFind": {
+ "type": "boolean"
+ },
+ "canGetNode": {
+ "type": "boolean"
+ },
+ "canReplace": {
+ "type": "boolean"
+ }
+ },
+ "required": ["canFind", "canGetNode", "canComment", "canReplace"],
+ "type": "object"
+ },
+ "counts": {
+ "additionalProperties": false,
+ "properties": {
+ "comments": {
+ "type": "integer"
+ },
+ "headings": {
+ "type": "integer"
+ },
+ "images": {
+ "type": "integer"
+ },
+ "paragraphs": {
+ "type": "integer"
+ },
+ "tables": {
+ "type": "integer"
+ },
+ "words": {
+ "type": "integer"
+ }
+ },
+ "required": ["words", "paragraphs", "headings", "tables", "images", "comments"],
+ "type": "object"
+ },
+ "outline": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "level": {
+ "type": "integer"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["level", "text", "nodeId"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["counts", "outline", "capabilities"],
+ "type": "object"
+ }
+ },
+ "insert": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["text"],
+ "type": "object"
+ },
+ "memberPath": "insert",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "non-idempotent",
+ "mutates": true,
+ "possibleFailureCodes": ["INVALID_TARGET", "NO_OP"],
+ "supportsDryRun": true,
+ "supportsTrackedMode": true,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "TRACK_CHANGE_COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ }
+ },
+ "lists.exit": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["target"],
+ "type": "object"
+ },
+ "memberPath": "lists.exit",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["INVALID_TARGET"],
+ "supportsDryRun": true,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "paragraph"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "paragraph"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "paragraph": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "paragraph"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "paragraph"],
+ "type": "object"
+ }
+ },
+ "lists.get": {
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["address"],
+ "type": "object"
+ },
+ "memberPath": "lists.get",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "mutates": false,
+ "possibleFailureCodes": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND"]
+ }
+ },
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "kind": {
+ "enum": ["ordered", "bullet"]
+ },
+ "level": {
+ "type": "integer"
+ },
+ "marker": {
+ "type": "string"
+ },
+ "ordinal": {
+ "type": "integer"
+ },
+ "path": {
+ "items": {
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["address"],
+ "type": "object"
+ }
+ },
+ "lists.indent": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["target"],
+ "type": "object"
+ },
+ "memberPath": "lists.indent",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["NO_OP", "INVALID_TARGET"],
+ "supportsDryRun": true,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ }
+ },
+ "lists.insert": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "position": {
+ "enum": ["before", "after"]
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "position"],
+ "type": "object"
+ },
+ "memberPath": "lists.insert",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "non-idempotent",
+ "mutates": true,
+ "possibleFailureCodes": ["INVALID_TARGET"],
+ "supportsDryRun": true,
+ "supportsTrackedMode": true,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "insertionPoint": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "item", "insertionPoint"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "insertionPoint": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "trackedChangeRefs": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "item", "insertionPoint"],
+ "type": "object"
+ }
+ },
+ "lists.list": {
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "enum": ["ordered", "bullet"]
+ },
+ "level": {
+ "type": "integer"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "ordinal": {
+ "type": "integer"
+ },
+ "within": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "enum": ["paragraph", "heading", "listItem", "table", "tableRow", "tableCell", "image", "sdt"]
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "memberPath": "lists.list",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "mutates": false,
+ "possibleFailureCodes": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND"]
+ }
+ },
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "items": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "kind": {
+ "enum": ["ordered", "bullet"]
+ },
+ "level": {
+ "type": "integer"
+ },
+ "marker": {
+ "type": "string"
+ },
+ "ordinal": {
+ "type": "integer"
+ },
+ "path": {
+ "items": {
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["address"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "matches": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ }
+ },
+ "required": ["matches", "total", "items"],
+ "type": "object"
+ }
+ },
+ "lists.outdent": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["target"],
+ "type": "object"
+ },
+ "memberPath": "lists.outdent",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["NO_OP", "INVALID_TARGET"],
+ "supportsDryRun": true,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ }
+ },
+ "lists.restart": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["target"],
+ "type": "object"
+ },
+ "memberPath": "lists.restart",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["NO_OP", "INVALID_TARGET"],
+ "supportsDryRun": true,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ }
+ },
+ "lists.setType": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "enum": ["ordered", "bullet"]
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ }
+ },
+ "required": ["target", "kind"],
+ "type": "object"
+ },
+ "memberPath": "lists.setType",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["NO_OP", "INVALID_TARGET"],
+ "supportsDryRun": true,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": [
+ "TARGET_NOT_FOUND",
+ "COMMAND_UNAVAILABLE",
+ "TRACK_CHANGE_COMMAND_UNAVAILABLE",
+ "CAPABILITY_UNAVAILABLE"
+ ]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP", "INVALID_TARGET"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "block"
+ },
+ "nodeId": {
+ "type": "string"
+ },
+ "nodeType": {
+ "const": "listItem"
+ }
+ },
+ "required": ["kind", "nodeType", "nodeId"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": ["success", "item"],
+ "type": "object"
+ }
+ },
+ "replace": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "text"],
+ "type": "object"
+ },
+ "memberPath": "replace",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["INVALID_TARGET", "NO_OP"],
+ "supportsDryRun": true,
+ "supportsTrackedMode": true,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "TRACK_CHANGE_COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["INVALID_TARGET", "NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure", "resolution"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "type": "integer"
+ },
+ "to": {
+ "type": "integer"
+ }
+ },
+ "required": ["from", "to"],
+ "type": "object"
+ },
+ "requestedTarget": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "target": {
+ "additionalProperties": false,
+ "properties": {
+ "blockId": {
+ "type": "string"
+ },
+ "kind": {
+ "const": "text"
+ },
+ "range": {
+ "additionalProperties": false,
+ "properties": {
+ "end": {
+ "type": "integer"
+ },
+ "start": {
+ "type": "integer"
+ }
+ },
+ "required": ["start", "end"],
+ "type": "object"
+ }
+ },
+ "required": ["kind", "blockId", "range"],
+ "type": "object"
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["target", "range", "text"],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success", "resolution"],
+ "type": "object"
+ }
+ },
+ "trackChanges.accept": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ },
+ "required": ["id"],
+ "type": "object"
+ },
+ "memberPath": "trackChanges.accept",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["NO_OP"],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ }
+ },
+ "trackChanges.acceptAll": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "memberPath": "trackChanges.acceptAll",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["NO_OP"],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ }
+ },
+ "trackChanges.get": {
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ },
+ "required": ["id"],
+ "type": "object"
+ },
+ "memberPath": "trackChanges.get",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "mutates": false,
+ "possibleFailureCodes": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND"]
+ }
+ },
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "author": {
+ "type": "string"
+ },
+ "authorEmail": {
+ "type": "string"
+ },
+ "authorImage": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "excerpt": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "enum": ["insert", "delete", "format"]
+ }
+ },
+ "required": ["address", "id", "type"],
+ "type": "object"
+ }
+ },
+ "trackChanges.list": {
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "type": {
+ "enum": ["insert", "delete", "format"]
+ }
+ },
+ "type": "object"
+ },
+ "memberPath": "trackChanges.list",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "idempotent",
+ "mutates": false,
+ "possibleFailureCodes": [],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": []
+ }
+ },
+ "outputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "changes": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "address": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "author": {
+ "type": "string"
+ },
+ "authorEmail": {
+ "type": "string"
+ },
+ "authorImage": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "excerpt": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "enum": ["insert", "delete", "format"]
+ }
+ },
+ "required": ["address", "id", "type"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "matches": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ }
+ },
+ "required": ["matches", "total"],
+ "type": "object"
+ }
+ },
+ "trackChanges.reject": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ },
+ "required": ["id"],
+ "type": "object"
+ },
+ "memberPath": "trackChanges.reject",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["NO_OP"],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["TARGET_NOT_FOUND", "COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ }
+ },
+ "trackChanges.rejectAll": {
+ "failureSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ },
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "memberPath": "trackChanges.rejectAll",
+ "metadata": {
+ "deterministicTargetResolution": true,
+ "idempotency": "conditional",
+ "mutates": true,
+ "possibleFailureCodes": ["NO_OP"],
+ "supportsDryRun": false,
+ "supportsTrackedMode": false,
+ "throws": {
+ "postApplyForbidden": true,
+ "preApply": ["COMMAND_UNAVAILABLE", "CAPABILITY_UNAVAILABLE"]
+ }
+ },
+ "outputSchema": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": ["NO_OP"]
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": ["success", "failure"],
+ "type": "object"
+ }
+ ]
+ },
+ "successSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "inserted": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "removed": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "success": {
+ "const": true
+ },
+ "updated": {
+ "items": {
+ "oneOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "comment"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "entityId": {
+ "type": "string"
+ },
+ "entityType": {
+ "const": "trackedChange"
+ },
+ "kind": {
+ "const": "entity"
+ }
+ },
+ "required": ["kind", "entityType", "entityId"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ },
+ "required": ["success"],
+ "type": "object"
+ }
+ }
+ },
+ "sourceCommit": null,
+ "sourceHash": "8d875ceb279d213c1e779882f103d39fa2106545ce94c6b1553ae06b6c321e03"
+}
diff --git a/packages/document-api/package.json b/packages/document-api/package.json
new file mode 100644
index 0000000000..914a519977
--- /dev/null
+++ b/packages/document-api/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@superdoc/document-api",
+ "version": "0.0.1",
+ "private": true,
+ "description": "The SuperDoc document API",
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./src/index.ts",
+ "source": "./src/index.ts",
+ "default": "./src/index.ts"
+ },
+ "./types": {
+ "types": "./src/types/index.ts",
+ "source": "./src/types/index.ts",
+ "default": "./src/types/index.ts"
+ }
+ },
+ "files": [
+ "src"
+ ]
+}
diff --git a/packages/document-api/scripts/README.md b/packages/document-api/scripts/README.md
new file mode 100644
index 0000000000..2da5786357
--- /dev/null
+++ b/packages/document-api/scripts/README.md
@@ -0,0 +1,57 @@
+# Document API Script Catalog
+
+This folder contains deterministic generator/check entry points for the Document API contract and docs.
+
+## Calling model
+
+- `generate-*` scripts write generated artifacts.
+- `check-*` scripts validate generated artifacts or docs and fail with non-zero exit code on drift.
+- Root `package.json` exposes three canonical entry points:
+ - `pnpm run docapi:sync` — runs `generate-contract-outputs.ts`
+ - `pnpm run docapi:check` — runs `check-contract-parity.ts` + `check-contract-outputs.ts`
+ - `pnpm run docapi:sync:check` — sync then check
+- Pre-commit hook (`lefthook.yml`) auto-runs `docapi:sync` when document-api source files are staged.
+- CI workflow (`ci-document-api.yml`) runs `docapi:check` on PRs touching document-api paths.
+
+## Manual vs generated boundaries
+
+- Hand-authored inputs:
+ - `packages/document-api/src/contract/*`
+ - `packages/document-api/src/index.ts` and related runtime/types
+ - `packages/document-api/scripts/*`
+- Generated outputs (checked into git):
+ - `packages/document-api/generated/*`
+ - `apps/docs/document-api/reference/*`
+ - generated marker block in `apps/docs/document-api/overview.mdx`
+
+Do not hand-edit generated output files. Regenerate instead.
+
+## Script index
+
+| Script | Kind | Purpose | Reads | Writes | Typical caller |
+| --- | --- | --- | --- | --- | --- |
+| `check-contract-outputs.ts` | check | Full generated-output gate across schemas/manifests/agent/reference + overview block | Contract snapshot + generated roots + docs overview | None | CI/local full verification |
+| `generate-contract-outputs.ts` | generate | Full regeneration across schemas/manifests/agent/reference + overview block | Contract snapshot + docs overview | `packages/document-api/generated/*`, `apps/docs/document-api/reference/*`, generated block in overview | Main local sync before commit |
+| `check-stable-schemas.ts` | check | Validate stable schema artifact drift | Contract snapshot + `packages/document-api/generated/schemas` | None | Focused check during schema work |
+| `generate-stable-schemas.ts` | generate | Regenerate stable schema artifacts | Contract snapshot | `packages/document-api/generated/schemas/*` | Focused schema regeneration |
+| `check-tool-manifests.ts` | check | Validate tool manifest artifact drift | Contract snapshot + `packages/document-api/generated/manifests` | None | Focused manifest check |
+| `generate-tool-manifests.ts` | generate | Regenerate tool manifest artifacts | Contract snapshot | `packages/document-api/generated/manifests/*` | Focused manifest regeneration |
+| `check-agent-artifacts.ts` | check | Validate agent artifact drift | Contract snapshot + `packages/document-api/generated/agent` | None | Focused agent-artifact check |
+| `generate-agent-artifacts.ts` | generate | Regenerate agent artifacts (remediation/workflow/compatibility) | Contract snapshot | `packages/document-api/generated/agent/*` | Focused agent-artifact regeneration |
+| `check-generated-reference-docs.ts` | check | Validate generated reference docs and overview generated block drift | Contract snapshot + `apps/docs/document-api/reference` + overview | None | Focused docs generation check |
+| `generate-reference-docs.ts` | generate | Regenerate generated reference docs and overview generated block | Contract snapshot + overview markers | `apps/docs/document-api/reference/*`, generated block in `apps/docs/document-api/overview.mdx` | Focused docs regeneration |
+| `check-overview-alignment.ts` | check | Enforce overview quality rules (required copy/markers, forbidden placeholders, known API paths only) | `apps/docs/document-api/overview.mdx` + `DOCUMENT_API_MEMBER_PATHS` | None | Docs consistency gate |
+| `check-doc-coverage.ts` | check | Ensure every operation has a `### \`\`` section in `src/README.md` | `packages/document-api/src/README.md` + `OPERATION_IDS` | None | Contract/docs coverage gate |
+| `check-examples.ts` | check | Ensure required workflow example headings exist in `src/README.md` | `packages/document-api/src/README.md` | None | Docs workflow example gate |
+| `check-contract-parity.ts` | check | Enforce parity between operation IDs, command catalog, maps, and runtime API member paths | `packages/document-api/src/index.js` exports + runtime API shape | None | Contract surface integrity gate |
+| `generate-internal-schemas.ts` | generate | Generate internal-only operation schema snapshot | Contract snapshot + schema dialect | `packages/document-api/.generated-internal/contract-schemas/index.json` | Local tooling/debugging |
+
+## Recommended usage
+
+1. Change contract/docs sources.
+2. Run `pnpm run docapi:sync` (or the individual `generate-*` script for focused work).
+3. Run `pnpm run docapi:check` to verify zero drift.
+
+Or combine: `pnpm run docapi:sync:check`
+
+The pre-commit hook handles step 2 automatically when document-api files are staged. CI enforces step 3.
diff --git a/packages/document-api/scripts/check-agent-artifacts.ts b/packages/document-api/scripts/check-agent-artifacts.ts
new file mode 100644
index 0000000000..ea46705bd5
--- /dev/null
+++ b/packages/document-api/scripts/check-agent-artifacts.ts
@@ -0,0 +1,13 @@
+/**
+ * Purpose: Verify generated agent artifacts match the current contract snapshot.
+ * Caller: Focused local/CI check; `check-contract-outputs.ts` is the broader superset.
+ * Reads: Contract snapshot + files under `packages/document-api/generated/agent`.
+ * Writes: None (exit code + console output only).
+ * Fails when: Expected files are missing/extra/stale.
+ */
+import { buildAgentArtifacts, getAgentArtifactRoot } from './lib/contract-output-artifacts.js';
+import { runArtifactCheck, runScript } from './lib/generation-utils.js';
+
+runScript('agent artifacts check', () =>
+ runArtifactCheck('agent artifacts', buildAgentArtifacts, [getAgentArtifactRoot()]),
+);
diff --git a/packages/document-api/scripts/check-contract-outputs.ts b/packages/document-api/scripts/check-contract-outputs.ts
new file mode 100644
index 0000000000..5bbd432913
--- /dev/null
+++ b/packages/document-api/scripts/check-contract-outputs.ts
@@ -0,0 +1,45 @@
+/**
+ * Purpose: Verify all contract-derived outputs are up to date.
+ * Caller: Main CI/local gate for generated Document API artifacts.
+ * Reads: Contract snapshot + generated schemas/manifests/agent artifacts/reference docs + overview.
+ * Writes: None (exit code + console output only).
+ * Fails when: Any generated output is missing/extra/stale or overview block is out of sync.
+ */
+import {
+ buildStableSchemaArtifacts,
+ buildToolManifestArtifacts,
+ buildAgentArtifacts,
+ getAgentArtifactRoot,
+ getStableSchemaRoot,
+ getToolManifestRoot,
+} from './lib/contract-output-artifacts.js';
+import { checkGeneratedFiles, formatGeneratedCheckIssues, runScript } from './lib/generation-utils.js';
+import {
+ buildReferenceDocsArtifacts,
+ checkReferenceDocsExtras,
+ getReferenceDocsOutputRoot,
+} from './lib/reference-docs-artifacts.js';
+
+runScript('contract output artifacts check', async () => {
+ const files = [
+ ...buildStableSchemaArtifacts(),
+ ...buildToolManifestArtifacts(),
+ ...buildAgentArtifacts(),
+ ...buildReferenceDocsArtifacts(),
+ ];
+
+ const issues = await checkGeneratedFiles(files, {
+ roots: [getStableSchemaRoot(), getToolManifestRoot(), getAgentArtifactRoot(), getReferenceDocsOutputRoot()],
+ });
+
+ await checkReferenceDocsExtras(files, issues);
+
+ if (issues.length > 0) {
+ console.error('contract output artifacts check failed');
+ console.error(formatGeneratedCheckIssues(issues));
+ process.exitCode = 1;
+ return;
+ }
+
+ console.log(`contract output artifacts check passed (${files.length} generated files + overview block)`);
+});
diff --git a/packages/document-api/scripts/check-contract-parity.ts b/packages/document-api/scripts/check-contract-parity.ts
new file mode 100644
index 0000000000..a539d81ff5
--- /dev/null
+++ b/packages/document-api/scripts/check-contract-parity.ts
@@ -0,0 +1,280 @@
+/**
+ * Purpose: Enforce parity between operation IDs, operation/member maps, and runtime API surface.
+ * Caller: Contract maintenance check (local or CI).
+ * Reads: `../src/index.js` contract metadata and runtime API shape.
+ * Writes: None (exit code + console output only).
+ * Fails when: Any catalog/map/member-path parity rule is violated.
+ */
+import {
+ COMMAND_CATALOG,
+ DOCUMENT_API_MEMBER_PATHS,
+ OPERATION_IDS,
+ OPERATION_MEMBER_PATH_MAP,
+ createDocumentApi,
+ isValidOperationIdFormat,
+ type DocumentApiAdapters,
+} from '../src/index.js';
+import { OPERATION_DEFINITIONS } from '../src/contract/operation-definitions.js';
+import { OPERATION_REFERENCE_DOC_PATH_MAP } from '../src/contract/reference-doc-map.js';
+import { buildDispatchTable } from '../src/invoke/invoke.js';
+
+/**
+ * Meta-methods on DocumentApi that are not operations.
+ * These are excluded from operation-to-member-path parity checks.
+ */
+const META_MEMBER_PATHS = ['invoke'] as const;
+
+function collectFunctionMemberPaths(value: unknown, prefix = ''): string[] {
+ if (!value || typeof value !== 'object') return [];
+
+ const paths: string[] = [];
+ const entries = Object.entries(value as Record).sort(([left], [right]) => left.localeCompare(right));
+
+ for (const [key, member] of entries) {
+ const path = prefix ? `${prefix}.${key}` : key;
+ if (typeof member === 'function') {
+ paths.push(path);
+ continue;
+ }
+ if (member && typeof member === 'object') {
+ paths.push(...collectFunctionMemberPaths(member, path));
+ }
+ }
+
+ return paths;
+}
+
+function createNoopAdapters(): DocumentApiAdapters {
+ return {
+ find: {
+ find: () => ({ matches: [], total: 0 }),
+ },
+ getNode: {
+ getNode: () => ({ kind: 'block', nodeType: 'paragraph', properties: {} }),
+ getNodeById: () => ({ kind: 'block', nodeType: 'paragraph', properties: {} }),
+ },
+ getText: {
+ getText: () => '',
+ },
+ info: {
+ info: () => ({
+ counts: { words: 0, paragraphs: 0, headings: 0, tables: 0, images: 0, comments: 0 },
+ outline: [],
+ capabilities: { canFind: true, canGetNode: true, canComment: true, canReplace: true },
+ }),
+ },
+ capabilities: {
+ get: () => ({
+ global: {
+ trackChanges: { enabled: false },
+ comments: { enabled: false },
+ lists: { enabled: false },
+ dryRun: { enabled: false },
+ },
+ operations: {} as ReturnType['operations'],
+ }),
+ },
+ comments: {
+ add: () => ({ success: true }),
+ edit: () => ({ success: true }),
+ reply: () => ({ success: true }),
+ move: () => ({ success: true }),
+ resolve: () => ({ success: true }),
+ remove: () => ({ success: true }),
+ setInternal: () => ({ success: true }),
+ setActive: () => ({ success: true }),
+ goTo: () => ({ success: true }),
+ get: () => ({
+ address: { kind: 'entity', entityType: 'comment', entityId: 'comment-1' },
+ commentId: 'comment-1',
+ status: 'open',
+ }),
+ list: () => ({ matches: [], total: 0 }),
+ },
+ write: {
+ write: () => ({
+ success: true,
+ resolution: {
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 0 } },
+ range: { from: 1, to: 1 },
+ text: '',
+ },
+ }),
+ },
+ format: {
+ bold: () => ({
+ success: true,
+ resolution: {
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 1 } },
+ range: { from: 1, to: 2 },
+ text: 'x',
+ },
+ }),
+ },
+ trackChanges: {
+ list: () => ({ matches: [], total: 0 }),
+ get: ({ id }) => ({
+ address: { kind: 'entity', entityType: 'trackedChange', entityId: id },
+ id,
+ type: 'insert',
+ }),
+ accept: () => ({ success: true }),
+ reject: () => ({ success: true }),
+ acceptAll: () => ({ success: true }),
+ rejectAll: () => ({ success: true }),
+ },
+ create: {
+ paragraph: () => ({
+ success: true,
+ paragraph: { kind: 'block', nodeType: 'paragraph', nodeId: 'p2' },
+ insertionPoint: { kind: 'text', blockId: 'p2', range: { start: 0, end: 0 } },
+ }),
+ },
+ lists: {
+ list: () => ({ matches: [], total: 0, items: [] }),
+ get: () => ({
+ address: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ }),
+ insert: () => ({
+ success: true,
+ item: { kind: 'block', nodeType: 'listItem', nodeId: 'li-2' },
+ insertionPoint: { kind: 'text', blockId: 'li-2', range: { start: 0, end: 0 } },
+ }),
+ setType: () => ({
+ success: true,
+ item: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ }),
+ indent: () => ({
+ success: true,
+ item: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ }),
+ outdent: () => ({
+ success: true,
+ item: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ }),
+ restart: () => ({
+ success: true,
+ item: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ }),
+ exit: () => ({
+ success: true,
+ paragraph: { kind: 'block', nodeType: 'paragraph', nodeId: 'p3' },
+ }),
+ },
+ };
+}
+
+function diff(left: string[], right: string[]) {
+ const rightSet = new Set(right);
+ return left.filter((value) => !rightSet.has(value));
+}
+
+function run(): void {
+ const errors: string[] = [];
+ const operationIds = [...OPERATION_IDS];
+ const catalogKeys = Object.keys(COMMAND_CATALOG);
+ const mappedKeys = Object.keys(OPERATION_MEMBER_PATH_MAP);
+
+ const invalidFormatIds = operationIds.filter((operationId) => !isValidOperationIdFormat(operationId));
+ if (invalidFormatIds.length > 0) {
+ errors.push(`Invalid operationId format: ${invalidFormatIds.join(', ')}`);
+ }
+
+ const missingFromCatalog = diff(operationIds, catalogKeys);
+ const extraInCatalog = diff(catalogKeys, operationIds);
+ if (missingFromCatalog.length > 0 || extraInCatalog.length > 0) {
+ errors.push(
+ `COMMAND_CATALOG parity failed (missing: ${missingFromCatalog.join(', ') || 'none'}, extra: ${extraInCatalog.join(', ') || 'none'})`,
+ );
+ }
+
+ const missingFromMap = diff(operationIds, mappedKeys);
+ const extraInMap = diff(mappedKeys, operationIds);
+ if (missingFromMap.length > 0 || extraInMap.length > 0) {
+ errors.push(
+ `operation-map key parity failed (missing: ${missingFromMap.join(', ') || 'none'}, extra: ${extraInMap.join(', ') || 'none'})`,
+ );
+ }
+
+ const api = createDocumentApi(createNoopAdapters());
+ const metaPathSet = new Set(META_MEMBER_PATHS);
+ const runtimeMemberPaths = collectFunctionMemberPaths(api)
+ .filter((path) => !metaPathSet.has(path))
+ .sort();
+ const declaredMemberPaths = [...DOCUMENT_API_MEMBER_PATHS].sort();
+
+ const missingRuntimeMembers = diff(declaredMemberPaths, runtimeMemberPaths);
+ const extraRuntimeMembers = diff(runtimeMemberPaths, declaredMemberPaths);
+ if (missingRuntimeMembers.length > 0 || extraRuntimeMembers.length > 0) {
+ errors.push(
+ `DocumentApi member-path parity failed (missing runtime: ${missingRuntimeMembers.join(', ') || 'none'}, extra runtime: ${extraRuntimeMembers.join(', ') || 'none'})`,
+ );
+ }
+
+ // Verify invoke dispatch table keys match OPERATION_IDS exactly.
+ const dispatchKeys = Object.keys(buildDispatchTable(api)).sort();
+ const missingDispatch = diff(operationIds, dispatchKeys);
+ const extraDispatch = diff(dispatchKeys, operationIds);
+ if (missingDispatch.length > 0 || extraDispatch.length > 0) {
+ errors.push(
+ `invoke dispatch table parity failed (missing: ${missingDispatch.join(', ') || 'none'}, extra: ${extraDispatch.join(', ') || 'none'})`,
+ );
+ }
+
+ const mappedMemberPaths = Object.values(OPERATION_MEMBER_PATH_MAP).sort();
+ const missingMapMembers = diff(declaredMemberPaths, mappedMemberPaths);
+ const extraMapMembers = diff(mappedMemberPaths, declaredMemberPaths);
+ if (missingMapMembers.length > 0 || extraMapMembers.length > 0) {
+ errors.push(
+ `operation-map value parity failed (missing map values: ${missingMapMembers.join(', ') || 'none'}, extra map values: ${extraMapMembers.join(', ') || 'none'})`,
+ );
+ }
+
+ for (const operationId of operationIds) {
+ const memberPath = OPERATION_MEMBER_PATH_MAP[operationId];
+ if (!declaredMemberPaths.includes(memberPath)) {
+ errors.push(`operationId "${operationId}" maps to undeclared member path "${memberPath}".`);
+ }
+ if (!runtimeMemberPaths.includes(memberPath)) {
+ errors.push(`operationId "${operationId}" maps to runtime-missing member path "${memberPath}".`);
+ }
+ }
+
+ // Verify OPERATION_DEFINITIONS keys match OPERATION_IDS exactly.
+ const definitionKeys = Object.keys(OPERATION_DEFINITIONS).sort();
+ const sortedOperationIds = [...operationIds].sort();
+ if (definitionKeys.join('|') !== sortedOperationIds.join('|')) {
+ errors.push(
+ `OPERATION_DEFINITIONS keys do not match OPERATION_IDS (definitions: ${definitionKeys.length}, ops: ${sortedOperationIds.length})`,
+ );
+ }
+
+ // Value-level projection checks — catches projection bugs, not just key bugs.
+ for (const id of operationIds) {
+ const defEntry = OPERATION_DEFINITIONS[id];
+ if (COMMAND_CATALOG[id] !== defEntry.metadata) {
+ errors.push(`COMMAND_CATALOG['${id}'] is not the same object as OPERATION_DEFINITIONS['${id}'].metadata`);
+ }
+ if (OPERATION_MEMBER_PATH_MAP[id] !== defEntry.memberPath) {
+ errors.push(`OPERATION_MEMBER_PATH_MAP['${id}'] !== OPERATION_DEFINITIONS['${id}'].memberPath`);
+ }
+ if (OPERATION_REFERENCE_DOC_PATH_MAP[id] !== defEntry.referenceDocPath) {
+ errors.push(`OPERATION_REFERENCE_DOC_PATH_MAP['${id}'] !== OPERATION_DEFINITIONS['${id}'].referenceDocPath`);
+ }
+ }
+
+ if (errors.length > 0) {
+ console.error('contract parity check failed:\n');
+ for (const error of errors) {
+ console.error(`- ${error}`);
+ }
+ process.exitCode = 1;
+ return;
+ }
+
+ console.log(
+ `contract parity check passed (${operationIds.length} operations, ${declaredMemberPaths.length} API members).`,
+ );
+}
+
+run();
diff --git a/packages/document-api/scripts/check-doc-coverage.ts b/packages/document-api/scripts/check-doc-coverage.ts
new file mode 100644
index 0000000000..b45983375e
--- /dev/null
+++ b/packages/document-api/scripts/check-doc-coverage.ts
@@ -0,0 +1,35 @@
+/**
+ * Purpose: Ensure every operation has a dedicated section in `src/README.md`.
+ * Caller: Documentation quality gate for operation-level docs.
+ * Reads: `packages/document-api/src/README.md` + `OPERATION_IDS`.
+ * Writes: None (exit code + console output only).
+ * Fails when: Any operation ID is missing a `### \`\`` heading.
+ */
+import { readFile } from 'node:fs/promises';
+import { resolve } from 'node:path';
+import { OPERATION_IDS } from '../src/index.js';
+import { runScript } from './lib/generation-utils.js';
+
+const README_PATH = resolve(process.cwd(), 'packages/document-api/src/README.md');
+
+function hasOperationSection(readme: string, operationId: string): boolean {
+ const escaped = operationId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const sectionPattern = new RegExp(`^###\\s+\`${escaped}\`\\s*$`, 'm');
+ return sectionPattern.test(readme);
+}
+
+runScript('doc coverage check', async () => {
+ const readme = await readFile(README_PATH, 'utf8');
+ const missing = OPERATION_IDS.filter((operationId) => !hasOperationSection(readme, operationId));
+
+ if (missing.length > 0) {
+ console.error('doc coverage check failed: missing operation sections in README.md');
+ for (const operationId of missing) {
+ console.error(`- ${operationId}`);
+ }
+ process.exitCode = 1;
+ return;
+ }
+
+ console.log(`doc coverage check passed (${OPERATION_IDS.length} operations documented).`);
+});
diff --git a/packages/document-api/scripts/check-examples.ts b/packages/document-api/scripts/check-examples.ts
new file mode 100644
index 0000000000..c05c808662
--- /dev/null
+++ b/packages/document-api/scripts/check-examples.ts
@@ -0,0 +1,36 @@
+/**
+ * Purpose: Ensure required workflow example headings exist in `src/README.md`.
+ * Caller: Documentation quality gate for canonical workflow examples.
+ * Reads: `packages/document-api/src/README.md`.
+ * Writes: None (exit code + console output only).
+ * Fails when: Any required workflow heading is missing.
+ */
+import { readFile } from 'node:fs/promises';
+import { resolve } from 'node:path';
+import { runScript } from './lib/generation-utils.js';
+
+const README_PATH = resolve(process.cwd(), 'packages/document-api/src/README.md');
+
+const REQUIRED_WORKFLOW_HEADINGS = [
+ '### Workflow: Find + Mutate',
+ '### Workflow: Tracked-Mode Insert',
+ '### Workflow: Comment Thread Lifecycle',
+ '### Workflow: List Manipulation',
+ '### Workflow: Capabilities-Aware Branching',
+] as const;
+
+runScript('workflow example check', async () => {
+ const readme = await readFile(README_PATH, 'utf8');
+ const missing = REQUIRED_WORKFLOW_HEADINGS.filter((heading) => !readme.includes(heading));
+
+ if (missing.length > 0) {
+ console.error('workflow example check failed: missing required README headings');
+ for (const heading of missing) {
+ console.error(`- ${heading}`);
+ }
+ process.exitCode = 1;
+ return;
+ }
+
+ console.log(`workflow example check passed (${REQUIRED_WORKFLOW_HEADINGS.length} examples found).`);
+});
diff --git a/packages/document-api/scripts/check-generated-reference-docs.ts b/packages/document-api/scripts/check-generated-reference-docs.ts
new file mode 100644
index 0000000000..fe73fe504d
--- /dev/null
+++ b/packages/document-api/scripts/check-generated-reference-docs.ts
@@ -0,0 +1,22 @@
+/**
+ * Purpose: Verify generated reference docs and related overview block are up to date.
+ * Caller: Focused local/CI check; `check-contract-outputs.ts` is the broader superset.
+ * Reads: Contract snapshot + files under `apps/docs/document-api/reference` + overview markers.
+ * Writes: None (exit code + console output only).
+ * Fails when: Generated reference docs or overview generated block drift from contract.
+ */
+import {
+ buildReferenceDocsArtifacts,
+ checkReferenceDocsExtras,
+ getReferenceDocsOutputRoot,
+} from './lib/reference-docs-artifacts.js';
+import { runArtifactCheck, runScript } from './lib/generation-utils.js';
+
+runScript('generated reference docs check', () =>
+ runArtifactCheck(
+ 'generated reference docs',
+ buildReferenceDocsArtifacts,
+ [getReferenceDocsOutputRoot()],
+ checkReferenceDocsExtras,
+ ),
+);
diff --git a/packages/document-api/scripts/check-overview-alignment.ts b/packages/document-api/scripts/check-overview-alignment.ts
new file mode 100644
index 0000000000..92deb31276
--- /dev/null
+++ b/packages/document-api/scripts/check-overview-alignment.ts
@@ -0,0 +1,108 @@
+/**
+ * Purpose: Enforce required/forbidden overview content and API-surface path validity.
+ * Caller: Documentation consistency gate for `apps/docs/document-api/overview.mdx`.
+ * Reads: Overview doc content + `DOCUMENT_API_MEMBER_PATHS`.
+ * Writes: None (exit code + console output only).
+ * Fails when: Disclaimers/markers are missing, forbidden placeholders exist, or unknown API paths appear.
+ */
+import { readFile } from 'node:fs/promises';
+import { resolve } from 'node:path';
+import { DOCUMENT_API_MEMBER_PATHS } from '../src/index.js';
+import { runScript } from './lib/generation-utils.js';
+import {
+ getOverviewApiSurfaceEndMarker,
+ getOverviewApiSurfaceStartMarker,
+ getOverviewDocsPath,
+} from './lib/reference-docs-artifacts.js';
+
+const OVERVIEW_PATH = resolve(process.cwd(), getOverviewDocsPath());
+
+const REQUIRED_PATTERNS = [
+ {
+ label: 'alpha disclaimer',
+ pattern: /\balpha\b/i,
+ },
+ {
+ label: 'subject-to-change disclaimer',
+ pattern: /subject to (?:breaking )?changes?/i,
+ },
+ {
+ label: 'generated reference link',
+ pattern: /\/document-api\/reference\/index/i,
+ },
+ {
+ label: 'generated API surface start marker',
+ pattern: new RegExp(getOverviewApiSurfaceStartMarker().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
+ },
+ {
+ label: 'generated API surface end marker',
+ pattern: new RegExp(getOverviewApiSurfaceEndMarker().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
+ },
+] as const;
+
+const FORBIDDEN_PATTERNS = [
+ {
+ label: 'legacy placeholder query API',
+ pattern: /\bdoc\.query\s*\(/,
+ },
+ {
+ label: 'legacy placeholder table API',
+ pattern: /\bdoc\.table\s*\(/,
+ },
+ {
+ label: 'legacy field-annotation selector example',
+ pattern: /field-annotation/i,
+ },
+ {
+ label: 'coming-soon placeholder copy',
+ pattern: /coming soon/i,
+ },
+] as const;
+
+const MEMBER_PATH_REGEX = /\beditor\.doc\.([A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*)/g;
+
+function extractOverviewMemberPaths(content: string): string[] {
+ const paths = new Set();
+ for (const match of content.matchAll(MEMBER_PATH_REGEX)) {
+ const path = match[1];
+ if (!path) continue;
+ paths.add(path);
+ }
+ return [...paths].sort();
+}
+
+runScript('document-api overview alignment check', async () => {
+ const content = await readFile(OVERVIEW_PATH, 'utf8');
+ const errors: string[] = [];
+
+ for (const requirement of REQUIRED_PATTERNS) {
+ if (!requirement.pattern.test(content)) {
+ errors.push(`missing ${requirement.label}`);
+ }
+ }
+
+ for (const forbidden of FORBIDDEN_PATTERNS) {
+ if (forbidden.pattern.test(content)) {
+ errors.push(`contains ${forbidden.label}`);
+ }
+ }
+
+ const knownMemberPaths = new Set(DOCUMENT_API_MEMBER_PATHS);
+ const overviewMemberPaths = extractOverviewMemberPaths(content);
+
+ const unknownPaths = overviewMemberPaths.filter((path) => !knownMemberPaths.has(path));
+ if (unknownPaths.length > 0) {
+ errors.push(`overview includes unknown Document API paths: ${unknownPaths.join(', ')}`);
+ }
+
+ if (errors.length > 0) {
+ console.error('document-api overview alignment check failed:');
+ for (const error of errors) {
+ console.error(`- ${error}`);
+ }
+ process.exitCode = 1;
+ return;
+ }
+
+ console.log(`document-api overview alignment check passed (${overviewMemberPaths.length} member paths referenced).`);
+});
diff --git a/packages/document-api/scripts/check-stable-schemas.ts b/packages/document-api/scripts/check-stable-schemas.ts
new file mode 100644
index 0000000000..18f088abfc
--- /dev/null
+++ b/packages/document-api/scripts/check-stable-schemas.ts
@@ -0,0 +1,13 @@
+/**
+ * Purpose: Verify generated stable schema artifacts match the current contract snapshot.
+ * Caller: Focused local/CI check; `check-contract-outputs.ts` is the broader superset.
+ * Reads: Contract snapshot + files under `packages/document-api/generated/schemas`.
+ * Writes: None (exit code + console output only).
+ * Fails when: Stable schema files are missing/extra/stale.
+ */
+import { buildStableSchemaArtifacts, getStableSchemaRoot } from './lib/contract-output-artifacts.js';
+import { runArtifactCheck, runScript } from './lib/generation-utils.js';
+
+runScript('stable schema check', () =>
+ runArtifactCheck('stable schema', buildStableSchemaArtifacts, [getStableSchemaRoot()]),
+);
diff --git a/packages/document-api/scripts/check-tool-manifests.ts b/packages/document-api/scripts/check-tool-manifests.ts
new file mode 100644
index 0000000000..3c348c31d2
--- /dev/null
+++ b/packages/document-api/scripts/check-tool-manifests.ts
@@ -0,0 +1,13 @@
+/**
+ * Purpose: Verify generated tool manifest artifacts match the current contract snapshot.
+ * Caller: Focused local/CI check; `check-contract-outputs.ts` is the broader superset.
+ * Reads: Contract snapshot + files under `packages/document-api/generated/manifests`.
+ * Writes: None (exit code + console output only).
+ * Fails when: Tool manifest files are missing/extra/stale.
+ */
+import { buildToolManifestArtifacts, getToolManifestRoot } from './lib/contract-output-artifacts.js';
+import { runArtifactCheck, runScript } from './lib/generation-utils.js';
+
+runScript('tool manifest check', () =>
+ runArtifactCheck('tool manifest', buildToolManifestArtifacts, [getToolManifestRoot()]),
+);
diff --git a/packages/document-api/scripts/generate-agent-artifacts.ts b/packages/document-api/scripts/generate-agent-artifacts.ts
new file mode 100644
index 0000000000..7a82ff0762
--- /dev/null
+++ b/packages/document-api/scripts/generate-agent-artifacts.ts
@@ -0,0 +1,11 @@
+/**
+ * Purpose: Generate agent-facing contract artifacts from the canonical contract snapshot.
+ * Caller: Focused local regeneration; `generate-contract-outputs.ts` is the broader superset.
+ * Reads: Contract snapshot.
+ * Writes: `packages/document-api/generated/agent/*`.
+ * Output: Deterministic JSON artifacts for agent remediation/workflow/compatibility guidance.
+ */
+import { buildAgentArtifacts } from './lib/contract-output-artifacts.js';
+import { runArtifactGenerate, runScript } from './lib/generation-utils.js';
+
+runScript('generate agent artifacts', () => runArtifactGenerate('agent artifacts', buildAgentArtifacts));
diff --git a/packages/document-api/scripts/generate-contract-outputs.ts b/packages/document-api/scripts/generate-contract-outputs.ts
new file mode 100644
index 0000000000..a92aa491df
--- /dev/null
+++ b/packages/document-api/scripts/generate-contract-outputs.ts
@@ -0,0 +1,28 @@
+/**
+ * Purpose: Generate all contract-derived outputs in one pass.
+ * Caller: Main local sync command before committing contract/docs changes.
+ * Reads: Contract snapshot + existing overview doc markers/content.
+ * Writes: Stable schemas, tool manifests, agent artifacts, reference docs, and overview generated block.
+ * Output: Deterministic generated files aligned to the current contract.
+ */
+import {
+ buildStableSchemaArtifacts,
+ buildToolManifestArtifacts,
+ buildAgentArtifacts,
+} from './lib/contract-output-artifacts.js';
+import { buildReferenceDocsArtifacts, buildOverviewArtifact } from './lib/reference-docs-artifacts.js';
+import { runScript, writeGeneratedFiles } from './lib/generation-utils.js';
+
+runScript('generate contract outputs', async () => {
+ const overview = await buildOverviewArtifact();
+ const files = [
+ ...buildStableSchemaArtifacts(),
+ ...buildToolManifestArtifacts(),
+ ...buildAgentArtifacts(),
+ ...buildReferenceDocsArtifacts(),
+ overview,
+ ];
+
+ await writeGeneratedFiles(files);
+ console.log(`generated contract outputs (${files.length} files, including overview block)`);
+});
diff --git a/packages/document-api/scripts/generate-internal-schemas.ts b/packages/document-api/scripts/generate-internal-schemas.ts
new file mode 100644
index 0000000000..aae6f447f4
--- /dev/null
+++ b/packages/document-api/scripts/generate-internal-schemas.ts
@@ -0,0 +1,34 @@
+/**
+ * Purpose: Generate an internal-only schema snapshot keyed by operation ID.
+ * Caller: Local tooling/debugging; not part of published/generated docs outputs.
+ * Reads: Contract snapshot + schema dialect from `../src/index.js`.
+ * Writes: `packages/document-api/.generated-internal/contract-schemas/index.json`.
+ * Output: Deterministic internal artifact for local inspection/tooling workflows.
+ */
+import { mkdir, writeFile } from 'node:fs/promises';
+import { dirname, resolve } from 'node:path';
+import { JSON_SCHEMA_DIALECT } from '../src/index.js';
+import { buildContractSnapshot } from './lib/contract-snapshot.js';
+import { runScript, stableStringify } from './lib/generation-utils.js';
+
+const DEFAULT_OUTPUT_PATH = resolve(
+ process.cwd(),
+ 'packages/document-api/.generated-internal/contract-schemas/index.json',
+);
+
+runScript('generate internal contract schemas', async () => {
+ const outputPath = DEFAULT_OUTPUT_PATH;
+ const snapshot = buildContractSnapshot();
+
+ const artifact = {
+ $schema: JSON_SCHEMA_DIALECT,
+ contractVersion: snapshot.contractVersion,
+ sourceHash: snapshot.sourceHash,
+ operations: Object.fromEntries(snapshot.operations.map((op) => [op.operationId, op.schemas])),
+ };
+
+ await mkdir(dirname(outputPath), { recursive: true });
+ await writeFile(outputPath, `${stableStringify(artifact)}\n`, 'utf8');
+
+ console.log(`generated internal contract schemas at ${outputPath}`);
+});
diff --git a/packages/document-api/scripts/generate-reference-docs.ts b/packages/document-api/scripts/generate-reference-docs.ts
new file mode 100644
index 0000000000..f93f29c718
--- /dev/null
+++ b/packages/document-api/scripts/generate-reference-docs.ts
@@ -0,0 +1,16 @@
+/**
+ * Purpose: Generate Document API reference docs and refresh the overview API-surface block.
+ * Caller: Focused local regeneration; `generate-contract-outputs.ts` is the broader superset.
+ * Reads: Contract snapshot + existing overview doc markers/content.
+ * Writes: `apps/docs/document-api/reference/*` + generated block in `apps/docs/document-api/overview.mdx`.
+ * Output: Deterministic MDX reference pages/index/manifest and synchronized overview section.
+ */
+import { buildReferenceDocsArtifacts, buildOverviewArtifact } from './lib/reference-docs-artifacts.js';
+import { runScript, writeGeneratedFiles } from './lib/generation-utils.js';
+
+runScript('generate reference docs', async () => {
+ const files = buildReferenceDocsArtifacts();
+ const overview = await buildOverviewArtifact();
+ await writeGeneratedFiles([...files, overview]);
+ console.log(`generated document-api reference docs and overview block (${files.length} reference files)`);
+});
diff --git a/packages/document-api/scripts/generate-stable-schemas.ts b/packages/document-api/scripts/generate-stable-schemas.ts
new file mode 100644
index 0000000000..8fde83bc2f
--- /dev/null
+++ b/packages/document-api/scripts/generate-stable-schemas.ts
@@ -0,0 +1,11 @@
+/**
+ * Purpose: Generate stable schema artifacts from the current contract snapshot.
+ * Caller: Focused local regeneration; `generate-contract-outputs.ts` is the broader superset.
+ * Reads: Contract snapshot.
+ * Writes: `packages/document-api/generated/schemas/*`.
+ * Output: Deterministic stable schema JSON/README artifacts.
+ */
+import { buildStableSchemaArtifacts } from './lib/contract-output-artifacts.js';
+import { runArtifactGenerate, runScript } from './lib/generation-utils.js';
+
+runScript('generate stable schemas', () => runArtifactGenerate('stable schemas', buildStableSchemaArtifacts));
diff --git a/packages/document-api/scripts/generate-tool-manifests.ts b/packages/document-api/scripts/generate-tool-manifests.ts
new file mode 100644
index 0000000000..53bca11064
--- /dev/null
+++ b/packages/document-api/scripts/generate-tool-manifests.ts
@@ -0,0 +1,11 @@
+/**
+ * Purpose: Generate tool manifest artifacts from the current contract snapshot.
+ * Caller: Focused local regeneration; `generate-contract-outputs.ts` is the broader superset.
+ * Reads: Contract snapshot.
+ * Writes: `packages/document-api/generated/manifests/*`.
+ * Output: Deterministic tool manifest JSON artifacts.
+ */
+import { buildToolManifestArtifacts } from './lib/contract-output-artifacts.js';
+import { runArtifactGenerate, runScript } from './lib/generation-utils.js';
+
+runScript('generate tool manifests', () => runArtifactGenerate('tool manifests', buildToolManifestArtifacts));
diff --git a/packages/document-api/scripts/lib/contract-output-artifacts.ts b/packages/document-api/scripts/lib/contract-output-artifacts.ts
new file mode 100644
index 0000000000..b939250573
--- /dev/null
+++ b/packages/document-api/scripts/lib/contract-output-artifacts.ts
@@ -0,0 +1,249 @@
+import { buildContractSnapshot } from './contract-snapshot.js';
+import { stableStringify, type GeneratedFile } from './generation-utils.js';
+
+const GENERATED_FILE_HEADER = 'GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`.\n';
+
+const STABLE_SCHEMA_ROOT = 'packages/document-api/generated/schemas';
+const TOOL_MANIFEST_ROOT = 'packages/document-api/generated/manifests';
+const AGENT_ARTIFACT_ROOT = 'packages/document-api/generated/agent';
+
+function buildOperationContractMap() {
+ const snapshot = buildContractSnapshot();
+
+ const operations = Object.fromEntries(
+ snapshot.operations.map((operation) => [
+ operation.operationId,
+ {
+ memberPath: operation.memberPath,
+ metadata: operation.metadata,
+ inputSchema: operation.schemas.input,
+ outputSchema: operation.schemas.output,
+ successSchema: operation.schemas.success,
+ failureSchema: operation.schemas.failure,
+ },
+ ]),
+ );
+
+ return {
+ contractVersion: snapshot.contractVersion,
+ schemaDialect: snapshot.schemaDialect,
+ sourceHash: snapshot.sourceHash,
+ operations,
+ };
+}
+
+export function buildStableSchemaArtifacts(): GeneratedFile[] {
+ const contractMap = buildOperationContractMap();
+
+ const artifact = {
+ $schema: contractMap.schemaDialect,
+ contractVersion: contractMap.contractVersion,
+ generatedAt: null,
+ sourceCommit: null,
+ sourceHash: contractMap.sourceHash,
+ operations: contractMap.operations,
+ };
+
+ return [
+ {
+ path: `${STABLE_SCHEMA_ROOT}/document-api-contract.json`,
+ content: stableStringify(artifact),
+ },
+ {
+ path: `${STABLE_SCHEMA_ROOT}/README.md`,
+ content: `# Generated Document API schemas\n\n${GENERATED_FILE_HEADER}This directory is generated from \`packages/document-api/src/contract/*\`.\n`,
+ },
+ ];
+}
+
+function toToolDescription(operationId: string, mutates: boolean): string {
+ if (mutates) {
+ return `Apply Document API mutation \`${operationId}\`.`;
+ }
+ return `Read Document API data via \`${operationId}\`.`;
+}
+
+export function buildToolManifestArtifacts(): GeneratedFile[] {
+ const contractMap = buildOperationContractMap();
+
+ const tools = Object.entries(contractMap.operations).map(([operationId, operation]) => ({
+ name: operationId,
+ memberPath: operation.memberPath,
+ description: toToolDescription(operationId, operation.metadata.mutates),
+ mutates: operation.metadata.mutates,
+ idempotency: operation.metadata.idempotency,
+ supportsTrackedMode: operation.metadata.supportsTrackedMode,
+ supportsDryRun: operation.metadata.supportsDryRun,
+ deterministicTargetResolution: operation.metadata.deterministicTargetResolution,
+ preApplyThrows: operation.metadata.throws.preApply,
+ possibleFailureCodes: operation.metadata.possibleFailureCodes,
+ remediationHints: operation.metadata.remediationHints ?? [],
+ inputSchema: operation.inputSchema,
+ outputSchema: operation.outputSchema,
+ successSchema: operation.successSchema,
+ failureSchema: operation.failureSchema,
+ }));
+
+ const manifest = {
+ contractVersion: contractMap.contractVersion,
+ sourceHash: contractMap.sourceHash,
+ generatedAt: null,
+ sourceCommit: null,
+ tools,
+ };
+
+ return [
+ {
+ path: `${TOOL_MANIFEST_ROOT}/document-api-tools.json`,
+ content: stableStringify(manifest),
+ },
+ ];
+}
+
+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.',
+};
+
+export function buildAgentArtifacts(): GeneratedFile[] {
+ const contractMap = buildOperationContractMap();
+
+ const remediationEntries = new Map<
+ string,
+ {
+ code: string;
+ message: string;
+ operations: string[];
+ preApplyOperations: string[];
+ nonAppliedOperations: string[];
+ }
+ >();
+
+ for (const [operationId, operation] of Object.entries(contractMap.operations)) {
+ for (const code of operation.metadata.throws.preApply) {
+ const entry = remediationEntries.get(code) ?? {
+ code,
+ message: DEFAULT_REMEDIATION_BY_CODE[code] ?? 'Inspect structured error details and operation capabilities.',
+ operations: [],
+ preApplyOperations: [],
+ nonAppliedOperations: [],
+ };
+ entry.operations.push(operationId);
+ entry.preApplyOperations.push(operationId);
+ remediationEntries.set(code, entry);
+ }
+
+ for (const code of operation.metadata.possibleFailureCodes) {
+ const entry = remediationEntries.get(code) ?? {
+ code,
+ message: DEFAULT_REMEDIATION_BY_CODE[code] ?? 'Inspect structured error details and operation capabilities.',
+ operations: [],
+ preApplyOperations: [],
+ nonAppliedOperations: [],
+ };
+ entry.operations.push(operationId);
+ entry.nonAppliedOperations.push(operationId);
+ remediationEntries.set(code, entry);
+ }
+ }
+
+ const remediationMap = {
+ contractVersion: contractMap.contractVersion,
+ sourceHash: contractMap.sourceHash,
+ entries: Array.from(remediationEntries.values())
+ .map((entry) => ({
+ ...entry,
+ operations: [...new Set(entry.operations)].sort(),
+ preApplyOperations: [...new Set(entry.preApplyOperations)].sort(),
+ nonAppliedOperations: [...new Set(entry.nonAppliedOperations)].sort(),
+ }))
+ .sort((left, right) => left.code.localeCompare(right.code)),
+ };
+
+ const workflowPlaybooks = {
+ contractVersion: contractMap.contractVersion,
+ sourceHash: contractMap.sourceHash,
+ workflows: [
+ {
+ id: 'find-mutate',
+ title: 'Find + mutate workflow',
+ operations: ['find', 'replace'],
+ },
+ {
+ id: 'tracked-insert',
+ title: 'Tracked insert workflow',
+ operations: ['capabilities.get', 'insert'],
+ },
+ {
+ id: 'comment-thread-lifecycle',
+ title: 'Comment lifecycle workflow',
+ operations: ['comments.add', 'comments.reply', 'comments.resolve'],
+ },
+ {
+ id: 'list-manipulation',
+ title: 'List manipulation workflow',
+ operations: ['lists.insert', 'lists.setType', 'lists.indent', 'lists.outdent', 'lists.exit'],
+ },
+ {
+ id: 'capabilities-aware-branching',
+ title: 'Capabilities-aware branching workflow',
+ operations: ['capabilities.get', 'replace', 'insert'],
+ },
+ {
+ id: 'track-change-review',
+ title: 'Track-change review workflow',
+ operations: ['trackChanges.list', 'trackChanges.accept', 'trackChanges.reject'],
+ },
+ ],
+ };
+
+ const compatibilityHints = {
+ contractVersion: contractMap.contractVersion,
+ sourceHash: contractMap.sourceHash,
+ operations: Object.fromEntries(
+ Object.entries(contractMap.operations).map(([operationId, operation]) => [
+ operationId,
+ {
+ memberPath: operation.memberPath,
+ mutates: operation.metadata.mutates,
+ supportsTrackedMode: operation.metadata.supportsTrackedMode,
+ supportsDryRun: operation.metadata.supportsDryRun,
+ requiresPreflightCapabilitiesCheck: operation.metadata.mutates,
+ postApplyThrowForbidden: operation.metadata.throws.postApplyForbidden,
+ deterministicTargetResolution: operation.metadata.deterministicTargetResolution,
+ },
+ ]),
+ ),
+ };
+
+ return [
+ {
+ path: `${AGENT_ARTIFACT_ROOT}/remediation-map.json`,
+ content: stableStringify(remediationMap),
+ },
+ {
+ path: `${AGENT_ARTIFACT_ROOT}/workflow-playbooks.json`,
+ content: stableStringify(workflowPlaybooks),
+ },
+ {
+ path: `${AGENT_ARTIFACT_ROOT}/compatibility-hints.json`,
+ content: stableStringify(compatibilityHints),
+ },
+ ];
+}
+
+export function getStableSchemaRoot(): string {
+ return STABLE_SCHEMA_ROOT;
+}
+
+export function getToolManifestRoot(): string {
+ return TOOL_MANIFEST_ROOT;
+}
+
+export function getAgentArtifactRoot(): string {
+ return AGENT_ARTIFACT_ROOT;
+}
diff --git a/packages/document-api/scripts/lib/contract-snapshot.ts b/packages/document-api/scripts/lib/contract-snapshot.ts
new file mode 100644
index 0000000000..af3f4f71c1
--- /dev/null
+++ b/packages/document-api/scripts/lib/contract-snapshot.ts
@@ -0,0 +1,57 @@
+import {
+ COMMAND_CATALOG,
+ CONTRACT_VERSION,
+ JSON_SCHEMA_DIALECT,
+ OPERATION_IDS,
+ OPERATION_MEMBER_PATH_MAP,
+ buildInternalContractSchemas,
+ type OperationId,
+} from '../../src/index.js';
+import { sha256 } from './generation-utils.js';
+
+export interface ContractOperationSnapshot {
+ operationId: OperationId;
+ memberPath: string;
+ metadata: (typeof COMMAND_CATALOG)[keyof typeof COMMAND_CATALOG];
+ schemas: ReturnType['operations'][keyof ReturnType<
+ typeof buildInternalContractSchemas
+ >['operations']];
+}
+
+export interface ContractSnapshot {
+ contractVersion: string;
+ schemaDialect: string;
+ sourceHash: string;
+ operations: ContractOperationSnapshot[];
+}
+
+let cached: ContractSnapshot | null = null;
+
+export function buildContractSnapshot(): ContractSnapshot {
+ if (cached) return cached;
+
+ const internalSchemas = buildInternalContractSchemas();
+ const operations = OPERATION_IDS.map((operationId) => ({
+ operationId,
+ memberPath: OPERATION_MEMBER_PATH_MAP[operationId],
+ metadata: COMMAND_CATALOG[operationId],
+ schemas: internalSchemas.operations[operationId],
+ }));
+
+ const sourcePayload = {
+ contractVersion: CONTRACT_VERSION,
+ schemaDialect: JSON_SCHEMA_DIALECT,
+ operationCatalog: COMMAND_CATALOG,
+ operationMap: OPERATION_MEMBER_PATH_MAP,
+ schemas: internalSchemas.operations,
+ };
+
+ cached = {
+ contractVersion: CONTRACT_VERSION,
+ schemaDialect: JSON_SCHEMA_DIALECT,
+ sourceHash: sha256(sourcePayload),
+ operations,
+ };
+
+ return cached;
+}
diff --git a/packages/document-api/scripts/lib/generation-utils.ts b/packages/document-api/scripts/lib/generation-utils.ts
new file mode 100644
index 0000000000..17bf22b47f
--- /dev/null
+++ b/packages/document-api/scripts/lib/generation-utils.ts
@@ -0,0 +1,183 @@
+import { createHash } from 'node:crypto';
+import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
+import { dirname, resolve } from 'node:path';
+import { format as prettierFormat, resolveConfig as prettierResolveConfig } from 'prettier';
+
+export interface GeneratedFile {
+ path: string;
+ content: string;
+}
+
+export interface GeneratedCheckIssue {
+ kind: 'missing' | 'extra' | 'content';
+ path: string;
+}
+
+export function stableSort(value: unknown): unknown {
+ if (Array.isArray(value)) {
+ return value.map((entry) => stableSort(entry));
+ }
+
+ if (value && typeof value === 'object') {
+ const sortedEntries = Object.entries(value as Record)
+ .sort(([left], [right]) => left.localeCompare(right))
+ .map(([key, nested]) => [key, stableSort(nested)]);
+ return Object.fromEntries(sortedEntries);
+ }
+
+ return value;
+}
+
+export function stableStringify(value: unknown): string {
+ return JSON.stringify(stableSort(value), null, 2);
+}
+
+export function sha256(value: unknown): string {
+ const payload = typeof value === 'string' ? value : stableStringify(value);
+ return createHash('sha256').update(payload, 'utf8').digest('hex');
+}
+
+export function normalizeFileContent(content: string): string {
+ return content.endsWith('\n') ? content : `${content}\n`;
+}
+
+async function formatGeneratedContent(file: GeneratedFile): Promise {
+ if (!file.path.endsWith('.json')) return file;
+ const config = await prettierResolveConfig(resolveWorkspacePath(file.path));
+ const formatted = await prettierFormat(file.content, { ...config, parser: 'json' });
+ return { ...file, content: formatted };
+}
+
+export function resolveWorkspacePath(path: string): string {
+ return resolve(process.cwd(), path);
+}
+
+export async function writeGeneratedFiles(files: GeneratedFile[]): Promise {
+ for (const file of files) {
+ const formatted = await formatGeneratedContent(file);
+ const absolutePath = resolveWorkspacePath(formatted.path);
+ await mkdir(dirname(absolutePath), { recursive: true });
+ await writeFile(absolutePath, normalizeFileContent(formatted.content), 'utf8');
+ }
+}
+
+async function listFilesRecursive(root: string): Promise {
+ const absoluteRoot = resolveWorkspacePath(root);
+ const entries = await readdir(absoluteRoot, { withFileTypes: true });
+ const files: string[] = [];
+
+ for (const entry of entries) {
+ const relativePath = `${root}/${entry.name}`;
+ if (entry.isDirectory()) {
+ files.push(...(await listFilesRecursive(relativePath)));
+ continue;
+ }
+ files.push(relativePath);
+ }
+
+ return files;
+}
+
+async function pathExists(path: string): Promise {
+ try {
+ await stat(resolveWorkspacePath(path));
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export async function checkGeneratedFiles(
+ expectedFiles: GeneratedFile[],
+ options: {
+ roots?: string[];
+ } = {},
+): Promise {
+ const issues: GeneratedCheckIssue[] = [];
+ const expected = new Map(expectedFiles.map((file) => [file.path, file]));
+
+ for (const [path, file] of expected.entries()) {
+ if (!(await pathExists(path))) {
+ issues.push({ kind: 'missing', path });
+ continue;
+ }
+
+ const formatted = await formatGeneratedContent(file);
+ const expectedContent = normalizeFileContent(formatted.content);
+ const actualContent = await readFile(resolveWorkspacePath(path), 'utf8');
+ if (actualContent !== expectedContent) {
+ issues.push({ kind: 'content', path });
+ }
+ }
+
+ const roots = options.roots ?? [];
+ const actualFiles = new Set();
+
+ for (const root of roots) {
+ if (!(await pathExists(root))) continue;
+ const rootFiles = await listFilesRecursive(root);
+ for (const path of rootFiles) {
+ actualFiles.add(path);
+ }
+ }
+
+ for (const path of actualFiles) {
+ if (!expected.has(path)) {
+ issues.push({ kind: 'extra', path });
+ }
+ }
+
+ return issues.sort((left, right) => {
+ if (left.kind !== right.kind) return left.kind.localeCompare(right.kind);
+ return left.path.localeCompare(right.path);
+ });
+}
+
+export function formatGeneratedCheckIssues(issues: GeneratedCheckIssue[]): string {
+ if (issues.length === 0) return '';
+
+ return issues
+ .map((issue) => {
+ if (issue.kind === 'missing') return `missing generated file: ${issue.path}`;
+ if (issue.kind === 'extra') return `unexpected generated file: ${issue.path}`;
+ return `stale generated file content: ${issue.path}`;
+ })
+ .join('\n');
+}
+
+export async function runArtifactCheck(
+ label: string,
+ buildFiles: () => GeneratedFile[],
+ roots: string[],
+ extraChecks?: (files: GeneratedFile[], issues: GeneratedCheckIssue[]) => Promise,
+): Promise {
+ const files = buildFiles();
+ const issues = await checkGeneratedFiles(files, { roots });
+
+ if (extraChecks) {
+ await extraChecks(files, issues);
+ }
+
+ if (issues.length > 0) {
+ console.error(`${label} check failed`);
+ console.error(formatGeneratedCheckIssues(issues));
+ process.exitCode = 1;
+ return;
+ }
+
+ console.log(`${label} check passed (${files.length} files)`);
+}
+
+export async function runArtifactGenerate(label: string, buildFiles: () => GeneratedFile[]): Promise {
+ const files = buildFiles();
+ await writeGeneratedFiles(files);
+ console.log(`generated ${label} (${files.length} files)`);
+}
+
+export function runScript(label: string, fn: () => Promise): void {
+ fn().catch((error) => {
+ console.error(`${label} failed with an unexpected error`);
+ console.error(error);
+ process.exitCode = 1;
+ });
+}
diff --git a/packages/document-api/scripts/lib/reference-docs-artifacts.ts b/packages/document-api/scripts/lib/reference-docs-artifacts.ts
new file mode 100644
index 0000000000..79151f311b
--- /dev/null
+++ b/packages/document-api/scripts/lib/reference-docs-artifacts.ts
@@ -0,0 +1,380 @@
+import { readFile } from 'node:fs/promises';
+import { posix as pathPosix } from 'node:path';
+import type { ContractOperationSnapshot } from './contract-snapshot.js';
+import { buildContractSnapshot } from './contract-snapshot.js';
+import {
+ resolveWorkspacePath,
+ stableStringify,
+ type GeneratedCheckIssue,
+ type GeneratedFile,
+} from './generation-utils.js';
+import {
+ OPERATION_REFERENCE_DOC_PATH_MAP,
+ REFERENCE_OPERATION_GROUPS,
+ type ReferenceOperationGroupDefinition,
+} from '../../src/index.js';
+
+const GENERATED_MARKER = '{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}';
+const OUTPUT_ROOT = 'apps/docs/document-api/reference';
+const REFERENCE_INDEX_PATH = `${OUTPUT_ROOT}/index.mdx`;
+const OVERVIEW_PATH = 'apps/docs/document-api/overview.mdx';
+const OVERVIEW_OPERATIONS_START = '{/* DOC_API_OPERATIONS_START */}';
+const OVERVIEW_OPERATIONS_END = '{/* DOC_API_OPERATIONS_END */}';
+
+interface OperationGroup {
+ definition: ReferenceOperationGroupDefinition;
+ pagePath: string;
+ operations: ContractOperationSnapshot[];
+}
+
+function formatMemberPath(memberPath: string): string {
+ return `editor.doc.${memberPath}${memberPath === 'capabilities' ? '()' : '(...)'}`;
+}
+
+function toOperationDocPath(operationId: ContractOperationSnapshot['operationId']): string {
+ return `${OUTPUT_ROOT}/${OPERATION_REFERENCE_DOC_PATH_MAP[operationId]}`;
+}
+
+function toGroupPath(group: ReferenceOperationGroupDefinition): string {
+ return `${OUTPUT_ROOT}/${group.pagePath}`;
+}
+
+function toRelativeDocHref(fromPath: string, toPath: string): string {
+ const fromDir = pathPosix.dirname(fromPath);
+ const relativePath = pathPosix.relative(fromDir, toPath).replace(/\.mdx$/u, '');
+ return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
+}
+
+function toPublicDocHref(path: string): string {
+ return `/${path.replace(/^apps\/docs\//u, '').replace(/\.mdx$/u, '')}`;
+}
+
+function renderList(values: readonly string[]): string {
+ if (values.length === 0) return '- None';
+ return values.map((value) => `- \`${value}\``).join('\n');
+}
+
+function buildOperationGroups(operations: ContractOperationSnapshot[]): OperationGroup[] {
+ const operationById = new Map(operations.map((operation) => [operation.operationId, operation] as const));
+
+ return REFERENCE_OPERATION_GROUPS.map((definition) => {
+ const groupedOperations = definition.operations.map((operationId) => {
+ const operation = operationById.get(operationId);
+ if (!operation) {
+ throw new Error(`Missing operation snapshot for "${operationId}" in reference docs generation.`);
+ }
+ return operation;
+ });
+
+ return {
+ definition,
+ pagePath: toGroupPath(definition),
+ operations: groupedOperations,
+ };
+ });
+}
+
+function renderOperationPage(operation: ContractOperationSnapshot): string {
+ const title = operation.operationId;
+ const metadata = operation.metadata;
+
+ return `---
+title: ${title}
+sidebarTitle: ${title}
+description: Generated reference for ${title}
+---
+
+${GENERATED_MARKER}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+## Summary
+
+- Operation ID: \`${operation.operationId}\`
+- API member path: \`${formatMemberPath(operation.memberPath)}\`
+- Mutates document: \`${metadata.mutates ? 'yes' : 'no'}\`
+- Idempotency: \`${metadata.idempotency}\`
+- Supports tracked mode: \`${metadata.supportsTrackedMode ? 'yes' : 'no'}\`
+- Supports dry run: \`${metadata.supportsDryRun ? 'yes' : 'no'}\`
+- Deterministic target resolution: \`${metadata.deterministicTargetResolution ? 'yes' : 'no'}\`
+
+## Pre-apply throws
+
+${renderList(metadata.throws.preApply)}
+
+## Non-applied failure codes
+
+${renderList(metadata.possibleFailureCodes)}
+
+## Input schema
+
+\`\`\`json
+${stableStringify(operation.schemas.input)}
+\`\`\`
+
+## Output schema
+
+\`\`\`json
+${stableStringify(operation.schemas.output)}
+\`\`\`
+${
+ operation.schemas.success
+ ? `
+## Success schema
+
+\`\`\`json
+${stableStringify(operation.schemas.success)}
+\`\`\`
+`
+ : ''
+}${
+ operation.schemas.failure
+ ? `
+## Failure schema
+
+\`\`\`json
+${stableStringify(operation.schemas.failure)}
+\`\`\`
+`
+ : ''
+ }${
+ metadata.remediationHints && metadata.remediationHints.length > 0
+ ? `
+## Remediation hints
+
+${renderList(metadata.remediationHints)}
+`
+ : ''
+ }`;
+}
+
+function renderGroupIndex(group: OperationGroup): string {
+ const rows = group.operations
+ .map((operation) => {
+ const metadata = operation.metadata;
+ return `| [\`${operation.operationId}\`](${toRelativeDocHref(group.pagePath, toOperationDocPath(operation.operationId))}) | \`${operation.memberPath}\` | ${metadata.mutates ? 'Yes' : 'No'} | \`${metadata.idempotency}\` | ${metadata.supportsTrackedMode ? 'Yes' : 'No'} | ${metadata.supportsDryRun ? 'Yes' : 'No'} |`;
+ })
+ .join('\n');
+
+ return `---
+title: ${group.definition.title} operations
+sidebarTitle: ${group.definition.title}
+description: Generated ${group.definition.title} operation reference from the canonical Document API contract.
+---
+
+${GENERATED_MARKER}
+
+> Alpha: Document API is currently alpha and subject to breaking changes.
+
+[Back to full reference](${toRelativeDocHref(group.pagePath, REFERENCE_INDEX_PATH)})
+
+${group.definition.description}
+
+| Operation | Member path | Mutates | Idempotency | Tracked | Dry run |
+| --- | --- | --- | --- | --- | --- |
+${rows}
+`;
+}
+
+function renderReferenceIndex(operations: ContractOperationSnapshot[], groups: OperationGroup[]): string {
+ const groupRows = groups
+ .map((group) => {
+ return `| ${group.definition.title} | ${group.operations.length} | [Open](${toRelativeDocHref(REFERENCE_INDEX_PATH, group.pagePath)}) |`;
+ })
+ .join('\n');
+
+ const operationGroupTitleById = new Map();
+ for (const group of groups) {
+ for (const operation of group.operations) {
+ operationGroupTitleById.set(operation.operationId, group.definition.title);
+ }
+ }
+
+ const operationRows = operations
+ .map((operation) => {
+ const metadata = operation.metadata;
+ const groupTitle = operationGroupTitleById.get(operation.operationId) ?? 'Unknown';
+ return `| [\`${operation.operationId}\`](${toRelativeDocHref(REFERENCE_INDEX_PATH, toOperationDocPath(operation.operationId))}) | ${groupTitle} | \`${operation.memberPath}\` | ${metadata.mutates ? 'Yes' : 'No'} | \`${metadata.idempotency}\` | ${metadata.supportsTrackedMode ? 'Yes' : 'No'} | ${metadata.supportsDryRun ? 'Yes' : 'No'} |`;
+ })
+ .join('\n');
+
+ return `---
+title: Document API reference
+sidebarTitle: Reference
+description: Generated operation reference from the canonical Document API contract.
+---
+
+${GENERATED_MARKER}
+
+This reference is generated from \`packages/document-api/src/contract/*\`.
+Document API is currently alpha and subject to breaking changes.
+
+## Browse by namespace
+
+| Namespace | Operations | Reference |
+| --- | --- | --- |
+${groupRows}
+
+## All operations
+
+| Operation | Namespace | Member path | Mutates | Idempotency | Tracked | Dry run |
+| --- | --- | --- | --- | --- | --- | --- |
+${operationRows}
+`;
+}
+
+function renderOverviewApiSurfaceSection(operations: ContractOperationSnapshot[], groups: OperationGroup[]): string {
+ const namespaceRows = groups
+ .map((group) => {
+ return `| ${group.definition.title} | ${group.operations.length} | [Reference](${toPublicDocHref(group.pagePath)}) |`;
+ })
+ .join('\n');
+
+ const operationRows = operations
+ .map((operation) => {
+ return `| \`${formatMemberPath(operation.memberPath)}\` | [\`${operation.operationId}\`](${toPublicDocHref(toOperationDocPath(operation.operationId))}) |`;
+ })
+ .join('\n');
+
+ return `${OVERVIEW_OPERATIONS_START}
+### Available operations
+
+Use the tables below to see what operations are available and where each one is documented.
+
+| Namespace | Operations | Reference |
+| --- | --- | --- |
+${namespaceRows}
+
+| Editor method | Operation ID |
+| --- | --- |
+${operationRows}
+${OVERVIEW_OPERATIONS_END}`;
+}
+
+function replaceOverviewSection(content: string, section: string): string {
+ const startIndex = content.indexOf(OVERVIEW_OPERATIONS_START);
+ const endIndex = content.indexOf(OVERVIEW_OPERATIONS_END);
+
+ if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
+ throw new Error(
+ `overview marker block not found in ${OVERVIEW_PATH}. Expected ${OVERVIEW_OPERATIONS_START} ... ${OVERVIEW_OPERATIONS_END}.`,
+ );
+ }
+
+ const endMarkerEndIndex = endIndex + OVERVIEW_OPERATIONS_END.length;
+ return `${content.slice(0, startIndex)}${section}${content.slice(endMarkerEndIndex)}`;
+}
+
+export function applyGeneratedOverviewApiSurface(overviewContent: string): string {
+ const snapshot = buildContractSnapshot();
+ const groups = buildOperationGroups(snapshot.operations);
+ const section = renderOverviewApiSurfaceSection(snapshot.operations, groups);
+ return replaceOverviewSection(overviewContent, section);
+}
+
+export async function buildOverviewArtifact(): Promise {
+ const overviewPath = OVERVIEW_PATH;
+ const currentOverview = await readFile(resolveWorkspacePath(overviewPath), 'utf8');
+ const nextOverview = applyGeneratedOverviewApiSurface(currentOverview);
+ return { path: overviewPath, content: nextOverview };
+}
+
+export function buildReferenceDocsArtifacts(): GeneratedFile[] {
+ const snapshot = buildContractSnapshot();
+ const groups = buildOperationGroups(snapshot.operations);
+
+ const operationFiles = snapshot.operations.map((operation) => ({
+ path: toOperationDocPath(operation.operationId),
+ content: renderOperationPage(operation),
+ }));
+
+ const groupFiles = groups.map((group) => ({
+ path: group.pagePath,
+ content: renderGroupIndex(group),
+ }));
+
+ const allFiles = [
+ {
+ path: REFERENCE_INDEX_PATH,
+ content: renderReferenceIndex(snapshot.operations, groups),
+ },
+ ...groupFiles,
+ ...operationFiles,
+ ];
+
+ const manifest = {
+ generatedBy: 'packages/document-api/scripts/generate-reference-docs.ts',
+ marker: GENERATED_MARKER,
+ contractVersion: snapshot.contractVersion,
+ sourceHash: snapshot.sourceHash,
+ groups: groups.map((group) => ({
+ key: group.definition.key,
+ title: group.definition.title,
+ pagePath: group.pagePath,
+ operationIds: group.operations.map((operation) => operation.operationId),
+ })),
+ files: allFiles.map((file) => file.path).sort(),
+ };
+
+ return [
+ ...allFiles,
+ {
+ path: `${OUTPUT_ROOT}/_generated-manifest.json`,
+ content: stableStringify(manifest),
+ },
+ ];
+}
+
+/**
+ * Checks that generated `.mdx` files contain the generated marker and that
+ * the overview doc's API-surface block is up to date. Skips files already
+ * present in {@link existingIssuePaths} to avoid duplicate reports.
+ */
+export async function checkReferenceDocsExtras(files: GeneratedFile[], issues: GeneratedCheckIssue[]): Promise {
+ const existingIssuePaths = new Set(issues.map((issue) => issue.path));
+
+ for (const file of files) {
+ if (!file.path.endsWith('.mdx') || existingIssuePaths.has(file.path)) continue;
+ const content = await readFile(resolveWorkspacePath(file.path), 'utf8').catch(() => null);
+ if (content == null || !content.includes(GENERATED_MARKER)) {
+ issues.push({ kind: 'content', path: file.path });
+ }
+ }
+
+ const overviewPath = OVERVIEW_PATH;
+ if (existingIssuePaths.has(overviewPath)) return;
+
+ const overviewContent = await readFile(resolveWorkspacePath(overviewPath), 'utf8').catch(() => null);
+ if (overviewContent == null) {
+ issues.push({ kind: 'missing', path: overviewPath });
+ } else {
+ try {
+ const expectedOverview = applyGeneratedOverviewApiSurface(overviewContent);
+ if (expectedOverview !== overviewContent) {
+ issues.push({ kind: 'content', path: overviewPath });
+ }
+ } catch {
+ issues.push({ kind: 'content', path: overviewPath });
+ }
+ }
+}
+
+export function getReferenceDocsOutputRoot(): string {
+ return OUTPUT_ROOT;
+}
+
+export function getReferenceDocsGeneratedMarker(): string {
+ return GENERATED_MARKER;
+}
+
+export function getOverviewDocsPath(): string {
+ return OVERVIEW_PATH;
+}
+
+export function getOverviewApiSurfaceStartMarker(): string {
+ return OVERVIEW_OPERATIONS_START;
+}
+
+export function getOverviewApiSurfaceEndMarker(): string {
+ return OVERVIEW_OPERATIONS_END;
+}
diff --git a/packages/document-api/src/README.md b/packages/document-api/src/README.md
new file mode 100644
index 0000000000..3679fb7379
--- /dev/null
+++ b/packages/document-api/src/README.md
@@ -0,0 +1,79 @@
+# Document API
+
+## Ownership boundary (manual vs generated)
+
+- Manual source of truth:
+ - `packages/document-api/src/**` (this folder)
+ - `packages/document-api/scripts/**`
+- Generated and committed:
+ - `packages/document-api/generated/**`
+ - `apps/docs/document-api/reference/**`
+ - marker block in `apps/docs/document-api/overview.mdx`
+
+Do not hand-edit generated files; regenerate via script.
+
+## Non-Negotiables
+
+- The Document API modules are engine-agnostic and must never parse or depend on ProseMirror directly.
+- The Document API must not implement new engine-specific domain logic. It defines types/contracts and delegates to adapters.
+- Adapters are engine-specific implementations (for `super-editor`, ProseMirror adapters) and may use engine internals and bridging logic to satisfy the API contract.
+- The Document API must receive adapters via dependency injection.
+- If a capability is missing, prefer adding an editor command. If a gap remains, put bridge logic in adapters, not in `document-api/*`.
+
+## Packaging Assumptions (Internal Only)
+
+- `@superdoc/document-api` is an internal workspace package (`"private": true`) with no external consumers.
+- Package exports intentionally point to source files (no `dist` build output) to match the monorepo's source-resolution setup.
+- This is valid only while all consumers resolve workspace source with the same conditions/tooling.
+- If this package is ever published or consumed outside this monorepo resolution model, add a build step and export compiled JS + `.d.ts` from `dist`.
+
+## 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.
+
+## Selector Semantics
+
+- For dual-context types (`sdt`, `image`), selectors without an explicit `kind` may return both block and inline matches.
+- Set `kind: 'block'` or `kind: 'inline'` on `{ type: 'node' }` selectors when you need only one context.
+
+## 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` also supports omitting `target`; adapters resolve a deterministic default insertion point (first paragraph start when available).
+- Structural creation is exposed under `create.*` (for example `create.paragraph`), separate from text mutations.
+
+## Adapter Error Convention
+
+- Return diagnostics for query/content issues (invalid regex input, unknown selector types, unresolved `within` targets).
+- Throw errors for engine capability/configuration failures (for example, required editor commands not being available).
+- For mutating operations, failure outcomes must be non-applied outcomes.
+ - `success: false` means the operation did not apply a durable document mutation.
+ - If a mutation is applied, adapters must return success (or a typed partial/warning outcome when explicitly modeled) and must not throw a post-apply not-found error.
+
+## Tracked-Change Semantics
+
+- Tracking is operation-scoped (`changeMode: 'direct' | 'tracked'`), not global editor-mode state.
+- `insert`, `replace`, `delete`, `format.bold`, and `create.paragraph` 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.
+
+## List Namespace Semantics
+
+- `lists.*` projects paragraph-based numbering into first-class `listItem` addresses.
+- `ListItemAddress.nodeId` reuses the underlying paragraph node id directly.
+- `lists.list({ within })` is inclusive when `within` itself is a list item.
+- `lists.setType` normalizes deterministically to canonical defaults (`ordered` decimal / `bullet` default bullet).
+- `lists.insert` returns `insertionPoint` at the inserted item start (`offset: 0`) even when text is provided.
+- `lists.restart` returns `NO_OP` only when target is already the first item of its contiguous run and effectively starts at `1`.
+
+Deterministic outcomes:
+- Unknown tracked-change ids must fail with `TARGET_NOT_FOUND` at adapter level.
+- `acceptAll`/`rejectAll` with no applicable changes must return `Receipt.failure.code = 'NO_OP'`.
+- Missing tracked-change capabilities must fail with `CAPABILITY_UNAVAILABLE`.
+- Text/format targets that cannot be resolved after remote edits must fail deterministically (`TARGET_NOT_FOUND` / `NO_OP`), never silently mutate the wrong range.
+- Tracked entity IDs returned by mutation receipts (`insert` / `replace` / `delete`) and `create.paragraph.trackedChangeRefs` must match canonical IDs from `trackChanges.list`.
+- `trackChanges.get` / `accept` / `reject` accept canonical IDs only.
diff --git a/packages/document-api/src/capabilities/capabilities.ts b/packages/document-api/src/capabilities/capabilities.ts
new file mode 100644
index 0000000000..2a446311f9
--- /dev/null
+++ b/packages/document-api/src/capabilities/capabilities.ts
@@ -0,0 +1,61 @@
+import type { OperationId } from '../contract/types.js';
+
+export const CAPABILITY_REASON_CODES = [
+ 'COMMAND_UNAVAILABLE',
+ 'OPERATION_UNAVAILABLE',
+ 'TRACKED_MODE_UNAVAILABLE',
+ 'DRY_RUN_UNAVAILABLE',
+ 'NAMESPACE_UNAVAILABLE',
+] as const;
+
+export type CapabilityReasonCode = (typeof CAPABILITY_REASON_CODES)[number];
+
+/**
+ * A boolean flag indicating whether a capability is active, with optional
+ * machine-readable reason codes explaining why it is disabled.
+ */
+export type CapabilityFlag = {
+ enabled: boolean;
+ reasons?: CapabilityReasonCode[];
+};
+
+/** Per-operation runtime capability describing availability, tracked-mode, and dry-run support. */
+export interface OperationRuntimeCapability {
+ available: boolean;
+ tracked: boolean;
+ dryRun: boolean;
+ reasons?: CapabilityReasonCode[];
+}
+
+export type OperationCapabilities = Record;
+
+/**
+ * Complete runtime capability snapshot for a Document API editor instance.
+ *
+ * `global` contains namespace-level flags (track changes, comments, lists, dry-run).
+ * `operations` contains per-operation availability details keyed by {@link OperationId}.
+ */
+export interface DocumentApiCapabilities {
+ global: {
+ trackChanges: CapabilityFlag;
+ comments: CapabilityFlag;
+ lists: CapabilityFlag;
+ dryRun: CapabilityFlag;
+ };
+ operations: OperationCapabilities;
+}
+
+/** Engine-specific adapter that resolves runtime capabilities for the current editor instance. */
+export interface CapabilitiesAdapter {
+ get(): DocumentApiCapabilities;
+}
+
+/**
+ * Delegates to the capabilities adapter to retrieve the current capability snapshot.
+ *
+ * @param adapter - The engine-specific capabilities adapter.
+ * @returns The resolved capabilities for this editor instance.
+ */
+export function executeCapabilities(adapter: CapabilitiesAdapter): DocumentApiCapabilities {
+ return adapter.get();
+}
diff --git a/packages/document-api/src/comments/comments.ts b/packages/document-api/src/comments/comments.ts
new file mode 100644
index 0000000000..719465a343
--- /dev/null
+++ b/packages/document-api/src/comments/comments.ts
@@ -0,0 +1,141 @@
+import type { Receipt, TextAddress } from '../types/index.js';
+import type { CommentInfo, CommentsListQuery, CommentsListResult } from './comments.types.js';
+
+/**
+ * Input for adding a comment to a text range.
+ */
+export interface AddCommentInput {
+ /**
+ * The text range to attach the comment to.
+ *
+ * Note: text matches can span multiple blocks; callers should pick a single
+ * block range (e.g., the first `textRanges` entry from `find`) until
+ * multi-block comment targets are supported.
+ */
+ target: TextAddress;
+ /** The comment body text. */
+ text: string;
+}
+
+export interface EditCommentInput {
+ commentId: string;
+ text: string;
+}
+
+export interface ReplyToCommentInput {
+ parentCommentId: string;
+ text: string;
+}
+
+export interface MoveCommentInput {
+ commentId: string;
+ target: TextAddress;
+}
+
+export interface ResolveCommentInput {
+ commentId: string;
+}
+
+export interface RemoveCommentInput {
+ commentId: string;
+}
+
+export interface SetCommentInternalInput {
+ commentId: string;
+ isInternal: boolean;
+}
+
+export interface SetCommentActiveInput {
+ commentId: string | null;
+}
+
+export interface GoToCommentInput {
+ commentId: string;
+}
+
+export interface GetCommentInput {
+ commentId: string;
+}
+
+/**
+ * Engine-specific adapter that the comments API delegates to.
+ */
+export interface CommentsAdapter {
+ /** Add a comment at the specified text range. */
+ add(input: AddCommentInput): Receipt;
+ /** Edit the body text of an existing comment. */
+ edit(input: EditCommentInput): Receipt;
+ /** Reply to an existing comment thread. */
+ reply(input: ReplyToCommentInput): Receipt;
+ /** Move a comment to a different text range. */
+ move(input: MoveCommentInput): Receipt;
+ /** Resolve an open comment. */
+ resolve(input: ResolveCommentInput): Receipt;
+ /** Remove a comment from the document. */
+ remove(input: RemoveCommentInput): Receipt;
+ /** Set the internal/private flag on a comment. */
+ setInternal(input: SetCommentInternalInput): Receipt;
+ /** Set which comment is currently active/focused. Pass `null` to clear. */
+ setActive(input: SetCommentActiveInput): Receipt;
+ /** Scroll to and focus a comment in the document. */
+ goTo(input: GoToCommentInput): Receipt;
+ /** Retrieve full information for a single comment. */
+ get(input: GetCommentInput): CommentInfo;
+ /** List comments matching the given query. */
+ list(query?: CommentsListQuery): CommentsListResult;
+}
+
+/**
+ * Public comments API surface exposed on `editor.doc.comments`.
+ */
+export type CommentsApi = CommentsAdapter;
+
+/**
+ * Execute wrappers below are the canonical interception point for input
+ * normalization and validation. Query-only operations currently pass through
+ * directly. Mutation operations will gain validation as the API matures.
+ * Keep the wrappers to preserve this extension surface.
+ */
+export function executeAddComment(adapter: CommentsAdapter, input: AddCommentInput): Receipt {
+ return adapter.add(input);
+}
+
+export function executeEditComment(adapter: CommentsAdapter, input: EditCommentInput): Receipt {
+ return adapter.edit(input);
+}
+
+export function executeReplyToComment(adapter: CommentsAdapter, input: ReplyToCommentInput): Receipt {
+ return adapter.reply(input);
+}
+
+export function executeMoveComment(adapter: CommentsAdapter, input: MoveCommentInput): Receipt {
+ return adapter.move(input);
+}
+
+export function executeResolveComment(adapter: CommentsAdapter, input: ResolveCommentInput): Receipt {
+ return adapter.resolve(input);
+}
+
+export function executeRemoveComment(adapter: CommentsAdapter, input: RemoveCommentInput): Receipt {
+ return adapter.remove(input);
+}
+
+export function executeSetCommentInternal(adapter: CommentsAdapter, input: SetCommentInternalInput): Receipt {
+ return adapter.setInternal(input);
+}
+
+export function executeSetCommentActive(adapter: CommentsAdapter, input: SetCommentActiveInput): Receipt {
+ return adapter.setActive(input);
+}
+
+export function executeGoToComment(adapter: CommentsAdapter, input: GoToCommentInput): Receipt {
+ return adapter.goTo(input);
+}
+
+export function executeGetComment(adapter: CommentsAdapter, input: GetCommentInput): CommentInfo {
+ return adapter.get(input);
+}
+
+export function executeListComments(adapter: CommentsAdapter, query?: CommentsListQuery): CommentsListResult {
+ return adapter.list(query);
+}
diff --git a/packages/document-api/src/comments/comments.types.ts b/packages/document-api/src/comments/comments.types.ts
new file mode 100644
index 0000000000..6ba083e209
--- /dev/null
+++ b/packages/document-api/src/comments/comments.types.ts
@@ -0,0 +1,26 @@
+import type { CommentAddress, CommentStatus, TextAddress } from '../types/index.js';
+
+export type { CommentStatus } from '../types/index.js';
+
+export interface CommentInfo {
+ address: CommentAddress;
+ commentId: string;
+ importedId?: string;
+ parentCommentId?: string;
+ text?: string;
+ isInternal?: boolean;
+ status: CommentStatus;
+ target?: TextAddress;
+ createdTime?: number;
+ creatorName?: string;
+ creatorEmail?: string;
+}
+
+export interface CommentsListQuery {
+ includeResolved?: boolean;
+}
+
+export interface CommentsListResult {
+ matches: CommentInfo[];
+ total: number;
+}
diff --git a/packages/document-api/src/contract/command-catalog.ts b/packages/document-api/src/contract/command-catalog.ts
new file mode 100644
index 0000000000..86adc33d2a
--- /dev/null
+++ b/packages/document-api/src/contract/command-catalog.ts
@@ -0,0 +1,17 @@
+import type { CommandCatalog, CommandStaticMetadata } from './types.js';
+import { OPERATION_IDS, projectFromDefinitions } from './operation-definitions.js';
+
+export const COMMAND_CATALOG: CommandCatalog = projectFromDefinitions((_id, entry) => entry.metadata);
+
+/** Operation IDs whose catalog entry has `mutates: true`. */
+export const MUTATING_OPERATION_IDS = OPERATION_IDS.filter((operationId) => COMMAND_CATALOG[operationId].mutates);
+
+/**
+ * Returns the static metadata for a given operation.
+ *
+ * @param operationId - A known operation identifier from the command catalog.
+ * @returns The compile-time metadata describing idempotency, failure codes, throw policy, etc.
+ */
+export function getCommandMetadata(operationId: keyof typeof COMMAND_CATALOG): CommandStaticMetadata {
+ return COMMAND_CATALOG[operationId];
+}
diff --git a/packages/document-api/src/contract/contract.test.ts b/packages/document-api/src/contract/contract.test.ts
new file mode 100644
index 0000000000..14ee4a4738
--- /dev/null
+++ b/packages/document-api/src/contract/contract.test.ts
@@ -0,0 +1,114 @@
+import { describe, expect, it } from 'vitest';
+import { COMMAND_CATALOG } from './command-catalog.js';
+import { OPERATION_DEFINITIONS, type ReferenceGroupKey } from './operation-definitions.js';
+import { DOCUMENT_API_MEMBER_PATHS, OPERATION_MEMBER_PATH_MAP, memberPathForOperation } from './operation-map.js';
+import { OPERATION_REFERENCE_DOC_PATH_MAP, REFERENCE_OPERATION_GROUPS } from './reference-doc-map.js';
+import { buildInternalContractSchemas } from './schemas.js';
+import { OPERATION_IDS, PRE_APPLY_THROW_CODES, isValidOperationIdFormat } from './types.js';
+
+describe('document-api contract catalog', () => {
+ it('keeps operation ids explicit and format-valid', () => {
+ expect([...new Set(OPERATION_IDS)]).toHaveLength(OPERATION_IDS.length);
+ for (const operationId of OPERATION_IDS) {
+ expect(isValidOperationIdFormat(operationId)).toBe(true);
+ }
+ });
+
+ it('keeps catalog key coverage in lockstep with operation ids', () => {
+ const catalogKeys = Object.keys(COMMAND_CATALOG).sort();
+ const operationIds = [...OPERATION_IDS].sort();
+ expect(catalogKeys).toEqual(operationIds);
+ });
+
+ it('derives member paths from operation ids with no duplicates', () => {
+ expect(new Set(DOCUMENT_API_MEMBER_PATHS).size).toBe(DOCUMENT_API_MEMBER_PATHS.length);
+ for (const operationId of OPERATION_IDS) {
+ expect(typeof memberPathForOperation(operationId)).toBe('string');
+ }
+ });
+
+ it('keeps reference-doc mappings explicit and coverage-complete', () => {
+ const operationIds = [...OPERATION_IDS].sort();
+ const docPathKeys = Object.keys(OPERATION_REFERENCE_DOC_PATH_MAP).sort();
+ expect(docPathKeys).toEqual(operationIds);
+
+ const grouped = REFERENCE_OPERATION_GROUPS.flatMap((group) => group.operations);
+ expect(grouped).toHaveLength(operationIds.length);
+ expect(new Set(grouped).size).toBe(grouped.length);
+ expect([...grouped].sort()).toEqual(operationIds);
+ });
+
+ it('enforces typed throw and post-apply policy metadata for mutation operations', () => {
+ const validPreApplyThrowCodes = new Set(PRE_APPLY_THROW_CODES);
+
+ for (const operationId of OPERATION_IDS) {
+ const metadata = COMMAND_CATALOG[operationId];
+ for (const throwCode of metadata.throws.preApply) {
+ expect(validPreApplyThrowCodes.has(throwCode)).toBe(true);
+ }
+
+ if (!metadata.mutates) continue;
+ expect(metadata.throws.postApplyForbidden).toBe(true);
+ }
+ });
+
+ it('includes CAPABILITY_UNAVAILABLE in throws.preApply for all mutation operations', () => {
+ for (const operationId of OPERATION_IDS) {
+ const metadata = COMMAND_CATALOG[operationId];
+ if (!metadata.mutates) continue;
+ expect(
+ metadata.throws.preApply,
+ `${operationId} should include CAPABILITY_UNAVAILABLE in throws.preApply`,
+ ).toContain('CAPABILITY_UNAVAILABLE');
+ }
+ });
+
+ it('keeps input schemas closed for object-shaped payloads', () => {
+ const schemas = buildInternalContractSchemas();
+
+ for (const operationId of OPERATION_IDS) {
+ const inputSchema = schemas.operations[operationId].input as { type?: string; additionalProperties?: unknown };
+ if (inputSchema.type !== 'object') continue;
+ expect(inputSchema.additionalProperties).toBe(false);
+ }
+ });
+
+ it('derives OPERATION_IDS from OPERATION_DEFINITIONS keys', () => {
+ const definitionKeys = Object.keys(OPERATION_DEFINITIONS).sort();
+ const operationIds = [...OPERATION_IDS].sort();
+ expect(definitionKeys).toEqual(operationIds);
+ });
+
+ it('ensures every definition entry has a valid referenceGroup', () => {
+ const validGroups: readonly ReferenceGroupKey[] = [
+ 'core',
+ 'capabilities',
+ 'create',
+ 'format',
+ 'lists',
+ 'comments',
+ 'trackChanges',
+ ];
+ for (const id of OPERATION_IDS) {
+ expect(validGroups, `${id} has invalid referenceGroup`).toContain(OPERATION_DEFINITIONS[id].referenceGroup);
+ }
+ });
+
+ it('projects COMMAND_CATALOG metadata from the same objects in OPERATION_DEFINITIONS', () => {
+ for (const id of OPERATION_IDS) {
+ expect(COMMAND_CATALOG[id]).toBe(OPERATION_DEFINITIONS[id].metadata);
+ }
+ });
+
+ it('projects member paths that match OPERATION_DEFINITIONS', () => {
+ for (const id of OPERATION_IDS) {
+ expect(OPERATION_MEMBER_PATH_MAP[id]).toBe(OPERATION_DEFINITIONS[id].memberPath);
+ }
+ });
+
+ it('projects reference doc paths that match OPERATION_DEFINITIONS', () => {
+ for (const id of OPERATION_IDS) {
+ expect(OPERATION_REFERENCE_DOC_PATH_MAP[id]).toBe(OPERATION_DEFINITIONS[id].referenceDocPath);
+ }
+ });
+});
diff --git a/packages/document-api/src/contract/index.ts b/packages/document-api/src/contract/index.ts
new file mode 100644
index 0000000000..36556488ae
--- /dev/null
+++ b/packages/document-api/src/contract/index.ts
@@ -0,0 +1,6 @@
+export * from './types.js';
+export * from './command-catalog.js';
+export * from './schemas.js';
+export * from './operation-map.js';
+export * from './reference-doc-map.js';
+export * from './operation-registry.js';
diff --git a/packages/document-api/src/contract/metadata-types.ts b/packages/document-api/src/contract/metadata-types.ts
new file mode 100644
index 0000000000..d6e6abab8c
--- /dev/null
+++ b/packages/document-api/src/contract/metadata-types.ts
@@ -0,0 +1,37 @@
+/**
+ * Shared leaf types for operation metadata.
+ *
+ * This file is the bottom of the contract import DAG — it imports only
+ * from `../types/receipt.js` and has no contract-internal dependencies.
+ */
+
+import type { ReceiptFailureCode } from '../types/receipt.js';
+
+export const OPERATION_IDEMPOTENCY_VALUES = ['idempotent', 'conditional', 'non-idempotent'] as const;
+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',
+] as const;
+
+export type PreApplyThrowCode = (typeof PRE_APPLY_THROW_CODES)[number];
+
+export interface CommandThrowPolicy {
+ preApply: readonly PreApplyThrowCode[];
+ postApplyForbidden: true;
+}
+
+export interface CommandStaticMetadata {
+ mutates: boolean;
+ idempotency: OperationIdempotency;
+ supportsDryRun: boolean;
+ supportsTrackedMode: boolean;
+ possibleFailureCodes: readonly ReceiptFailureCode[];
+ throws: CommandThrowPolicy;
+ deterministicTargetResolution: boolean;
+ remediationHints?: readonly string[];
+}
diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts
new file mode 100644
index 0000000000..cc1d0cb4f3
--- /dev/null
+++ b/packages/document-api/src/contract/operation-definitions.ts
@@ -0,0 +1,545 @@
+/**
+ * Canonical operation definitions — single source of truth for keys, metadata, and paths.
+ *
+ * Every operation in the Document API is defined exactly once here.
+ * All downstream artifacts (COMMAND_CATALOG, OPERATION_MEMBER_PATH_MAP,
+ * OPERATION_REFERENCE_DOC_PATH_MAP, REFERENCE_OPERATION_GROUPS) are
+ * projected from this object.
+ *
+ * ## Adding a new operation
+ *
+ * 1. **Here** (`operation-definitions.ts`) — add an entry to `OPERATION_DEFINITIONS`
+ * with `memberPath`, `metadata`, `referenceDocPath`, and `referenceGroup`.
+ * 2. **`operation-registry.ts`** — add a type entry (`input`, `options`, `output`).
+ * The bidirectional `Assert` checks will error until this is done.
+ * 3. **`invoke.ts`** (`buildDispatchTable`) — add a one-line dispatch entry calling
+ * the API method. `TypedDispatchTable` will error until this is done.
+ * 4. **Implement** — the API method on `DocumentApi` + its adapter.
+ *
+ * That's 4 touch points. The catalog, maps, and reference docs are derived
+ * automatically. If you forget step 1 or 2, compile-time assertions fail.
+ * If you forget step 3, the `TypedDispatchTable` mapped type errors.
+ *
+ * Import DAG: this file imports only from `metadata-types.ts` and
+ * `../types/receipt.js` — no contract-internal circular deps.
+ */
+
+import type { ReceiptFailureCode } from '../types/receipt.js';
+import type { CommandStaticMetadata, OperationIdempotency, PreApplyThrowCode } from './metadata-types.js';
+
+// ---------------------------------------------------------------------------
+// Reference group key
+// ---------------------------------------------------------------------------
+
+export type ReferenceGroupKey = 'core' | 'capabilities' | 'create' | 'format' | 'lists' | 'comments' | 'trackChanges';
+
+// ---------------------------------------------------------------------------
+// Entry shape
+// ---------------------------------------------------------------------------
+
+export interface OperationDefinitionEntry {
+ memberPath: string;
+ metadata: CommandStaticMetadata;
+ referenceDocPath: string;
+ referenceGroup: ReferenceGroupKey;
+}
+
+// ---------------------------------------------------------------------------
+// Metadata helpers (moved from command-catalog.ts)
+// ---------------------------------------------------------------------------
+
+const NONE_FAILURES: readonly ReceiptFailureCode[] = [];
+const NONE_THROWS: readonly PreApplyThrowCode[] = [];
+
+function readOperation(
+ options: {
+ idempotency?: OperationIdempotency;
+ throws?: readonly PreApplyThrowCode[];
+ deterministicTargetResolution?: boolean;
+ remediationHints?: readonly string[];
+ } = {},
+): CommandStaticMetadata {
+ return {
+ mutates: false,
+ idempotency: options.idempotency ?? 'idempotent',
+ supportsDryRun: false,
+ supportsTrackedMode: false,
+ possibleFailureCodes: NONE_FAILURES,
+ throws: {
+ preApply: options.throws ?? NONE_THROWS,
+ postApplyForbidden: true,
+ },
+ deterministicTargetResolution: options.deterministicTargetResolution ?? true,
+ remediationHints: options.remediationHints,
+ };
+}
+
+function mutationOperation(options: {
+ idempotency: OperationIdempotency;
+ supportsDryRun: boolean;
+ supportsTrackedMode: boolean;
+ possibleFailureCodes: readonly ReceiptFailureCode[];
+ throws: readonly PreApplyThrowCode[];
+ deterministicTargetResolution?: boolean;
+ remediationHints?: readonly string[];
+}): CommandStaticMetadata {
+ return {
+ mutates: true,
+ idempotency: options.idempotency,
+ supportsDryRun: options.supportsDryRun,
+ supportsTrackedMode: options.supportsTrackedMode,
+ possibleFailureCodes: options.possibleFailureCodes,
+ throws: {
+ preApply: options.throws,
+ postApplyForbidden: true,
+ },
+ deterministicTargetResolution: options.deterministicTargetResolution ?? true,
+ remediationHints: options.remediationHints,
+ };
+}
+
+// Throw-code shorthand arrays
+const T_NOT_FOUND = ['TARGET_NOT_FOUND'] as const;
+const T_COMMAND = ['COMMAND_UNAVAILABLE', 'CAPABILITY_UNAVAILABLE'] 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;
+
+// ---------------------------------------------------------------------------
+// Canonical definitions
+// ---------------------------------------------------------------------------
+
+export const OPERATION_DEFINITIONS = {
+ find: {
+ memberPath: 'find',
+ metadata: readOperation({
+ idempotency: 'idempotent',
+ deterministicTargetResolution: false,
+ }),
+ referenceDocPath: 'find.mdx',
+ referenceGroup: 'core',
+ },
+ getNode: {
+ memberPath: 'getNode',
+ metadata: readOperation({
+ idempotency: 'idempotent',
+ throws: T_NOT_FOUND,
+ }),
+ referenceDocPath: 'get-node.mdx',
+ referenceGroup: 'core',
+ },
+ getNodeById: {
+ memberPath: 'getNodeById',
+ metadata: readOperation({
+ idempotency: 'idempotent',
+ throws: T_NOT_FOUND,
+ }),
+ referenceDocPath: 'get-node-by-id.mdx',
+ referenceGroup: 'core',
+ },
+ getText: {
+ memberPath: 'getText',
+ metadata: readOperation(),
+ referenceDocPath: 'get-text.mdx',
+ referenceGroup: 'core',
+ },
+ info: {
+ memberPath: 'info',
+ metadata: readOperation(),
+ referenceDocPath: 'info.mdx',
+ referenceGroup: 'core',
+ },
+
+ insert: {
+ memberPath: 'insert',
+ metadata: mutationOperation({
+ idempotency: 'non-idempotent',
+ supportsDryRun: true,
+ supportsTrackedMode: true,
+ possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'],
+ throws: T_NOT_FOUND_TRACKED,
+ }),
+ referenceDocPath: 'insert.mdx',
+ referenceGroup: 'core',
+ },
+ replace: {
+ memberPath: 'replace',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: true,
+ supportsTrackedMode: true,
+ possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'],
+ throws: T_NOT_FOUND_TRACKED,
+ }),
+ referenceDocPath: 'replace.mdx',
+ referenceGroup: 'core',
+ },
+ delete: {
+ memberPath: 'delete',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: true,
+ supportsTrackedMode: true,
+ possibleFailureCodes: ['NO_OP'],
+ throws: T_NOT_FOUND_TRACKED,
+ }),
+ referenceDocPath: 'delete.mdx',
+ referenceGroup: 'core',
+ },
+
+ 'format.bold': {
+ memberPath: 'format.bold',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: true,
+ supportsTrackedMode: true,
+ possibleFailureCodes: ['INVALID_TARGET'],
+ throws: T_NOT_FOUND_COMMAND_TRACKED,
+ }),
+ referenceDocPath: 'format/bold.mdx',
+ referenceGroup: 'format',
+ },
+
+ 'create.paragraph': {
+ memberPath: 'create.paragraph',
+ metadata: mutationOperation({
+ idempotency: 'non-idempotent',
+ supportsDryRun: true,
+ supportsTrackedMode: true,
+ possibleFailureCodes: ['INVALID_TARGET'],
+ throws: T_NOT_FOUND_COMMAND_TRACKED,
+ }),
+ referenceDocPath: 'create/paragraph.mdx',
+ referenceGroup: 'create',
+ },
+
+ 'lists.list': {
+ memberPath: 'lists.list',
+ metadata: readOperation({
+ idempotency: 'idempotent',
+ throws: T_NOT_FOUND,
+ }),
+ referenceDocPath: 'lists/list.mdx',
+ referenceGroup: 'lists',
+ },
+ 'lists.get': {
+ memberPath: 'lists.get',
+ metadata: readOperation({
+ idempotency: 'idempotent',
+ throws: T_NOT_FOUND,
+ }),
+ referenceDocPath: 'lists/get.mdx',
+ referenceGroup: 'lists',
+ },
+ 'lists.insert': {
+ memberPath: 'lists.insert',
+ metadata: mutationOperation({
+ idempotency: 'non-idempotent',
+ supportsDryRun: true,
+ supportsTrackedMode: true,
+ possibleFailureCodes: ['INVALID_TARGET'],
+ throws: T_NOT_FOUND_COMMAND_TRACKED,
+ }),
+ referenceDocPath: 'lists/insert.mdx',
+ referenceGroup: 'lists',
+ },
+ 'lists.setType': {
+ memberPath: 'lists.setType',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: true,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP', 'INVALID_TARGET'],
+ throws: T_NOT_FOUND_COMMAND_TRACKED,
+ }),
+ referenceDocPath: 'lists/set-type.mdx',
+ referenceGroup: 'lists',
+ },
+ 'lists.indent': {
+ memberPath: 'lists.indent',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: true,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP', 'INVALID_TARGET'],
+ throws: T_NOT_FOUND_COMMAND_TRACKED,
+ }),
+ referenceDocPath: 'lists/indent.mdx',
+ referenceGroup: 'lists',
+ },
+ 'lists.outdent': {
+ memberPath: 'lists.outdent',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: true,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP', 'INVALID_TARGET'],
+ throws: T_NOT_FOUND_COMMAND_TRACKED,
+ }),
+ referenceDocPath: 'lists/outdent.mdx',
+ referenceGroup: 'lists',
+ },
+ 'lists.restart': {
+ memberPath: 'lists.restart',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: true,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP', 'INVALID_TARGET'],
+ throws: T_NOT_FOUND_COMMAND_TRACKED,
+ }),
+ referenceDocPath: 'lists/restart.mdx',
+ referenceGroup: 'lists',
+ },
+ 'lists.exit': {
+ memberPath: 'lists.exit',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: true,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['INVALID_TARGET'],
+ throws: T_NOT_FOUND_COMMAND_TRACKED,
+ }),
+ referenceDocPath: 'lists/exit.mdx',
+ referenceGroup: 'lists',
+ },
+
+ 'comments.add': {
+ memberPath: 'comments.add',
+ metadata: mutationOperation({
+ idempotency: 'non-idempotent',
+ supportsDryRun: false,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'],
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'comments/add.mdx',
+ referenceGroup: 'comments',
+ },
+ 'comments.edit': {
+ memberPath: 'comments.edit',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: false,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP'],
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'comments/edit.mdx',
+ referenceGroup: 'comments',
+ },
+ 'comments.reply': {
+ memberPath: 'comments.reply',
+ metadata: mutationOperation({
+ idempotency: 'non-idempotent',
+ supportsDryRun: false,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['INVALID_TARGET'],
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'comments/reply.mdx',
+ referenceGroup: 'comments',
+ },
+ 'comments.move': {
+ memberPath: 'comments.move',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: false,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['INVALID_TARGET', 'NO_OP'],
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'comments/move.mdx',
+ referenceGroup: 'comments',
+ },
+ 'comments.resolve': {
+ memberPath: 'comments.resolve',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: false,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP'],
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'comments/resolve.mdx',
+ referenceGroup: 'comments',
+ },
+ 'comments.remove': {
+ memberPath: 'comments.remove',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: false,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP'],
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'comments/remove.mdx',
+ referenceGroup: 'comments',
+ },
+ 'comments.setInternal': {
+ memberPath: 'comments.setInternal',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: false,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP', 'INVALID_TARGET'],
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'comments/set-internal.mdx',
+ referenceGroup: 'comments',
+ },
+ 'comments.setActive': {
+ memberPath: 'comments.setActive',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: false,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['INVALID_TARGET'],
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'comments/set-active.mdx',
+ referenceGroup: 'comments',
+ },
+ 'comments.goTo': {
+ memberPath: 'comments.goTo',
+ metadata: readOperation({
+ idempotency: 'conditional',
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'comments/go-to.mdx',
+ referenceGroup: 'comments',
+ },
+ 'comments.get': {
+ memberPath: 'comments.get',
+ metadata: readOperation({
+ idempotency: 'idempotent',
+ throws: T_NOT_FOUND,
+ }),
+ referenceDocPath: 'comments/get.mdx',
+ referenceGroup: 'comments',
+ },
+ 'comments.list': {
+ memberPath: 'comments.list',
+ metadata: readOperation({
+ idempotency: 'idempotent',
+ }),
+ referenceDocPath: 'comments/list.mdx',
+ referenceGroup: 'comments',
+ },
+
+ 'trackChanges.list': {
+ memberPath: 'trackChanges.list',
+ metadata: readOperation({
+ idempotency: 'idempotent',
+ }),
+ referenceDocPath: 'track-changes/list.mdx',
+ referenceGroup: 'trackChanges',
+ },
+ 'trackChanges.get': {
+ memberPath: 'trackChanges.get',
+ metadata: readOperation({
+ idempotency: 'idempotent',
+ throws: T_NOT_FOUND,
+ }),
+ referenceDocPath: 'track-changes/get.mdx',
+ referenceGroup: 'trackChanges',
+ },
+ 'trackChanges.accept': {
+ memberPath: 'trackChanges.accept',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: false,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP'],
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'track-changes/accept.mdx',
+ referenceGroup: 'trackChanges',
+ },
+ 'trackChanges.reject': {
+ memberPath: 'trackChanges.reject',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: false,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP'],
+ throws: T_NOT_FOUND_COMMAND,
+ }),
+ referenceDocPath: 'track-changes/reject.mdx',
+ referenceGroup: 'trackChanges',
+ },
+ 'trackChanges.acceptAll': {
+ memberPath: 'trackChanges.acceptAll',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: false,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP'],
+ throws: T_COMMAND,
+ }),
+ referenceDocPath: 'track-changes/accept-all.mdx',
+ referenceGroup: 'trackChanges',
+ },
+ 'trackChanges.rejectAll': {
+ memberPath: 'trackChanges.rejectAll',
+ metadata: mutationOperation({
+ idempotency: 'conditional',
+ supportsDryRun: false,
+ supportsTrackedMode: false,
+ possibleFailureCodes: ['NO_OP'],
+ throws: T_COMMAND,
+ }),
+ referenceDocPath: 'track-changes/reject-all.mdx',
+ referenceGroup: 'trackChanges',
+ },
+
+ 'capabilities.get': {
+ memberPath: 'capabilities',
+ metadata: readOperation({
+ idempotency: 'idempotent',
+ throws: NONE_THROWS,
+ }),
+ referenceDocPath: 'capabilities/get.mdx',
+ referenceGroup: 'capabilities',
+ },
+} as const satisfies Record;
+
+// ---------------------------------------------------------------------------
+// Derived identities (immutable)
+// ---------------------------------------------------------------------------
+
+export type OperationId = keyof typeof OPERATION_DEFINITIONS;
+
+export const OPERATION_IDS: readonly OperationId[] = Object.freeze(Object.keys(OPERATION_DEFINITIONS) as OperationId[]);
+
+export const SINGLETON_OPERATION_IDS: readonly OperationId[] = Object.freeze(
+ OPERATION_IDS.filter((id) => !id.includes('.')),
+);
+
+export const NAMESPACED_OPERATION_IDS: readonly OperationId[] = Object.freeze(
+ OPERATION_IDS.filter((id) => id.includes('.')),
+);
+
+// ---------------------------------------------------------------------------
+// Typed projection helper (single contained cast)
+// ---------------------------------------------------------------------------
+
+/**
+ * Projects a value from each operation definition entry into a keyed record.
+ *
+ * The cast is needed because `Object.fromEntries` returns `Record`;
+ * all callers validate the result via explicit type annotations.
+ */
+export function projectFromDefinitions(
+ fn: (id: OperationId, entry: OperationDefinitionEntry) => V,
+): Record {
+ return Object.fromEntries(OPERATION_IDS.map((id) => [id, fn(id, OPERATION_DEFINITIONS[id])])) as Record<
+ OperationId,
+ V
+ >;
+}
diff --git a/packages/document-api/src/contract/operation-map.ts b/packages/document-api/src/contract/operation-map.ts
new file mode 100644
index 0000000000..ff2c9f3bdb
--- /dev/null
+++ b/packages/document-api/src/contract/operation-map.ts
@@ -0,0 +1,20 @@
+import {
+ OPERATION_DEFINITIONS,
+ OPERATION_IDS,
+ projectFromDefinitions,
+ type OperationId,
+} from './operation-definitions.js';
+
+export type DocumentApiMemberPath = (typeof OPERATION_DEFINITIONS)[OperationId]['memberPath'];
+
+export function memberPathForOperation(operationId: OperationId): DocumentApiMemberPath {
+ return OPERATION_DEFINITIONS[operationId].memberPath;
+}
+
+export const OPERATION_MEMBER_PATH_MAP: Record = projectFromDefinitions(
+ (_id, entry) => entry.memberPath as DocumentApiMemberPath,
+);
+
+export const DOCUMENT_API_MEMBER_PATHS: readonly DocumentApiMemberPath[] = [
+ ...new Set(OPERATION_IDS.map((id) => OPERATION_DEFINITIONS[id].memberPath)),
+];
diff --git a/packages/document-api/src/contract/operation-registry.ts b/packages/document-api/src/contract/operation-registry.ts
new file mode 100644
index 0000000000..c86834fea4
--- /dev/null
+++ b/packages/document-api/src/contract/operation-registry.ts
@@ -0,0 +1,151 @@
+/**
+ * Canonical type-level mapping from OperationId to input, options, and output types.
+ *
+ * This interface is the single source of truth for the invoke dispatch layer.
+ * The bidirectional completeness checks at the bottom of this file guarantee
+ * that every OperationId has a registry entry and vice versa.
+ */
+
+import type { OperationId } from './types.js';
+
+import type { NodeAddress, NodeInfo, QueryResult, Selector, Query } from '../types/index.js';
+import type { TextMutationReceipt, Receipt } from '../types/receipt.js';
+import type { DocumentInfo } from '../types/info.types.js';
+import type { CreateParagraphInput, CreateParagraphResult } from '../types/create.types.js';
+
+import type { FindOptions } from '../find/find.js';
+import type { GetNodeByIdInput } from '../get-node/get-node.js';
+import type { GetTextInput } from '../get-text/get-text.js';
+import type { InfoInput } from '../info/info.js';
+import type { InsertInput } from '../insert/insert.js';
+import type { ReplaceInput } from '../replace/replace.js';
+import type { DeleteInput } from '../delete/delete.js';
+import type { MutationOptions } from '../write/write.js';
+import type { FormatBoldInput } from '../format/format.js';
+import type {
+ AddCommentInput,
+ EditCommentInput,
+ ReplyToCommentInput,
+ MoveCommentInput,
+ ResolveCommentInput,
+ RemoveCommentInput,
+ SetCommentInternalInput,
+ SetCommentActiveInput,
+ GoToCommentInput,
+ GetCommentInput,
+} from '../comments/comments.js';
+import type { CommentInfo, CommentsListQuery, CommentsListResult } from '../comments/comments.types.js';
+import type {
+ TrackChangesListInput,
+ TrackChangesGetInput,
+ TrackChangesAcceptInput,
+ TrackChangesRejectInput,
+ TrackChangesAcceptAllInput,
+ TrackChangesRejectAllInput,
+} from '../track-changes/track-changes.js';
+import type { TrackChangeInfo, TrackChangesListResult } from '../types/track-changes.types.js';
+import type { DocumentApiCapabilities } from '../capabilities/capabilities.js';
+import type {
+ ListsListQuery,
+ ListsListResult,
+ ListsGetInput,
+ ListItemInfo,
+ ListInsertInput,
+ ListsInsertResult,
+ ListSetTypeInput,
+ ListsMutateItemResult,
+ ListTargetInput,
+ ListsExitResult,
+} from '../lists/lists.types.js';
+
+export interface OperationRegistry {
+ // --- Singleton reads ---
+ find: { input: Selector | Query; options: FindOptions; output: QueryResult };
+ getNode: { input: NodeAddress; options: never; output: NodeInfo };
+ getNodeById: { input: GetNodeByIdInput; options: never; output: NodeInfo };
+ getText: { input: GetTextInput; options: never; output: string };
+ info: { input: InfoInput; options: never; output: DocumentInfo };
+
+ // --- Singleton mutations ---
+ insert: { input: InsertInput; options: MutationOptions; output: TextMutationReceipt };
+ replace: { input: ReplaceInput; options: MutationOptions; output: TextMutationReceipt };
+ delete: { input: DeleteInput; options: MutationOptions; output: TextMutationReceipt };
+
+ // --- format.* ---
+ 'format.bold': { input: FormatBoldInput; options: MutationOptions; output: TextMutationReceipt };
+
+ // --- create.* ---
+ 'create.paragraph': { input: CreateParagraphInput; options: MutationOptions; output: CreateParagraphResult };
+
+ // --- lists.* ---
+ 'lists.list': { input: ListsListQuery | undefined; options: never; output: ListsListResult };
+ 'lists.get': { input: ListsGetInput; options: never; output: ListItemInfo };
+ 'lists.insert': { input: ListInsertInput; options: MutationOptions; output: ListsInsertResult };
+ 'lists.setType': { input: ListSetTypeInput; options: MutationOptions; output: ListsMutateItemResult };
+ 'lists.indent': { input: ListTargetInput; options: MutationOptions; output: ListsMutateItemResult };
+ 'lists.outdent': { input: ListTargetInput; options: MutationOptions; output: ListsMutateItemResult };
+ 'lists.restart': { input: ListTargetInput; options: MutationOptions; output: ListsMutateItemResult };
+ 'lists.exit': { input: ListTargetInput; options: MutationOptions; output: ListsExitResult };
+
+ // --- comments.* ---
+ 'comments.add': { input: AddCommentInput; options: never; output: Receipt };
+ 'comments.edit': { input: EditCommentInput; options: never; output: Receipt };
+ 'comments.reply': { input: ReplyToCommentInput; options: never; output: Receipt };
+ 'comments.move': { input: MoveCommentInput; options: never; output: Receipt };
+ 'comments.resolve': { input: ResolveCommentInput; options: never; output: Receipt };
+ 'comments.remove': { input: RemoveCommentInput; options: never; output: Receipt };
+ 'comments.setInternal': { input: SetCommentInternalInput; options: never; output: Receipt };
+ 'comments.setActive': { input: SetCommentActiveInput; options: never; output: Receipt };
+ 'comments.goTo': { input: GoToCommentInput; options: never; output: Receipt };
+ 'comments.get': { input: GetCommentInput; options: never; output: CommentInfo };
+ 'comments.list': { input: CommentsListQuery | undefined; options: never; output: CommentsListResult };
+
+ // --- trackChanges.* ---
+ 'trackChanges.list': { input: TrackChangesListInput | undefined; options: never; output: TrackChangesListResult };
+ 'trackChanges.get': { input: TrackChangesGetInput; options: never; output: TrackChangeInfo };
+ 'trackChanges.accept': { input: TrackChangesAcceptInput; options: never; output: Receipt };
+ 'trackChanges.reject': { input: TrackChangesRejectInput; options: never; output: Receipt };
+ 'trackChanges.acceptAll': { input: TrackChangesAcceptAllInput; options: never; output: Receipt };
+ 'trackChanges.rejectAll': { input: TrackChangesRejectAllInput; options: never; output: Receipt };
+
+ // --- capabilities ---
+ 'capabilities.get': { input: undefined; options: never; output: DocumentApiCapabilities };
+}
+
+// --- Bidirectional completeness checks ---
+// If either assertion fails, the `false extends true` branch produces a compile error.
+
+type Assert<_T extends true> = void;
+
+/** Fails to compile if OperationRegistry is missing any OperationId key. */
+type _AllOpsHaveRegistryEntry = Assert;
+
+/** Fails to compile if OperationRegistry has extra keys not in OperationId. */
+type _NoExtraRegistryKeys = Assert;
+
+// --- Invoke request/result types ---
+
+/**
+ * Typed invoke request. TypeScript narrows input and options based on operationId.
+ */
+export type InvokeRequest = {
+ operationId: T;
+ input: OperationRegistry[T]['input'];
+} & (OperationRegistry[T]['options'] extends never
+ ? Record
+ : { options?: OperationRegistry[T]['options'] });
+
+/**
+ * Typed invoke result, narrowed by operationId.
+ */
+export type InvokeResult = OperationRegistry[T]['output'];
+
+/**
+ * Loose invoke request for dynamic callers who don't know the operation at compile time.
+ * Invalid inputs will produce adapter-level errors, not input-validation errors.
+ */
+export type DynamicInvokeRequest = {
+ operationId: OperationId;
+ input: unknown;
+ options?: unknown;
+};
diff --git a/packages/document-api/src/contract/reference-doc-map.ts b/packages/document-api/src/contract/reference-doc-map.ts
new file mode 100644
index 0000000000..4f29d4123e
--- /dev/null
+++ b/packages/document-api/src/contract/reference-doc-map.ts
@@ -0,0 +1,67 @@
+import {
+ OPERATION_DEFINITIONS,
+ OPERATION_IDS,
+ projectFromDefinitions,
+ type ReferenceGroupKey,
+} from './operation-definitions.js';
+import type { OperationId } from './types.js';
+
+export type { ReferenceGroupKey } from './operation-definitions.js';
+
+export interface ReferenceOperationGroupDefinition {
+ key: ReferenceGroupKey;
+ title: string;
+ description: string;
+ pagePath: string;
+ operations: readonly OperationId[];
+}
+
+export const OPERATION_REFERENCE_DOC_PATH_MAP: Record = projectFromDefinitions(
+ (_id, entry) => entry.referenceDocPath,
+);
+
+const GROUP_METADATA: Record = {
+ core: {
+ title: 'Core',
+ description: 'Primary read and write operations.',
+ pagePath: 'core/index.mdx',
+ },
+ capabilities: {
+ title: 'Capabilities',
+ description: 'Runtime support discovery for capability-aware branching.',
+ pagePath: 'capabilities/index.mdx',
+ },
+ create: {
+ title: 'Create',
+ description: 'Structured creation helpers.',
+ pagePath: 'create/index.mdx',
+ },
+ format: {
+ title: 'Format',
+ description: 'Formatting mutations.',
+ pagePath: 'format/index.mdx',
+ },
+ lists: {
+ title: 'Lists',
+ description: 'List inspection and list mutations.',
+ pagePath: 'lists/index.mdx',
+ },
+ comments: {
+ title: 'Comments',
+ description: 'Comment authoring and thread lifecycle operations.',
+ pagePath: 'comments/index.mdx',
+ },
+ trackChanges: {
+ title: 'Track Changes',
+ description: 'Tracked-change inspection and review operations.',
+ pagePath: 'track-changes/index.mdx',
+ },
+};
+
+export const REFERENCE_OPERATION_GROUPS: readonly ReferenceOperationGroupDefinition[] = (
+ Object.keys(GROUP_METADATA) as ReferenceGroupKey[]
+).map((key) => ({
+ key,
+ ...GROUP_METADATA[key],
+ operations: OPERATION_IDS.filter((id) => OPERATION_DEFINITIONS[id].referenceGroup === key),
+}));
diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts
new file mode 100644
index 0000000000..9a3defcf8a
--- /dev/null
+++ b/packages/document-api/src/contract/schemas.ts
@@ -0,0 +1,911 @@
+import { COMMAND_CATALOG } from './command-catalog.js';
+import { CONTRACT_VERSION, JSON_SCHEMA_DIALECT, OPERATION_IDS, type OperationId } from './types.js';
+import { NODE_TYPES, BLOCK_NODE_TYPES, INLINE_NODE_TYPES } from '../types/base.js';
+
+type JsonSchema = Record;
+
+/** JSON Schema descriptors for a single operation's input, output, and result variants. */
+export interface OperationSchemaSet {
+ /** Schema describing the operation's accepted input payload. */
+ input: JsonSchema;
+ /** Schema describing the full output (success | failure union for mutations). */
+ output: JsonSchema;
+ /** Schema describing only the success branch of a mutation result. */
+ success?: JsonSchema;
+ /** Schema describing only the failure branch of a mutation result. */
+ failure?: JsonSchema;
+}
+
+/** Top-level contract envelope containing versioned operation schemas. */
+export interface InternalContractSchemas {
+ /** JSON Schema dialect URI (e.g. `https://json-schema.org/draft/2020-12/schema`). */
+ $schema: string;
+ /** Semantic version of the document-api contract these schemas describe. */
+ contractVersion: string;
+ /** Per-operation schema sets keyed by {@link OperationId}. */
+ operations: Record;
+}
+
+function objectSchema(properties: Record, required: readonly string[] = []): JsonSchema {
+ const schema: JsonSchema = {
+ type: 'object',
+ properties,
+ additionalProperties: false,
+ };
+ if (required.length > 0) {
+ schema.required = [...required];
+ }
+ return schema;
+}
+
+function arraySchema(items: JsonSchema): JsonSchema {
+ return {
+ type: 'array',
+ items,
+ };
+}
+
+const nodeTypeValues = NODE_TYPES;
+const blockNodeTypeValues = BLOCK_NODE_TYPES;
+const inlineNodeTypeValues = INLINE_NODE_TYPES;
+
+const rangeSchema = objectSchema(
+ {
+ start: { type: 'integer' },
+ end: { type: 'integer' },
+ },
+ ['start', 'end'],
+);
+
+const positionSchema = objectSchema(
+ {
+ blockId: { type: 'string' },
+ offset: { type: 'integer' },
+ },
+ ['blockId', 'offset'],
+);
+
+const inlineAnchorSchema = objectSchema(
+ {
+ start: positionSchema,
+ end: positionSchema,
+ },
+ ['start', 'end'],
+);
+
+const textAddressSchema = objectSchema(
+ {
+ kind: { const: 'text' },
+ blockId: { type: 'string' },
+ range: rangeSchema,
+ },
+ ['kind', 'blockId', 'range'],
+);
+
+const blockNodeAddressSchema = objectSchema(
+ {
+ kind: { const: 'block' },
+ nodeType: { enum: [...blockNodeTypeValues] },
+ nodeId: { type: 'string' },
+ },
+ ['kind', 'nodeType', 'nodeId'],
+);
+
+const paragraphAddressSchema = objectSchema(
+ {
+ kind: { const: 'block' },
+ nodeType: { const: 'paragraph' },
+ nodeId: { type: 'string' },
+ },
+ ['kind', 'nodeType', 'nodeId'],
+);
+
+const listItemAddressSchema = objectSchema(
+ {
+ kind: { const: 'block' },
+ nodeType: { const: 'listItem' },
+ nodeId: { type: 'string' },
+ },
+ ['kind', 'nodeType', 'nodeId'],
+);
+
+const inlineNodeAddressSchema = objectSchema(
+ {
+ kind: { const: 'inline' },
+ nodeType: { enum: [...inlineNodeTypeValues] },
+ anchor: inlineAnchorSchema,
+ },
+ ['kind', 'nodeType', 'anchor'],
+);
+
+const nodeAddressSchema: JsonSchema = {
+ oneOf: [blockNodeAddressSchema, inlineNodeAddressSchema],
+};
+
+const commentAddressSchema = objectSchema(
+ {
+ kind: { const: 'entity' },
+ entityType: { const: 'comment' },
+ entityId: { type: 'string' },
+ },
+ ['kind', 'entityType', 'entityId'],
+);
+
+const trackedChangeAddressSchema = objectSchema(
+ {
+ kind: { const: 'entity' },
+ entityType: { const: 'trackedChange' },
+ entityId: { type: 'string' },
+ },
+ ['kind', 'entityType', 'entityId'],
+);
+
+const entityAddressSchema: JsonSchema = {
+ oneOf: [commentAddressSchema, trackedChangeAddressSchema],
+};
+
+function possibleFailureCodes(operationId: OperationId): string[] {
+ return [...COMMAND_CATALOG[operationId].possibleFailureCodes];
+}
+
+function receiptFailureSchemaFor(operationId: OperationId): JsonSchema {
+ const codes = possibleFailureCodes(operationId);
+ if (codes.length === 0) {
+ throw new Error(`Operation "${operationId}" does not declare non-applied failure codes.`);
+ }
+
+ return objectSchema(
+ {
+ code: {
+ enum: codes,
+ },
+ message: { type: 'string' },
+ details: {},
+ },
+ ['code', 'message'],
+ );
+}
+
+const receiptSuccessSchema = objectSchema(
+ {
+ success: { const: true },
+ inserted: arraySchema(entityAddressSchema),
+ updated: arraySchema(entityAddressSchema),
+ removed: arraySchema(entityAddressSchema),
+ },
+ ['success'],
+);
+
+function receiptFailureResultSchemaFor(operationId: OperationId): JsonSchema {
+ return objectSchema(
+ {
+ success: { const: false },
+ failure: receiptFailureSchemaFor(operationId),
+ },
+ ['success', 'failure'],
+ );
+}
+
+function receiptResultSchemaFor(operationId: OperationId): JsonSchema {
+ return {
+ oneOf: [receiptSuccessSchema, receiptFailureResultSchemaFor(operationId)],
+ };
+}
+
+const textMutationRangeSchema = objectSchema(
+ {
+ from: { type: 'integer' },
+ to: { type: 'integer' },
+ },
+ ['from', 'to'],
+);
+
+const textMutationResolutionSchema = objectSchema(
+ {
+ requestedTarget: textAddressSchema,
+ target: textAddressSchema,
+ range: textMutationRangeSchema,
+ text: { type: 'string' },
+ },
+ ['target', 'range', 'text'],
+);
+
+const textMutationSuccessSchema = objectSchema(
+ {
+ success: { const: true },
+ resolution: textMutationResolutionSchema,
+ inserted: arraySchema(entityAddressSchema),
+ updated: arraySchema(entityAddressSchema),
+ removed: arraySchema(entityAddressSchema),
+ },
+ ['success', 'resolution'],
+);
+
+function textMutationFailureSchemaFor(operationId: OperationId): JsonSchema {
+ return objectSchema(
+ {
+ success: { const: false },
+ failure: receiptFailureSchemaFor(operationId),
+ resolution: textMutationResolutionSchema,
+ },
+ ['success', 'failure', 'resolution'],
+ );
+}
+
+function textMutationResultSchemaFor(operationId: OperationId): JsonSchema {
+ return {
+ oneOf: [textMutationSuccessSchema, textMutationFailureSchemaFor(operationId)],
+ };
+}
+
+const trackChangeRefSchema = trackedChangeAddressSchema;
+
+const createParagraphSuccessSchema = objectSchema(
+ {
+ success: { const: true },
+ paragraph: paragraphAddressSchema,
+ insertionPoint: textAddressSchema,
+ trackedChangeRefs: arraySchema(trackChangeRefSchema),
+ },
+ ['success', 'paragraph', 'insertionPoint'],
+);
+
+function createParagraphFailureSchemaFor(operationId: OperationId): JsonSchema {
+ return objectSchema(
+ {
+ success: { const: false },
+ failure: receiptFailureSchemaFor(operationId),
+ },
+ ['success', 'failure'],
+ );
+}
+
+function createParagraphResultSchemaFor(operationId: OperationId): JsonSchema {
+ return {
+ oneOf: [createParagraphSuccessSchema, createParagraphFailureSchemaFor(operationId)],
+ };
+}
+
+const listsInsertSuccessSchema = objectSchema(
+ {
+ success: { const: true },
+ item: listItemAddressSchema,
+ insertionPoint: textAddressSchema,
+ trackedChangeRefs: arraySchema(trackChangeRefSchema),
+ },
+ ['success', 'item', 'insertionPoint'],
+);
+
+const listsMutateItemSuccessSchema = objectSchema(
+ {
+ success: { const: true },
+ item: listItemAddressSchema,
+ },
+ ['success', 'item'],
+);
+
+const listsExitSuccessSchema = objectSchema(
+ {
+ success: { const: true },
+ paragraph: paragraphAddressSchema,
+ },
+ ['success', 'paragraph'],
+);
+
+function listsFailureSchemaFor(operationId: OperationId): JsonSchema {
+ return objectSchema(
+ {
+ success: { const: false },
+ failure: receiptFailureSchemaFor(operationId),
+ },
+ ['success', 'failure'],
+ );
+}
+
+function listsInsertResultSchemaFor(operationId: OperationId): JsonSchema {
+ return {
+ oneOf: [listsInsertSuccessSchema, listsFailureSchemaFor(operationId)],
+ };
+}
+
+function listsMutateItemResultSchemaFor(operationId: OperationId): JsonSchema {
+ return {
+ oneOf: [listsMutateItemSuccessSchema, listsFailureSchemaFor(operationId)],
+ };
+}
+
+function listsExitResultSchemaFor(operationId: OperationId): JsonSchema {
+ return {
+ oneOf: [listsExitSuccessSchema, listsFailureSchemaFor(operationId)],
+ };
+}
+
+const nodeSummarySchema = objectSchema({
+ label: { type: 'string' },
+ text: { type: 'string' },
+});
+
+const nodeInfoSchema: JsonSchema = {
+ type: 'object',
+ required: ['nodeType', 'kind'],
+ properties: {
+ nodeType: { enum: [...nodeTypeValues] },
+ kind: { enum: ['block', 'inline'] },
+ summary: nodeSummarySchema,
+ text: { type: 'string' },
+ nodes: arraySchema({ type: 'object' }),
+ properties: { type: 'object' },
+ bodyText: { type: 'string' },
+ bodyNodes: arraySchema({ type: 'object' }),
+ },
+ additionalProperties: false,
+};
+
+const matchContextSchema = objectSchema(
+ {
+ address: nodeAddressSchema,
+ snippet: { type: 'string' },
+ highlightRange: rangeSchema,
+ textRanges: arraySchema(textAddressSchema),
+ },
+ ['address', 'snippet', 'highlightRange'],
+);
+
+const unknownNodeDiagnosticSchema = objectSchema(
+ {
+ message: { type: 'string' },
+ address: nodeAddressSchema,
+ hint: { type: 'string' },
+ },
+ ['message'],
+);
+
+const textSelectorSchema = objectSchema(
+ {
+ type: { const: 'text' },
+ pattern: { type: 'string' },
+ mode: { enum: ['contains', 'regex'] },
+ caseSensitive: { type: 'boolean' },
+ },
+ ['type', 'pattern'],
+);
+
+const nodeSelectorSchema = objectSchema(
+ {
+ type: { const: 'node' },
+ nodeType: { enum: [...nodeTypeValues] },
+ kind: { enum: ['block', 'inline'] },
+ },
+ ['type'],
+);
+
+const selectorShorthandSchema = objectSchema(
+ {
+ nodeType: { enum: [...nodeTypeValues] },
+ },
+ ['nodeType'],
+);
+
+const selectSchema: JsonSchema = {
+ anyOf: [textSelectorSchema, nodeSelectorSchema, selectorShorthandSchema],
+};
+
+const findInputSchema = objectSchema(
+ {
+ select: selectSchema,
+ within: nodeAddressSchema,
+ limit: { type: 'integer' },
+ offset: { type: 'integer' },
+ includeNodes: { type: 'boolean' },
+ includeUnknown: { type: 'boolean' },
+ },
+ ['select'],
+);
+
+const findOutputSchema = objectSchema(
+ {
+ matches: arraySchema(nodeAddressSchema),
+ total: { type: 'integer' },
+ nodes: arraySchema(nodeInfoSchema),
+ context: arraySchema(matchContextSchema),
+ diagnostics: arraySchema(unknownNodeDiagnosticSchema),
+ },
+ ['matches', 'total'],
+);
+
+const documentInfoCountsSchema = objectSchema(
+ {
+ words: { type: 'integer' },
+ paragraphs: { type: 'integer' },
+ headings: { type: 'integer' },
+ tables: { type: 'integer' },
+ images: { type: 'integer' },
+ comments: { type: 'integer' },
+ },
+ ['words', 'paragraphs', 'headings', 'tables', 'images', 'comments'],
+);
+
+const documentInfoOutlineItemSchema = objectSchema(
+ {
+ level: { type: 'integer' },
+ text: { type: 'string' },
+ nodeId: { type: 'string' },
+ },
+ ['level', 'text', 'nodeId'],
+);
+
+const documentInfoCapabilitiesSchema = objectSchema(
+ {
+ canFind: { type: 'boolean' },
+ canGetNode: { type: 'boolean' },
+ canComment: { type: 'boolean' },
+ canReplace: { type: 'boolean' },
+ },
+ ['canFind', 'canGetNode', 'canComment', 'canReplace'],
+);
+
+const documentInfoSchema = objectSchema(
+ {
+ counts: documentInfoCountsSchema,
+ outline: arraySchema(documentInfoOutlineItemSchema),
+ capabilities: documentInfoCapabilitiesSchema,
+ },
+ ['counts', 'outline', 'capabilities'],
+);
+
+const listKindSchema: JsonSchema = { enum: ['ordered', 'bullet'] };
+const listInsertPositionSchema: JsonSchema = { enum: ['before', 'after'] };
+
+const listItemInfoSchema = objectSchema(
+ {
+ address: listItemAddressSchema,
+ marker: { type: 'string' },
+ ordinal: { type: 'integer' },
+ path: arraySchema({ type: 'integer' }),
+ level: { type: 'integer' },
+ kind: listKindSchema,
+ text: { type: 'string' },
+ },
+ ['address'],
+);
+
+const listsListResultSchema = objectSchema(
+ {
+ matches: arraySchema(listItemAddressSchema),
+ total: { type: 'integer' },
+ items: arraySchema(listItemInfoSchema),
+ },
+ ['matches', 'total', 'items'],
+);
+
+const commentInfoSchema = objectSchema(
+ {
+ address: commentAddressSchema,
+ commentId: { type: 'string' },
+ importedId: { type: 'string' },
+ parentCommentId: { type: 'string' },
+ text: { type: 'string' },
+ isInternal: { type: 'boolean' },
+ status: { enum: ['open', 'resolved'] },
+ target: textAddressSchema,
+ createdTime: { type: 'number' },
+ creatorName: { type: 'string' },
+ creatorEmail: { type: 'string' },
+ },
+ ['address', 'commentId', 'status'],
+);
+
+const commentsListResultSchema = objectSchema(
+ {
+ matches: arraySchema(commentInfoSchema),
+ total: { type: 'integer' },
+ },
+ ['matches', 'total'],
+);
+
+const trackChangeInfoSchema = objectSchema(
+ {
+ address: trackedChangeAddressSchema,
+ id: { type: 'string' },
+ type: { enum: ['insert', 'delete', 'format'] },
+ author: { type: 'string' },
+ authorEmail: { type: 'string' },
+ authorImage: { type: 'string' },
+ date: { type: 'string' },
+ excerpt: { type: 'string' },
+ },
+ ['address', 'id', 'type'],
+);
+
+const trackChangesListResultSchema = objectSchema(
+ {
+ matches: arraySchema(trackedChangeAddressSchema),
+ total: { type: 'integer' },
+ changes: arraySchema(trackChangeInfoSchema),
+ },
+ ['matches', 'total'],
+);
+
+const capabilityReasonCodeSchema: JsonSchema = {
+ enum: [
+ 'COMMAND_UNAVAILABLE',
+ 'OPERATION_UNAVAILABLE',
+ 'TRACKED_MODE_UNAVAILABLE',
+ 'DRY_RUN_UNAVAILABLE',
+ 'NAMESPACE_UNAVAILABLE',
+ ],
+};
+
+const capabilityReasonsSchema = arraySchema(capabilityReasonCodeSchema);
+
+const capabilityFlagSchema = objectSchema(
+ {
+ enabled: { type: 'boolean' },
+ reasons: capabilityReasonsSchema,
+ },
+ ['enabled'],
+);
+
+const operationRuntimeCapabilitySchema = objectSchema(
+ {
+ available: { type: 'boolean' },
+ tracked: { type: 'boolean' },
+ dryRun: { type: 'boolean' },
+ reasons: capabilityReasonsSchema,
+ },
+ ['available', 'tracked', 'dryRun'],
+);
+
+const operationCapabilitiesSchema = objectSchema(
+ Object.fromEntries(OPERATION_IDS.map((operationId) => [operationId, operationRuntimeCapabilitySchema])) as Record<
+ string,
+ JsonSchema
+ >,
+ OPERATION_IDS,
+);
+
+const capabilitiesOutputSchema = objectSchema(
+ {
+ global: objectSchema(
+ {
+ trackChanges: capabilityFlagSchema,
+ comments: capabilityFlagSchema,
+ lists: capabilityFlagSchema,
+ dryRun: capabilityFlagSchema,
+ },
+ ['trackChanges', 'comments', 'lists', 'dryRun'],
+ ),
+ operations: operationCapabilitiesSchema,
+ },
+ ['global', 'operations'],
+);
+
+const strictEmptyObjectSchema = objectSchema({});
+
+const operationSchemas: Record = {
+ find: {
+ input: findInputSchema,
+ output: findOutputSchema,
+ },
+ getNode: {
+ input: nodeAddressSchema,
+ output: nodeInfoSchema,
+ },
+ getNodeById: {
+ input: objectSchema(
+ {
+ nodeId: { type: 'string' },
+ nodeType: { enum: [...blockNodeTypeValues] },
+ },
+ ['nodeId'],
+ ),
+ output: nodeInfoSchema,
+ },
+ getText: {
+ input: strictEmptyObjectSchema,
+ output: { type: 'string' },
+ },
+ info: {
+ input: strictEmptyObjectSchema,
+ output: documentInfoSchema,
+ },
+ insert: {
+ input: objectSchema(
+ {
+ target: textAddressSchema,
+ text: { type: 'string' },
+ },
+ ['text'],
+ ),
+ output: textMutationResultSchemaFor('insert'),
+ success: textMutationSuccessSchema,
+ failure: textMutationFailureSchemaFor('insert'),
+ },
+ replace: {
+ input: objectSchema(
+ {
+ target: textAddressSchema,
+ text: { type: 'string' },
+ },
+ ['target', 'text'],
+ ),
+ output: textMutationResultSchemaFor('replace'),
+ success: textMutationSuccessSchema,
+ failure: textMutationFailureSchemaFor('replace'),
+ },
+ delete: {
+ input: objectSchema(
+ {
+ target: textAddressSchema,
+ },
+ ['target'],
+ ),
+ output: textMutationResultSchemaFor('delete'),
+ success: textMutationSuccessSchema,
+ failure: textMutationFailureSchemaFor('delete'),
+ },
+ 'format.bold': {
+ input: objectSchema(
+ {
+ target: textAddressSchema,
+ },
+ ['target'],
+ ),
+ output: textMutationResultSchemaFor('format.bold'),
+ success: textMutationSuccessSchema,
+ failure: textMutationFailureSchemaFor('format.bold'),
+ },
+ 'create.paragraph': {
+ input: objectSchema({
+ at: {
+ oneOf: [
+ objectSchema({ kind: { const: 'documentStart' } }, ['kind']),
+ objectSchema({ kind: { const: 'documentEnd' } }, ['kind']),
+ objectSchema(
+ {
+ kind: { const: 'before' },
+ target: blockNodeAddressSchema,
+ },
+ ['kind', 'target'],
+ ),
+ objectSchema(
+ {
+ kind: { const: 'after' },
+ target: blockNodeAddressSchema,
+ },
+ ['kind', 'target'],
+ ),
+ ],
+ },
+ text: { type: 'string' },
+ }),
+ output: createParagraphResultSchemaFor('create.paragraph'),
+ success: createParagraphSuccessSchema,
+ failure: createParagraphFailureSchemaFor('create.paragraph'),
+ },
+ 'lists.list': {
+ input: objectSchema({
+ within: blockNodeAddressSchema,
+ limit: { type: 'integer' },
+ offset: { type: 'integer' },
+ kind: listKindSchema,
+ level: { type: 'integer' },
+ ordinal: { type: 'integer' },
+ }),
+ output: listsListResultSchema,
+ },
+ 'lists.get': {
+ input: objectSchema({ address: listItemAddressSchema }, ['address']),
+ output: listItemInfoSchema,
+ },
+ 'lists.insert': {
+ input: objectSchema(
+ {
+ target: listItemAddressSchema,
+ position: listInsertPositionSchema,
+ text: { type: 'string' },
+ },
+ ['target', 'position'],
+ ),
+ output: listsInsertResultSchemaFor('lists.insert'),
+ success: listsInsertSuccessSchema,
+ failure: listsFailureSchemaFor('lists.insert'),
+ },
+ 'lists.setType': {
+ input: objectSchema(
+ {
+ target: listItemAddressSchema,
+ kind: listKindSchema,
+ },
+ ['target', 'kind'],
+ ),
+ output: listsMutateItemResultSchemaFor('lists.setType'),
+ success: listsMutateItemSuccessSchema,
+ failure: listsFailureSchemaFor('lists.setType'),
+ },
+ 'lists.indent': {
+ input: objectSchema({ target: listItemAddressSchema }, ['target']),
+ output: listsMutateItemResultSchemaFor('lists.indent'),
+ success: listsMutateItemSuccessSchema,
+ failure: listsFailureSchemaFor('lists.indent'),
+ },
+ 'lists.outdent': {
+ input: objectSchema({ target: listItemAddressSchema }, ['target']),
+ output: listsMutateItemResultSchemaFor('lists.outdent'),
+ success: listsMutateItemSuccessSchema,
+ failure: listsFailureSchemaFor('lists.outdent'),
+ },
+ 'lists.restart': {
+ input: objectSchema({ target: listItemAddressSchema }, ['target']),
+ output: listsMutateItemResultSchemaFor('lists.restart'),
+ success: listsMutateItemSuccessSchema,
+ failure: listsFailureSchemaFor('lists.restart'),
+ },
+ 'lists.exit': {
+ input: objectSchema({ target: listItemAddressSchema }, ['target']),
+ output: listsExitResultSchemaFor('lists.exit'),
+ success: listsExitSuccessSchema,
+ failure: listsFailureSchemaFor('lists.exit'),
+ },
+ 'comments.add': {
+ input: objectSchema(
+ {
+ target: textAddressSchema,
+ text: { type: 'string' },
+ },
+ ['target', 'text'],
+ ),
+ output: receiptResultSchemaFor('comments.add'),
+ success: receiptSuccessSchema,
+ failure: receiptFailureResultSchemaFor('comments.add'),
+ },
+ 'comments.edit': {
+ input: objectSchema(
+ {
+ commentId: { type: 'string' },
+ text: { type: 'string' },
+ },
+ ['commentId', 'text'],
+ ),
+ output: receiptResultSchemaFor('comments.edit'),
+ success: receiptSuccessSchema,
+ failure: receiptFailureResultSchemaFor('comments.edit'),
+ },
+ 'comments.reply': {
+ input: objectSchema(
+ {
+ parentCommentId: { type: 'string' },
+ text: { type: 'string' },
+ },
+ ['parentCommentId', 'text'],
+ ),
+ output: receiptResultSchemaFor('comments.reply'),
+ success: receiptSuccessSchema,
+ failure: receiptFailureResultSchemaFor('comments.reply'),
+ },
+ 'comments.move': {
+ input: objectSchema(
+ {
+ commentId: { type: 'string' },
+ target: textAddressSchema,
+ },
+ ['commentId', 'target'],
+ ),
+ output: receiptResultSchemaFor('comments.move'),
+ success: receiptSuccessSchema,
+ failure: receiptFailureResultSchemaFor('comments.move'),
+ },
+ 'comments.resolve': {
+ input: objectSchema({ commentId: { type: 'string' } }, ['commentId']),
+ output: receiptResultSchemaFor('comments.resolve'),
+ success: receiptSuccessSchema,
+ failure: receiptFailureResultSchemaFor('comments.resolve'),
+ },
+ 'comments.remove': {
+ input: objectSchema({ commentId: { type: 'string' } }, ['commentId']),
+ output: receiptResultSchemaFor('comments.remove'),
+ success: receiptSuccessSchema,
+ failure: receiptFailureResultSchemaFor('comments.remove'),
+ },
+ 'comments.setInternal': {
+ input: objectSchema(
+ {
+ commentId: { type: 'string' },
+ isInternal: { type: 'boolean' },
+ },
+ ['commentId', 'isInternal'],
+ ),
+ output: receiptResultSchemaFor('comments.setInternal'),
+ success: receiptSuccessSchema,
+ failure: receiptFailureResultSchemaFor('comments.setInternal'),
+ },
+ 'comments.setActive': {
+ input: objectSchema({ commentId: { type: ['string', 'null'] } }, ['commentId']),
+ output: receiptResultSchemaFor('comments.setActive'),
+ success: receiptSuccessSchema,
+ failure: receiptFailureResultSchemaFor('comments.setActive'),
+ },
+ 'comments.goTo': {
+ input: objectSchema({ commentId: { type: 'string' } }, ['commentId']),
+ output: receiptSuccessSchema,
+ },
+ 'comments.get': {
+ input: objectSchema({ commentId: { type: 'string' } }, ['commentId']),
+ output: commentInfoSchema,
+ },
+ 'comments.list': {
+ input: objectSchema({ includeResolved: { type: 'boolean' } }),
+ output: commentsListResultSchema,
+ },
+ 'trackChanges.list': {
+ input: objectSchema({
+ limit: { type: 'integer' },
+ offset: { type: 'integer' },
+ type: { enum: ['insert', 'delete', 'format'] },
+ }),
+ output: trackChangesListResultSchema,
+ },
+ 'trackChanges.get': {
+ input: objectSchema({ id: { type: 'string' } }, ['id']),
+ output: trackChangeInfoSchema,
+ },
+ 'trackChanges.accept': {
+ input: objectSchema({ id: { type: 'string' } }, ['id']),
+ output: receiptResultSchemaFor('trackChanges.accept'),
+ success: receiptSuccessSchema,
+ failure: receiptFailureResultSchemaFor('trackChanges.accept'),
+ },
+ 'trackChanges.reject': {
+ input: objectSchema({ id: { type: 'string' } }, ['id']),
+ output: receiptResultSchemaFor('trackChanges.reject'),
+ success: receiptSuccessSchema,
+ failure: receiptFailureResultSchemaFor('trackChanges.reject'),
+ },
+ 'trackChanges.acceptAll': {
+ input: strictEmptyObjectSchema,
+ output: receiptResultSchemaFor('trackChanges.acceptAll'),
+ success: receiptSuccessSchema,
+ failure: receiptFailureResultSchemaFor('trackChanges.acceptAll'),
+ },
+ 'trackChanges.rejectAll': {
+ input: strictEmptyObjectSchema,
+ output: receiptResultSchemaFor('trackChanges.rejectAll'),
+ success: receiptSuccessSchema,
+ failure: receiptFailureResultSchemaFor('trackChanges.rejectAll'),
+ },
+ 'capabilities.get': {
+ input: strictEmptyObjectSchema,
+ output: capabilitiesOutputSchema,
+ },
+};
+
+/**
+ * Builds the complete set of JSON Schema definitions for every document-api operation.
+ *
+ * Validates that every {@link OperationId} has a corresponding schema entry and
+ * that no unknown operations are present.
+ *
+ * @returns A versioned {@link InternalContractSchemas} envelope.
+ * @throws {Error} If any operation is missing a schema or an unknown operation is found.
+ */
+export function buildInternalContractSchemas(): InternalContractSchemas {
+ const operations = { ...operationSchemas };
+
+ for (const operationId of OPERATION_IDS) {
+ if (!operations[operationId]) {
+ throw new Error(`Schema generation missing operation "${operationId}".`);
+ }
+ }
+
+ for (const operationId of Object.keys(operations) as OperationId[]) {
+ if (!COMMAND_CATALOG[operationId]) {
+ throw new Error(`Schema generation encountered unknown operation "${operationId}".`);
+ }
+ }
+
+ return {
+ $schema: JSON_SCHEMA_DIALECT,
+ contractVersion: CONTRACT_VERSION,
+ operations,
+ };
+}
diff --git a/packages/document-api/src/contract/types.test.ts b/packages/document-api/src/contract/types.test.ts
new file mode 100644
index 0000000000..cfe11e5f7d
--- /dev/null
+++ b/packages/document-api/src/contract/types.test.ts
@@ -0,0 +1,82 @@
+import { assertOperationId, isOperationId, isValidOperationIdFormat, OPERATION_IDS } from './types.js';
+import type { DocumentApiMemberPath } from './operation-map.js';
+
+describe('isValidOperationIdFormat', () => {
+ it('accepts simple camelCase identifiers', () => {
+ expect(isValidOperationIdFormat('find')).toBe(true);
+ expect(isValidOperationIdFormat('getNode')).toBe(true);
+ expect(isValidOperationIdFormat('getText')).toBe(true);
+ });
+
+ it('accepts namespaced identifiers (namespace.camelCase)', () => {
+ expect(isValidOperationIdFormat('comments.add')).toBe(true);
+ expect(isValidOperationIdFormat('trackChanges.list')).toBe(true);
+ expect(isValidOperationIdFormat('lists.setType')).toBe(true);
+ });
+
+ it('rejects empty strings', () => {
+ expect(isValidOperationIdFormat('')).toBe(false);
+ });
+
+ it('rejects identifiers starting with uppercase', () => {
+ expect(isValidOperationIdFormat('Find')).toBe(false);
+ expect(isValidOperationIdFormat('Comments.add')).toBe(false);
+ });
+
+ it('rejects identifiers with multiple dots', () => {
+ expect(isValidOperationIdFormat('a.b.c')).toBe(false);
+ });
+
+ it('rejects identifiers with special characters', () => {
+ expect(isValidOperationIdFormat('find-all')).toBe(false);
+ expect(isValidOperationIdFormat('find_all')).toBe(false);
+ expect(isValidOperationIdFormat('find all')).toBe(false);
+ });
+
+ it('rejects trailing or leading dots', () => {
+ expect(isValidOperationIdFormat('.find')).toBe(false);
+ expect(isValidOperationIdFormat('find.')).toBe(false);
+ });
+});
+
+describe('isOperationId', () => {
+ it('returns true for every known operation ID', () => {
+ for (const id of OPERATION_IDS) {
+ expect(isOperationId(id)).toBe(true);
+ }
+ });
+
+ it('returns false for unknown but validly formatted strings', () => {
+ expect(isOperationId('unknown')).toBe(false);
+ expect(isOperationId('comments.unknown')).toBe(false);
+ });
+
+ it('returns false for invalid format strings', () => {
+ expect(isOperationId('')).toBe(false);
+ expect(isOperationId('FIND')).toBe(false);
+ });
+});
+
+describe('assertOperationId', () => {
+ it('does not throw for known operation IDs', () => {
+ for (const id of OPERATION_IDS) {
+ expect(() => assertOperationId(id)).not.toThrow();
+ }
+ });
+
+ it('throws for unknown operation IDs', () => {
+ expect(() => assertOperationId('nonexistent')).toThrow(/Unknown operationId "nonexistent"/);
+ });
+
+ it('throws for invalid format strings', () => {
+ expect(() => assertOperationId('BAD FORMAT')).toThrow(/Unknown operationId/);
+ });
+});
+
+describe('DocumentApiMemberPath type safety', () => {
+ it('is narrower than string', () => {
+ type IsWideString = string extends DocumentApiMemberPath ? true : false;
+ const isWideString: IsWideString = false;
+ expect(isWideString).toBe(false);
+ });
+});
diff --git a/packages/document-api/src/contract/types.ts b/packages/document-api/src/contract/types.ts
new file mode 100644
index 0000000000..43e18ee970
--- /dev/null
+++ b/packages/document-api/src/contract/types.ts
@@ -0,0 +1,62 @@
+export {
+ OPERATION_IDEMPOTENCY_VALUES,
+ type OperationIdempotency,
+ PRE_APPLY_THROW_CODES,
+ type PreApplyThrowCode,
+ type CommandThrowPolicy,
+ type CommandStaticMetadata,
+} from './metadata-types.js';
+
+export {
+ type OperationId,
+ OPERATION_IDS,
+ SINGLETON_OPERATION_IDS,
+ NAMESPACED_OPERATION_IDS,
+} from './operation-definitions.js';
+
+import type { OperationId } from './operation-definitions.js';
+import { OPERATION_IDS } from './operation-definitions.js';
+import type { CommandStaticMetadata } from './metadata-types.js';
+
+export const CONTRACT_VERSION = '0.1.0';
+
+export const JSON_SCHEMA_DIALECT = 'https://json-schema.org/draft/2020-12/schema';
+
+export type CommandCatalog = {
+ readonly [K in OperationId]: CommandStaticMetadata;
+};
+
+const OPERATION_ID_FORMAT = /^(?:[a-z][a-zA-Z0-9]*|[a-z][a-zA-Z0-9]*\.[a-z][a-zA-Z0-9]*)$/;
+
+/**
+ * Checks whether a string matches the syntactic format of an operation ID
+ * (`camelCase` or `namespace.camelCase`).
+ *
+ * @param operationId - The string to validate.
+ * @returns `true` if the string matches the expected format.
+ */
+export function isValidOperationIdFormat(operationId: string): boolean {
+ return OPERATION_ID_FORMAT.test(operationId);
+}
+
+/**
+ * Type-guard that narrows a string to the {@link OperationId} union.
+ *
+ * @param operationId - The string to check.
+ * @returns `true` if the string is a known operation ID.
+ */
+export function isOperationId(operationId: string): operationId is OperationId {
+ return (OPERATION_IDS as readonly string[]).includes(operationId);
+}
+
+/**
+ * Asserts that a string is a valid, known {@link OperationId}.
+ *
+ * @param operationId - The string to assert.
+ * @throws {Error} If the string is not a recognised operation ID.
+ */
+export function assertOperationId(operationId: string): asserts operationId is OperationId {
+ if (!isValidOperationIdFormat(operationId) || !isOperationId(operationId)) {
+ throw new Error(`Unknown operationId "${operationId}".`);
+ }
+}
diff --git a/packages/document-api/src/create/create.test.ts b/packages/document-api/src/create/create.test.ts
new file mode 100644
index 0000000000..f125a65f28
--- /dev/null
+++ b/packages/document-api/src/create/create.test.ts
@@ -0,0 +1,62 @@
+import { normalizeCreateParagraphInput } from './create.js';
+
+describe('normalizeCreateParagraphInput', () => {
+ it('defaults location to documentEnd when at is omitted', () => {
+ const result = normalizeCreateParagraphInput({});
+
+ expect(result.at).toEqual({ kind: 'documentEnd' });
+ });
+
+ it('defaults text to empty string when omitted', () => {
+ const result = normalizeCreateParagraphInput({});
+
+ expect(result.text).toBe('');
+ });
+
+ it('defaults both at and text when input is empty', () => {
+ const result = normalizeCreateParagraphInput({});
+
+ expect(result).toEqual({
+ at: { kind: 'documentEnd' },
+ text: '',
+ });
+ });
+
+ it('preserves explicit documentStart location', () => {
+ const result = normalizeCreateParagraphInput({ at: { kind: 'documentStart' } });
+
+ expect(result.at).toEqual({ kind: 'documentStart' });
+ });
+
+ it('preserves explicit before location with target', () => {
+ const target = { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'p1' };
+ const result = normalizeCreateParagraphInput({ at: { kind: 'before', target } });
+
+ expect(result.at).toEqual({ kind: 'before', target });
+ });
+
+ it('preserves explicit after location with target', () => {
+ const target = { kind: 'block' as const, nodeType: 'heading' as const, nodeId: 'h1' };
+ const result = normalizeCreateParagraphInput({ at: { kind: 'after', target } });
+
+ expect(result.at).toEqual({ kind: 'after', target });
+ });
+
+ it('preserves explicit text', () => {
+ const result = normalizeCreateParagraphInput({ text: 'Hello world' });
+
+ expect(result.text).toBe('Hello world');
+ });
+
+ it('preserves both explicit at and text', () => {
+ const result = normalizeCreateParagraphInput({
+ at: { kind: 'documentStart' },
+ text: 'First paragraph',
+ });
+
+ expect(result).toEqual({
+ at: { kind: 'documentStart' },
+ text: 'First paragraph',
+ });
+ });
+});
diff --git a/packages/document-api/src/create/create.ts b/packages/document-api/src/create/create.ts
new file mode 100644
index 0000000000..7645c982c7
--- /dev/null
+++ b/packages/document-api/src/create/create.ts
@@ -0,0 +1,28 @@
+import type { MutationOptions } from '../write/write.js';
+import { normalizeMutationOptions } from '../write/write.js';
+import type { CreateParagraphInput, CreateParagraphResult, ParagraphCreateLocation } from '../types/create.types.js';
+
+export interface CreateApi {
+ paragraph(input: CreateParagraphInput, options?: MutationOptions): CreateParagraphResult;
+}
+
+export type CreateAdapter = CreateApi;
+
+function normalizeParagraphCreateLocation(location?: ParagraphCreateLocation): ParagraphCreateLocation {
+ return location ?? { kind: 'documentEnd' };
+}
+
+export function normalizeCreateParagraphInput(input: CreateParagraphInput): CreateParagraphInput {
+ return {
+ at: normalizeParagraphCreateLocation(input.at),
+ text: input.text ?? '',
+ };
+}
+
+export function executeCreateParagraph(
+ adapter: CreateAdapter,
+ input: CreateParagraphInput,
+ options?: MutationOptions,
+): CreateParagraphResult {
+ return adapter.paragraph(normalizeCreateParagraphInput(input), normalizeMutationOptions(options));
+}
diff --git a/packages/document-api/src/delete/delete.ts b/packages/document-api/src/delete/delete.ts
new file mode 100644
index 0000000000..6616814a8a
--- /dev/null
+++ b/packages/document-api/src/delete/delete.ts
@@ -0,0 +1,22 @@
+import { executeWrite, type MutationOptions, type WriteAdapter } from '../write/write.js';
+import type { TextAddress, TextMutationReceipt } from '../types/index.js';
+
+export interface DeleteInput {
+ target: TextAddress;
+}
+
+export function executeDelete(
+ adapter: WriteAdapter,
+ input: DeleteInput,
+ options?: MutationOptions,
+): TextMutationReceipt {
+ return executeWrite(
+ adapter,
+ {
+ kind: 'delete',
+ target: input.target,
+ text: '',
+ },
+ options,
+ );
+}
diff --git a/packages/document-api/src/find/find.test.ts b/packages/document-api/src/find/find.test.ts
new file mode 100644
index 0000000000..271ca95835
--- /dev/null
+++ b/packages/document-api/src/find/find.test.ts
@@ -0,0 +1,99 @@
+import { executeFind, normalizeFindQuery } from './find.js';
+import type { Query, QueryResult, Selector } from '../types/index.js';
+import type { FindAdapter } from './find.js';
+
+describe('normalizeFindQuery', () => {
+ it('passes through a full Query object with canonical selector', () => {
+ const query: Query = {
+ select: { type: 'node', nodeType: 'paragraph' },
+ limit: 10,
+ };
+
+ const result = normalizeFindQuery(query);
+ expect(result).toStrictEqual(query);
+ });
+
+ it('wraps a NodeSelector into a Query', () => {
+ const selector: Selector = { type: 'node', nodeType: 'heading' };
+
+ expect(normalizeFindQuery(selector)).toEqual({ select: selector });
+ });
+
+ it('normalizes the nodeType shorthand into a canonical NodeSelector', () => {
+ const selector: Selector = { nodeType: 'paragraph' };
+
+ expect(normalizeFindQuery(selector)).toEqual({
+ select: { type: 'node', nodeType: 'paragraph' },
+ limit: undefined,
+ offset: undefined,
+ within: undefined,
+ includeNodes: undefined,
+ includeUnknown: undefined,
+ });
+ });
+
+ it('maps FindOptions fields onto the Query', () => {
+ const selector: Selector = { type: 'text', pattern: 'hello' };
+ const within = { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'p1' };
+
+ const result = normalizeFindQuery(selector, {
+ limit: 5,
+ offset: 2,
+ within,
+ includeNodes: true,
+ includeUnknown: true,
+ });
+
+ expect(result).toEqual({
+ select: selector,
+ limit: 5,
+ offset: 2,
+ within,
+ includeNodes: true,
+ includeUnknown: true,
+ });
+ });
+
+ it('leaves optional fields undefined when options are omitted', () => {
+ const selector: Selector = { type: 'node', nodeType: 'table' };
+
+ const result = normalizeFindQuery(selector);
+
+ expect(result.select).toStrictEqual(selector);
+ expect(result.limit).toBeUndefined();
+ expect(result.offset).toBeUndefined();
+ expect(result.within).toBeUndefined();
+ expect(result.includeNodes).toBeUndefined();
+ expect(result.includeUnknown).toBeUndefined();
+ });
+});
+
+describe('executeFind', () => {
+ it('normalizes the input and delegates to the adapter', () => {
+ const expected: QueryResult = { matches: [], total: 0 };
+ const adapter: FindAdapter = { find: vi.fn(() => expected) };
+
+ const result = executeFind(adapter, { nodeType: 'paragraph' }, { limit: 5 });
+
+ expect(result).toBe(expected);
+ expect(adapter.find).toHaveBeenCalledWith({
+ select: { type: 'node', nodeType: 'paragraph' },
+ limit: 5,
+ offset: undefined,
+ within: undefined,
+ includeNodes: undefined,
+ includeUnknown: undefined,
+ });
+ });
+
+ it('passes a full Query through to the adapter', () => {
+ const expected: QueryResult = { matches: [], total: 0 };
+ const adapter: FindAdapter = { find: vi.fn(() => expected) };
+ const query: Query = { select: { type: 'text', pattern: 'hello' }, limit: 10 };
+
+ const result = executeFind(adapter, query);
+
+ expect(result).toBe(expected);
+ expect(adapter.find).toHaveBeenCalledWith(query);
+ });
+});
diff --git a/packages/document-api/src/find/find.ts b/packages/document-api/src/find/find.ts
new file mode 100644
index 0000000000..3f32595701
--- /dev/null
+++ b/packages/document-api/src/find/find.ts
@@ -0,0 +1,77 @@
+import type { NodeAddress, NodeSelector, Query, QueryResult, Selector, TextSelector } from '../types/index.js';
+
+/**
+ * Options for the `find` method when using a selector shorthand.
+ */
+export interface FindOptions {
+ /** Maximum number of results to return. */
+ limit?: number;
+ /** Number of results to skip before returning matches. */
+ offset?: number;
+ /** Constrain the search to descendants of the specified node. */
+ within?: NodeAddress;
+ /** Whether to hydrate `result.nodes` for matched addresses. */
+ includeNodes?: Query['includeNodes'];
+ /** Whether to include unknown/unsupported nodes in diagnostics. */
+ includeUnknown?: Query['includeUnknown'];
+}
+
+/**
+ * Engine-specific adapter that the find API delegates to.
+ */
+export interface FindAdapter {
+ /**
+ * Execute a normalized query against the document.
+ *
+ * @param query - The normalized query to execute.
+ * @returns The query result containing matches and metadata.
+ */
+ find(query: Query): QueryResult;
+}
+
+/** Normalizes a selector shorthand into its canonical discriminated-union form. */
+function normalizeSelector(selector: Selector): NodeSelector | TextSelector {
+ if ('type' in selector) {
+ return selector;
+ }
+ return { type: 'node', nodeType: selector.nodeType };
+}
+
+/**
+ * Normalizes a selector-or-query argument into a canonical {@link Query} object.
+ *
+ * @param selectorOrQuery - A selector shorthand or a full query object.
+ * @param options - Options applied when `selectorOrQuery` is a selector.
+ * @returns A normalized query.
+ */
+export function normalizeFindQuery(selectorOrQuery: Selector | Query, options?: FindOptions): Query {
+ if ('select' in selectorOrQuery) {
+ return { ...selectorOrQuery, select: normalizeSelector(selectorOrQuery.select) };
+ }
+
+ return {
+ select: normalizeSelector(selectorOrQuery),
+ limit: options?.limit,
+ offset: options?.offset,
+ within: options?.within,
+ includeNodes: options?.includeNodes,
+ includeUnknown: options?.includeUnknown,
+ };
+}
+
+/**
+ * Executes a find operation by normalizing the input and delegating to the adapter.
+ *
+ * @param adapter - The engine-specific find adapter.
+ * @param selectorOrQuery - A selector shorthand or a full query object.
+ * @param options - Options applied when `selectorOrQuery` is a selector.
+ * @returns The query result from the adapter.
+ */
+export function executeFind(
+ adapter: FindAdapter,
+ selectorOrQuery: Selector | Query,
+ options?: FindOptions,
+): QueryResult {
+ const query = normalizeFindQuery(selectorOrQuery, options);
+ return adapter.find(query);
+}
diff --git a/packages/document-api/src/format/format.ts b/packages/document-api/src/format/format.ts
new file mode 100644
index 0000000000..6888714086
--- /dev/null
+++ b/packages/document-api/src/format/format.ts
@@ -0,0 +1,21 @@
+import { normalizeMutationOptions, type MutationOptions } from '../write/write.js';
+import type { TextAddress, TextMutationReceipt } from '../types/index.js';
+
+export interface FormatBoldInput {
+ target: TextAddress;
+}
+
+export interface FormatAdapter {
+ /** Apply or toggle bold formatting on the target text range. */
+ bold(input: FormatBoldInput, options?: MutationOptions): TextMutationReceipt;
+}
+
+export type FormatApi = FormatAdapter;
+
+export function executeFormatBold(
+ adapter: FormatAdapter,
+ input: FormatBoldInput,
+ options?: MutationOptions,
+): TextMutationReceipt {
+ return adapter.bold(input, normalizeMutationOptions(options));
+}
diff --git a/packages/document-api/src/get-node/get-node.test.ts b/packages/document-api/src/get-node/get-node.test.ts
new file mode 100644
index 0000000000..d1bb087809
--- /dev/null
+++ b/packages/document-api/src/get-node/get-node.test.ts
@@ -0,0 +1,40 @@
+import type { NodeAddress, NodeInfo } from '../types/index.js';
+import { executeGetNode, executeGetNodeById } from './get-node.js';
+import type { GetNodeAdapter } from './get-node.js';
+
+const PARAGRAPH_ADDRESS: NodeAddress = { kind: 'block', nodeType: 'paragraph', nodeId: 'p1' };
+
+const PARAGRAPH_INFO: NodeInfo = {
+ nodeType: 'paragraph',
+ kind: 'block',
+ properties: {},
+};
+
+describe('executeGetNode', () => {
+ it('delegates to adapter.getNode with the address', () => {
+ const adapter: GetNodeAdapter = {
+ getNode: vi.fn(() => PARAGRAPH_INFO),
+ getNodeById: vi.fn(() => PARAGRAPH_INFO),
+ };
+
+ const result = executeGetNode(adapter, PARAGRAPH_ADDRESS);
+
+ expect(result).toBe(PARAGRAPH_INFO);
+ expect(adapter.getNode).toHaveBeenCalledWith(PARAGRAPH_ADDRESS);
+ });
+});
+
+describe('executeGetNodeById', () => {
+ it('delegates to adapter.getNodeById with the input', () => {
+ const adapter: GetNodeAdapter = {
+ getNode: vi.fn(() => PARAGRAPH_INFO),
+ getNodeById: vi.fn(() => PARAGRAPH_INFO),
+ };
+ const input = { nodeId: 'p1', nodeType: 'paragraph' as const };
+
+ const result = executeGetNodeById(adapter, input);
+
+ expect(result).toBe(PARAGRAPH_INFO);
+ expect(adapter.getNodeById).toHaveBeenCalledWith(input);
+ });
+});
diff --git a/packages/document-api/src/get-node/get-node.ts b/packages/document-api/src/get-node/get-node.ts
new file mode 100644
index 0000000000..4c73cfed85
--- /dev/null
+++ b/packages/document-api/src/get-node/get-node.ts
@@ -0,0 +1,53 @@
+import type { BlockNodeType, NodeAddress, NodeInfo } from '../types/index.js';
+
+/**
+ * Input for resolving a block node by its unique ID.
+ */
+export interface GetNodeByIdInput {
+ nodeId: string;
+ nodeType?: BlockNodeType;
+}
+
+/**
+ * Engine-specific adapter that the getNode API delegates to.
+ */
+export interface GetNodeAdapter {
+ /**
+ * Resolve a node address to full node information.
+ *
+ * @param address - The node address to resolve.
+ * @returns Full node information including typed properties.
+ * @throws When the address cannot be resolved.
+ */
+ getNode(address: NodeAddress): NodeInfo;
+ /**
+ * Resolve a block node by its ID.
+ *
+ * @param input - The node-id input payload.
+ * @returns Full node information including typed properties.
+ * @throws When the node ID cannot be found.
+ */
+ getNodeById(input: GetNodeByIdInput): NodeInfo;
+}
+
+/**
+ * Execute a getNode operation via the provided adapter.
+ *
+ * @param adapter - Engine-specific getNode adapter.
+ * @param address - The node address to resolve.
+ * @returns Full node information including typed properties.
+ */
+export function executeGetNode(adapter: GetNodeAdapter, address: NodeAddress): NodeInfo {
+ return adapter.getNode(address);
+}
+
+/**
+ * Execute a getNodeById operation via the provided adapter.
+ *
+ * @param adapter - Engine-specific getNode adapter.
+ * @param input - The node-id input payload.
+ * @returns Full node information including typed properties.
+ */
+export function executeGetNodeById(adapter: GetNodeAdapter, input: GetNodeByIdInput): NodeInfo {
+ return adapter.getNodeById(input);
+}
diff --git a/packages/document-api/src/get-text/get-text.test.ts b/packages/document-api/src/get-text/get-text.test.ts
new file mode 100644
index 0000000000..911cd3a536
--- /dev/null
+++ b/packages/document-api/src/get-text/get-text.test.ts
@@ -0,0 +1,15 @@
+import { executeGetText } from './get-text.js';
+import type { GetTextAdapter } from './get-text.js';
+
+describe('executeGetText', () => {
+ it('delegates to adapter.getText with the input', () => {
+ const adapter: GetTextAdapter = {
+ getText: vi.fn(() => 'Hello world'),
+ };
+
+ const result = executeGetText(adapter, {});
+
+ expect(result).toBe('Hello world');
+ expect(adapter.getText).toHaveBeenCalledWith({});
+ });
+});
diff --git a/packages/document-api/src/get-text/get-text.ts b/packages/document-api/src/get-text/get-text.ts
new file mode 100644
index 0000000000..e9132d27e5
--- /dev/null
+++ b/packages/document-api/src/get-text/get-text.ts
@@ -0,0 +1,22 @@
+export type GetTextInput = Record;
+
+/**
+ * Engine-specific adapter that the getText API delegates to.
+ */
+export interface GetTextAdapter {
+ /**
+ * Return the full document text content.
+ */
+ getText(input: GetTextInput): string;
+}
+
+/**
+ * Execute a getText operation via the provided adapter.
+ *
+ * @param adapter - Engine-specific getText adapter.
+ * @param input - Canonical getText input object.
+ * @returns The full document text content.
+ */
+export function executeGetText(adapter: GetTextAdapter, input: GetTextInput): string {
+ return adapter.getText(input);
+}
diff --git a/packages/document-api/src/index.test.ts b/packages/document-api/src/index.test.ts
new file mode 100644
index 0000000000..6c90cfcc42
--- /dev/null
+++ b/packages/document-api/src/index.test.ts
@@ -0,0 +1,632 @@
+import type { DocumentInfo, NodeAddress, NodeInfo, Query, QueryResult } from './types/index.js';
+import type {
+ AddCommentInput,
+ CommentsAdapter,
+ EditCommentInput,
+ GetCommentInput,
+ GoToCommentInput,
+ MoveCommentInput,
+ RemoveCommentInput,
+ ReplyToCommentInput,
+ ResolveCommentInput,
+ SetCommentActiveInput,
+ SetCommentInternalInput,
+} from './comments/comments.js';
+import type { FormatAdapter } from './format/format.js';
+import type { FindAdapter } from './find/find.js';
+import type { GetNodeAdapter } from './get-node/get-node.js';
+import type { TrackChangesAdapter } from './track-changes/track-changes.js';
+import type { WriteAdapter } from './write/write.js';
+import { createDocumentApi } from './index.js';
+import type { CommentInfo, CommentsListQuery, CommentsListResult } from './comments/comments.types.js';
+import type { CreateAdapter } from './create/create.js';
+import type { ListsAdapter } from './lists/lists.js';
+import type { CapabilitiesAdapter, DocumentApiCapabilities } from './capabilities/capabilities.js';
+
+function makeFindAdapter(result: QueryResult): FindAdapter {
+ return { find: vi.fn(() => result) };
+}
+
+function makeGetNodeAdapter(info: NodeInfo): GetNodeAdapter {
+ return {
+ getNode: vi.fn(() => info),
+ getNodeById: vi.fn((_input) => info),
+ };
+}
+
+function makeGetTextAdapter(text = '') {
+ return {
+ getText: vi.fn((_input) => text),
+ };
+}
+
+function makeInfoAdapter(result?: Partial) {
+ const defaultResult: DocumentInfo = {
+ counts: {
+ words: 0,
+ paragraphs: 0,
+ headings: 0,
+ tables: 0,
+ images: 0,
+ comments: 0,
+ },
+ outline: [],
+ capabilities: {
+ canFind: true,
+ canGetNode: true,
+ canComment: true,
+ canReplace: true,
+ },
+ };
+
+ return {
+ info: vi.fn((_input) => ({
+ ...defaultResult,
+ ...result,
+ counts: {
+ ...defaultResult.counts,
+ ...(result?.counts ?? {}),
+ },
+ capabilities: {
+ ...defaultResult.capabilities,
+ ...(result?.capabilities ?? {}),
+ },
+ outline: result?.outline ?? defaultResult.outline,
+ })),
+ };
+}
+
+function makeCommentsAdapter(): CommentsAdapter {
+ return {
+ add: vi.fn(() => ({ success: true as const })),
+ edit: vi.fn(() => ({ success: true as const })),
+ reply: vi.fn(() => ({ success: true as const })),
+ move: vi.fn(() => ({ success: true as const })),
+ resolve: vi.fn(() => ({ success: true as const })),
+ remove: vi.fn(() => ({ success: true as const })),
+ setInternal: vi.fn(() => ({ success: true as const })),
+ setActive: vi.fn(() => ({ success: true as const })),
+ goTo: vi.fn(() => ({ success: true as const })),
+ get: vi.fn(() => ({
+ address: { kind: 'entity' as const, entityType: 'comment' as const, entityId: 'c1' },
+ commentId: 'c1',
+ status: 'open' as const,
+ })),
+ list: vi.fn(() => ({ matches: [], total: 0 })),
+ };
+}
+
+function makeWriteAdapter(): WriteAdapter {
+ return {
+ write: vi.fn(() => ({
+ success: true as const,
+ resolution: {
+ target: { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 0 } },
+ range: { from: 1, to: 1 },
+ text: '',
+ },
+ })),
+ };
+}
+
+function makeFormatAdapter(): FormatAdapter {
+ return {
+ bold: vi.fn(() => ({
+ success: true as const,
+ resolution: {
+ target: { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 2 } },
+ range: { from: 1, to: 3 },
+ text: 'Hi',
+ },
+ })),
+ };
+}
+
+function makeTrackChangesAdapter(): TrackChangesAdapter {
+ return {
+ list: vi.fn((_input) => ({ matches: [], total: 0 })),
+ get: vi.fn((input: { id: string }) => ({
+ address: { kind: 'entity' as const, entityType: 'trackedChange' as const, entityId: input.id },
+ id: input.id,
+ type: 'insert' as const,
+ })),
+ accept: vi.fn((_input) => ({ success: true as const })),
+ reject: vi.fn((_input) => ({ success: true as const })),
+ acceptAll: vi.fn((_input) => ({ success: true as const })),
+ rejectAll: vi.fn((_input) => ({ success: true as const })),
+ };
+}
+
+function makeCreateAdapter(): CreateAdapter {
+ return {
+ paragraph: vi.fn(() => ({
+ success: true as const,
+ paragraph: { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'new-p' },
+ insertionPoint: { kind: 'text' as const, blockId: 'new-p', range: { start: 0, end: 0 } },
+ })),
+ };
+}
+
+function makeListsAdapter(): ListsAdapter {
+ return {
+ list: vi.fn(() => ({ matches: [], total: 0, items: [] })),
+ get: vi.fn(() => ({
+ address: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' },
+ kind: 'ordered' as const,
+ level: 0,
+ text: 'List item',
+ })),
+ insert: vi.fn(() => ({
+ success: true as const,
+ item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-2' },
+ insertionPoint: { kind: 'text' as const, blockId: 'li-2', range: { start: 0, end: 0 } },
+ })),
+ setType: vi.fn(() => ({
+ success: true as const,
+ item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' },
+ })),
+ indent: vi.fn(() => ({
+ success: true as const,
+ item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' },
+ })),
+ outdent: vi.fn(() => ({
+ success: true as const,
+ item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' },
+ })),
+ restart: vi.fn(() => ({
+ success: true as const,
+ item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' },
+ })),
+ exit: vi.fn(() => ({
+ success: true as const,
+ paragraph: { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'p1' },
+ })),
+ };
+}
+
+function makeCapabilitiesAdapter(overrides?: Partial): CapabilitiesAdapter {
+ const defaultCapabilities: DocumentApiCapabilities = {
+ global: {
+ trackChanges: { enabled: false },
+ comments: { enabled: false },
+ lists: { enabled: false },
+ dryRun: { enabled: false },
+ },
+ operations: {} as DocumentApiCapabilities['operations'],
+ };
+ return {
+ get: vi.fn(() => ({ ...defaultCapabilities, ...overrides })),
+ };
+}
+
+const PARAGRAPH_ADDRESS: NodeAddress = { kind: 'block', nodeType: 'paragraph', nodeId: 'p1' };
+
+const PARAGRAPH_INFO: NodeInfo = {
+ nodeType: 'paragraph',
+ kind: 'block',
+ properties: {},
+};
+
+const QUERY_RESULT: QueryResult = {
+ matches: [PARAGRAPH_ADDRESS],
+ total: 1,
+};
+
+describe('createDocumentApi', () => {
+ it('delegates find to the find adapter', () => {
+ const findAdapter = makeFindAdapter(QUERY_RESULT);
+ const api = createDocumentApi({
+ find: findAdapter,
+ getNode: makeGetNodeAdapter(PARAGRAPH_INFO),
+ getText: makeGetTextAdapter(),
+ info: makeInfoAdapter(),
+ comments: makeCommentsAdapter(),
+ write: makeWriteAdapter(),
+ format: makeFormatAdapter(),
+ trackChanges: makeTrackChangesAdapter(),
+ create: makeCreateAdapter(),
+ lists: makeListsAdapter(),
+ });
+
+ const query: Query = { select: { nodeType: 'paragraph' } };
+ const result = api.find(query);
+
+ expect(result).toEqual(QUERY_RESULT);
+ expect(findAdapter.find).toHaveBeenCalledTimes(1);
+ });
+
+ it('delegates find with selector shorthand', () => {
+ const findAdapter = makeFindAdapter(QUERY_RESULT);
+ const api = createDocumentApi({
+ find: findAdapter,
+ getNode: makeGetNodeAdapter(PARAGRAPH_INFO),
+ getText: makeGetTextAdapter(),
+ info: makeInfoAdapter(),
+ comments: makeCommentsAdapter(),
+ write: makeWriteAdapter(),
+ format: makeFormatAdapter(),
+ trackChanges: makeTrackChangesAdapter(),
+ create: makeCreateAdapter(),
+ lists: makeListsAdapter(),
+ });
+
+ const result = api.find({ nodeType: 'paragraph' }, { limit: 5 });
+
+ expect(result).toEqual(QUERY_RESULT);
+ expect(findAdapter.find).toHaveBeenCalledTimes(1);
+ });
+
+ it('delegates getNode to the getNode adapter', () => {
+ const getNodeAdpt = makeGetNodeAdapter(PARAGRAPH_INFO);
+ const api = createDocumentApi({
+ find: makeFindAdapter(QUERY_RESULT),
+ getNode: getNodeAdpt,
+ getText: makeGetTextAdapter(),
+ info: makeInfoAdapter(),
+ comments: makeCommentsAdapter(),
+ write: makeWriteAdapter(),
+ format: makeFormatAdapter(),
+ trackChanges: makeTrackChangesAdapter(),
+ create: makeCreateAdapter(),
+ lists: makeListsAdapter(),
+ });
+
+ const info = api.getNode(PARAGRAPH_ADDRESS);
+
+ expect(info).toEqual(PARAGRAPH_INFO);
+ expect(getNodeAdpt.getNode).toHaveBeenCalledWith(PARAGRAPH_ADDRESS);
+ });
+
+ it('delegates getNodeById to the getNode adapter', () => {
+ const getNodeAdpt = makeGetNodeAdapter(PARAGRAPH_INFO);
+ const api = createDocumentApi({
+ find: makeFindAdapter(QUERY_RESULT),
+ getNode: getNodeAdpt,
+ getText: makeGetTextAdapter(),
+ info: makeInfoAdapter(),
+ comments: makeCommentsAdapter(),
+ write: makeWriteAdapter(),
+ format: makeFormatAdapter(),
+ trackChanges: makeTrackChangesAdapter(),
+ create: makeCreateAdapter(),
+ lists: makeListsAdapter(),
+ });
+
+ const info = api.getNodeById({ nodeId: 'p1', nodeType: 'paragraph' });
+
+ expect(info).toEqual(PARAGRAPH_INFO);
+ expect(getNodeAdpt.getNodeById).toHaveBeenCalledWith({ nodeId: 'p1', nodeType: 'paragraph' });
+ });
+
+ it('delegates getText to the getText adapter', () => {
+ const getTextAdpt = makeGetTextAdapter('Hello world');
+ const api = createDocumentApi({
+ find: makeFindAdapter(QUERY_RESULT),
+ getNode: makeGetNodeAdapter(PARAGRAPH_INFO),
+ getText: getTextAdpt,
+ info: makeInfoAdapter(),
+ comments: makeCommentsAdapter(),
+ write: makeWriteAdapter(),
+ format: makeFormatAdapter(),
+ trackChanges: makeTrackChangesAdapter(),
+ create: makeCreateAdapter(),
+ lists: makeListsAdapter(),
+ });
+
+ const text = api.getText({});
+
+ expect(text).toBe('Hello world');
+ expect(getTextAdpt.getText).toHaveBeenCalledWith({});
+ });
+
+ it('delegates info to the info adapter', () => {
+ const infoAdpt = makeInfoAdapter({
+ counts: { words: 42 },
+ outline: [{ level: 1, text: 'Heading', nodeId: 'h1' }],
+ });
+ const api = createDocumentApi({
+ find: makeFindAdapter(QUERY_RESULT),
+ getNode: makeGetNodeAdapter(PARAGRAPH_INFO),
+ getText: makeGetTextAdapter(),
+ info: infoAdpt,
+ comments: makeCommentsAdapter(),
+ write: makeWriteAdapter(),
+ format: makeFormatAdapter(),
+ trackChanges: makeTrackChangesAdapter(),
+ create: makeCreateAdapter(),
+ lists: makeListsAdapter(),
+ });
+
+ const result = api.info({});
+
+ expect(result.counts.words).toBe(42);
+ expect(result.outline).toEqual([{ level: 1, text: 'Heading', nodeId: 'h1' }]);
+ expect(infoAdpt.info).toHaveBeenCalledWith({});
+ });
+
+ it('delegates comments.add through the comments adapter', () => {
+ const commentsAdpt = makeCommentsAdapter();
+ const api = createDocumentApi({
+ find: makeFindAdapter(QUERY_RESULT),
+ getNode: makeGetNodeAdapter(PARAGRAPH_INFO),
+ getText: makeGetTextAdapter(),
+ info: makeInfoAdapter(),
+ comments: commentsAdpt,
+ write: makeWriteAdapter(),
+ format: makeFormatAdapter(),
+ trackChanges: makeTrackChangesAdapter(),
+ create: makeCreateAdapter(),
+ lists: makeListsAdapter(),
+ });
+
+ const input: AddCommentInput = {
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'test comment',
+ };
+ const receipt = api.comments.add(input);
+
+ expect(receipt.success).toBe(true);
+ expect(commentsAdpt.add).toHaveBeenCalledWith(input);
+ });
+
+ it('delegates all comments namespace commands through the comments adapter', () => {
+ const commentsAdpt = makeCommentsAdapter();
+ const api = createDocumentApi({
+ find: makeFindAdapter(QUERY_RESULT),
+ getNode: makeGetNodeAdapter(PARAGRAPH_INFO),
+ getText: makeGetTextAdapter(),
+ info: makeInfoAdapter(),
+ comments: commentsAdpt,
+ write: makeWriteAdapter(),
+ format: makeFormatAdapter(),
+ trackChanges: makeTrackChangesAdapter(),
+ create: makeCreateAdapter(),
+ lists: makeListsAdapter(),
+ });
+
+ const editInput: EditCommentInput = { commentId: 'c1', text: 'edited' };
+ const replyInput: ReplyToCommentInput = { parentCommentId: 'c1', text: 'reply' };
+ const moveInput: MoveCommentInput = {
+ commentId: 'c1',
+ target: { kind: 'text', blockId: 'p1', range: { start: 1, end: 3 } },
+ };
+ const resolveInput: ResolveCommentInput = { commentId: 'c1' };
+ const removeInput: RemoveCommentInput = { commentId: 'c1' };
+ const setInternalInput: SetCommentInternalInput = { commentId: 'c1', isInternal: true };
+ const setActiveInput: SetCommentActiveInput = { commentId: 'c1' };
+ const goToInput: GoToCommentInput = { commentId: 'c1' };
+ const getInput: GetCommentInput = { commentId: 'c1' };
+ const listQuery: CommentsListQuery = { includeResolved: false };
+
+ const editReceipt = api.comments.edit(editInput);
+ const replyReceipt = api.comments.reply(replyInput);
+ const moveReceipt = api.comments.move(moveInput);
+ const resolveReceipt = api.comments.resolve(resolveInput);
+ const removeReceipt = api.comments.remove(removeInput);
+ const setInternalReceipt = api.comments.setInternal(setInternalInput);
+ const setActiveReceipt = api.comments.setActive(setActiveInput);
+ const goToReceipt = api.comments.goTo(goToInput);
+ const getResult = api.comments.get(getInput);
+ const listResult = api.comments.list(listQuery);
+
+ expect(editReceipt.success).toBe(true);
+ expect(replyReceipt.success).toBe(true);
+ expect(moveReceipt.success).toBe(true);
+ expect(resolveReceipt.success).toBe(true);
+ expect(removeReceipt.success).toBe(true);
+ expect(setInternalReceipt.success).toBe(true);
+ expect(setActiveReceipt.success).toBe(true);
+ expect(goToReceipt.success).toBe(true);
+ expect((getResult as CommentInfo).commentId).toBe('c1');
+ expect((listResult as CommentsListResult).total).toBe(0);
+
+ expect(commentsAdpt.edit).toHaveBeenCalledWith(editInput);
+ expect(commentsAdpt.reply).toHaveBeenCalledWith(replyInput);
+ expect(commentsAdpt.move).toHaveBeenCalledWith(moveInput);
+ expect(commentsAdpt.resolve).toHaveBeenCalledWith(resolveInput);
+ expect(commentsAdpt.remove).toHaveBeenCalledWith(removeInput);
+ expect(commentsAdpt.setInternal).toHaveBeenCalledWith(setInternalInput);
+ expect(commentsAdpt.setActive).toHaveBeenCalledWith(setActiveInput);
+ expect(commentsAdpt.goTo).toHaveBeenCalledWith(goToInput);
+ expect(commentsAdpt.get).toHaveBeenCalledWith(getInput);
+ expect(commentsAdpt.list).toHaveBeenCalledWith(listQuery);
+ });
+
+ it('delegates write operations through the shared write adapter', () => {
+ const writeAdpt = makeWriteAdapter();
+ const api = createDocumentApi({
+ find: makeFindAdapter(QUERY_RESULT),
+ getNode: makeGetNodeAdapter(PARAGRAPH_INFO),
+ getText: makeGetTextAdapter(),
+ info: makeInfoAdapter(),
+ comments: makeCommentsAdapter(),
+ write: writeAdpt,
+ format: makeFormatAdapter(),
+ trackChanges: makeTrackChangesAdapter(),
+ create: makeCreateAdapter(),
+ lists: makeListsAdapter(),
+ });
+
+ const target = { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } } as const;
+ api.insert({ text: 'Hi' });
+ api.insert({ target, text: 'Yo' });
+ api.replace({ target, text: 'Hello' }, { changeMode: 'tracked' });
+ api.delete({ target });
+
+ expect(writeAdpt.write).toHaveBeenNthCalledWith(
+ 1,
+ { kind: 'insert', text: 'Hi' },
+ { changeMode: 'direct', dryRun: false },
+ );
+ expect(writeAdpt.write).toHaveBeenNthCalledWith(
+ 2,
+ { kind: 'insert', target, text: 'Yo' },
+ { changeMode: 'direct', dryRun: false },
+ );
+ expect(writeAdpt.write).toHaveBeenNthCalledWith(
+ 3,
+ { kind: 'replace', target, text: 'Hello' },
+ { changeMode: 'tracked', dryRun: false },
+ );
+ expect(writeAdpt.write).toHaveBeenNthCalledWith(
+ 4,
+ { kind: 'delete', target, text: '' },
+ { changeMode: 'direct', dryRun: false },
+ );
+ });
+
+ it('delegates format.bold to the format adapter', () => {
+ const formatAdpt = makeFormatAdapter();
+ const api = createDocumentApi({
+ find: makeFindAdapter(QUERY_RESULT),
+ getNode: makeGetNodeAdapter(PARAGRAPH_INFO),
+ getText: makeGetTextAdapter(),
+ info: makeInfoAdapter(),
+ comments: makeCommentsAdapter(),
+ write: makeWriteAdapter(),
+ format: formatAdpt,
+ trackChanges: makeTrackChangesAdapter(),
+ create: makeCreateAdapter(),
+ lists: makeListsAdapter(),
+ });
+
+ const target = { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } } as const;
+ api.format.bold({ target }, { changeMode: 'tracked' });
+ expect(formatAdpt.bold).toHaveBeenCalledWith({ target }, { changeMode: 'tracked', dryRun: false });
+ });
+
+ it('delegates trackChanges namespace operations', () => {
+ const trackAdpt = makeTrackChangesAdapter();
+ const api = createDocumentApi({
+ find: makeFindAdapter(QUERY_RESULT),
+ getNode: makeGetNodeAdapter(PARAGRAPH_INFO),
+ getText: makeGetTextAdapter(),
+ info: makeInfoAdapter(),
+ comments: makeCommentsAdapter(),
+ write: makeWriteAdapter(),
+ format: makeFormatAdapter(),
+ trackChanges: trackAdpt,
+ create: makeCreateAdapter(),
+ lists: makeListsAdapter(),
+ });
+
+ const listResult = api.trackChanges.list({ limit: 1 });
+ const getResult = api.trackChanges.get({ id: 'tc-1' });
+ const acceptResult = api.trackChanges.accept({ id: 'tc-1' });
+ const rejectResult = api.trackChanges.reject({ id: 'tc-1' });
+ const acceptAllResult = api.trackChanges.acceptAll({});
+ const rejectAllResult = api.trackChanges.rejectAll({});
+
+ expect(listResult.total).toBe(0);
+ expect(getResult.id).toBe('tc-1');
+ expect(acceptResult.success).toBe(true);
+ expect(rejectResult.success).toBe(true);
+ expect(acceptAllResult.success).toBe(true);
+ expect(rejectAllResult.success).toBe(true);
+ expect(trackAdpt.list).toHaveBeenCalledWith({ limit: 1 });
+ expect(trackAdpt.get).toHaveBeenCalledWith({ id: 'tc-1' });
+ });
+
+ it('delegates create.paragraph to the create adapter', () => {
+ const createAdpt = makeCreateAdapter();
+ const api = createDocumentApi({
+ find: makeFindAdapter(QUERY_RESULT),
+ getNode: makeGetNodeAdapter(PARAGRAPH_INFO),
+ getText: makeGetTextAdapter(),
+ info: makeInfoAdapter(),
+ comments: makeCommentsAdapter(),
+ write: makeWriteAdapter(),
+ format: makeFormatAdapter(),
+ trackChanges: makeTrackChangesAdapter(),
+ create: createAdpt,
+ lists: makeListsAdapter(),
+ });
+
+ const result = api.create.paragraph(
+ {
+ at: { kind: 'documentEnd' },
+ text: 'Created paragraph',
+ },
+ { changeMode: 'tracked' },
+ );
+
+ expect(result.success).toBe(true);
+ expect(createAdpt.paragraph).toHaveBeenCalledWith(
+ {
+ at: { kind: 'documentEnd' },
+ text: 'Created paragraph',
+ },
+ { changeMode: 'tracked', dryRun: false },
+ );
+ });
+
+ it('delegates lists namespace operations', () => {
+ const listsAdpt = makeListsAdapter();
+ const api = createDocumentApi({
+ find: makeFindAdapter(QUERY_RESULT),
+ getNode: makeGetNodeAdapter(PARAGRAPH_INFO),
+ getText: makeGetTextAdapter(),
+ info: makeInfoAdapter(),
+ comments: makeCommentsAdapter(),
+ write: makeWriteAdapter(),
+ format: makeFormatAdapter(),
+ trackChanges: makeTrackChangesAdapter(),
+ create: makeCreateAdapter(),
+ lists: listsAdpt,
+ });
+
+ const target = { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } as const;
+ const listResult = api.lists.list({ limit: 1 });
+ const getResult = api.lists.get({ address: target });
+ const insertResult = api.lists.insert({ target, position: 'after', text: 'Inserted' }, { changeMode: 'tracked' });
+ const setTypeResult = api.lists.setType({ target, kind: 'bullet' });
+ const indentResult = api.lists.indent({ target });
+ const outdentResult = api.lists.outdent({ target });
+ const restartResult = api.lists.restart({ target });
+ const exitResult = api.lists.exit({ target });
+
+ expect(listResult.total).toBe(0);
+ expect(getResult.address).toEqual(target);
+ expect(insertResult.success).toBe(true);
+ expect(setTypeResult.success).toBe(true);
+ expect(indentResult.success).toBe(true);
+ expect(outdentResult.success).toBe(true);
+ expect(restartResult.success).toBe(true);
+ expect(exitResult.success).toBe(true);
+
+ expect(listsAdpt.list).toHaveBeenCalledWith({ limit: 1 });
+ expect(listsAdpt.get).toHaveBeenCalledWith({ address: target });
+ expect(listsAdpt.insert).toHaveBeenCalledWith(
+ { target, position: 'after', text: 'Inserted' },
+ { changeMode: 'tracked', dryRun: false },
+ );
+ expect(listsAdpt.setType).toHaveBeenCalledWith({ target, kind: 'bullet' }, { changeMode: 'direct', dryRun: false });
+ expect(listsAdpt.indent).toHaveBeenCalledWith({ target }, { changeMode: 'direct', dryRun: false });
+ expect(listsAdpt.outdent).toHaveBeenCalledWith({ target }, { changeMode: 'direct', dryRun: false });
+ expect(listsAdpt.restart).toHaveBeenCalledWith({ target }, { changeMode: 'direct', dryRun: false });
+ expect(listsAdpt.exit).toHaveBeenCalledWith({ target }, { changeMode: 'direct', dryRun: false });
+ });
+
+ it('exposes capabilities as a callable function with .get() alias', () => {
+ const capAdpt = makeCapabilitiesAdapter();
+ const api = createDocumentApi({
+ find: makeFindAdapter(QUERY_RESULT),
+ getNode: makeGetNodeAdapter(PARAGRAPH_INFO),
+ getText: makeGetTextAdapter(),
+ info: makeInfoAdapter(),
+ capabilities: capAdpt,
+ comments: makeCommentsAdapter(),
+ write: makeWriteAdapter(),
+ format: makeFormatAdapter(),
+ trackChanges: makeTrackChangesAdapter(),
+ create: makeCreateAdapter(),
+ lists: makeListsAdapter(),
+ });
+
+ const directResult = api.capabilities();
+ const getResult = api.capabilities.get();
+
+ expect(directResult).toEqual(getResult);
+ expect(capAdpt.get).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/packages/document-api/src/index.ts b/packages/document-api/src/index.ts
new file mode 100644
index 0000000000..6d85076540
--- /dev/null
+++ b/packages/document-api/src/index.ts
@@ -0,0 +1,429 @@
+/**
+ * Engine-agnostic Document API surface.
+ */
+
+export * from './types/index.js';
+export * from './contract/index.js';
+export * from './capabilities/capabilities.js';
+
+import type {
+ CreateParagraphInput,
+ CreateParagraphResult,
+ DocumentInfo,
+ NodeAddress,
+ NodeInfo,
+ Query,
+ QueryResult,
+ Receipt,
+ Selector,
+ TextMutationReceipt,
+ TrackChangeInfo,
+ TrackChangesListResult,
+} from './types/index.js';
+import type { CommentInfo, CommentsListQuery, CommentsListResult } from './comments/comments.types.js';
+import type {
+ AddCommentInput,
+ CommentsAdapter,
+ CommentsApi,
+ EditCommentInput,
+ GetCommentInput,
+ GoToCommentInput,
+ MoveCommentInput,
+ RemoveCommentInput,
+ ReplyToCommentInput,
+ ResolveCommentInput,
+ SetCommentActiveInput,
+ SetCommentInternalInput,
+} from './comments/comments.js';
+import {
+ executeAddComment,
+ executeEditComment,
+ executeGetComment,
+ executeGoToComment,
+ executeListComments,
+ executeMoveComment,
+ executeRemoveComment,
+ executeReplyToComment,
+ executeResolveComment,
+ executeSetCommentActive,
+ executeSetCommentInternal,
+} from './comments/comments.js';
+import type { DeleteInput } from './delete/delete.js';
+import { executeFind, type FindAdapter, type FindOptions } from './find/find.js';
+import type { FormatAdapter, FormatApi, FormatBoldInput } from './format/format.js';
+import { executeFormatBold } from './format/format.js';
+import type { GetNodeAdapter, GetNodeByIdInput } from './get-node/get-node.js';
+import { executeGetNode, executeGetNodeById } from './get-node/get-node.js';
+import { executeGetText, type GetTextAdapter, type GetTextInput } from './get-text/get-text.js';
+import { executeInfo, type InfoAdapter, type InfoInput } from './info/info.js';
+import type { InsertInput } from './insert/insert.js';
+import { executeDelete } from './delete/delete.js';
+import { executeInsert } from './insert/insert.js';
+import type { ListsAdapter, ListsApi } from './lists/lists.js';
+import type {
+ ListItemInfo,
+ ListInsertInput,
+ ListSetTypeInput,
+ ListsExitResult,
+ ListsGetInput,
+ ListsInsertResult,
+ ListsListQuery,
+ ListsListResult,
+ ListsMutateItemResult,
+ ListTargetInput,
+} from './lists/lists.types.js';
+import {
+ executeListsExit,
+ executeListsGet,
+ executeListsIndent,
+ executeListsInsert,
+ executeListsList,
+ executeListsOutdent,
+ executeListsRestart,
+ executeListsSetType,
+} from './lists/lists.js';
+import { executeReplace, type ReplaceInput } from './replace/replace.js';
+import type { CreateAdapter, CreateApi } from './create/create.js';
+import { executeCreateParagraph } from './create/create.js';
+import type {
+ TrackChangesAcceptAllInput,
+ TrackChangesAcceptInput,
+ TrackChangesAdapter,
+ TrackChangesApi,
+ TrackChangesGetInput,
+ TrackChangesListInput,
+ TrackChangesRejectAllInput,
+ TrackChangesRejectInput,
+} from './track-changes/track-changes.js';
+import {
+ executeTrackChangesAccept,
+ executeTrackChangesAcceptAll,
+ executeTrackChangesGet,
+ executeTrackChangesList,
+ executeTrackChangesReject,
+ executeTrackChangesRejectAll,
+} from './track-changes/track-changes.js';
+import type { MutationOptions, WriteAdapter } from './write/write.js';
+import {
+ executeCapabilities,
+ type CapabilitiesAdapter,
+ type DocumentApiCapabilities,
+} from './capabilities/capabilities.js';
+import type { OperationId } from './contract/types.js';
+import type { DynamicInvokeRequest, InvokeRequest, InvokeResult } from './contract/operation-registry.js';
+import { buildDispatchTable } from './invoke/invoke.js';
+
+export type { FindAdapter, FindOptions } from './find/find.js';
+export type { GetNodeAdapter, GetNodeByIdInput } from './get-node/get-node.js';
+export type { GetTextAdapter, GetTextInput } from './get-text/get-text.js';
+export type { InfoAdapter, InfoInput } from './info/info.js';
+export type { MutationOptions, WriteAdapter, WriteRequest } from './write/write.js';
+export type { FormatAdapter, FormatBoldInput } from './format/format.js';
+export type { CreateAdapter } from './create/create.js';
+export type {
+ TrackChangesAcceptAllInput,
+ TrackChangesAcceptInput,
+ TrackChangesAdapter,
+ TrackChangesGetInput,
+ TrackChangesListInput,
+ TrackChangesRejectAllInput,
+ TrackChangesRejectInput,
+} from './track-changes/track-changes.js';
+export type { ListsAdapter } from './lists/lists.js';
+export type {
+ ListInsertInput,
+ ListItemAddress,
+ ListItemInfo,
+ ListKind,
+ ListsExitResult,
+ ListsGetInput,
+ ListsInsertResult,
+ ListsListQuery,
+ ListsListResult,
+ ListsMutateItemResult,
+ ListSetTypeInput,
+ ListTargetInput,
+} from './lists/lists.types.js';
+export { LIST_KINDS, LIST_INSERT_POSITIONS } from './lists/lists.types.js';
+export type {
+ AddCommentInput,
+ CommentsAdapter,
+ EditCommentInput,
+ GetCommentInput,
+ GoToCommentInput,
+ MoveCommentInput,
+ RemoveCommentInput,
+ ReplyToCommentInput,
+ ResolveCommentInput,
+ SetCommentActiveInput,
+ SetCommentInternalInput,
+} from './comments/comments.js';
+export type { CommentInfo, CommentsListQuery, CommentsListResult } from './comments/comments.types.js';
+
+/**
+ * Callable capability accessor returned by `createDocumentApi`.
+ *
+ * Can be invoked directly (`capabilities()`) or via the `.get()` alias.
+ */
+export interface CapabilitiesApi {
+ (): DocumentApiCapabilities;
+ get(): DocumentApiCapabilities;
+}
+
+/**
+ * The Document API interface for querying and inspecting document nodes.
+ */
+export interface DocumentApi {
+ /**
+ * Find nodes in the document matching a query.
+ * @param query - A full query object specifying selection criteria.
+ * @returns The query result containing matches and metadata.
+ */
+ find(query: Query): QueryResult;
+ /**
+ * Find nodes in the document matching a selector with optional options.
+ * @param selector - A selector specifying what to find.
+ * @param options - Optional find options (limit, offset, within, etc.).
+ * @returns The query result containing matches and metadata.
+ */
+ find(selector: Selector, options?: FindOptions): QueryResult;
+ /**
+ * Get detailed information about a specific node by its address.
+ * @param address - The node address to resolve.
+ * @returns Full node information including typed properties.
+ */
+ getNode(address: NodeAddress): NodeInfo;
+ /**
+ * Get detailed information about a block node by its ID.
+ * @param input - The node-id input payload.
+ * @returns Full node information including typed properties.
+ */
+ getNodeById(input: GetNodeByIdInput): NodeInfo;
+ /**
+ * Return the full document text content.
+ */
+ getText(input: GetTextInput): string;
+ /**
+ * Return document summary info used by `doc.info`.
+ */
+ info(input: InfoInput): DocumentInfo;
+ /**
+ * Comment operations.
+ */
+ comments: CommentsApi;
+ /**
+ * Insert text at a target location.
+ * If target is omitted, adapters resolve a deterministic default insertion point.
+ */
+ insert(input: InsertInput, options?: MutationOptions): TextMutationReceipt;
+ /**
+ * Replace text at a target range.
+ */
+ replace(input: ReplaceInput, options?: MutationOptions): TextMutationReceipt;
+ /**
+ * Delete text at a target range.
+ */
+ delete(input: DeleteInput, options?: MutationOptions): TextMutationReceipt;
+ /**
+ * Formatting operations.
+ */
+ format: FormatApi;
+ /**
+ * Tracked-change lifecycle operations.
+ */
+ trackChanges: TrackChangesApi;
+ /**
+ * Structural creation operations.
+ */
+ create: CreateApi;
+ /**
+ * List item operations.
+ */
+ lists: ListsApi;
+ /**
+ * Runtime capability introspection.
+ *
+ * Callable directly (`capabilities()`) or via `.get()`.
+ */
+ capabilities: CapabilitiesApi;
+ /**
+ * Dynamically dispatch any operation by its operation ID.
+ *
+ * For TypeScript consumers, the return type narrows based on the operationId.
+ * For dynamic callers (AI agents, automation), accepts {@link DynamicInvokeRequest}
+ * with `unknown` input. Invalid inputs produce adapter-level errors.
+ *
+ * @param request - Operation envelope with operationId, input, and optional options.
+ * @returns The operation-specific result payload from the dispatched handler.
+ * @throws {Error} When operationId is unknown.
+ */
+ invoke(request: InvokeRequest): InvokeResult;
+ invoke(request: DynamicInvokeRequest): unknown;
+}
+
+export interface DocumentApiAdapters {
+ find: FindAdapter;
+ getNode: GetNodeAdapter;
+ getText: GetTextAdapter;
+ info: InfoAdapter;
+ capabilities: CapabilitiesAdapter;
+ comments: CommentsAdapter;
+ write: WriteAdapter;
+ format: FormatAdapter;
+ trackChanges: TrackChangesAdapter;
+ create: CreateAdapter;
+ lists: ListsAdapter;
+}
+
+/**
+ * Creates a Document API instance from the provided adapters.
+ *
+ * @param adapters - Engine-specific adapters (find, getNode, comments, write, format, trackChanges, create, lists).
+ * @returns A {@link DocumentApi} instance.
+ *
+ * @example
+ * ```ts
+ * const api = createDocumentApi(adapters);
+ * const result = api.find({ nodeType: 'heading' });
+ * for (const address of result.matches) {
+ * const node = api.getNode(address);
+ * console.log(node.properties);
+ * }
+ * ```
+ */
+export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi {
+ const capFn = () => executeCapabilities(adapters.capabilities);
+ const capabilities: CapabilitiesApi = Object.assign(capFn, { get: capFn });
+
+ const api: DocumentApi = {
+ find(selectorOrQuery: Selector | Query, options?: FindOptions): QueryResult {
+ return executeFind(adapters.find, selectorOrQuery, options);
+ },
+ getNode(address: NodeAddress): NodeInfo {
+ return executeGetNode(adapters.getNode, address);
+ },
+ getNodeById(input: GetNodeByIdInput): NodeInfo {
+ return executeGetNodeById(adapters.getNode, input);
+ },
+ getText(input: GetTextInput): string {
+ return executeGetText(adapters.getText, input);
+ },
+ info(input: InfoInput): DocumentInfo {
+ return executeInfo(adapters.info, input);
+ },
+ comments: {
+ add(input: AddCommentInput): Receipt {
+ return executeAddComment(adapters.comments, input);
+ },
+ edit(input: EditCommentInput): Receipt {
+ return executeEditComment(adapters.comments, input);
+ },
+ reply(input: ReplyToCommentInput): Receipt {
+ return executeReplyToComment(adapters.comments, input);
+ },
+ move(input: MoveCommentInput): Receipt {
+ return executeMoveComment(adapters.comments, input);
+ },
+ resolve(input: ResolveCommentInput): Receipt {
+ return executeResolveComment(adapters.comments, input);
+ },
+ remove(input: RemoveCommentInput): Receipt {
+ return executeRemoveComment(adapters.comments, input);
+ },
+ setInternal(input: SetCommentInternalInput): Receipt {
+ return executeSetCommentInternal(adapters.comments, input);
+ },
+ setActive(input: SetCommentActiveInput): Receipt {
+ return executeSetCommentActive(adapters.comments, input);
+ },
+ goTo(input: GoToCommentInput): Receipt {
+ return executeGoToComment(adapters.comments, input);
+ },
+ get(input: GetCommentInput): CommentInfo {
+ return executeGetComment(adapters.comments, input);
+ },
+ list(query?: CommentsListQuery): CommentsListResult {
+ return executeListComments(adapters.comments, query);
+ },
+ },
+ insert(input: InsertInput, options?: MutationOptions): TextMutationReceipt {
+ return executeInsert(adapters.write, input, options);
+ },
+ replace(input: ReplaceInput, options?: MutationOptions): TextMutationReceipt {
+ return executeReplace(adapters.write, input, options);
+ },
+ delete(input: DeleteInput, options?: MutationOptions): TextMutationReceipt {
+ return executeDelete(adapters.write, input, options);
+ },
+ format: {
+ bold(input: FormatBoldInput, options?: MutationOptions): TextMutationReceipt {
+ return executeFormatBold(adapters.format, input, options);
+ },
+ },
+ trackChanges: {
+ list(input?: TrackChangesListInput): TrackChangesListResult {
+ return executeTrackChangesList(adapters.trackChanges, input);
+ },
+ get(input: TrackChangesGetInput): TrackChangeInfo {
+ return executeTrackChangesGet(adapters.trackChanges, input);
+ },
+ accept(input: TrackChangesAcceptInput): Receipt {
+ return executeTrackChangesAccept(adapters.trackChanges, input);
+ },
+ reject(input: TrackChangesRejectInput): Receipt {
+ return executeTrackChangesReject(adapters.trackChanges, input);
+ },
+ acceptAll(input: TrackChangesAcceptAllInput): Receipt {
+ return executeTrackChangesAcceptAll(adapters.trackChanges, input);
+ },
+ rejectAll(input: TrackChangesRejectAllInput): Receipt {
+ return executeTrackChangesRejectAll(adapters.trackChanges, input);
+ },
+ },
+ create: {
+ paragraph(input: CreateParagraphInput, options?: MutationOptions): CreateParagraphResult {
+ return executeCreateParagraph(adapters.create, input, options);
+ },
+ },
+ capabilities,
+ lists: {
+ list(query?: ListsListQuery): ListsListResult {
+ return executeListsList(adapters.lists, query);
+ },
+ get(input: ListsGetInput): ListItemInfo {
+ return executeListsGet(adapters.lists, input);
+ },
+ insert(input: ListInsertInput, options?: MutationOptions): ListsInsertResult {
+ return executeListsInsert(adapters.lists, input, options);
+ },
+ setType(input: ListSetTypeInput, options?: MutationOptions): ListsMutateItemResult {
+ return executeListsSetType(adapters.lists, input, options);
+ },
+ indent(input: ListTargetInput, options?: MutationOptions): ListsMutateItemResult {
+ return executeListsIndent(adapters.lists, input, options);
+ },
+ outdent(input: ListTargetInput, options?: MutationOptions): ListsMutateItemResult {
+ return executeListsOutdent(adapters.lists, input, options);
+ },
+ restart(input: ListTargetInput, options?: MutationOptions): ListsMutateItemResult {
+ return executeListsRestart(adapters.lists, input, options);
+ },
+ exit(input: ListTargetInput, options?: MutationOptions): ListsExitResult {
+ return executeListsExit(adapters.lists, input, options);
+ },
+ },
+ invoke(request: DynamicInvokeRequest): unknown {
+ if (!Object.prototype.hasOwnProperty.call(dispatch, request.operationId)) {
+ throw new Error(`Unknown operationId: "${request.operationId}"`);
+ }
+ // Safe: InvokeRequest provides caller-side type safety.
+ // Dynamic callers accept adapter-level validation.
+ const handler = dispatch[request.operationId] as unknown as (input: unknown, options?: unknown) => unknown;
+ return handler(request.input, request.options);
+ },
+ };
+
+ const dispatch = buildDispatchTable(api);
+
+ return api;
+}
diff --git a/packages/document-api/src/info/info.test.ts b/packages/document-api/src/info/info.test.ts
new file mode 100644
index 0000000000..ed9588d1be
--- /dev/null
+++ b/packages/document-api/src/info/info.test.ts
@@ -0,0 +1,22 @@
+import type { DocumentInfo } from '../types/index.js';
+import { executeInfo } from './info.js';
+import type { InfoAdapter } from './info.js';
+
+const DEFAULT_INFO: DocumentInfo = {
+ counts: { words: 42, paragraphs: 3, headings: 1, tables: 0, images: 0, comments: 0 },
+ outline: [{ level: 1, text: 'Heading', nodeId: 'h1' }],
+ capabilities: { canFind: true, canGetNode: true, canComment: true, canReplace: true },
+};
+
+describe('executeInfo', () => {
+ it('delegates to adapter.info with the input', () => {
+ const adapter: InfoAdapter = {
+ info: vi.fn(() => DEFAULT_INFO),
+ };
+
+ const result = executeInfo(adapter, {});
+
+ expect(result).toBe(DEFAULT_INFO);
+ expect(adapter.info).toHaveBeenCalledWith({});
+ });
+});
diff --git a/packages/document-api/src/info/info.ts b/packages/document-api/src/info/info.ts
new file mode 100644
index 0000000000..34e5b8b852
--- /dev/null
+++ b/packages/document-api/src/info/info.ts
@@ -0,0 +1,24 @@
+import type { DocumentInfo } from '../types/info.types.js';
+
+export type InfoInput = Record;
+
+/**
+ * Engine-specific adapter that provides document summary information.
+ */
+export interface InfoAdapter {
+ /**
+ * Return summary info used by the `doc.info` operation.
+ */
+ info(input: InfoInput): DocumentInfo;
+}
+
+/**
+ * Execute an info operation through the provided adapter.
+ *
+ * @param adapter - Engine-specific info adapter.
+ * @param input - Canonical info input object.
+ * @returns Structured document summary info.
+ */
+export function executeInfo(adapter: InfoAdapter, input: InfoInput): DocumentInfo {
+ return adapter.info(input);
+}
diff --git a/packages/document-api/src/insert/insert.ts b/packages/document-api/src/insert/insert.ts
new file mode 100644
index 0000000000..b603c8331d
--- /dev/null
+++ b/packages/document-api/src/insert/insert.ts
@@ -0,0 +1,26 @@
+import { executeWrite, type MutationOptions, type WriteAdapter } from '../write/write.js';
+import type { TextAddress, TextMutationReceipt } from '../types/index.js';
+
+export interface InsertInput {
+ target?: TextAddress;
+ text: string;
+}
+
+export function executeInsert(
+ adapter: WriteAdapter,
+ input: InsertInput,
+ options?: MutationOptions,
+): TextMutationReceipt {
+ const request = input.target
+ ? {
+ kind: 'insert' as const,
+ target: input.target,
+ text: input.text,
+ }
+ : {
+ kind: 'insert' as const,
+ text: input.text,
+ };
+
+ return executeWrite(adapter, request, options);
+}
diff --git a/packages/document-api/src/invoke/invoke.test.ts b/packages/document-api/src/invoke/invoke.test.ts
new file mode 100644
index 0000000000..93493f2340
--- /dev/null
+++ b/packages/document-api/src/invoke/invoke.test.ts
@@ -0,0 +1,273 @@
+import { describe, it, expect, vi } from 'vitest';
+import { OPERATION_IDS, type OperationId } from '../contract/types.js';
+import { createDocumentApi, type DocumentApiAdapters } from '../index.js';
+import { buildDispatchTable } from './invoke.js';
+import type { FindAdapter } from '../find/find.js';
+import type { GetNodeAdapter } from '../get-node/get-node.js';
+import type { WriteAdapter } from '../write/write.js';
+import type { FormatAdapter } from '../format/format.js';
+import type { TrackChangesAdapter } from '../track-changes/track-changes.js';
+import type { CreateAdapter } from '../create/create.js';
+import type { ListsAdapter } from '../lists/lists.js';
+import type { CommentsAdapter } from '../comments/comments.js';
+import type { CapabilitiesAdapter, DocumentApiCapabilities } from '../capabilities/capabilities.js';
+
+function makeAdapters() {
+ const findAdapter: FindAdapter = { find: vi.fn(() => ({ matches: [], total: 0 })) };
+ const getNodeAdapter: GetNodeAdapter = {
+ getNode: vi.fn(() => ({ kind: 'block' as const, nodeType: 'paragraph' as const, properties: {} })),
+ getNodeById: vi.fn(() => ({ kind: 'block' as const, nodeType: 'paragraph' as const, properties: {} })),
+ };
+ const getTextAdapter = { getText: vi.fn(() => 'hello') };
+ const infoAdapter = {
+ info: vi.fn(() => ({
+ counts: { words: 1, paragraphs: 1, headings: 0, tables: 0, images: 0, comments: 0 },
+ outline: [],
+ capabilities: { canFind: true, canGetNode: true, canComment: true, canReplace: true },
+ })),
+ };
+ const capabilitiesAdapter: CapabilitiesAdapter = {
+ get: vi.fn(
+ (): DocumentApiCapabilities => ({
+ global: {
+ trackChanges: { enabled: false },
+ comments: { enabled: false },
+ lists: { enabled: false },
+ dryRun: { enabled: false },
+ },
+ operations: {} as DocumentApiCapabilities['operations'],
+ }),
+ ),
+ };
+ const commentsAdapter: CommentsAdapter = {
+ add: vi.fn(() => ({ success: true as const })),
+ edit: vi.fn(() => ({ success: true as const })),
+ reply: vi.fn(() => ({ success: true as const })),
+ move: vi.fn(() => ({ success: true as const })),
+ resolve: vi.fn(() => ({ success: true as const })),
+ remove: vi.fn(() => ({ success: true as const })),
+ setInternal: vi.fn(() => ({ success: true as const })),
+ setActive: vi.fn(() => ({ success: true as const })),
+ goTo: vi.fn(() => ({ success: true as const })),
+ get: vi.fn(() => ({
+ address: { kind: 'entity' as const, entityType: 'comment' as const, entityId: 'c1' },
+ commentId: 'c1',
+ status: 'open' as const,
+ })),
+ list: vi.fn(() => ({ matches: [], total: 0 })),
+ };
+ const writeAdapter: WriteAdapter = {
+ write: vi.fn(() => ({
+ success: true as const,
+ resolution: {
+ target: { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 0 } },
+ range: { from: 1, to: 1 },
+ text: '',
+ },
+ })),
+ };
+ const formatAdapter: FormatAdapter = {
+ bold: vi.fn(() => ({
+ success: true as const,
+ resolution: {
+ target: { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 2 } },
+ range: { from: 1, to: 3 },
+ text: 'Hi',
+ },
+ })),
+ };
+ const trackChangesAdapter: TrackChangesAdapter = {
+ list: vi.fn(() => ({ matches: [], total: 0 })),
+ get: vi.fn((input: { id: string }) => ({
+ address: { kind: 'entity' as const, entityType: 'trackedChange' as const, entityId: input.id },
+ id: input.id,
+ type: 'insert' as const,
+ })),
+ accept: vi.fn(() => ({ success: true as const })),
+ reject: vi.fn(() => ({ success: true as const })),
+ acceptAll: vi.fn(() => ({ success: true as const })),
+ rejectAll: vi.fn(() => ({ success: true as const })),
+ };
+ const createAdapter: CreateAdapter = {
+ paragraph: vi.fn(() => ({
+ success: true as const,
+ paragraph: { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'new-p' },
+ insertionPoint: { kind: 'text' as const, blockId: 'new-p', range: { start: 0, end: 0 } },
+ })),
+ };
+ const listsAdapter: ListsAdapter = {
+ list: vi.fn(() => ({ matches: [], total: 0, items: [] })),
+ get: vi.fn(() => ({
+ address: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' },
+ })),
+ insert: vi.fn(() => ({
+ success: true as const,
+ item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-2' },
+ insertionPoint: { kind: 'text' as const, blockId: 'li-2', range: { start: 0, end: 0 } },
+ })),
+ setType: vi.fn(() => ({
+ success: true as const,
+ item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' },
+ })),
+ indent: vi.fn(() => ({
+ success: true as const,
+ item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' },
+ })),
+ outdent: vi.fn(() => ({
+ success: true as const,
+ item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' },
+ })),
+ restart: vi.fn(() => ({
+ success: true as const,
+ item: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' },
+ })),
+ exit: vi.fn(() => ({
+ success: true as const,
+ paragraph: { kind: 'block' as const, nodeType: 'paragraph' as const, nodeId: 'p3' },
+ })),
+ };
+
+ const adapters: DocumentApiAdapters = {
+ find: findAdapter,
+ getNode: getNodeAdapter,
+ getText: getTextAdapter,
+ info: infoAdapter,
+ capabilities: capabilitiesAdapter,
+ comments: commentsAdapter,
+ write: writeAdapter,
+ format: formatAdapter,
+ trackChanges: trackChangesAdapter,
+ create: createAdapter,
+ lists: listsAdapter,
+ };
+
+ return { adapters, findAdapter, writeAdapter, commentsAdapter, trackChangesAdapter };
+}
+
+describe('invoke', () => {
+ describe('dispatch table completeness', () => {
+ it('has an entry for every OperationId', () => {
+ const { adapters } = makeAdapters();
+ const api = createDocumentApi(adapters);
+ const dispatchKeys = Object.keys(buildDispatchTable(api)).sort();
+ const operationIds = [...OPERATION_IDS].sort();
+ expect(dispatchKeys).toEqual(operationIds);
+ });
+
+ it('has no extra entries beyond OperationId', () => {
+ const { adapters } = makeAdapters();
+ const api = createDocumentApi(adapters);
+ const dispatchKeys = Object.keys(buildDispatchTable(api));
+ const operationIdSet = new Set(OPERATION_IDS);
+ const extraKeys = dispatchKeys.filter((key) => !operationIdSet.has(key));
+ expect(extraKeys).toEqual([]);
+ });
+ });
+
+ describe('representative parity (invoke matches direct method)', () => {
+ it('find: invoke returns same result as direct call', () => {
+ const { adapters } = makeAdapters();
+ const api = createDocumentApi(adapters);
+ const query = { nodeType: 'paragraph' as const };
+ const direct = api.find(query);
+ const invoked = api.invoke({ operationId: 'find', input: query });
+ expect(invoked).toEqual(direct);
+ });
+
+ it('insert: invoke returns same result as direct call', () => {
+ const { adapters } = makeAdapters();
+ const api = createDocumentApi(adapters);
+ const input = { text: 'hello' };
+ const direct = api.insert(input);
+ const invoked = api.invoke({ operationId: 'insert', input });
+ expect(invoked).toEqual(direct);
+ });
+
+ it('insert: invoke forwards options through to adapter-backed execution', () => {
+ const { adapters, writeAdapter } = makeAdapters();
+ const api = createDocumentApi(adapters);
+ api.invoke({ operationId: 'insert', input: { text: 'hello' }, options: { changeMode: 'tracked' } });
+ expect(writeAdapter.write).toHaveBeenCalledWith(
+ { kind: 'insert', text: 'hello' },
+ { changeMode: 'tracked', dryRun: false },
+ );
+ });
+
+ it('comments.add: invoke returns same result as direct call', () => {
+ const { adapters } = makeAdapters();
+ const api = createDocumentApi(adapters);
+ const input = {
+ target: { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'A comment',
+ };
+ const direct = api.comments.add(input);
+ const invoked = api.invoke({ operationId: 'comments.add', input });
+ expect(invoked).toEqual(direct);
+ });
+
+ it('trackChanges.list: invoke returns same result as direct call', () => {
+ const { adapters } = makeAdapters();
+ const api = createDocumentApi(adapters);
+ const direct = api.trackChanges.list();
+ const invoked = api.invoke({ operationId: 'trackChanges.list', input: undefined });
+ expect(invoked).toEqual(direct);
+ });
+
+ it('capabilities.get: invoke returns same result as direct call', () => {
+ const { adapters } = makeAdapters();
+ const api = createDocumentApi(adapters);
+ const direct = api.capabilities();
+ const invoked = api.invoke({ operationId: 'capabilities.get', input: undefined });
+ expect(invoked).toEqual(direct);
+ });
+
+ it('lists.get: invoke returns same result as direct call', () => {
+ const { adapters } = makeAdapters();
+ const api = createDocumentApi(adapters);
+ const input = { address: { kind: 'block' as const, nodeType: 'listItem' as const, nodeId: 'li-1' } };
+ const direct = api.lists.get(input);
+ const invoked = api.invoke({ operationId: 'lists.get', input });
+ expect(invoked).toEqual(direct);
+ });
+ });
+
+ describe('error handling', () => {
+ it('throws for inherited prototype keys used as operationId', () => {
+ const { adapters } = makeAdapters();
+ const api = createDocumentApi(adapters);
+ expect(() => {
+ api.invoke({ operationId: 'toString' as OperationId, input: undefined });
+ }).toThrow('Unknown operationId');
+ });
+
+ it('throws for unknown operationId with a clear message', () => {
+ const { adapters } = makeAdapters();
+ const api = createDocumentApi(adapters);
+ expect(() => {
+ api.invoke({ operationId: 'nonexistent' as OperationId, input: {} });
+ }).toThrow('Unknown operationId: "nonexistent"');
+ });
+ });
+
+ describe('DynamicInvokeRequest (untyped input)', () => {
+ it('accepts unknown input and dispatches to the correct handler', () => {
+ const { adapters } = makeAdapters();
+ const api = createDocumentApi(adapters);
+ const input: unknown = { nodeType: 'paragraph' };
+ const result = api.invoke({ operationId: 'find', input });
+ expect(result).toEqual({ matches: [], total: 0 });
+ });
+
+ it('forwards unknown options through to the handler', () => {
+ const { adapters, writeAdapter } = makeAdapters();
+ const api = createDocumentApi(adapters);
+ const input: unknown = { text: 'dynamic' };
+ const options: unknown = { changeMode: 'tracked' };
+ api.invoke({ operationId: 'insert', input, options });
+ expect(writeAdapter.write).toHaveBeenCalledWith(
+ { kind: 'insert', text: 'dynamic' },
+ { changeMode: 'tracked', dryRun: false },
+ );
+ });
+ });
+});
diff --git a/packages/document-api/src/invoke/invoke.ts b/packages/document-api/src/invoke/invoke.ts
new file mode 100644
index 0000000000..ef54fbb242
--- /dev/null
+++ b/packages/document-api/src/invoke/invoke.ts
@@ -0,0 +1,87 @@
+/**
+ * Runtime dispatch table for the invoke API.
+ *
+ * Maps every OperationId to a function that delegates to the corresponding
+ * direct method on DocumentApi. Built once per createDocumentApi call.
+ */
+
+import type { OperationId } from '../contract/types.js';
+import type { OperationRegistry } from '../contract/operation-registry.js';
+import type { DocumentApi } from '../index.js';
+
+// ---------------------------------------------------------------------------
+// TypedDispatchTable — compile-time contract between registry and dispatch
+// ---------------------------------------------------------------------------
+
+type TypedDispatchHandler = OperationRegistry[K]['options'] extends never
+ ? (input: OperationRegistry[K]['input']) => OperationRegistry[K]['output']
+ : (input: OperationRegistry[K]['input'], options?: OperationRegistry[K]['options']) => OperationRegistry[K]['output'];
+
+export type TypedDispatchTable = {
+ [K in OperationId]: TypedDispatchHandler;
+};
+
+/**
+ * Builds a dispatch table that maps every OperationId to the corresponding
+ * direct method call on the given DocumentApi instance.
+ *
+ * Each entry delegates to the direct method — no parallel execution path.
+ * The return type is {@link TypedDispatchTable}, which validates at compile
+ * time that each handler conforms to the {@link OperationRegistry} contract.
+ */
+export function buildDispatchTable(api: DocumentApi): TypedDispatchTable {
+ return {
+ // --- Singleton reads ---
+ find: (input, options) =>
+ api.find(input as Parameters[0], options as Parameters[1]),
+ getNode: (input) => api.getNode(input),
+ getNodeById: (input) => api.getNodeById(input),
+ getText: (input) => api.getText(input),
+ info: (input) => api.info(input),
+
+ // --- Singleton mutations ---
+ insert: (input, options) => api.insert(input, options),
+ replace: (input, options) => api.replace(input, options),
+ delete: (input, options) => api.delete(input, options),
+
+ // --- format.* ---
+ 'format.bold': (input, options) => api.format.bold(input, options),
+
+ // --- create.* ---
+ 'create.paragraph': (input, options) => api.create.paragraph(input, options),
+
+ // --- lists.* ---
+ 'lists.list': (input) => api.lists.list(input),
+ 'lists.get': (input) => api.lists.get(input),
+ 'lists.insert': (input, options) => api.lists.insert(input, options),
+ 'lists.setType': (input, options) => api.lists.setType(input, options),
+ 'lists.indent': (input, options) => api.lists.indent(input, options),
+ 'lists.outdent': (input, options) => api.lists.outdent(input, options),
+ 'lists.restart': (input, options) => api.lists.restart(input, options),
+ 'lists.exit': (input, options) => api.lists.exit(input, options),
+
+ // --- comments.* ---
+ 'comments.add': (input) => api.comments.add(input),
+ 'comments.edit': (input) => api.comments.edit(input),
+ 'comments.reply': (input) => api.comments.reply(input),
+ 'comments.move': (input) => api.comments.move(input),
+ 'comments.resolve': (input) => api.comments.resolve(input),
+ 'comments.remove': (input) => api.comments.remove(input),
+ 'comments.setInternal': (input) => api.comments.setInternal(input),
+ 'comments.setActive': (input) => api.comments.setActive(input),
+ 'comments.goTo': (input) => api.comments.goTo(input),
+ 'comments.get': (input) => api.comments.get(input),
+ 'comments.list': (input) => api.comments.list(input),
+
+ // --- trackChanges.* ---
+ 'trackChanges.list': (input) => api.trackChanges.list(input),
+ 'trackChanges.get': (input) => api.trackChanges.get(input),
+ 'trackChanges.accept': (input) => api.trackChanges.accept(input),
+ 'trackChanges.reject': (input) => api.trackChanges.reject(input),
+ 'trackChanges.acceptAll': (input) => api.trackChanges.acceptAll(input),
+ 'trackChanges.rejectAll': (input) => api.trackChanges.rejectAll(input),
+
+ // --- capabilities ---
+ 'capabilities.get': () => api.capabilities(),
+ };
+}
diff --git a/packages/document-api/src/lists/lists.ts b/packages/document-api/src/lists/lists.ts
new file mode 100644
index 0000000000..ab78b4e58e
--- /dev/null
+++ b/packages/document-api/src/lists/lists.ts
@@ -0,0 +1,103 @@
+import type { MutationOptions } from '../write/write.js';
+import { normalizeMutationOptions } from '../write/write.js';
+import type {
+ ListInsertInput,
+ ListSetTypeInput,
+ ListsExitResult,
+ ListsGetInput,
+ ListsInsertResult,
+ ListsListQuery,
+ ListsListResult,
+ ListsMutateItemResult,
+ ListTargetInput,
+ ListItemInfo,
+} from './lists.types.js';
+export type {
+ ListInsertInput,
+ ListSetTypeInput,
+ ListsExitResult,
+ ListsGetInput,
+ ListsInsertResult,
+ ListsListQuery,
+ ListsListResult,
+ ListsMutateItemResult,
+ ListTargetInput,
+ ListItemInfo,
+} from './lists.types.js';
+
+export interface ListsAdapter {
+ /** List items matching the given query. */
+ list(query?: ListsListQuery): ListsListResult;
+ /** Retrieve full information for a single list item. */
+ get(input: ListsGetInput): ListItemInfo;
+ /** Insert a new list item relative to the target. */
+ insert(input: ListInsertInput, options?: MutationOptions): ListsInsertResult;
+ /** Change the list kind (ordered/bullet) for the target item. */
+ setType(input: ListSetTypeInput, options?: MutationOptions): ListsMutateItemResult;
+ /** Increase the nesting level of the target item. */
+ indent(input: ListTargetInput, options?: MutationOptions): ListsMutateItemResult;
+ /** Decrease the nesting level of the target item. */
+ outdent(input: ListTargetInput, options?: MutationOptions): ListsMutateItemResult;
+ /** Restart numbering at the target item. */
+ restart(input: ListTargetInput, options?: MutationOptions): ListsMutateItemResult;
+ /** Exit the list, converting the target item to a plain paragraph. */
+ exit(input: ListTargetInput, options?: MutationOptions): ListsExitResult;
+}
+
+export type ListsApi = ListsAdapter;
+
+export function executeListsList(adapter: ListsAdapter, query?: ListsListQuery): ListsListResult {
+ return adapter.list(query);
+}
+
+export function executeListsGet(adapter: ListsAdapter, input: ListsGetInput): ListItemInfo {
+ return adapter.get(input);
+}
+
+export function executeListsInsert(
+ adapter: ListsAdapter,
+ input: ListInsertInput,
+ options?: MutationOptions,
+): ListsInsertResult {
+ return adapter.insert(input, normalizeMutationOptions(options));
+}
+
+export function executeListsSetType(
+ adapter: ListsAdapter,
+ input: ListSetTypeInput,
+ options?: MutationOptions,
+): ListsMutateItemResult {
+ return adapter.setType(input, normalizeMutationOptions(options));
+}
+
+export function executeListsIndent(
+ adapter: ListsAdapter,
+ input: ListTargetInput,
+ options?: MutationOptions,
+): ListsMutateItemResult {
+ return adapter.indent(input, normalizeMutationOptions(options));
+}
+
+export function executeListsOutdent(
+ adapter: ListsAdapter,
+ input: ListTargetInput,
+ options?: MutationOptions,
+): ListsMutateItemResult {
+ return adapter.outdent(input, normalizeMutationOptions(options));
+}
+
+export function executeListsRestart(
+ adapter: ListsAdapter,
+ input: ListTargetInput,
+ options?: MutationOptions,
+): ListsMutateItemResult {
+ return adapter.restart(input, normalizeMutationOptions(options));
+}
+
+export function executeListsExit(
+ adapter: ListsAdapter,
+ input: ListTargetInput,
+ options?: MutationOptions,
+): ListsExitResult {
+ return adapter.exit(input, normalizeMutationOptions(options));
+}
diff --git a/packages/document-api/src/lists/lists.types.ts b/packages/document-api/src/lists/lists.types.ts
new file mode 100644
index 0000000000..7d1ecb0a92
--- /dev/null
+++ b/packages/document-api/src/lists/lists.types.ts
@@ -0,0 +1,91 @@
+import type { BlockNodeType, ReceiptFailure, ReceiptInsert, TextAddress } from '../types/index.js';
+
+export type ListItemAddress = {
+ kind: 'block';
+ nodeType: 'listItem';
+ nodeId: string;
+};
+
+export type ListWithinAddress = {
+ kind: 'block';
+ nodeType: BlockNodeType;
+ nodeId: string;
+};
+export type ListKind = 'ordered' | 'bullet';
+export type ListInsertPosition = 'before' | 'after';
+
+export const LIST_KINDS = ['ordered', 'bullet'] as const satisfies readonly ListKind[];
+export const LIST_INSERT_POSITIONS = ['before', 'after'] as const satisfies readonly ListInsertPosition[];
+
+export interface ListsListQuery {
+ within?: ListWithinAddress;
+ limit?: number;
+ offset?: number;
+ kind?: ListKind;
+ level?: number;
+ ordinal?: number;
+}
+
+export interface ListsGetInput {
+ address: ListItemAddress;
+}
+
+export interface ListItemInfo {
+ address: ListItemAddress;
+ marker?: string;
+ ordinal?: number;
+ path?: number[];
+ level?: number;
+ kind?: ListKind;
+ text?: string;
+}
+
+export interface ListsListResult {
+ matches: ListItemAddress[];
+ total: number;
+ items: ListItemInfo[];
+}
+
+export interface ListInsertInput {
+ target: ListItemAddress;
+ position: ListInsertPosition;
+ text?: string;
+}
+
+export interface ListTargetInput {
+ target: ListItemAddress;
+}
+
+export interface ListSetTypeInput extends ListTargetInput {
+ kind: ListKind;
+}
+
+export interface ListsInsertSuccessResult {
+ success: true;
+ item: ListItemAddress;
+ insertionPoint: TextAddress;
+ trackedChangeRefs?: ReceiptInsert[];
+}
+
+export interface ListsMutateItemSuccessResult {
+ success: true;
+ item: ListItemAddress;
+}
+
+export interface ListsExitSuccessResult {
+ success: true;
+ paragraph: {
+ kind: 'block';
+ nodeType: 'paragraph';
+ nodeId: string;
+ };
+}
+
+export interface ListsFailureResult {
+ success: false;
+ failure: ReceiptFailure;
+}
+
+export type ListsInsertResult = ListsInsertSuccessResult | ListsFailureResult;
+export type ListsMutateItemResult = ListsMutateItemSuccessResult | ListsFailureResult;
+export type ListsExitResult = ListsExitSuccessResult | ListsFailureResult;
diff --git a/packages/document-api/src/replace/replace.ts b/packages/document-api/src/replace/replace.ts
new file mode 100644
index 0000000000..60563e3b57
--- /dev/null
+++ b/packages/document-api/src/replace/replace.ts
@@ -0,0 +1,23 @@
+import { executeWrite, type MutationOptions, type WriteAdapter } from '../write/write.js';
+import type { TextAddress, TextMutationReceipt } from '../types/index.js';
+
+export interface ReplaceInput {
+ target: TextAddress;
+ text: string;
+}
+
+export function executeReplace(
+ adapter: WriteAdapter,
+ input: ReplaceInput,
+ options?: MutationOptions,
+): TextMutationReceipt {
+ return executeWrite(
+ adapter,
+ {
+ kind: 'replace',
+ target: input.target,
+ text: input.text,
+ },
+ options,
+ );
+}
diff --git a/packages/document-api/src/track-changes/track-changes.ts b/packages/document-api/src/track-changes/track-changes.ts
new file mode 100644
index 0000000000..a46bc40f17
--- /dev/null
+++ b/packages/document-api/src/track-changes/track-changes.ts
@@ -0,0 +1,69 @@
+import type { Receipt, TrackChangeInfo, TrackChangesListQuery, TrackChangesListResult } from '../types/index.js';
+
+export type TrackChangesListInput = TrackChangesListQuery;
+
+export interface TrackChangesGetInput {
+ id: string;
+}
+
+export interface TrackChangesAcceptInput {
+ id: string;
+}
+
+export interface TrackChangesRejectInput {
+ id: string;
+}
+
+export type TrackChangesAcceptAllInput = Record;
+
+export type TrackChangesRejectAllInput = Record;
+
+export interface TrackChangesAdapter {
+ /** List tracked changes matching the given query. */
+ list(input?: TrackChangesListInput): TrackChangesListResult;
+ /** Retrieve full information for a single tracked change. */
+ get(input: TrackChangesGetInput): TrackChangeInfo;
+ /** Accept a tracked change, applying it to the document. */
+ accept(input: TrackChangesAcceptInput): Receipt;
+ /** Reject a tracked change, reverting it from the document. */
+ reject(input: TrackChangesRejectInput): Receipt;
+ /** Accept all tracked changes in the document. */
+ acceptAll(input: TrackChangesAcceptAllInput): Receipt;
+ /** Reject all tracked changes in the document. */
+ rejectAll(input: TrackChangesRejectAllInput): Receipt;
+}
+
+export type TrackChangesApi = TrackChangesAdapter;
+
+/**
+ * Execute wrappers below are the canonical interception point for input
+ * normalization and validation. Query-only operations currently pass through
+ * directly. Mutation operations will gain validation as the API matures.
+ * Keep the wrappers to preserve this extension surface.
+ */
+export function executeTrackChangesList(
+ adapter: TrackChangesAdapter,
+ input?: TrackChangesListInput,
+): TrackChangesListResult {
+ return adapter.list(input);
+}
+
+export function executeTrackChangesGet(adapter: TrackChangesAdapter, input: TrackChangesGetInput): TrackChangeInfo {
+ return adapter.get(input);
+}
+
+export function executeTrackChangesAccept(adapter: TrackChangesAdapter, input: TrackChangesAcceptInput): Receipt {
+ return adapter.accept(input);
+}
+
+export function executeTrackChangesReject(adapter: TrackChangesAdapter, input: TrackChangesRejectInput): Receipt {
+ return adapter.reject(input);
+}
+
+export function executeTrackChangesAcceptAll(adapter: TrackChangesAdapter, input: TrackChangesAcceptAllInput): Receipt {
+ return adapter.acceptAll(input);
+}
+
+export function executeTrackChangesRejectAll(adapter: TrackChangesAdapter, input: TrackChangesRejectAllInput): Receipt {
+ return adapter.rejectAll(input);
+}
diff --git a/packages/document-api/src/types/address.ts b/packages/document-api/src/types/address.ts
new file mode 100644
index 0000000000..534fd90d11
--- /dev/null
+++ b/packages/document-api/src/types/address.ts
@@ -0,0 +1,28 @@
+export type Range = {
+ /** Inclusive start offset (0-based, UTF-16 code units). */
+ start: number;
+ /** Exclusive end offset (0-based, UTF-16 code units). */
+ end: number;
+};
+
+export type TextAddress = {
+ kind: 'text';
+ blockId: string;
+ range: Range;
+};
+
+export type EntityType = 'comment' | 'trackedChange';
+
+export type CommentAddress = {
+ kind: 'entity';
+ entityType: 'comment';
+ entityId: string;
+};
+
+export type TrackedChangeAddress = {
+ kind: 'entity';
+ entityType: 'trackedChange';
+ entityId: string;
+};
+
+export type EntityAddress = CommentAddress | TrackedChangeAddress;
diff --git a/packages/document-api/src/types/base.ts b/packages/document-api/src/types/base.ts
new file mode 100644
index 0000000000..7891ca4332
--- /dev/null
+++ b/packages/document-api/src/types/base.ts
@@ -0,0 +1,138 @@
+/**
+ * Base types for the Document API node model.
+ *
+ * This file is the foundation of the type hierarchy — leaf node-info files
+ * (paragraph.types.ts, inline.types.ts, etc.) import from here, and node.ts
+ * assembles the full NodeInfo union from those leaves.
+ *
+ * Nothing in this file imports from leaf node-info files.
+ */
+
+export type NodeKind = 'block' | 'inline';
+
+export const NODE_KINDS = ['block', 'inline'] as const satisfies readonly NodeKind[];
+
+export type NodeType =
+ // Block-level
+ | 'paragraph'
+ | 'heading'
+ | 'listItem'
+ | 'table'
+ | 'tableRow'
+ | 'tableCell'
+ // Inline-level
+ | 'run'
+ | 'bookmark'
+ | 'comment'
+ | 'hyperlink'
+ | 'footnoteRef'
+ | 'tab'
+ | 'lineBreak'
+
+ // Both block and inline
+ | 'image'
+ | 'sdt';
+
+export const NODE_TYPES = [
+ 'paragraph',
+ 'heading',
+ 'listItem',
+ 'table',
+ 'tableRow',
+ 'tableCell',
+ 'image',
+ 'sdt',
+ 'run',
+ 'bookmark',
+ 'comment',
+ 'hyperlink',
+ 'footnoteRef',
+ 'tab',
+ 'lineBreak',
+] as const satisfies readonly NodeType[];
+
+/**
+ * Node types that can appear in block context.
+ * Note: 'sdt' and 'image' can appear in both block and inline contexts.
+ */
+export type BlockNodeType = Extract<
+ NodeType,
+ 'paragraph' | 'heading' | 'listItem' | 'table' | 'tableRow' | 'tableCell' | 'image' | 'sdt'
+>;
+
+export const BLOCK_NODE_TYPES = [
+ 'paragraph',
+ 'heading',
+ 'listItem',
+ 'table',
+ 'tableRow',
+ 'tableCell',
+ 'image',
+ 'sdt',
+] as const satisfies readonly BlockNodeType[];
+
+/**
+ * Node types that can appear in inline context.
+ * Note: 'sdt' and 'image' can appear in both block and inline contexts.
+ */
+export type InlineNodeType = Extract<
+ NodeType,
+ 'run' | 'bookmark' | 'comment' | 'hyperlink' | 'sdt' | 'image' | 'footnoteRef' | 'tab' | 'lineBreak'
+>;
+
+export const INLINE_NODE_TYPES = [
+ 'run',
+ 'bookmark',
+ 'comment',
+ 'hyperlink',
+ 'sdt',
+ 'image',
+ 'footnoteRef',
+ 'tab',
+ 'lineBreak',
+] as const satisfies readonly InlineNodeType[];
+
+export type Position = {
+ blockId: string;
+ /**
+ * 0-based offset into the block's flattened text representation.
+ *
+ * - Text runs contribute their character length.
+ * - Leaf inline nodes (images, tabs, etc.) contribute a single placeholder character.
+ * - Transparent inline wrappers (hyperlinks, bookmarks, etc.) contribute only their inner text.
+ */
+ offset: number;
+};
+
+export type InlineAnchor = {
+ start: Position;
+ end: Position;
+};
+
+export type BlockNodeAddress = {
+ kind: 'block';
+ nodeType: BlockNodeType;
+ nodeId: string;
+};
+
+export type InlineNodeAddress = {
+ kind: 'inline';
+ nodeType: InlineNodeType;
+ anchor: InlineAnchor;
+};
+
+export type NodeAddress = BlockNodeAddress | InlineNodeAddress;
+
+export type NodeSummary = {
+ label?: string;
+ text?: string;
+};
+
+export interface BaseNodeInfo {
+ nodeType: NodeType;
+ kind: NodeKind;
+ summary?: NodeSummary;
+ text?: string;
+ /** Child nodes. Typed as BaseNodeInfo[] to avoid circular imports; narrow via `nodeType`. */
+ nodes?: BaseNodeInfo[];
+}
diff --git a/packages/document-api/src/types/comments.types.ts b/packages/document-api/src/types/comments.types.ts
new file mode 100644
index 0000000000..649b70c661
--- /dev/null
+++ b/packages/document-api/src/types/comments.types.ts
@@ -0,0 +1,20 @@
+import type { BaseNodeInfo } from './base.js';
+
+export interface CommentNodeInfo extends BaseNodeInfo {
+ nodeType: 'comment';
+ kind: 'inline';
+ properties: CommentProperties;
+ bodyText?: string;
+ bodyNodes?: BaseNodeInfo[];
+}
+
+export type CommentStatus = 'open' | 'resolved';
+
+export interface CommentProperties {
+ commentId: string;
+ author?: string;
+ status?: CommentStatus;
+ createdAt?: string;
+ /** User-visible sidebar text */
+ commentText?: string;
+}
diff --git a/packages/document-api/src/types/create.types.ts b/packages/document-api/src/types/create.types.ts
new file mode 100644
index 0000000000..3d1ce31322
--- /dev/null
+++ b/packages/document-api/src/types/create.types.ts
@@ -0,0 +1,28 @@
+import type { TextAddress } from './address.js';
+import type { BlockNodeAddress } from './base.js';
+import type { ReceiptFailure, ReceiptInsert } from './receipt.js';
+
+export type ParagraphCreateLocation =
+ | { kind: 'documentStart' }
+ | { kind: 'documentEnd' }
+ | { kind: 'before'; target: BlockNodeAddress }
+ | { kind: 'after'; target: BlockNodeAddress };
+
+export interface CreateParagraphInput {
+ at?: ParagraphCreateLocation;
+ text?: string;
+}
+
+export interface CreateParagraphSuccessResult {
+ success: true;
+ paragraph: BlockNodeAddress;
+ insertionPoint: TextAddress;
+ trackedChangeRefs?: ReceiptInsert[];
+}
+
+export interface CreateParagraphFailureResult {
+ success: false;
+ failure: ReceiptFailure;
+}
+
+export type CreateParagraphResult = CreateParagraphSuccessResult | CreateParagraphFailureResult;
diff --git a/packages/document-api/src/types/index.ts b/packages/document-api/src/types/index.ts
new file mode 100644
index 0000000000..a109a63979
--- /dev/null
+++ b/packages/document-api/src/types/index.ts
@@ -0,0 +1,15 @@
+export * from './base.js';
+export * from './node.js';
+export * from './query.js';
+export * from './address.js';
+export * from './receipt.js';
+export * from './paragraph.types.js';
+export * from './inline.types.js';
+export * from './tables.types.js';
+export * from './media.types.js';
+export * from './structured.types.js';
+export * from './comments.types.js';
+export * from './references.types.js';
+export * from './track-changes.types.js';
+export * from './create.types.js';
+export * from './info.types.js';
diff --git a/packages/document-api/src/types/info.types.ts b/packages/document-api/src/types/info.types.ts
new file mode 100644
index 0000000000..48b7344d99
--- /dev/null
+++ b/packages/document-api/src/types/info.types.ts
@@ -0,0 +1,27 @@
+export interface DocumentInfoCounts {
+ words: number;
+ paragraphs: number;
+ headings: number;
+ tables: number;
+ images: number;
+ comments: number;
+}
+
+export interface DocumentInfoOutlineItem {
+ level: number;
+ text: string;
+ nodeId: string;
+}
+
+export interface DocumentInfoCapabilities {
+ canFind: boolean;
+ canGetNode: boolean;
+ canComment: boolean;
+ canReplace: boolean;
+}
+
+export interface DocumentInfo {
+ counts: DocumentInfoCounts;
+ outline: DocumentInfoOutlineItem[];
+ capabilities: DocumentInfoCapabilities;
+}
diff --git a/packages/document-api/src/types/inline.types.ts b/packages/document-api/src/types/inline.types.ts
new file mode 100644
index 0000000000..1cb68b1eca
--- /dev/null
+++ b/packages/document-api/src/types/inline.types.ts
@@ -0,0 +1,31 @@
+import type { BaseNodeInfo } from './base.js';
+
+export interface RunNodeInfo extends BaseNodeInfo {
+ nodeType: 'run';
+ kind: 'inline';
+ properties: RunProperties;
+}
+
+export interface TabNodeInfo extends BaseNodeInfo {
+ nodeType: 'tab';
+ kind: 'inline';
+ properties: Record;
+}
+
+export interface LineBreakNodeInfo extends BaseNodeInfo {
+ nodeType: 'lineBreak';
+ kind: 'inline';
+ properties: Record;
+}
+
+export interface RunProperties {
+ bold?: boolean;
+ italic?: boolean;
+ underline?: boolean;
+ font?: string;
+ size?: number;
+ color?: string;
+ highlight?: string;
+ styleId?: string;
+ language?: string;
+}
diff --git a/packages/document-api/src/types/media.types.ts b/packages/document-api/src/types/media.types.ts
new file mode 100644
index 0000000000..058d20c750
--- /dev/null
+++ b/packages/document-api/src/types/media.types.ts
@@ -0,0 +1,20 @@
+import type { BaseNodeInfo, NodeKind } from './base.js';
+
+export interface ImageNodeInfo extends BaseNodeInfo {
+ nodeType: 'image';
+ kind: NodeKind;
+ properties: ImageProperties;
+}
+
+export interface ImageSize {
+ width?: number;
+ height?: number;
+ unit?: 'px' | 'pt' | 'twip';
+}
+
+export interface ImageProperties {
+ src?: string;
+ alt?: string;
+ size?: ImageSize;
+ wrap?: string;
+}
diff --git a/packages/document-api/src/types/node.ts b/packages/document-api/src/types/node.ts
new file mode 100644
index 0000000000..25177d710d
--- /dev/null
+++ b/packages/document-api/src/types/node.ts
@@ -0,0 +1,29 @@
+/**
+ * Full NodeInfo union — assembled from leaf node-info files.
+ * Base types (NodeKind, NodeType, BaseNodeInfo, addresses) live in base.ts.
+ */
+
+import type { HeadingNodeInfo, ListItemNodeInfo, ParagraphNodeInfo } from './paragraph.types.js';
+import type { LineBreakNodeInfo, RunNodeInfo, TabNodeInfo } from './inline.types.js';
+import type { TableCellNodeInfo, TableNodeInfo, TableRowNodeInfo } from './tables.types.js';
+import type { ImageNodeInfo } from './media.types.js';
+import type { BookmarkNodeInfo, HyperlinkNodeInfo, SdtNodeInfo } from './structured.types.js';
+import type { CommentNodeInfo } from './comments.types.js';
+import type { FootnoteRefNodeInfo } from './references.types.js';
+
+export type NodeInfo =
+ | ParagraphNodeInfo
+ | HeadingNodeInfo
+ | ListItemNodeInfo
+ | TableNodeInfo
+ | TableRowNodeInfo
+ | TableCellNodeInfo
+ | ImageNodeInfo
+ | SdtNodeInfo
+ | RunNodeInfo
+ | BookmarkNodeInfo
+ | CommentNodeInfo
+ | HyperlinkNodeInfo
+ | FootnoteRefNodeInfo
+ | TabNodeInfo
+ | LineBreakNodeInfo;
diff --git a/packages/document-api/src/types/paragraph.types.ts b/packages/document-api/src/types/paragraph.types.ts
new file mode 100644
index 0000000000..34758273ee
--- /dev/null
+++ b/packages/document-api/src/types/paragraph.types.ts
@@ -0,0 +1,71 @@
+import type { BaseNodeInfo } from './base.js';
+
+export interface ParagraphNodeInfo extends BaseNodeInfo {
+ nodeType: 'paragraph';
+ kind: 'block';
+ properties: ParagraphProperties;
+}
+
+export interface HeadingNodeInfo extends BaseNodeInfo {
+ nodeType: 'heading';
+ kind: 'block';
+ properties: HeadingProperties;
+}
+
+export interface ListItemNodeInfo extends BaseNodeInfo {
+ nodeType: 'listItem';
+ kind: 'block';
+ properties: ListItemProperties;
+}
+
+export type ParagraphIndentation = {
+ left?: number;
+ right?: number;
+ firstLine?: number;
+ hanging?: number;
+ unit?: 'twip' | 'pt' | 'px';
+};
+
+export type ParagraphSpacing = {
+ before?: number;
+ after?: number;
+ line?: number;
+ unit?: 'twip' | 'pt' | 'px';
+};
+
+export type ParagraphNumbering = {
+ numId?: number;
+ level?: number;
+};
+
+export type ListNumbering = {
+ marker?: string;
+ path?: number[];
+ ordinal?: number;
+ listIndex?: number;
+};
+
+export interface ParagraphProperties {
+ styleId?: string;
+ alignment?: 'left' | 'right' | 'center' | 'justify' | 'start' | 'end' | 'distributed';
+ indentation?: ParagraphIndentation;
+ spacing?: ParagraphSpacing;
+ keepWithNext?: boolean;
+ outlineLevel?: number;
+ paragraphNumbering?: ParagraphNumbering;
+}
+
+export interface HeadingProperties extends ParagraphProperties {
+ /**
+ * Headings are paragraphs with a heading style.
+ */
+ headingLevel: 1 | 2 | 3 | 4 | 5 | 6;
+}
+
+export interface ListItemProperties extends ParagraphProperties {
+ /**
+ * List items are paragraphs with numbering.
+ * This keeps list semantics explicit without creating a separate structure.
+ */
+ numbering?: ListNumbering;
+}
diff --git a/packages/document-api/src/types/query.ts b/packages/document-api/src/types/query.ts
new file mode 100644
index 0000000000..c1af9a0c29
--- /dev/null
+++ b/packages/document-api/src/types/query.ts
@@ -0,0 +1,87 @@
+import type { NodeAddress, NodeKind, NodeType } from './base.js';
+import type { NodeInfo } from './node.js';
+import type { Range, TextAddress } from './address.js';
+
+export interface TextSelector {
+ type: 'text';
+ pattern: string;
+ /**
+ * Controls text matching strategy.
+ * - `contains`: literal substring matching (default)
+ * - `regex`: regular expression matching
+ */
+ mode?: 'contains' | 'regex';
+ /**
+ * Controls case sensitivity for text matching.
+ * Defaults to false (case-insensitive).
+ */
+ caseSensitive?: boolean;
+}
+
+export interface NodeSelector {
+ type: 'node';
+ nodeType?: NodeType;
+ kind?: NodeKind;
+}
+
+/**
+ * Selector shorthand for find queries.
+ *
+ * `{ nodeType: 'paragraph' }` is sugar for `{ type: 'node', nodeType: 'paragraph' }`.
+ *
+ * For dual-context node types (`sdt`, `image`), omitting `kind`
+ * may return both block and inline matches.
+ */
+export type Selector = { nodeType: NodeType } | NodeSelector | TextSelector;
+
+export interface Query {
+ /** Selector that determines which nodes to match. */
+ select: NodeSelector | TextSelector;
+ within?: NodeAddress;
+ limit?: number;
+ offset?: number;
+ /**
+ * Whether to hydrate `result.nodes` for matched addresses.
+ * This is independent from text-match context, which is intrinsic for text selectors.
+ */
+ includeNodes?: boolean;
+ /**
+ * Controls whether unknown nodes are returned in diagnostics.
+ * Unknown nodes are never included in matches.
+ */
+ includeUnknown?: boolean;
+}
+
+export interface MatchContext {
+ address: NodeAddress;
+ snippet: string;
+ highlightRange: Range;
+ /**
+ * Text ranges matching the query, expressed as block-relative offsets.
+ * For cross-paragraph matches, this will include one range per block.
+ *
+ * These ranges can be passed as targets to mutation operations.
+ */
+ textRanges?: TextAddress[];
+}
+
+export interface UnknownNodeDiagnostic {
+ message: string;
+ address?: NodeAddress;
+ hint?: string;
+}
+
+export interface QueryResult {
+ /**
+ * Matched node addresses.
+ *
+ * For text selectors, these addresses identify containing block nodes.
+ * Exact matched spans are exposed via `context[*].textRanges`.
+ */
+ matches: NodeAddress[];
+ total: number;
+ /** Optional hydrated node payloads aligned with `matches` when `includeNodes` is true. */
+ nodes?: NodeInfo[];
+ context?: MatchContext[];
+ diagnostics?: UnknownNodeDiagnostic[];
+}
diff --git a/packages/document-api/src/types/receipt.ts b/packages/document-api/src/types/receipt.ts
new file mode 100644
index 0000000000..78a358102b
--- /dev/null
+++ b/packages/document-api/src/types/receipt.ts
@@ -0,0 +1,56 @@
+import type { EntityAddress, TextAddress, TrackedChangeAddress } from './address.js';
+
+export type ReceiptInsert = TrackedChangeAddress;
+export type ReceiptEntity = EntityAddress;
+
+export type ReceiptFailureCode = 'NO_OP' | 'INVALID_TARGET' | 'TARGET_NOT_FOUND' | 'CAPABILITY_UNAVAILABLE';
+
+export type ReceiptFailure = {
+ code: ReceiptFailureCode;
+ message: string;
+ details?: unknown;
+};
+
+export type ReceiptSuccess = {
+ success: true;
+ inserted?: ReceiptEntity[];
+ updated?: ReceiptEntity[];
+ removed?: ReceiptEntity[];
+};
+
+export type ReceiptFailureResult = {
+ success: false;
+ failure: ReceiptFailure;
+};
+
+export type Receipt = ReceiptSuccess | ReceiptFailureResult;
+
+export type TextMutationRange = {
+ from: number;
+ to: number;
+};
+
+export type TextMutationResolution = {
+ /**
+ * Requested input target from the caller, when provided.
+ * For insert-without-target calls this is omitted.
+ */
+ requestedTarget?: TextAddress;
+ /**
+ * Effective target used by the adapter after canonical resolution.
+ */
+ target: TextAddress;
+ /**
+ * Engine-resolved absolute document range for the effective target.
+ */
+ range: TextMutationRange;
+ /**
+ * Snapshot of text currently covered by the resolved range.
+ * Empty for collapsed insert targets.
+ */
+ text: string;
+};
+
+export type TextMutationReceipt =
+ | (ReceiptSuccess & { resolution: TextMutationResolution })
+ | (ReceiptFailureResult & { resolution: TextMutationResolution });
diff --git a/packages/document-api/src/types/references.types.ts b/packages/document-api/src/types/references.types.ts
new file mode 100644
index 0000000000..6cf807482f
--- /dev/null
+++ b/packages/document-api/src/types/references.types.ts
@@ -0,0 +1,11 @@
+import type { BaseNodeInfo } from './base.js';
+
+export interface FootnoteRefNodeInfo extends BaseNodeInfo {
+ nodeType: 'footnoteRef';
+ kind: 'inline';
+ properties: FootnoteRefProperties;
+}
+
+export interface FootnoteRefProperties {
+ noteId?: string;
+}
diff --git a/packages/document-api/src/types/structured.types.ts b/packages/document-api/src/types/structured.types.ts
new file mode 100644
index 0000000000..459d493472
--- /dev/null
+++ b/packages/document-api/src/types/structured.types.ts
@@ -0,0 +1,38 @@
+import type { BaseNodeInfo, NodeKind } from './base.js';
+
+export interface SdtNodeInfo extends BaseNodeInfo {
+ nodeType: 'sdt';
+ kind: NodeKind;
+ properties: SdtProperties;
+}
+
+export interface SdtProperties {
+ tag?: string;
+ alias?: string;
+ type?: string;
+ appearance?: string;
+ placeholder?: string;
+}
+
+export interface BookmarkNodeInfo extends BaseNodeInfo {
+ nodeType: 'bookmark';
+ kind: 'inline';
+ properties: BookmarkProperties;
+}
+
+export interface HyperlinkNodeInfo extends BaseNodeInfo {
+ nodeType: 'hyperlink';
+ kind: 'inline';
+ properties: HyperlinkProperties;
+}
+
+export interface BookmarkProperties {
+ name?: string;
+ bookmarkId?: string;
+}
+
+export interface HyperlinkProperties {
+ href?: string;
+ anchor?: string;
+ tooltip?: string;
+}
diff --git a/packages/document-api/src/types/tables.types.ts b/packages/document-api/src/types/tables.types.ts
new file mode 100644
index 0000000000..f4e6c09106
--- /dev/null
+++ b/packages/document-api/src/types/tables.types.ts
@@ -0,0 +1,50 @@
+import type { BaseNodeInfo } from './base.js';
+
+export interface TableNodeInfo extends BaseNodeInfo {
+ nodeType: 'table';
+ kind: 'block';
+ properties: TableProperties;
+}
+
+export interface TableRowNodeInfo extends BaseNodeInfo {
+ nodeType: 'tableRow';
+ kind: 'block';
+ properties: TableRowProperties;
+}
+
+export interface TableCellNodeInfo extends BaseNodeInfo {
+ nodeType: 'tableCell';
+ kind: 'block';
+ properties: TableCellProperties;
+}
+
+export interface TableBorders {
+ top?: string;
+ right?: string;
+ bottom?: string;
+ left?: string;
+ insideH?: string;
+ insideV?: string;
+}
+
+export interface TableProperties {
+ layout?: string;
+ width?: number;
+ alignment?: 'left' | 'center' | 'right' | 'inside' | 'outside';
+ borders?: TableBorders;
+}
+
+export interface TableRowProperties {
+ rowIndex?: number;
+}
+
+export interface TableCellProperties {
+ rowIndex?: number;
+ colIndex?: number;
+ width?: number;
+ shading?: string;
+ vMerge?: boolean;
+ gridSpan?: number;
+ padding?: number;
+ borders?: TableBorders;
+}
diff --git a/packages/document-api/src/types/track-changes.types.ts b/packages/document-api/src/types/track-changes.types.ts
new file mode 100644
index 0000000000..ae2938ca62
--- /dev/null
+++ b/packages/document-api/src/types/track-changes.types.ts
@@ -0,0 +1,27 @@
+import type { TrackedChangeAddress } from './address.js';
+
+export type TrackChangeType = 'insert' | 'delete' | 'format';
+
+export interface TrackChangeInfo {
+ address: TrackedChangeAddress;
+ /** Convenience alias for `address.entityId`. */
+ id: string;
+ type: TrackChangeType;
+ author?: string;
+ authorEmail?: string;
+ authorImage?: string;
+ date?: string;
+ excerpt?: string;
+}
+
+export interface TrackChangesListQuery {
+ limit?: number;
+ offset?: number;
+ type?: TrackChangeType;
+}
+
+export interface TrackChangesListResult {
+ matches: TrackedChangeAddress[];
+ total: number;
+ changes?: TrackChangeInfo[];
+}
diff --git a/packages/document-api/src/write/write.test.ts b/packages/document-api/src/write/write.test.ts
new file mode 100644
index 0000000000..3035fc948b
--- /dev/null
+++ b/packages/document-api/src/write/write.test.ts
@@ -0,0 +1,23 @@
+import { normalizeMutationOptions } from './write.js';
+
+describe('normalizeMutationOptions', () => {
+ it('defaults changeMode to direct when options are omitted', () => {
+ expect(normalizeMutationOptions()).toEqual({ changeMode: 'direct', dryRun: false });
+ });
+
+ it('defaults changeMode to direct when changeMode is undefined', () => {
+ expect(normalizeMutationOptions({})).toEqual({ changeMode: 'direct', dryRun: false });
+ });
+
+ it('preserves explicit direct changeMode', () => {
+ expect(normalizeMutationOptions({ changeMode: 'direct' })).toEqual({ changeMode: 'direct', dryRun: false });
+ });
+
+ it('preserves explicit tracked changeMode', () => {
+ expect(normalizeMutationOptions({ changeMode: 'tracked' })).toEqual({ changeMode: 'tracked', dryRun: false });
+ });
+
+ it('preserves explicit dryRun true', () => {
+ expect(normalizeMutationOptions({ dryRun: true })).toEqual({ changeMode: 'direct', dryRun: true });
+ });
+});
diff --git a/packages/document-api/src/write/write.ts b/packages/document-api/src/write/write.ts
new file mode 100644
index 0000000000..dd7abb7993
--- /dev/null
+++ b/packages/document-api/src/write/write.ts
@@ -0,0 +1,61 @@
+import type { TextAddress, TextMutationReceipt } from '../types/index.js';
+
+export type ChangeMode = 'direct' | 'tracked';
+
+export interface MutationOptions {
+ /**
+ * Controls whether mutation applies directly or as a tracked change.
+ * Defaults to `direct`.
+ */
+ changeMode?: ChangeMode;
+ /**
+ * When true, adapters validate and resolve the operation but must not mutate state.
+ * Defaults to `false`.
+ */
+ dryRun?: boolean;
+}
+
+export type WriteKind = 'insert' | 'replace' | 'delete';
+
+export type InsertWriteRequest = {
+ kind: 'insert';
+ /**
+ * Optional insertion target.
+ * When omitted, adapters may resolve a deterministic default insertion point.
+ */
+ target?: TextAddress;
+ text: string;
+};
+
+export type ReplaceWriteRequest = {
+ kind: 'replace';
+ target: TextAddress;
+ text: string;
+};
+
+export type DeleteWriteRequest = {
+ kind: 'delete';
+ target: TextAddress;
+ text?: '';
+};
+
+export type WriteRequest = InsertWriteRequest | ReplaceWriteRequest | DeleteWriteRequest;
+
+export interface WriteAdapter {
+ write(request: WriteRequest, options?: MutationOptions): TextMutationReceipt;
+}
+
+export function normalizeMutationOptions(options?: MutationOptions): MutationOptions {
+ return {
+ changeMode: options?.changeMode ?? 'direct',
+ dryRun: options?.dryRun ?? false,
+ };
+}
+
+export function executeWrite(
+ adapter: WriteAdapter,
+ request: WriteRequest,
+ options?: MutationOptions,
+): TextMutationReceipt {
+ return adapter.write(request, normalizeMutationOptions(options));
+}
diff --git a/packages/document-api/tsconfig.json b/packages/document-api/tsconfig.json
new file mode 100644
index 0000000000..a2da0aeb8b
--- /dev/null
+++ b/packages/document-api/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "declaration": true,
+ "emitDeclarationOnly": true,
+ "outDir": "dist",
+ "types": ["vitest/globals"]
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["src/**/*.test.ts"]
+}
diff --git a/packages/document-api/vite.config.js b/packages/document-api/vite.config.js
new file mode 100644
index 0000000000..4f34b51ad3
--- /dev/null
+++ b/packages/document-api/vite.config.js
@@ -0,0 +1,10 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ name: '@superdoc/document-api',
+ globals: true,
+ environment: 'node',
+ include: ['src/**/*.test.ts'],
+ },
+});
diff --git a/packages/super-editor/package.json b/packages/super-editor/package.json
index 9ffc0ec3f6..935ab73f81 100644
--- a/packages/super-editor/package.json
+++ b/packages/super-editor/package.json
@@ -115,6 +115,7 @@
"devDependencies": {
"@floating-ui/dom": "catalog:",
"@superdoc/common": "workspace:*",
+ "@superdoc/document-api": "workspace:*",
"@superdoc/contracts": "workspace:*",
"@superdoc/layout-bridge": "workspace:*",
"@superdoc/measuring-dom": "workspace:*",
diff --git a/packages/super-editor/scripts/vendor-document-api-types.cjs b/packages/super-editor/scripts/vendor-document-api-types.cjs
new file mode 100644
index 0000000000..25436279fa
--- /dev/null
+++ b/packages/super-editor/scripts/vendor-document-api-types.cjs
@@ -0,0 +1,91 @@
+#!/usr/bin/env node
+
+const fs = require('node:fs');
+const path = require('node:path');
+
+const packageRoot = path.resolve(__dirname, '..');
+const distRoot = path.join(packageRoot, 'dist');
+const documentApiDistRoot = path.resolve(packageRoot, '..', 'document-api', 'dist', 'src');
+const vendoredDocumentApiRoot = path.join(distRoot, 'document-api');
+
+const toPosix = (value) => value.split(path.sep).join('/');
+
+const ensureDotRelative = (value) => {
+ if (!value) return '.';
+ if (value.startsWith('.')) return value;
+ return `./${value}`;
+};
+
+const copyDir = (src, dest) => {
+ fs.mkdirSync(dest, { recursive: true });
+ const entries = fs.readdirSync(src, { withFileTypes: true });
+ for (const entry of entries) {
+ const srcPath = path.join(src, entry.name);
+ const destPath = path.join(dest, entry.name);
+ if (entry.isDirectory()) {
+ copyDir(srcPath, destPath);
+ continue;
+ }
+ if (entry.isFile()) {
+ fs.copyFileSync(srcPath, destPath);
+ }
+ }
+};
+
+const rewriteDeclarationImports = (filePath) => {
+ const original = fs.readFileSync(filePath, 'utf8');
+ if (!original.includes('@superdoc/document-api')) return false;
+
+ const relativeBase = ensureDotRelative(toPosix(path.relative(path.dirname(filePath), vendoredDocumentApiRoot)));
+ const localIndexPath = `${relativeBase}/index.js`;
+ const localTypesPath = `${relativeBase}/types/index.js`;
+
+ const rewritten = original
+ .replace(/(['"])@superdoc\/document-api\/types\1/g, `$1${localTypesPath}$1`)
+ .replace(/(['"])@superdoc\/document-api\1/g, `$1${localIndexPath}$1`);
+
+ if (rewritten === original) return false;
+ fs.writeFileSync(filePath, rewritten, 'utf8');
+ return true;
+};
+
+const visitDeclarations = (dirPath, onFile) => {
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
+ for (const entry of entries) {
+ const childPath = path.join(dirPath, entry.name);
+ if (entry.isDirectory()) {
+ visitDeclarations(childPath, onFile);
+ continue;
+ }
+ if (entry.isFile() && entry.name.endsWith('.d.ts')) {
+ onFile(childPath);
+ }
+ }
+};
+
+if (!fs.existsSync(distRoot)) {
+ console.error(`[vendor-document-api-types] Missing dist directory: ${distRoot}`);
+ process.exit(1);
+}
+
+if (!fs.existsSync(documentApiDistRoot)) {
+ console.error(
+ `[vendor-document-api-types] Missing document-api declarations at ${documentApiDistRoot}. ` +
+ 'Run `pnpm --dir ../document-api exec tsc -p tsconfig.json` first.',
+ );
+ process.exit(1);
+}
+
+copyDir(documentApiDistRoot, vendoredDocumentApiRoot);
+
+let rewrittenCount = 0;
+visitDeclarations(distRoot, (filePath) => {
+ if (rewriteDeclarationImports(filePath)) {
+ rewrittenCount += 1;
+ }
+});
+
+console.log(
+ `[vendor-document-api-types] Vendored document-api declarations into ${path.relative(packageRoot, vendoredDocumentApiRoot)}.`,
+);
+console.log(`[vendor-document-api-types] Rewrote ${rewrittenCount} declaration file(s).`);
diff --git a/packages/super-editor/src/core/Editor.coordsAtPos.test.ts b/packages/super-editor/src/core/Editor.coordsAtPos.test.ts
new file mode 100644
index 0000000000..6974289aff
--- /dev/null
+++ b/packages/super-editor/src/core/Editor.coordsAtPos.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it, vi } from 'vitest';
+import { Editor } from './Editor.js';
+
+function makeCoords(left: number, top: number) {
+ return {
+ left,
+ top,
+ right: left + 10,
+ bottom: top + 10,
+ width: 10,
+ height: 10,
+ };
+}
+
+describe('Editor.coordsAtPos', () => {
+ it('prefers PresentationEditor coordinates when available', () => {
+ const presentationCoords = makeCoords(100, 200);
+ const pmCoords = makeCoords(1, 2);
+
+ const presentationEditor = {
+ coordsAtPos: vi.fn(() => presentationCoords),
+ };
+ const view = {
+ coordsAtPos: vi.fn(() => pmCoords),
+ };
+
+ const editor = { presentationEditor, view } as unknown as Editor;
+ const result = Editor.prototype.coordsAtPos.call(editor, 7);
+
+ expect(result).toEqual(presentationCoords);
+ expect(presentationEditor.coordsAtPos).toHaveBeenCalledWith(7);
+ expect(view.coordsAtPos).not.toHaveBeenCalled();
+ });
+
+ it('falls back to ProseMirror view coordinates when presentation editor is absent', () => {
+ const pmCoords = makeCoords(3, 4);
+ const view = {
+ coordsAtPos: vi.fn(() => pmCoords),
+ };
+
+ const editor = { presentationEditor: null, view } as unknown as Editor;
+ const result = Editor.prototype.coordsAtPos.call(editor, 5);
+
+ expect(result).toEqual(pmCoords);
+ expect(view.coordsAtPos).toHaveBeenCalledWith(5);
+ });
+});
diff --git a/packages/super-editor/src/core/Editor.track-changes-dispatch.test.js b/packages/super-editor/src/core/Editor.track-changes-dispatch.test.js
new file mode 100644
index 0000000000..33e391570d
--- /dev/null
+++ b/packages/super-editor/src/core/Editor.track-changes-dispatch.test.js
@@ -0,0 +1,107 @@
+import { afterEach, describe, expect, it } from 'vitest';
+import { initTestEditor } from '@tests/helpers/helpers.js';
+import { getTrackChanges } from '@extensions/track-changes/trackChangesHelpers/getTrackChanges.js';
+import { TrackInsertMarkName } from '@extensions/track-changes/constants.js';
+import { TrackChangesBasePluginKey } from '@extensions/track-changes/plugins/trackChangesBasePlugin.js';
+
+describe('Editor dispatch tracked-change meta', () => {
+ let editor;
+
+ afterEach(() => {
+ if (editor && !editor.isDestroyed) {
+ editor.destroy();
+ editor = null;
+ }
+ });
+
+ it('treats forceTrackChanges programmatic transactions as tracked even when global mode is off', () => {
+ ({ editor } = initTestEditor({
+ mode: 'text',
+ content: 'Hello
',
+ user: { name: 'Test', email: 'test@example.com' },
+ useImmediateSetTimeout: false,
+ }));
+
+ const trackInsertMark = editor.schema?.marks?.[TrackInsertMarkName];
+ expect(trackInsertMark).toBeDefined();
+
+ const trackState = TrackChangesBasePluginKey.getState(editor.state);
+ expect(trackState?.isTrackChangesActive ?? false).toBe(false);
+ expect(getTrackChanges(editor.state)).toHaveLength(0);
+
+ const tr = editor.state.tr
+ .insertText('X', 1, 1)
+ .setMeta('inputType', 'programmatic')
+ .setMeta('forceTrackChanges', true);
+
+ editor.dispatch(tr);
+
+ const tracked = getTrackChanges(editor.state);
+ expect(tracked.some((entry) => entry.mark.type.name === TrackInsertMarkName)).toBe(true);
+ });
+
+ it('skipTrackChanges overrides forceTrackChanges — no tracking applied', () => {
+ ({ editor } = initTestEditor({
+ mode: 'text',
+ content: 'Hello
',
+ user: { name: 'Test', email: 'test@example.com' },
+ useImmediateSetTimeout: false,
+ }));
+
+ const trackState = TrackChangesBasePluginKey.getState(editor.state);
+ expect(trackState?.isTrackChangesActive ?? false).toBe(false);
+
+ const tr = editor.state.tr
+ .insertText('X', 1, 1)
+ .setMeta('inputType', 'programmatic')
+ .setMeta('forceTrackChanges', true)
+ .setMeta('skipTrackChanges', true);
+
+ editor.dispatch(tr);
+
+ const tracked = getTrackChanges(editor.state);
+ expect(tracked).toHaveLength(0);
+ });
+
+ it('throws a clear error when forceTrackChanges is used without a configured user', () => {
+ ({ editor } = initTestEditor({
+ mode: 'text',
+ content: 'Hello
',
+ useImmediateSetTimeout: false,
+ }));
+
+ const tr = editor.state.tr
+ .insertText('X', 1, 1)
+ .setMeta('inputType', 'programmatic')
+ .setMeta('forceTrackChanges', true);
+
+ expect(() => editor.dispatch(tr)).toThrow(
+ 'forceTrackChanges requires a user to be configured on the editor instance.',
+ );
+ });
+
+ it('global track-changes mode still produces tracked entities without forceTrackChanges', () => {
+ ({ editor } = initTestEditor({
+ mode: 'text',
+ content: 'Hello
',
+ user: { name: 'Test', email: 'test@example.com' },
+ useImmediateSetTimeout: false,
+ }));
+
+ const enableTr = editor.state.tr.setMeta(TrackChangesBasePluginKey, {
+ type: 'TRACK_CHANGES_ENABLE',
+ value: true,
+ });
+ editor.dispatch(enableTr);
+
+ const trackState = TrackChangesBasePluginKey.getState(editor.state);
+ expect(trackState?.isTrackChangesActive).toBe(true);
+
+ const tr = editor.state.tr.insertText('Y', 1, 1).setMeta('inputType', 'programmatic');
+
+ editor.dispatch(tr);
+
+ const tracked = getTrackChanges(editor.state);
+ expect(tracked.some((entry) => entry.mark.type.name === TrackInsertMarkName)).toBe(true);
+ });
+});
diff --git a/packages/super-editor/src/core/Editor.ts b/packages/super-editor/src/core/Editor.ts
index 399853625d..f5f9cb6945 100644
--- a/packages/super-editor/src/core/Editor.ts
+++ b/packages/super-editor/src/core/Editor.ts
@@ -2090,23 +2090,29 @@ export class Editor extends EventEmitter {
const prevState = this.state;
let nextState: EditorState;
let transactionToApply = transaction;
+ const forceTrackChanges = transactionToApply.getMeta('forceTrackChanges') === true;
try {
const trackChangesState = TrackChangesBasePluginKey.getState(prevState);
const isTrackChangesActive = trackChangesState?.isTrackChangesActive ?? false;
const skipTrackChanges = transactionToApply.getMeta('skipTrackChanges') === true;
- transactionToApply =
- isTrackChangesActive && !skipTrackChanges
- ? trackedTransaction({
- tr: transactionToApply,
- state: prevState,
- user: this.options.user!,
- })
- : transactionToApply;
+ const shouldTrack = (isTrackChangesActive || forceTrackChanges) && !skipTrackChanges;
+ if (shouldTrack && forceTrackChanges && !this.options.user) {
+ throw new Error('forceTrackChanges requires a user to be configured on the editor instance.');
+ }
+
+ transactionToApply = shouldTrack
+ ? trackedTransaction({
+ tr: transactionToApply,
+ state: prevState,
+ user: this.options.user!,
+ })
+ : transactionToApply;
const { state: appliedState } = prevState.applyTransaction(transactionToApply);
nextState = appliedState;
} catch (error) {
+ if (forceTrackChanges) throw error;
// just in case
nextState = prevState.apply(transactionToApply);
console.log(error);
diff --git a/packages/super-editor/src/core/commands/core-command-map.d.ts b/packages/super-editor/src/core/commands/core-command-map.d.ts
index 6c7618dec9..b71184b191 100644
--- a/packages/super-editor/src/core/commands/core-command-map.d.ts
+++ b/packages/super-editor/src/core/commands/core-command-map.d.ts
@@ -38,6 +38,7 @@ type CoreCommandNames =
| 'selectTextblockEnd'
| 'insertContent'
| 'insertContentAt'
+ | 'insertParagraphAt'
| 'undoInputRule'
| 'setSectionPageMarginsAtSelection'
| 'toggleList'
@@ -45,6 +46,9 @@ type CoreCommandNames =
| 'decreaseListIndent'
| 'changeListLevel'
| 'removeNumberingProperties'
+ | 'insertListItemAt'
+ | 'setListTypeAt'
+ | 'exitListItemAt'
| 'restoreSelection'
| 'setTextSelection'
| 'getSelectionMarks';
diff --git a/packages/super-editor/src/core/commands/exitListItemAt.js b/packages/super-editor/src/core/commands/exitListItemAt.js
new file mode 100644
index 0000000000..66a011d573
--- /dev/null
+++ b/packages/super-editor/src/core/commands/exitListItemAt.js
@@ -0,0 +1,29 @@
+import { updateNumberingProperties } from './changeListLevel.js';
+import { getResolvedParagraphProperties } from '@extensions/paragraph/resolvedPropertiesCache.js';
+
+/**
+ * Remove list numbering from the paragraph at a specific position.
+ *
+ * Unlike cursor-driven removeNumbering commands, this operation is explicit
+ * and ignores caret/empty-line guards.
+ *
+ * @param {{ pos: number }} options
+ * @returns {import('./types/index.js').Command}
+ */
+export const exitListItemAt =
+ ({ pos }) =>
+ ({ state, tr, editor, dispatch }) => {
+ if (!Number.isInteger(pos) || pos < 0 || pos > state.doc.content.size) return false;
+
+ const paragraph = state.doc.nodeAt(pos);
+ if (!paragraph || paragraph.type.name !== 'paragraph') return false;
+
+ const resolvedProps = getResolvedParagraphProperties(paragraph);
+ const numberingProperties =
+ resolvedProps?.numberingProperties ?? paragraph.attrs?.paragraphProperties?.numberingProperties;
+ if (!numberingProperties) return false;
+
+ updateNumberingProperties(null, paragraph, pos, editor, tr);
+ if (dispatch) dispatch(tr);
+ return true;
+ };
diff --git a/packages/super-editor/src/core/commands/exitListItemAt.test.js b/packages/super-editor/src/core/commands/exitListItemAt.test.js
new file mode 100644
index 0000000000..712254ebe4
--- /dev/null
+++ b/packages/super-editor/src/core/commands/exitListItemAt.test.js
@@ -0,0 +1,105 @@
+// @ts-check
+import { describe, it, expect, vi } from 'vitest';
+
+vi.mock('./changeListLevel.js', () => ({
+ updateNumberingProperties: vi.fn(),
+}));
+
+vi.mock('@extensions/paragraph/resolvedPropertiesCache.js', () => ({
+ getResolvedParagraphProperties: vi.fn((node) => node.attrs?.paragraphProperties ?? {}),
+}));
+
+import { exitListItemAt } from './exitListItemAt.js';
+import { updateNumberingProperties } from './changeListLevel.js';
+
+function createListParagraph(numId = 1, ilvl = 0) {
+ return {
+ type: { name: 'paragraph' },
+ attrs: {
+ paragraphProperties: {
+ numberingProperties: { numId, ilvl },
+ },
+ },
+ nodeSize: 7,
+ };
+}
+
+function createMockState(nodeAtResult = createListParagraph()) {
+ return {
+ state: {
+ doc: {
+ content: { size: 100 },
+ nodeAt: vi.fn(() => nodeAtResult),
+ },
+ },
+ tr: {},
+ editor: {},
+ dispatch: vi.fn(),
+ };
+}
+
+describe('exitListItemAt', () => {
+ it('returns false when pos is negative', () => {
+ const props = createMockState();
+ const result = exitListItemAt({ pos: -1 })(props);
+ expect(result).toBe(false);
+ });
+
+ it('returns false when pos exceeds document size', () => {
+ const props = createMockState();
+ props.state.doc.content.size = 10;
+ const result = exitListItemAt({ pos: 11 })(props);
+ expect(result).toBe(false);
+ });
+
+ it('returns false when pos is not an integer', () => {
+ const props = createMockState();
+ const result = exitListItemAt({ pos: 2.5 })(props);
+ expect(result).toBe(false);
+ });
+
+ it('returns false when node at pos is null', () => {
+ const props = createMockState();
+ props.state.doc.nodeAt.mockReturnValue(null);
+ const result = exitListItemAt({ pos: 0 })(props);
+ expect(result).toBe(false);
+ });
+
+ it('returns false when node at pos is not a paragraph', () => {
+ const props = createMockState({ type: { name: 'table' }, attrs: {} });
+ const result = exitListItemAt({ pos: 0 })(props);
+ expect(result).toBe(false);
+ });
+
+ it('returns false when paragraph has no numbering properties', () => {
+ const plainParagraph = {
+ type: { name: 'paragraph' },
+ attrs: { paragraphProperties: {} },
+ };
+ const props = createMockState(plainParagraph);
+ const result = exitListItemAt({ pos: 0 })(props);
+ expect(result).toBe(false);
+ });
+
+ it('calls updateNumberingProperties with null and dispatches on success', () => {
+ const node = createListParagraph();
+ const props = createMockState(node);
+
+ const result = exitListItemAt({ pos: 5 })(props);
+
+ expect(result).toBe(true);
+ expect(updateNumberingProperties).toHaveBeenCalledWith(null, node, 5, props.editor, props.tr);
+ expect(props.dispatch).toHaveBeenCalledWith(props.tr);
+ });
+
+ it('does not dispatch when dispatch is not provided', () => {
+ const node = createListParagraph();
+ const props = createMockState(node);
+ props.dispatch = undefined;
+
+ const result = exitListItemAt({ pos: 5 })(props);
+
+ expect(result).toBe(true);
+ expect(updateNumberingProperties).toHaveBeenCalled();
+ });
+});
diff --git a/packages/super-editor/src/core/commands/index.js b/packages/super-editor/src/core/commands/index.js
index a3b3363fa9..8b63ba863d 100644
--- a/packages/super-editor/src/core/commands/index.js
+++ b/packages/super-editor/src/core/commands/index.js
@@ -33,6 +33,7 @@ export * from './selectTextblockStart.js';
export * from './selectTextblockEnd.js';
export * from './insertContent.js';
export * from './insertContentAt.js';
+export * from './insertParagraphAt.js';
export * from './undoInputRule.js';
export * from './setBodyHeaderFooter.js';
export * from './setSectionHeaderFooterAtSelection.js';
@@ -57,6 +58,9 @@ export * from './increaseListIndent.js';
export * from './decreaseListIndent.js';
export * from './changeListLevel.js';
export * from './removeNumberingProperties.js';
+export * from './insertListItemAt.js';
+export * from './setListTypeAt.js';
+export * from './exitListItemAt.js';
// Selection
export * from './restoreSelection.js';
diff --git a/packages/super-editor/src/core/commands/insertListItemAt.js b/packages/super-editor/src/core/commands/insertListItemAt.js
new file mode 100644
index 0000000000..eab3bc760e
--- /dev/null
+++ b/packages/super-editor/src/core/commands/insertListItemAt.js
@@ -0,0 +1,70 @@
+import { getResolvedParagraphProperties } from '@extensions/paragraph/resolvedPropertiesCache.js';
+
+/**
+ * Insert a list-item paragraph before/after a target list paragraph position.
+ *
+ * This command preserves numbering metadata (numId/ilvl) from the target item,
+ * and always leaves marker rendering to the numbering plugin.
+ *
+ * @param {{ pos: number; position: 'before' | 'after'; text?: string; sdBlockId?: string; tracked?: boolean }} options
+ * @returns {import('./types/index.js').Command}
+ */
+export const insertListItemAt =
+ ({ pos, position, text = '', sdBlockId, tracked }) =>
+ ({ state, dispatch }) => {
+ if (!Number.isInteger(pos) || pos < 0 || pos > state.doc.content.size) return false;
+ if (position !== 'before' && position !== 'after') return false;
+
+ const targetNode = state.doc.nodeAt(pos);
+ if (!targetNode || targetNode.type.name !== 'paragraph') return false;
+
+ const resolvedProps = getResolvedParagraphProperties(targetNode);
+ const paragraphProperties = targetNode.attrs?.paragraphProperties ?? {};
+ const numberingProperties = resolvedProps?.numberingProperties ?? paragraphProperties?.numberingProperties;
+ if (!numberingProperties) return false;
+
+ const paragraphType = state.schema.nodes.paragraph;
+ if (!paragraphType) return false;
+
+ const newParagraphProperties = {
+ ...paragraphProperties,
+ numberingProperties: { ...numberingProperties },
+ };
+
+ const attrs = {
+ ...(targetNode.attrs ?? {}),
+ sdBlockId: sdBlockId ?? null,
+ paraId: null,
+ textId: null,
+ listRendering: null,
+ paragraphProperties: newParagraphProperties,
+ numberingProperties: newParagraphProperties.numberingProperties,
+ };
+
+ const normalizedText = typeof text === 'string' ? text : '';
+ const textNode = normalizedText.length > 0 ? state.schema.text(normalizedText) : undefined;
+
+ let paragraphNode;
+ try {
+ paragraphNode =
+ paragraphType.createAndFill(attrs, textNode) ?? paragraphType.create(attrs, textNode ? [textNode] : undefined);
+ } catch {
+ return false;
+ }
+ if (!paragraphNode) return false;
+
+ const insertPos = position === 'before' ? pos : pos + targetNode.nodeSize;
+ if (!Number.isInteger(insertPos) || insertPos < 0 || insertPos > state.doc.content.size) return false;
+
+ if (!dispatch) return true;
+
+ try {
+ const tr = state.tr.insert(insertPos, paragraphNode).setMeta('inputType', 'programmatic');
+ if (tracked === true) tr.setMeta('forceTrackChanges', true);
+ else if (tracked === false) tr.setMeta('skipTrackChanges', true);
+ dispatch(tr);
+ return true;
+ } catch {
+ return false;
+ }
+ };
diff --git a/packages/super-editor/src/core/commands/insertListItemAt.test.js b/packages/super-editor/src/core/commands/insertListItemAt.test.js
new file mode 100644
index 0000000000..ee94140729
--- /dev/null
+++ b/packages/super-editor/src/core/commands/insertListItemAt.test.js
@@ -0,0 +1,209 @@
+// @ts-check
+import { describe, it, expect, vi } from 'vitest';
+
+vi.mock('@extensions/paragraph/resolvedPropertiesCache.js', () => ({
+ getResolvedParagraphProperties: vi.fn((node) => node.attrs?.paragraphProperties ?? {}),
+}));
+
+import { insertListItemAt } from './insertListItemAt.js';
+
+const numberingProperties = { numId: 1, ilvl: 0 };
+
+function createListParagraph(text = 'Hello') {
+ return {
+ type: { name: 'paragraph' },
+ attrs: {
+ paragraphProperties: { numberingProperties },
+ numberingProperties,
+ },
+ nodeSize: text.length + 2,
+ };
+}
+
+function createMockState(targetNode = createListParagraph()) {
+ const paragraphType = {
+ createAndFill: vi.fn(() => ({ type: { name: 'paragraph' }, nodeSize: 2 })),
+ create: vi.fn(() => ({ type: { name: 'paragraph' }, nodeSize: 2 })),
+ };
+
+ const mockTr = {
+ insert: vi.fn().mockReturnThis(),
+ setMeta: vi.fn().mockReturnThis(),
+ };
+
+ return {
+ state: {
+ doc: {
+ content: { size: 100 },
+ nodeAt: vi.fn((pos) => (pos === 0 ? targetNode : null)),
+ },
+ schema: {
+ nodes: { paragraph: paragraphType },
+ text: vi.fn((text) => ({ type: { name: 'text' }, text, nodeSize: text.length })),
+ },
+ tr: mockTr,
+ },
+ paragraphType,
+ dispatch: vi.fn(),
+ };
+}
+
+describe('insertListItemAt', () => {
+ it('returns false when pos is negative', () => {
+ const { state, dispatch } = createMockState();
+ const result = insertListItemAt({ pos: -1, position: 'after' })({ state, dispatch });
+ expect(result).toBe(false);
+ });
+
+ it('returns false when pos is not an integer', () => {
+ const { state, dispatch } = createMockState();
+ const result = insertListItemAt({ pos: 1.5, position: 'after' })({ state, dispatch });
+ expect(result).toBe(false);
+ });
+
+ it('returns false when position is invalid', () => {
+ const { state, dispatch } = createMockState();
+ // @ts-expect-error - testing invalid input
+ const result = insertListItemAt({ pos: 0, position: 'middle' })({ state, dispatch });
+ expect(result).toBe(false);
+ });
+
+ it('returns false when target node is not a paragraph', () => {
+ const nonParagraph = { type: { name: 'table' }, attrs: {}, nodeSize: 10 };
+ const { state, dispatch } = createMockState();
+ state.doc.nodeAt.mockReturnValue(nonParagraph);
+ const result = insertListItemAt({ pos: 0, position: 'after' })({ state, dispatch });
+ expect(result).toBe(false);
+ });
+
+ it('returns false when target has no numbering properties', () => {
+ const plainParagraph = {
+ type: { name: 'paragraph' },
+ attrs: { paragraphProperties: {} },
+ nodeSize: 7,
+ };
+ const { state, dispatch } = createMockState();
+ state.doc.nodeAt.mockReturnValue(plainParagraph);
+ const result = insertListItemAt({ pos: 0, position: 'after' })({ state, dispatch });
+ expect(result).toBe(false);
+ });
+
+ it('returns true without dispatching when dispatch is not provided', () => {
+ const { state } = createMockState();
+ const result = insertListItemAt({ pos: 0, position: 'after' })({ state, dispatch: undefined });
+ expect(result).toBe(true);
+ });
+
+ it('inserts after target when position is after', () => {
+ const target = createListParagraph('Hello');
+ const { state, dispatch } = createMockState(target);
+ state.doc.nodeAt.mockReturnValue(target);
+
+ const result = insertListItemAt({ pos: 0, position: 'after' })({ state, dispatch });
+
+ expect(result).toBe(true);
+ expect(state.tr.insert).toHaveBeenCalledWith(
+ target.nodeSize, // pos + nodeSize for 'after'
+ expect.any(Object),
+ );
+ expect(dispatch).toHaveBeenCalled();
+ });
+
+ it('inserts before target when position is before', () => {
+ const { state, dispatch } = createMockState();
+
+ const result = insertListItemAt({ pos: 0, position: 'before' })({ state, dispatch });
+
+ expect(result).toBe(true);
+ expect(state.tr.insert).toHaveBeenCalledWith(0, expect.any(Object));
+ });
+
+ it('sets forceTrackChanges meta when tracked is true', () => {
+ const { state, dispatch } = createMockState();
+
+ insertListItemAt({ pos: 0, position: 'after', tracked: true })({ state, dispatch });
+
+ expect(state.tr.setMeta).toHaveBeenCalledWith('forceTrackChanges', true);
+ });
+
+ it('sets skipTrackChanges meta when tracked is false to preserve direct mode semantics', () => {
+ const { state, dispatch } = createMockState();
+
+ insertListItemAt({ pos: 0, position: 'after', tracked: false })({ state, dispatch });
+
+ expect(state.tr.setMeta).toHaveBeenCalledWith('skipTrackChanges', true);
+ });
+
+ it('sets inputType programmatic meta', () => {
+ const { state, dispatch } = createMockState();
+
+ insertListItemAt({ pos: 0, position: 'after' })({ state, dispatch });
+
+ expect(state.tr.setMeta).toHaveBeenCalledWith('inputType', 'programmatic');
+ });
+
+ it('passes sdBlockId into the created node attrs', () => {
+ const { state, dispatch, paragraphType } = createMockState();
+
+ insertListItemAt({ pos: 0, position: 'after', sdBlockId: 'custom-id' })({
+ state,
+ dispatch,
+ });
+
+ const callArgs = paragraphType.createAndFill.mock.calls[0];
+ expect(callArgs?.[0]).toMatchObject({ sdBlockId: 'custom-id' });
+ });
+
+ it('preserves numbering properties from the target node', () => {
+ const { state, dispatch, paragraphType } = createMockState();
+
+ insertListItemAt({ pos: 0, position: 'after' })({ state, dispatch });
+
+ const callArgs = paragraphType.createAndFill.mock.calls[0];
+ expect(callArgs?.[0]).toMatchObject({
+ paragraphProperties: {
+ numberingProperties: { numId: 1, ilvl: 0 },
+ },
+ });
+ });
+
+ it('creates text content when text is provided', () => {
+ const { state, dispatch } = createMockState();
+
+ insertListItemAt({ pos: 0, position: 'after', text: 'New item' })({
+ state,
+ dispatch,
+ });
+
+ expect(state.schema.text).toHaveBeenCalledWith('New item');
+ });
+
+ it('does not inherit sdBlockId from the target node when omitted', () => {
+ const targetWithBlockId = {
+ type: { name: 'paragraph' },
+ attrs: {
+ sdBlockId: 'source-block-id',
+ paragraphProperties: { numberingProperties },
+ numberingProperties,
+ },
+ nodeSize: 7,
+ };
+ const { state, dispatch, paragraphType } = createMockState(targetWithBlockId);
+ state.doc.nodeAt.mockReturnValue(targetWithBlockId);
+
+ insertListItemAt({ pos: 0, position: 'after' })({ state, dispatch });
+
+ const callArgs = paragraphType.createAndFill.mock.calls[0];
+ expect(callArgs?.[0]?.sdBlockId).toBeNull();
+ });
+
+ it('returns false when createAndFill throws', () => {
+ const { state, dispatch, paragraphType } = createMockState();
+ paragraphType.createAndFill.mockImplementation(() => {
+ throw new Error('schema error');
+ });
+
+ const result = insertListItemAt({ pos: 0, position: 'after' })({ state, dispatch });
+ expect(result).toBe(false);
+ });
+});
diff --git a/packages/super-editor/src/core/commands/insertParagraphAt.js b/packages/super-editor/src/core/commands/insertParagraphAt.js
new file mode 100644
index 0000000000..4c69025d5d
--- /dev/null
+++ b/packages/super-editor/src/core/commands/insertParagraphAt.js
@@ -0,0 +1,45 @@
+/**
+ * Insert a paragraph node at an absolute document position.
+ *
+ * Supports optional seed text, deterministic block id assignment, and
+ * operation-scoped tracked-change conversion via transaction meta.
+ *
+ * @param {{ pos: number; text?: string; sdBlockId?: string; tracked?: boolean }} options
+ * @returns {import('./types/index.js').Command}
+ */
+export const insertParagraphAt =
+ ({ pos, text = '', sdBlockId, tracked }) =>
+ ({ state, dispatch }) => {
+ const paragraphType = state.schema.nodes.paragraph;
+ if (!paragraphType) return false;
+ if (!Number.isInteger(pos) || pos < 0 || pos > state.doc.content.size) return false;
+
+ const attrs = sdBlockId ? { sdBlockId } : undefined;
+ const normalizedText = typeof text === 'string' ? text : '';
+ const textNode = normalizedText.length > 0 ? state.schema.text(normalizedText) : null;
+
+ let paragraphNode;
+ try {
+ paragraphNode =
+ paragraphType.createAndFill(attrs, textNode ?? undefined) ??
+ paragraphType.create(attrs, textNode ? [textNode] : undefined);
+ } catch {
+ return false;
+ }
+
+ if (!paragraphNode) return false;
+
+ // Validate the structural insertion before the dispatch guard so that
+ // editor.can().insertParagraphAt() accurately reflects feasibility.
+ try {
+ const tr = state.tr.insert(pos, paragraphNode);
+ if (!dispatch) return true;
+ tr.setMeta('inputType', 'programmatic');
+ if (tracked === true) tr.setMeta('forceTrackChanges', true);
+ else if (tracked === false) tr.setMeta('skipTrackChanges', true);
+ dispatch(tr);
+ return true;
+ } catch {
+ return false;
+ }
+ };
diff --git a/packages/super-editor/src/core/commands/insertParagraphAt.test.js b/packages/super-editor/src/core/commands/insertParagraphAt.test.js
new file mode 100644
index 0000000000..979e3c3654
--- /dev/null
+++ b/packages/super-editor/src/core/commands/insertParagraphAt.test.js
@@ -0,0 +1,169 @@
+// @ts-check
+import { describe, it, expect, vi } from 'vitest';
+import { insertParagraphAt } from './insertParagraphAt.js';
+
+/**
+ * @param {{ size?: number }} [options]
+ */
+function createMockState(options = {}) {
+ const { size = 100 } = options;
+
+ const paragraphType = {
+ createAndFill: vi.fn(),
+ create: vi.fn(),
+ };
+
+ const schema = {
+ nodes: { paragraph: paragraphType },
+ text: vi.fn((text) => ({ type: { name: 'text' }, text, nodeSize: text.length })),
+ };
+
+ const mockTr = {
+ insert: vi.fn().mockReturnThis(),
+ setMeta: vi.fn().mockReturnThis(),
+ };
+
+ return {
+ state: {
+ doc: { content: { size } },
+ schema,
+ tr: mockTr,
+ },
+ tr: mockTr,
+ paragraphType,
+ dispatch: vi.fn(),
+ };
+}
+
+describe('insertParagraphAt', () => {
+ it('returns false when pos is negative', () => {
+ const { state, dispatch } = createMockState();
+ const result = insertParagraphAt({ pos: -1 })({ state, dispatch });
+ expect(result).toBe(false);
+ });
+
+ it('returns false when pos exceeds document size', () => {
+ const { state, dispatch } = createMockState({ size: 10 });
+ const result = insertParagraphAt({ pos: 11 })({ state, dispatch });
+ expect(result).toBe(false);
+ });
+
+ it('returns false when pos is not an integer', () => {
+ const { state, dispatch } = createMockState();
+ const result = insertParagraphAt({ pos: 1.5 })({ state, dispatch });
+ expect(result).toBe(false);
+ });
+
+ it('returns false when paragraph type is not in schema', () => {
+ const { state, dispatch } = createMockState();
+ state.schema.nodes.paragraph = undefined;
+ const result = insertParagraphAt({ pos: 0 })({ state, dispatch });
+ expect(result).toBe(false);
+ });
+
+ it('returns true without dispatching when dispatch is not provided (dry run)', () => {
+ const { state, paragraphType } = createMockState();
+ const mockNode = { type: { name: 'paragraph' } };
+ paragraphType.createAndFill.mockReturnValue(mockNode);
+
+ const result = insertParagraphAt({ pos: 0 })({ state, dispatch: undefined });
+ expect(result).toBe(true);
+ });
+
+ it('inserts a paragraph at the given position', () => {
+ const { state, dispatch, paragraphType, tr } = createMockState();
+ const mockNode = { type: { name: 'paragraph' } };
+ paragraphType.createAndFill.mockReturnValue(mockNode);
+
+ const result = insertParagraphAt({ pos: 5 })({ state, dispatch });
+
+ expect(result).toBe(true);
+ expect(tr.insert).toHaveBeenCalledWith(5, mockNode);
+ expect(tr.setMeta).toHaveBeenCalledWith('inputType', 'programmatic');
+ expect(dispatch).toHaveBeenCalledWith(tr);
+ });
+
+ it('passes sdBlockId as attrs when provided', () => {
+ const { state, dispatch, paragraphType } = createMockState();
+ const mockNode = { type: { name: 'paragraph' } };
+ paragraphType.createAndFill.mockReturnValue(mockNode);
+
+ insertParagraphAt({ pos: 0, sdBlockId: 'block-1' })({ state, dispatch });
+
+ expect(paragraphType.createAndFill).toHaveBeenCalledWith({ sdBlockId: 'block-1' }, undefined);
+ });
+
+ it('creates a text node when text is provided', () => {
+ const { state, dispatch, paragraphType } = createMockState();
+ const mockNode = { type: { name: 'paragraph' } };
+ paragraphType.createAndFill.mockReturnValue(mockNode);
+
+ insertParagraphAt({ pos: 0, text: 'Hello' })({ state, dispatch });
+
+ expect(state.schema.text).toHaveBeenCalledWith('Hello');
+ });
+
+ it('sets forceTrackChanges meta when tracked is true', () => {
+ const { state, dispatch, paragraphType, tr } = createMockState();
+ const mockNode = { type: { name: 'paragraph' } };
+ paragraphType.createAndFill.mockReturnValue(mockNode);
+
+ insertParagraphAt({ pos: 0, tracked: true })({ state, dispatch });
+
+ expect(tr.setMeta).toHaveBeenCalledWith('forceTrackChanges', true);
+ });
+
+ it('does not set forceTrackChanges when tracked is false', () => {
+ const { state, dispatch, paragraphType, tr } = createMockState();
+ const mockNode = { type: { name: 'paragraph' } };
+ paragraphType.createAndFill.mockReturnValue(mockNode);
+
+ insertParagraphAt({ pos: 0, tracked: false })({ state, dispatch });
+
+ const metaCalls = tr.setMeta.mock.calls.map((call) => call[0]);
+ expect(metaCalls).not.toContain('forceTrackChanges');
+ });
+
+ it('sets skipTrackChanges meta when tracked is false to preserve direct mode semantics', () => {
+ const { state, dispatch, paragraphType, tr } = createMockState();
+ const mockNode = { type: { name: 'paragraph' } };
+ paragraphType.createAndFill.mockReturnValue(mockNode);
+
+ insertParagraphAt({ pos: 0, tracked: false })({ state, dispatch });
+
+ expect(tr.setMeta).toHaveBeenCalledWith('skipTrackChanges', true);
+ });
+
+ it('falls back to paragraphType.create when createAndFill returns null', () => {
+ const { state, dispatch, paragraphType } = createMockState();
+ const mockNode = { type: { name: 'paragraph' } };
+ paragraphType.createAndFill.mockReturnValue(null);
+ paragraphType.create.mockReturnValue(mockNode);
+
+ const result = insertParagraphAt({ pos: 0 })({ state, dispatch });
+ expect(result).toBe(true);
+ expect(paragraphType.create).toHaveBeenCalled();
+ });
+
+ it('returns false when both createAndFill and create throw', () => {
+ const { state, dispatch, paragraphType } = createMockState();
+ paragraphType.createAndFill.mockImplementation(() => {
+ throw new Error('invalid');
+ });
+
+ const result = insertParagraphAt({ pos: 0 })({ state, dispatch });
+ expect(result).toBe(false);
+ });
+
+ it('returns false when tr.insert throws', () => {
+ const { state, dispatch, paragraphType, tr } = createMockState();
+ const mockNode = { type: { name: 'paragraph' } };
+ paragraphType.createAndFill.mockReturnValue(mockNode);
+ tr.insert.mockImplementation(() => {
+ throw new Error('Position out of range');
+ });
+
+ const result = insertParagraphAt({ pos: 0 })({ state, dispatch });
+ expect(result).toBe(false);
+ });
+});
diff --git a/packages/super-editor/src/core/commands/setListTypeAt.js b/packages/super-editor/src/core/commands/setListTypeAt.js
new file mode 100644
index 0000000000..2f71171880
--- /dev/null
+++ b/packages/super-editor/src/core/commands/setListTypeAt.js
@@ -0,0 +1,57 @@
+import { ListHelpers } from '@helpers/list-numbering-helpers.js';
+import { updateNumberingProperties } from './changeListLevel.js';
+import { getResolvedParagraphProperties } from '@extensions/paragraph/resolvedPropertiesCache.js';
+
+/**
+ * Set the list kind for a paragraph-based list item at a specific position.
+ *
+ * Uses deterministic semantics:
+ * - `kind: "ordered"` -> default ordered numbering definition
+ * - `kind: "bullet"` -> default bullet numbering definition
+ *
+ * @param {{ pos: number; kind: 'ordered' | 'bullet' }} options
+ * @returns {import('./types/index.js').Command}
+ */
+export const setListTypeAt =
+ ({ pos, kind }) =>
+ ({ state, tr, editor, dispatch }) => {
+ if (!Number.isInteger(pos) || pos < 0 || pos > state.doc.content.size) return false;
+ if (kind !== 'ordered' && kind !== 'bullet') return false;
+
+ const paragraph = state.doc.nodeAt(pos);
+ if (!paragraph || paragraph.type.name !== 'paragraph') return false;
+
+ const resolvedProps = getResolvedParagraphProperties(paragraph);
+ const numberingProperties =
+ resolvedProps?.numberingProperties ?? paragraph.attrs?.paragraphProperties?.numberingProperties;
+ if (!numberingProperties) return false;
+
+ const level = Number(numberingProperties.ilvl ?? 0) || 0;
+ const listType = kind === 'bullet' ? 'bulletList' : 'orderedList';
+
+ if (!dispatch) return true;
+
+ const newNumId = Number(ListHelpers.getNewListId(editor));
+ if (!Number.isFinite(newNumId)) return false;
+
+ ListHelpers.generateNewListDefinition({
+ numId: newNumId,
+ listType,
+ editor,
+ });
+
+ updateNumberingProperties(
+ {
+ ...numberingProperties,
+ numId: newNumId,
+ ilvl: level,
+ },
+ paragraph,
+ pos,
+ editor,
+ tr,
+ );
+
+ dispatch(tr);
+ return true;
+ };
diff --git a/packages/super-editor/src/core/commands/setListTypeAt.test.js b/packages/super-editor/src/core/commands/setListTypeAt.test.js
new file mode 100644
index 0000000000..be1197c76b
--- /dev/null
+++ b/packages/super-editor/src/core/commands/setListTypeAt.test.js
@@ -0,0 +1,155 @@
+// @ts-check
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+vi.mock('@helpers/list-numbering-helpers.js', () => ({
+ ListHelpers: {
+ getNewListId: vi.fn(() => '42'),
+ generateNewListDefinition: vi.fn(),
+ },
+}));
+
+vi.mock('./changeListLevel.js', () => ({
+ updateNumberingProperties: vi.fn(),
+}));
+
+vi.mock('@extensions/paragraph/resolvedPropertiesCache.js', () => ({
+ getResolvedParagraphProperties: vi.fn((node) => node.attrs?.paragraphProperties ?? {}),
+}));
+
+import { setListTypeAt } from './setListTypeAt.js';
+import { ListHelpers } from '@helpers/list-numbering-helpers.js';
+import { updateNumberingProperties } from './changeListLevel.js';
+
+function createListParagraph(numId = 1, ilvl = 0) {
+ return {
+ type: { name: 'paragraph' },
+ attrs: {
+ paragraphProperties: {
+ numberingProperties: { numId, ilvl },
+ },
+ },
+ nodeSize: 7,
+ };
+}
+
+function createMockProps(nodeAtResult = createListParagraph()) {
+ return {
+ state: {
+ doc: {
+ content: { size: 100 },
+ nodeAt: vi.fn(() => nodeAtResult),
+ },
+ },
+ tr: {},
+ editor: {},
+ dispatch: vi.fn(),
+ };
+}
+
+describe('setListTypeAt', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ ListHelpers.getNewListId.mockReturnValue('42');
+ ListHelpers.generateNewListDefinition.mockReturnValue(undefined);
+ });
+
+ it('returns false when pos is negative', () => {
+ const props = createMockProps();
+ const result = setListTypeAt({ pos: -1, kind: 'bullet' })(props);
+ expect(result).toBe(false);
+ });
+
+ it('returns false when pos exceeds document size', () => {
+ const props = createMockProps();
+ props.state.doc.content.size = 10;
+ const result = setListTypeAt({ pos: 11, kind: 'bullet' })(props);
+ expect(result).toBe(false);
+ });
+
+ it('returns false when kind is invalid', () => {
+ const props = createMockProps();
+ // @ts-expect-error - testing invalid input
+ const result = setListTypeAt({ pos: 0, kind: 'numbered' })(props);
+ expect(result).toBe(false);
+ });
+
+ it('returns false when node at pos is not a paragraph', () => {
+ const props = createMockProps({ type: { name: 'table' }, attrs: {} });
+ const result = setListTypeAt({ pos: 0, kind: 'bullet' })(props);
+ expect(result).toBe(false);
+ });
+
+ it('returns false when paragraph has no numbering properties', () => {
+ const plainParagraph = {
+ type: { name: 'paragraph' },
+ attrs: { paragraphProperties: {} },
+ };
+ const props = createMockProps(plainParagraph);
+ const result = setListTypeAt({ pos: 0, kind: 'bullet' })(props);
+ expect(result).toBe(false);
+ });
+
+ it('generates a bulletList definition for bullet kind', () => {
+ const props = createMockProps();
+
+ setListTypeAt({ pos: 0, kind: 'bullet' })(props);
+
+ expect(ListHelpers.generateNewListDefinition).toHaveBeenCalledWith(
+ expect.objectContaining({ listType: 'bulletList' }),
+ );
+ });
+
+ it('generates an orderedList definition for ordered kind', () => {
+ const props = createMockProps();
+
+ setListTypeAt({ pos: 0, kind: 'ordered' })(props);
+
+ expect(ListHelpers.generateNewListDefinition).toHaveBeenCalledWith(
+ expect.objectContaining({ listType: 'orderedList' }),
+ );
+ });
+
+ it('calls updateNumberingProperties with the new numId and existing level', () => {
+ const node = createListParagraph(1, 2);
+ const props = createMockProps(node);
+
+ setListTypeAt({ pos: 5, kind: 'bullet' })(props);
+
+ expect(updateNumberingProperties).toHaveBeenCalledWith(
+ expect.objectContaining({ numId: 42, ilvl: 2 }),
+ node,
+ 5,
+ props.editor,
+ props.tr,
+ );
+ });
+
+ it('dispatches the transaction on success', () => {
+ const props = createMockProps();
+
+ const result = setListTypeAt({ pos: 0, kind: 'bullet' })(props);
+
+ expect(result).toBe(true);
+ expect(props.dispatch).toHaveBeenCalledWith(props.tr);
+ });
+
+ it('returns false when getNewListId returns a non-finite number', () => {
+ ListHelpers.getNewListId.mockReturnValue('NaN');
+ const props = createMockProps();
+
+ const result = setListTypeAt({ pos: 0, kind: 'bullet' })(props);
+ expect(result).toBe(false);
+ });
+
+ it('is side-effect-free when dispatch is not provided', () => {
+ const props = createMockProps();
+ props.dispatch = undefined;
+
+ const result = setListTypeAt({ pos: 0, kind: 'bullet' })(props);
+
+ expect(result).toBe(true);
+ expect(ListHelpers.getNewListId).not.toHaveBeenCalled();
+ expect(ListHelpers.generateNewListDefinition).not.toHaveBeenCalled();
+ expect(updateNumberingProperties).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/super-editor/src/core/commands/toggleList.js b/packages/super-editor/src/core/commands/toggleList.js
index 341e0787b2..bd88d425e9 100644
--- a/packages/super-editor/src/core/commands/toggleList.js
+++ b/packages/super-editor/src/core/commands/toggleList.js
@@ -86,6 +86,11 @@ export const toggleList =
} else {
// If list paragraph was not found, create a new list definition and apply it to all paragraphs in selection
mode = 'create';
+ }
+
+ if (!dispatch) return true;
+
+ if (mode === 'create') {
const numId = ListHelpers.getNewListId(editor);
ListHelpers.generateNewListDefinition({ numId: Number(numId), listType, editor });
sharedNumberingProperties = {
@@ -141,6 +146,6 @@ export const toggleList =
}
}
}
- if (dispatch) dispatch(tr);
+ dispatch(tr);
return true;
};
diff --git a/packages/super-editor/src/core/commands/toggleList.test.js b/packages/super-editor/src/core/commands/toggleList.test.js
index d8cf56ed9f..17a0f34c54 100644
--- a/packages/super-editor/src/core/commands/toggleList.test.js
+++ b/packages/super-editor/src/core/commands/toggleList.test.js
@@ -140,7 +140,7 @@ describe('toggleList', () => {
const state = createState(paragraphs);
const handler = toggleList('orderedList');
- const result = handler({ editor, state, tr, dispatch: undefined });
+ const result = handler({ editor, state, tr, dispatch });
expect(result).toBe(true);
expect(updateNumberingProperties).toHaveBeenCalledTimes(1);
@@ -153,6 +153,7 @@ describe('toggleList', () => {
editor,
tr,
);
+ expect(dispatch).toHaveBeenCalledWith(tr);
});
it('creates a new list definition when no matching list exists in or before the selection', () => {
@@ -164,7 +165,7 @@ describe('toggleList', () => {
const state = createState(paragraphs);
const handler = toggleList('orderedList');
- const result = handler({ editor, state, tr, dispatch: undefined });
+ const result = handler({ editor, state, tr, dispatch });
expect(result).toBe(true);
expect(ListHelpers.getNewListId).toHaveBeenCalledWith(editor);
@@ -177,6 +178,7 @@ describe('toggleList', () => {
for (const [index, { node, pos }] of paragraphs.entries()) {
expect(updateNumberingProperties).toHaveBeenNthCalledWith(index + 1, expectedNumbering, node, pos, editor, tr);
}
+ expect(dispatch).toHaveBeenCalledWith(tr);
});
it('borrows numbering from the previous list paragraph when selection lacks one', () => {
@@ -195,7 +197,7 @@ describe('toggleList', () => {
const state = createState(paragraphs, { beforeNode, parentIndex: 1 });
const handler = toggleList('orderedList');
- const result = handler({ editor, state, tr, dispatch: undefined });
+ const result = handler({ editor, state, tr, dispatch });
expect(result).toBe(true);
expect(ListHelpers.getNewListId).not.toHaveBeenCalled();
@@ -204,5 +206,61 @@ describe('toggleList', () => {
for (const [index, { node, pos }] of paragraphs.entries()) {
expect(updateNumberingProperties).toHaveBeenNthCalledWith(index + 1, expectedNumbering, node, pos, editor, tr);
}
+ expect(dispatch).toHaveBeenCalledWith(tr);
+ });
+
+ it('is side-effect-free when dispatch is not provided (create mode)', () => {
+ ListHelpers.getNewListId.mockReturnValue('42');
+ const paragraphs = [createParagraph({ paragraphProperties: {} }, 3)];
+ const state = createState(paragraphs);
+ const handler = toggleList('orderedList');
+
+ const result = handler({ editor, state, tr, dispatch: undefined });
+
+ expect(result).toBe(true);
+ expect(ListHelpers.getNewListId).not.toHaveBeenCalled();
+ expect(ListHelpers.generateNewListDefinition).not.toHaveBeenCalled();
+ expect(updateNumberingProperties).not.toHaveBeenCalled();
+ });
+
+ it('is side-effect-free when dispatch is not provided (remove mode)', () => {
+ const paragraphs = [
+ createParagraph(
+ {
+ paragraphProperties: { numberingProperties: { numId: 5, ilvl: 0 } },
+ listRendering: { numberingType: 'bullet' },
+ },
+ 1,
+ ),
+ ];
+ const state = createState(paragraphs);
+ const handler = toggleList('bulletList');
+
+ const result = handler({ editor, state, tr, dispatch: undefined });
+
+ expect(result).toBe(true);
+ expect(updateNumberingProperties).not.toHaveBeenCalled();
+ expect(ListHelpers.generateNewListDefinition).not.toHaveBeenCalled();
+ });
+
+ it('is side-effect-free when dispatch is not provided (reuse mode)', () => {
+ const paragraphs = [
+ createParagraph(
+ {
+ paragraphProperties: { numberingProperties: { numId: 12, ilvl: 0 } },
+ listRendering: { numberingType: 'decimal' },
+ },
+ 2,
+ ),
+ createParagraph({ paragraphProperties: {} }, 6),
+ ];
+ const state = createState(paragraphs);
+ const handler = toggleList('orderedList');
+
+ const result = handler({ editor, state, tr, dispatch: undefined });
+
+ expect(result).toBe(true);
+ expect(updateNumberingProperties).not.toHaveBeenCalled();
+ expect(ListHelpers.generateNewListDefinition).not.toHaveBeenCalled();
});
});
diff --git a/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts b/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts
new file mode 100644
index 0000000000..269ad4ed95
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts
@@ -0,0 +1,1131 @@
+import type { Node as ProseMirrorNode } from 'prosemirror-model';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Editor } from '../../core/Editor.js';
+import {
+ COMMAND_CATALOG,
+ MUTATING_OPERATION_IDS,
+ OPERATION_IDS,
+ buildInternalContractSchemas,
+ type OperationId,
+} from '@superdoc/document-api';
+import {
+ TrackDeleteMarkName,
+ TrackFormatMarkName,
+ TrackInsertMarkName,
+} from '../../extensions/track-changes/constants.js';
+import { ListHelpers } from '../../core/helpers/list-numbering-helpers.js';
+import { createCommentsAdapter } from '../comments-adapter.js';
+import { createParagraphAdapter } from '../create-adapter.js';
+import { formatBoldAdapter } from '../format-adapter.js';
+import { getDocumentApiCapabilities } from '../capabilities-adapter.js';
+import {
+ listsExitAdapter,
+ listsIndentAdapter,
+ listsInsertAdapter,
+ listsOutdentAdapter,
+ listsRestartAdapter,
+ listsSetTypeAdapter,
+} from '../lists-adapter.js';
+import {
+ trackChangesAcceptAdapter,
+ trackChangesAcceptAllAdapter,
+ trackChangesRejectAdapter,
+ trackChangesRejectAllAdapter,
+} from '../track-changes-adapter.js';
+import { toCanonicalTrackedChangeId } from '../helpers/tracked-change-resolver.js';
+import { writeAdapter } from '../write-adapter.js';
+import { validateJsonSchema } from './schema-validator.js';
+
+const mockedDeps = vi.hoisted(() => ({
+ resolveCommentAnchorsById: vi.fn(() => []),
+ listCommentAnchors: vi.fn(() => []),
+ getTrackChanges: vi.fn(() => []),
+}));
+
+vi.mock('../helpers/comment-target-resolver.js', () => ({
+ resolveCommentAnchorsById: mockedDeps.resolveCommentAnchorsById,
+ listCommentAnchors: mockedDeps.listCommentAnchors,
+}));
+
+vi.mock('../../extensions/track-changes/trackChangesHelpers/getTrackChanges.js', () => ({
+ getTrackChanges: mockedDeps.getTrackChanges,
+}));
+
+const INTERNAL_SCHEMAS = buildInternalContractSchemas();
+
+type MutationVector = {
+ throwCase: () => unknown;
+ failureCase: () => unknown;
+ applyCase: () => unknown;
+};
+
+type NodeOptions = {
+ attrs?: Record;
+ text?: string;
+ isInline?: boolean;
+ isBlock?: boolean;
+ isLeaf?: boolean;
+ inlineContent?: boolean;
+ nodeSize?: number;
+};
+
+type MockParagraphNode = {
+ type: { name: 'paragraph' };
+ attrs: Record;
+ nodeSize: number;
+ isBlock: true;
+ textContent: string;
+};
+
+function createNode(typeName: string, children: ProseMirrorNode[] = [], options: NodeOptions = {}): ProseMirrorNode {
+ const attrs = options.attrs ?? {};
+ const text = options.text ?? '';
+ const isText = typeName === 'text';
+ const isInline = options.isInline ?? isText;
+ const isBlock = options.isBlock ?? (!isInline && typeName !== 'doc');
+ const inlineContent = options.inlineContent ?? isBlock;
+ const isLeaf = options.isLeaf ?? (isInline && !isText && children.length === 0);
+
+ const contentSize = children.reduce((sum, child) => sum + child.nodeSize, 0);
+ const nodeSize = isText ? text.length : options.nodeSize != null ? options.nodeSize : isLeaf ? 1 : contentSize + 2;
+
+ return {
+ type: { name: typeName },
+ attrs,
+ text: isText ? text : undefined,
+ content: { size: contentSize },
+ nodeSize,
+ isText,
+ isInline,
+ isBlock,
+ inlineContent,
+ isTextblock: inlineContent,
+ isLeaf,
+ childCount: children.length,
+ child(index: number) {
+ return children[index]!;
+ },
+ descendants(callback: (node: ProseMirrorNode, pos: number) => void) {
+ let offset = 0;
+ for (const child of children) {
+ callback(child, offset);
+ offset += child.nodeSize;
+ }
+ },
+ } as unknown as ProseMirrorNode;
+}
+
+function makeTextEditor(
+ text = 'Hello',
+ overrides: Partial & {
+ commands?: Record;
+ schema?: Record;
+ } = {},
+): {
+ editor: Editor;
+ dispatch: ReturnType;
+ tr: {
+ insertText: ReturnType;
+ delete: ReturnType;
+ addMark: ReturnType;
+ setMeta: ReturnType;
+ };
+} {
+ const textNode = createNode('text', [], { text });
+ const paragraph = createNode('paragraph', [textNode], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const tr = {
+ insertText: vi.fn(),
+ delete: vi.fn(),
+ addMark: vi.fn(),
+ setMeta: vi.fn(),
+ };
+ tr.insertText.mockReturnValue(tr);
+ tr.delete.mockReturnValue(tr);
+ tr.addMark.mockReturnValue(tr);
+ tr.setMeta.mockReturnValue(tr);
+
+ const dispatch = vi.fn();
+
+ const baseCommands = {
+ insertTrackedChange: vi.fn(() => true),
+ setTextSelection: vi.fn(() => true),
+ addComment: vi.fn(() => true),
+ editComment: vi.fn(() => true),
+ addCommentReply: vi.fn(() => true),
+ moveComment: vi.fn(() => true),
+ resolveComment: vi.fn(() => true),
+ removeComment: vi.fn(() => true),
+ setCommentInternal: vi.fn(() => true),
+ setActiveComment: vi.fn(() => true),
+ setCursorById: vi.fn(() => true),
+ acceptTrackedChangeById: vi.fn(() => true),
+ rejectTrackedChangeById: vi.fn(() => true),
+ acceptAllTrackedChanges: vi.fn(() => true),
+ rejectAllTrackedChanges: vi.fn(() => true),
+ insertParagraphAt: vi.fn(() => true),
+ insertListItemAt: vi.fn(() => true),
+ setListTypeAt: vi.fn(() => true),
+ increaseListIndent: vi.fn(() => true),
+ decreaseListIndent: vi.fn(() => true),
+ restartNumbering: vi.fn(() => true),
+ exitListItemAt: vi.fn(() => true),
+ };
+
+ const baseSchema = {
+ marks: {
+ bold: {
+ create: vi.fn(() => ({ type: 'bold' })),
+ },
+ [TrackFormatMarkName]: {
+ create: vi.fn(() => ({ type: TrackFormatMarkName })),
+ },
+ },
+ };
+
+ const editor = {
+ state: {
+ doc: {
+ ...doc,
+ textBetween: vi.fn((from: number, to: number) => {
+ const start = Math.max(0, from - 1);
+ const end = Math.max(start, to - 1);
+ return text.slice(start, end);
+ }),
+ },
+ tr,
+ },
+ can: vi.fn(() => ({
+ insertParagraphAt: vi.fn(() => true),
+ insertListItemAt: vi.fn(() => true),
+ setListTypeAt: vi.fn(() => true),
+ increaseListIndent: vi.fn(() => true),
+ decreaseListIndent: vi.fn(() => true),
+ restartNumbering: vi.fn(() => true),
+ exitListItemAt: vi.fn(() => true),
+ })),
+ dispatch,
+ ...overrides,
+ schema: {
+ ...baseSchema,
+ ...(overrides.schema ?? {}),
+ },
+ commands: {
+ ...baseCommands,
+ ...(overrides.commands ?? {}),
+ },
+ } as unknown as Editor;
+
+ return { editor, dispatch, tr };
+}
+
+function makeListParagraph(options: {
+ id: string;
+ text?: string;
+ numId?: number;
+ ilvl?: number;
+ numberingType?: string;
+ markerText?: string;
+ path?: number[];
+}): MockParagraphNode {
+ const text = options.text ?? '';
+ const numberingProperties =
+ options.numId != null
+ ? {
+ numId: options.numId,
+ ilvl: options.ilvl ?? 0,
+ }
+ : undefined;
+
+ return {
+ type: { name: 'paragraph' },
+ attrs: {
+ paraId: options.id,
+ sdBlockId: options.id,
+ paragraphProperties: numberingProperties ? { numberingProperties } : {},
+ listRendering:
+ options.numId != null
+ ? {
+ markerText: options.markerText ?? '',
+ path: options.path ?? [1],
+ numberingType: options.numberingType ?? 'decimal',
+ }
+ : null,
+ },
+ nodeSize: Math.max(2, text.length + 2),
+ isBlock: true,
+ textContent: text,
+ };
+}
+
+function makeListEditor(children: MockParagraphNode[], commandOverrides: Record = {}): Editor {
+ const doc = {
+ get content() {
+ return {
+ size: children.reduce((sum, child) => sum + child.nodeSize, 0),
+ };
+ },
+ descendants(callback: (node: MockParagraphNode, pos: number) => void) {
+ let pos = 0;
+ for (const child of children) {
+ callback(child, pos);
+ pos += child.nodeSize;
+ }
+ return undefined;
+ },
+ nodesBetween(_from: number, _to: number, callback: (node: unknown) => void) {
+ for (const child of children) {
+ callback(child);
+ }
+ return undefined;
+ },
+ };
+
+ const baseCommands = {
+ insertListItemAt: vi.fn(() => true),
+ setListTypeAt: vi.fn(() => true),
+ setTextSelection: vi.fn(() => true),
+ increaseListIndent: vi.fn(() => true),
+ decreaseListIndent: vi.fn(() => true),
+ restartNumbering: vi.fn(() => true),
+ exitListItemAt: vi.fn(() => true),
+ insertTrackedChange: vi.fn(() => true),
+ };
+
+ return {
+ state: { doc },
+ commands: {
+ ...baseCommands,
+ ...commandOverrides,
+ },
+ converter: {
+ numbering: { definitions: {}, abstracts: {} },
+ },
+ } as unknown as Editor;
+}
+
+function makeCommentRecord(
+ commentId: string,
+ overrides: Record = {},
+): Record & { commentId: string } {
+ return {
+ commentId,
+ commentText: 'Original',
+ isDone: false,
+ isInternal: false,
+ ...overrides,
+ };
+}
+
+function makeCommentsEditor(
+ records: Array> = [],
+ commandOverrides: Record = {},
+): Editor {
+ const { editor } = makeTextEditor('Hello', { commands: commandOverrides });
+ return {
+ ...editor,
+ converter: {
+ comments: [...records],
+ },
+ options: {
+ documentId: 'doc-1',
+ user: {
+ name: 'Agent',
+ email: 'agent@example.com',
+ },
+ },
+ } as unknown as Editor;
+}
+
+function setTrackChanges(changes: Array>): void {
+ mockedDeps.getTrackChanges.mockReturnValue(changes as never);
+}
+
+function makeTrackedChange(id = 'tc-1') {
+ return {
+ mark: {
+ type: { name: TrackInsertMarkName },
+ attrs: { id },
+ },
+ from: 1,
+ to: 3,
+ };
+}
+
+function requireCanonicalTrackChangeId(editor: Editor, rawId: string): string {
+ const canonicalId = toCanonicalTrackedChangeId(editor, rawId);
+ expect(canonicalId).toBeTruthy();
+ return canonicalId!;
+}
+
+function assertSchema(operationId: OperationId, schemaType: 'output' | 'success' | 'failure', value: unknown): void {
+ const schemaSet = INTERNAL_SCHEMAS.operations[operationId];
+ const schema = schemaSet[schemaType];
+ expect(schema).toBeDefined();
+
+ const result = validateJsonSchema(schema as Parameters[0], value);
+ expect(
+ result.valid,
+ `Schema validation failed for ${operationId} (${schemaType}):\n${result.errors.join('\n')}`,
+ ).toBe(true);
+}
+
+function expectThrowCode(operationId: OperationId, run: () => unknown): void {
+ let capturedCode: string | null = null;
+ try {
+ run();
+ } catch (error) {
+ capturedCode = (error as { code?: string }).code ?? null;
+ }
+
+ expect(capturedCode).toBeTruthy();
+ expect(COMMAND_CATALOG[operationId].throws.preApply).toContain(capturedCode);
+}
+
+const mutationVectors: Partial> = {
+ insert: {
+ throwCase: () => {
+ const { editor } = makeTextEditor();
+ return writeAdapter(
+ editor,
+ { kind: 'insert', target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 0 } }, text: 'X' },
+ { changeMode: 'direct' },
+ );
+ },
+ failureCase: () => {
+ const { editor } = makeTextEditor();
+ return writeAdapter(
+ editor,
+ { kind: 'insert', target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 0 } }, text: '' },
+ { changeMode: 'direct' },
+ );
+ },
+ applyCase: () => {
+ const { editor } = makeTextEditor();
+ return writeAdapter(
+ editor,
+ { kind: 'insert', target: { kind: 'text', blockId: 'p1', range: { start: 1, end: 1 } }, text: 'X' },
+ { changeMode: 'direct' },
+ );
+ },
+ },
+ replace: {
+ throwCase: () => {
+ const { editor } = makeTextEditor();
+ return writeAdapter(
+ editor,
+ { kind: 'replace', target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 1 } }, text: 'X' },
+ { changeMode: 'direct' },
+ );
+ },
+ failureCase: () => {
+ const { editor } = makeTextEditor('Hello');
+ return writeAdapter(
+ editor,
+ { kind: 'replace', target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, text: 'Hello' },
+ { changeMode: 'direct' },
+ );
+ },
+ applyCase: () => {
+ const { editor } = makeTextEditor('Hello');
+ return writeAdapter(
+ editor,
+ { kind: 'replace', target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, text: 'World' },
+ { changeMode: 'direct' },
+ );
+ },
+ },
+ delete: {
+ throwCase: () => {
+ const { editor } = makeTextEditor();
+ return writeAdapter(
+ editor,
+ { kind: 'delete', target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 1 } } },
+ { changeMode: 'direct' },
+ );
+ },
+ failureCase: () => {
+ const { editor } = makeTextEditor();
+ return writeAdapter(
+ editor,
+ { kind: 'delete', target: { kind: 'text', blockId: 'p1', range: { start: 2, end: 2 } } },
+ { changeMode: 'direct' },
+ );
+ },
+ applyCase: () => {
+ const { editor } = makeTextEditor();
+ return writeAdapter(
+ editor,
+ { kind: 'delete', target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } } },
+ { changeMode: 'direct' },
+ );
+ },
+ },
+ 'format.bold': {
+ throwCase: () => {
+ const { editor } = makeTextEditor();
+ return formatBoldAdapter(
+ editor,
+ {
+ target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 1 } },
+ },
+ { changeMode: 'direct' },
+ );
+ },
+ failureCase: () => {
+ const { editor } = makeTextEditor();
+ return formatBoldAdapter(
+ editor,
+ {
+ target: { kind: 'text', blockId: 'p1', range: { start: 2, end: 2 } },
+ },
+ { changeMode: 'direct' },
+ );
+ },
+ applyCase: () => {
+ const { editor } = makeTextEditor();
+ return formatBoldAdapter(
+ editor,
+ {
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ },
+ { changeMode: 'direct' },
+ );
+ },
+ },
+ 'create.paragraph': {
+ throwCase: () => {
+ const { editor } = makeTextEditor('Hello', { commands: { insertParagraphAt: undefined } });
+ return createParagraphAdapter(editor, { at: { kind: 'documentEnd' }, text: 'X' }, { changeMode: 'direct' });
+ },
+ failureCase: () => {
+ const { editor } = makeTextEditor('Hello', { commands: { insertParagraphAt: vi.fn(() => false) } });
+ return createParagraphAdapter(editor, { at: { kind: 'documentEnd' }, text: 'X' }, { changeMode: 'direct' });
+ },
+ applyCase: () => {
+ const { editor } = makeTextEditor('Hello', { commands: { insertParagraphAt: vi.fn(() => true) } });
+ return createParagraphAdapter(editor, { at: { kind: 'documentEnd' }, text: 'X' }, { changeMode: 'direct' });
+ },
+ },
+ 'lists.insert': {
+ throwCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, numberingType: 'decimal' })]);
+ return listsInsertAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'missing' }, position: 'after', text: 'X' },
+ { changeMode: 'direct' },
+ );
+ },
+ failureCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, numberingType: 'decimal' })], {
+ insertListItemAt: vi.fn(() => false),
+ });
+ return listsInsertAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' }, position: 'after', text: 'X' },
+ { changeMode: 'direct' },
+ );
+ },
+ applyCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, numberingType: 'decimal' })]);
+ return listsInsertAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' }, position: 'after', text: 'X' },
+ { changeMode: 'direct' },
+ );
+ },
+ },
+ 'lists.setType': {
+ throwCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, numberingType: 'bullet' })]);
+ return listsSetTypeAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' }, kind: 'ordered' },
+ { changeMode: 'tracked' },
+ );
+ },
+ failureCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, numberingType: 'bullet' })]);
+ return listsSetTypeAdapter(editor, {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ kind: 'bullet',
+ });
+ },
+ applyCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, numberingType: 'bullet' })]);
+ return listsSetTypeAdapter(editor, {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ kind: 'ordered',
+ });
+ },
+ },
+ 'lists.indent': {
+ throwCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, numberingType: 'decimal' })]);
+ return listsIndentAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } },
+ { changeMode: 'tracked' },
+ );
+ },
+ failureCase: () => {
+ const hasDefinitionSpy = vi.spyOn(ListHelpers, 'hasListDefinition').mockReturnValue(false);
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, numberingType: 'decimal' })]);
+ const result = listsIndentAdapter(editor, { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } });
+ hasDefinitionSpy.mockRestore();
+ return result;
+ },
+ applyCase: () => {
+ const hasDefinitionSpy = vi.spyOn(ListHelpers, 'hasListDefinition').mockReturnValue(true);
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, numberingType: 'decimal' })]);
+ const result = listsIndentAdapter(editor, { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } });
+ hasDefinitionSpy.mockRestore();
+ return result;
+ },
+ },
+ 'lists.outdent': {
+ throwCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 1, numberingType: 'decimal' })]);
+ return listsOutdentAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } },
+ { changeMode: 'tracked' },
+ );
+ },
+ failureCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, numberingType: 'decimal' })]);
+ return listsOutdentAdapter(editor, { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } });
+ },
+ applyCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 1, numberingType: 'decimal' })]);
+ return listsOutdentAdapter(editor, { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } });
+ },
+ },
+ 'lists.restart': {
+ throwCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, numberingType: 'decimal' })]);
+ return listsRestartAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } },
+ { changeMode: 'tracked' },
+ );
+ },
+ failureCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, numberingType: 'decimal' })]);
+ return listsRestartAdapter(editor, { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } });
+ },
+ applyCase: () => {
+ const editor = makeListEditor([
+ makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, numberingType: 'decimal', markerText: '1.', path: [1] }),
+ makeListParagraph({ id: 'li-2', numId: 1, ilvl: 0, numberingType: 'decimal', markerText: '2.', path: [2] }),
+ ]);
+ return listsRestartAdapter(editor, { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-2' } });
+ },
+ },
+ 'lists.exit': {
+ throwCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, numberingType: 'decimal' })]);
+ return listsExitAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } },
+ { changeMode: 'tracked' },
+ );
+ },
+ failureCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, numberingType: 'decimal' })], {
+ exitListItemAt: vi.fn(() => false),
+ });
+ return listsExitAdapter(editor, { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } });
+ },
+ applyCase: () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, numberingType: 'decimal' })]);
+ return listsExitAdapter(editor, { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } });
+ },
+ },
+ 'comments.add': {
+ throwCase: () => {
+ const editor = makeCommentsEditor([], { addComment: undefined });
+ return createCommentsAdapter(editor).add({
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 3 } },
+ text: 'X',
+ });
+ },
+ failureCase: () => {
+ const editor = makeCommentsEditor();
+ return createCommentsAdapter(editor).add({
+ target: { kind: 'text', blockId: 'p1', range: { start: 1, end: 1 } },
+ text: 'X',
+ });
+ },
+ applyCase: () => {
+ const editor = makeCommentsEditor();
+ return createCommentsAdapter(editor).add({
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 3 } },
+ text: 'X',
+ });
+ },
+ },
+ 'comments.edit': {
+ throwCase: () => {
+ const editor = makeCommentsEditor();
+ return createCommentsAdapter(editor).edit({ commentId: 'missing', text: 'X' });
+ },
+ failureCase: () => {
+ const editor = makeCommentsEditor([makeCommentRecord('c1', { commentText: 'Same' })]);
+ return createCommentsAdapter(editor).edit({ commentId: 'c1', text: 'Same' });
+ },
+ applyCase: () => {
+ const editor = makeCommentsEditor([makeCommentRecord('c1', { commentText: 'Old' })]);
+ return createCommentsAdapter(editor).edit({ commentId: 'c1', text: 'New' });
+ },
+ },
+ 'comments.reply': {
+ throwCase: () => {
+ const editor = makeCommentsEditor();
+ return createCommentsAdapter(editor).reply({ parentCommentId: 'missing', text: 'X' });
+ },
+ failureCase: () => {
+ const editor = makeCommentsEditor([makeCommentRecord('c1')]);
+ return createCommentsAdapter(editor).reply({ parentCommentId: '', text: 'X' });
+ },
+ applyCase: () => {
+ const editor = makeCommentsEditor([makeCommentRecord('c1')]);
+ return createCommentsAdapter(editor).reply({ parentCommentId: 'c1', text: 'Reply' });
+ },
+ },
+ 'comments.move': {
+ throwCase: () => {
+ const editor = makeCommentsEditor([makeCommentRecord('c1')]);
+ return createCommentsAdapter(editor).move({
+ commentId: 'c1',
+ target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 2 } },
+ });
+ },
+ failureCase: () => {
+ mockedDeps.resolveCommentAnchorsById.mockImplementation(() => []);
+ const editor = makeCommentsEditor([makeCommentRecord('c1')]);
+ return createCommentsAdapter(editor).move({
+ commentId: 'c1',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } },
+ });
+ },
+ applyCase: () => {
+ mockedDeps.resolveCommentAnchorsById.mockImplementation((_editor, id) =>
+ id === 'c1'
+ ? [
+ {
+ commentId: 'c1',
+ status: 'open',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 1 } },
+ pos: 1,
+ end: 2,
+ attrs: {},
+ },
+ ]
+ : [],
+ );
+ const editor = makeCommentsEditor([makeCommentRecord('c1')]);
+ return createCommentsAdapter(editor).move({
+ commentId: 'c1',
+ target: { kind: 'text', blockId: 'p1', range: { start: 1, end: 3 } },
+ });
+ },
+ },
+ 'comments.resolve': {
+ throwCase: () => {
+ const editor = makeCommentsEditor();
+ return createCommentsAdapter(editor).resolve({ commentId: 'missing' });
+ },
+ failureCase: () => {
+ const editor = makeCommentsEditor([makeCommentRecord('c1', { isDone: true })]);
+ return createCommentsAdapter(editor).resolve({ commentId: 'c1' });
+ },
+ applyCase: () => {
+ const editor = makeCommentsEditor([makeCommentRecord('c1', { isDone: false })]);
+ return createCommentsAdapter(editor).resolve({ commentId: 'c1' });
+ },
+ },
+ 'comments.remove': {
+ throwCase: () => {
+ const editor = makeCommentsEditor();
+ return createCommentsAdapter(editor).remove({ commentId: 'missing' });
+ },
+ failureCase: () => {
+ mockedDeps.resolveCommentAnchorsById.mockImplementation((_editor, id) =>
+ id === 'c1'
+ ? [
+ {
+ commentId: 'c1',
+ status: 'open',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 1 } },
+ pos: 1,
+ end: 2,
+ attrs: {},
+ },
+ ]
+ : [],
+ );
+ const editor = makeCommentsEditor([], { removeComment: vi.fn(() => false) });
+ return createCommentsAdapter(editor).remove({ commentId: 'c1' });
+ },
+ applyCase: () => {
+ const editor = makeCommentsEditor([makeCommentRecord('c1')], { removeComment: vi.fn(() => true) });
+ return createCommentsAdapter(editor).remove({ commentId: 'c1' });
+ },
+ },
+ 'comments.setInternal': {
+ throwCase: () => {
+ const editor = makeCommentsEditor();
+ return createCommentsAdapter(editor).setInternal({ commentId: 'missing', isInternal: true });
+ },
+ failureCase: () => {
+ const editor = makeCommentsEditor([makeCommentRecord('c1', { isInternal: true })]);
+ return createCommentsAdapter(editor).setInternal({ commentId: 'c1', isInternal: true });
+ },
+ applyCase: () => {
+ const editor = makeCommentsEditor([makeCommentRecord('c1', { isInternal: false })]);
+ return createCommentsAdapter(editor).setInternal({ commentId: 'c1', isInternal: true });
+ },
+ },
+ 'comments.setActive': {
+ throwCase: () => {
+ const editor = makeCommentsEditor();
+ return createCommentsAdapter(editor).setActive({ commentId: 'missing' });
+ },
+ failureCase: () => {
+ const editor = makeCommentsEditor([], { setActiveComment: vi.fn(() => false) });
+ return createCommentsAdapter(editor).setActive({ commentId: null });
+ },
+ applyCase: () => {
+ const editor = makeCommentsEditor([], { setActiveComment: vi.fn(() => true) });
+ return createCommentsAdapter(editor).setActive({ commentId: null });
+ },
+ },
+ 'trackChanges.accept': {
+ throwCase: () => {
+ setTrackChanges([]);
+ const { editor } = makeTextEditor();
+ return trackChangesAcceptAdapter(editor, { id: 'missing' });
+ },
+ failureCase: () => {
+ setTrackChanges([makeTrackedChange('tc-1')]);
+ const { editor } = makeTextEditor('Hello', { commands: { acceptTrackedChangeById: vi.fn(() => false) } });
+ return trackChangesAcceptAdapter(editor, { id: requireCanonicalTrackChangeId(editor, 'tc-1') });
+ },
+ applyCase: () => {
+ setTrackChanges([makeTrackedChange('tc-1')]);
+ const { editor } = makeTextEditor('Hello', { commands: { acceptTrackedChangeById: vi.fn(() => true) } });
+ return trackChangesAcceptAdapter(editor, { id: requireCanonicalTrackChangeId(editor, 'tc-1') });
+ },
+ },
+ 'trackChanges.reject': {
+ throwCase: () => {
+ setTrackChanges([]);
+ const { editor } = makeTextEditor();
+ return trackChangesRejectAdapter(editor, { id: 'missing' });
+ },
+ failureCase: () => {
+ setTrackChanges([makeTrackedChange('tc-1')]);
+ const { editor } = makeTextEditor('Hello', { commands: { rejectTrackedChangeById: vi.fn(() => false) } });
+ return trackChangesRejectAdapter(editor, { id: requireCanonicalTrackChangeId(editor, 'tc-1') });
+ },
+ applyCase: () => {
+ setTrackChanges([makeTrackedChange('tc-1')]);
+ const { editor } = makeTextEditor('Hello', { commands: { rejectTrackedChangeById: vi.fn(() => true) } });
+ return trackChangesRejectAdapter(editor, { id: requireCanonicalTrackChangeId(editor, 'tc-1') });
+ },
+ },
+ 'trackChanges.acceptAll': {
+ throwCase: () => {
+ const { editor } = makeTextEditor('Hello', { commands: { acceptAllTrackedChanges: undefined } });
+ return trackChangesAcceptAllAdapter(editor, {});
+ },
+ failureCase: () => {
+ const { editor } = makeTextEditor('Hello', { commands: { acceptAllTrackedChanges: vi.fn(() => false) } });
+ return trackChangesAcceptAllAdapter(editor, {});
+ },
+ applyCase: () => {
+ setTrackChanges([makeTrackedChange('tc-1')]);
+ const { editor } = makeTextEditor('Hello', { commands: { acceptAllTrackedChanges: vi.fn(() => true) } });
+ return trackChangesAcceptAllAdapter(editor, {});
+ },
+ },
+ 'trackChanges.rejectAll': {
+ throwCase: () => {
+ const { editor } = makeTextEditor('Hello', { commands: { rejectAllTrackedChanges: undefined } });
+ return trackChangesRejectAllAdapter(editor, {});
+ },
+ failureCase: () => {
+ const { editor } = makeTextEditor('Hello', { commands: { rejectAllTrackedChanges: vi.fn(() => false) } });
+ return trackChangesRejectAllAdapter(editor, {});
+ },
+ applyCase: () => {
+ setTrackChanges([makeTrackedChange('tc-1')]);
+ const { editor } = makeTextEditor('Hello', { commands: { rejectAllTrackedChanges: vi.fn(() => true) } });
+ return trackChangesRejectAllAdapter(editor, {});
+ },
+ },
+};
+
+const dryRunVectors: Partial unknown>> = {
+ insert: () => {
+ const { editor, dispatch, tr } = makeTextEditor();
+ const result = writeAdapter(
+ editor,
+ { kind: 'insert', target: { kind: 'text', blockId: 'p1', range: { start: 1, end: 1 } }, text: 'X' },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(dispatch).not.toHaveBeenCalled();
+ expect(tr.insertText).not.toHaveBeenCalled();
+ return result;
+ },
+ replace: () => {
+ const { editor, dispatch, tr } = makeTextEditor();
+ const result = writeAdapter(
+ editor,
+ { kind: 'replace', target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } }, text: 'World' },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(dispatch).not.toHaveBeenCalled();
+ expect(tr.insertText).not.toHaveBeenCalled();
+ return result;
+ },
+ delete: () => {
+ const { editor, dispatch, tr } = makeTextEditor();
+ const result = writeAdapter(
+ editor,
+ { kind: 'delete', target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } } },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(dispatch).not.toHaveBeenCalled();
+ expect(tr.delete).not.toHaveBeenCalled();
+ return result;
+ },
+ 'format.bold': () => {
+ const { editor, dispatch, tr } = makeTextEditor();
+ const result = formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(dispatch).not.toHaveBeenCalled();
+ expect(tr.addMark).not.toHaveBeenCalled();
+ return result;
+ },
+ 'create.paragraph': () => {
+ const insertParagraphAt = vi.fn(() => true);
+ const { editor } = makeTextEditor('Hello', { commands: { insertParagraphAt } });
+ const result = createParagraphAdapter(
+ editor,
+ { at: { kind: 'documentEnd' }, text: 'Dry run paragraph' },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(insertParagraphAt).not.toHaveBeenCalled();
+ return result;
+ },
+ 'lists.insert': () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, numberingType: 'decimal' })]);
+ const insertListItemAt = editor.commands!.insertListItemAt as ReturnType;
+ const result = listsInsertAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' }, position: 'after', text: 'X' },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(insertListItemAt).not.toHaveBeenCalled();
+ return result;
+ },
+ 'lists.setType': () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, numberingType: 'bullet' })]);
+ const setListTypeAt = editor.commands!.setListTypeAt as ReturnType;
+ const result = listsSetTypeAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' }, kind: 'ordered' },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(setListTypeAt).not.toHaveBeenCalled();
+ return result;
+ },
+ 'lists.indent': () => {
+ const hasDefinitionSpy = vi.spyOn(ListHelpers, 'hasListDefinition').mockReturnValue(true);
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, numberingType: 'decimal' })]);
+ const increaseListIndent = editor.commands!.increaseListIndent as ReturnType;
+ const result = listsIndentAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(increaseListIndent).not.toHaveBeenCalled();
+ hasDefinitionSpy.mockRestore();
+ return result;
+ },
+ 'lists.outdent': () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 1, numberingType: 'decimal' })]);
+ const decreaseListIndent = editor.commands!.decreaseListIndent as ReturnType;
+ const result = listsOutdentAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(decreaseListIndent).not.toHaveBeenCalled();
+ return result;
+ },
+ 'lists.restart': () => {
+ const editor = makeListEditor([
+ makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, numberingType: 'decimal', markerText: '1.', path: [1] }),
+ makeListParagraph({ id: 'li-2', numId: 1, ilvl: 0, numberingType: 'decimal', markerText: '2.', path: [2] }),
+ ]);
+ const restartNumbering = editor.commands!.restartNumbering as ReturnType;
+ const result = listsRestartAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-2' } },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(restartNumbering).not.toHaveBeenCalled();
+ return result;
+ },
+ 'lists.exit': () => {
+ const editor = makeListEditor([makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, numberingType: 'decimal' })]);
+ const exitListItemAt = editor.commands!.exitListItemAt as ReturnType;
+ const result = listsExitAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } },
+ { changeMode: 'direct', dryRun: true },
+ );
+ expect(exitListItemAt).not.toHaveBeenCalled();
+ return result;
+ },
+};
+
+beforeEach(() => {
+ vi.restoreAllMocks();
+ mockedDeps.resolveCommentAnchorsById.mockReset();
+ mockedDeps.resolveCommentAnchorsById.mockImplementation(() => []);
+ mockedDeps.listCommentAnchors.mockReset();
+ mockedDeps.listCommentAnchors.mockImplementation(() => []);
+ mockedDeps.getTrackChanges.mockReset();
+ mockedDeps.getTrackChanges.mockImplementation(() => []);
+});
+
+describe('document-api adapter conformance', () => {
+ it('has schema coverage for every operation and mutation policy metadata', () => {
+ for (const operationId of OPERATION_IDS) {
+ const schema = INTERNAL_SCHEMAS.operations[operationId];
+ expect(schema).toBeDefined();
+ expect(schema.input).toBeDefined();
+ expect(schema.output).toBeDefined();
+
+ if (!COMMAND_CATALOG[operationId].mutates) continue;
+ expect(COMMAND_CATALOG[operationId].throws.postApplyForbidden).toBe(true);
+ expect(schema.success).toBeDefined();
+ expect(schema.failure).toBeDefined();
+ }
+ });
+
+ it('covers every mutating operation with throw/failure/apply vectors', () => {
+ const vectorKeys = Object.keys(mutationVectors).sort();
+ const expectedKeys = [...MUTATING_OPERATION_IDS].sort();
+ expect(vectorKeys).toEqual(expectedKeys);
+ });
+
+ it('enforces pre-apply throw behavior for every mutating operation', () => {
+ for (const operationId of MUTATING_OPERATION_IDS) {
+ const vector = mutationVectors[operationId];
+ expect(vector).toBeDefined();
+ expectThrowCode(operationId, () => vector!.throwCase());
+ }
+ });
+
+ it('enforces structured non-applied outcomes for every mutating operation', () => {
+ for (const operationId of MUTATING_OPERATION_IDS) {
+ const vector = mutationVectors[operationId]!;
+ const result = vector.failureCase() as { success?: boolean; failure?: { code: string } };
+ expect(result.success).toBe(false);
+ if (result.success !== false || !result.failure) continue;
+ expect(COMMAND_CATALOG[operationId].possibleFailureCodes).toContain(result.failure.code);
+ assertSchema(operationId, 'output', result);
+ assertSchema(operationId, 'failure', result);
+ }
+ });
+
+ it('enforces no post-apply throws across every mutating operation', () => {
+ for (const operationId of MUTATING_OPERATION_IDS) {
+ const vector = mutationVectors[operationId]!;
+ const apply = () => vector.applyCase();
+ expect(apply).not.toThrow();
+ const result = apply() as { success?: boolean };
+ expect(result.success).toBe(true);
+ assertSchema(operationId, 'output', result);
+ assertSchema(operationId, 'success', result);
+ }
+ });
+
+ it('enforces dryRun non-mutation invariants for every dryRun-capable mutation', () => {
+ const expectedDryRunOperations = MUTATING_OPERATION_IDS.filter(
+ (operationId) => COMMAND_CATALOG[operationId].supportsDryRun,
+ );
+ const vectorKeys = Object.keys(dryRunVectors).sort();
+ expect(vectorKeys).toEqual([...expectedDryRunOperations].sort());
+
+ for (const operationId of expectedDryRunOperations) {
+ const run = dryRunVectors[operationId]!;
+ const result = run() as { success?: boolean };
+ expect(result.success).toBe(true);
+ assertSchema(operationId, 'output', result);
+ assertSchema(operationId, 'success', result);
+ }
+ });
+
+ it('keeps capabilities tracked/dryRun flags aligned with static contract metadata', () => {
+ const fullCapabilities = getDocumentApiCapabilities(makeTextEditor('Hello').editor);
+
+ for (const operationId of OPERATION_IDS) {
+ const metadata = COMMAND_CATALOG[operationId];
+ const runtime = fullCapabilities.operations[operationId];
+
+ if (!metadata.supportsTrackedMode) {
+ expect(runtime.tracked).toBe(false);
+ }
+
+ if (!metadata.supportsDryRun) {
+ expect(runtime.dryRun).toBe(false);
+ }
+ }
+
+ const noTrackedEditor = makeTextEditor('Hello', {
+ commands: {
+ insertTrackedChange: undefined,
+ acceptTrackedChangeById: vi.fn(() => true),
+ rejectTrackedChangeById: vi.fn(() => true),
+ acceptAllTrackedChanges: vi.fn(() => true),
+ rejectAllTrackedChanges: vi.fn(() => true),
+ },
+ }).editor;
+ const noTrackedCapabilities = getDocumentApiCapabilities(noTrackedEditor);
+ for (const operationId of OPERATION_IDS) {
+ if (!COMMAND_CATALOG[operationId].supportsTrackedMode) continue;
+ expect(noTrackedCapabilities.operations[operationId].tracked).toBe(false);
+ }
+ });
+
+ it('keeps tracked change vectors deterministic for accept/reject coverage', () => {
+ const change = {
+ mark: {
+ type: { name: TrackDeleteMarkName },
+ attrs: { id: 'tc-delete-1' },
+ },
+ from: 3,
+ to: 4,
+ };
+ setTrackChanges([change]);
+ const { editor } = makeTextEditor();
+ const reject = trackChangesRejectAdapter(editor, { id: requireCanonicalTrackChangeId(editor, 'tc-delete-1') });
+ expect(reject.success).toBe(true);
+ assertSchema('trackChanges.reject', 'output', reject);
+ assertSchema('trackChanges.reject', 'success', reject);
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/__conformance__/schema-validator.ts b/packages/super-editor/src/document-api-adapters/__conformance__/schema-validator.ts
new file mode 100644
index 0000000000..ae9f0f8173
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/__conformance__/schema-validator.ts
@@ -0,0 +1,150 @@
+type JsonSchema = {
+ type?: string | string[];
+ required?: string[];
+ properties?: Record;
+ additionalProperties?: boolean | JsonSchema;
+ items?: JsonSchema;
+ const?: unknown;
+ enum?: unknown[];
+ oneOf?: JsonSchema[];
+ anyOf?: JsonSchema[];
+};
+
+const SUPPORTED_SCHEMA_KEYWORDS = new Set([
+ 'type',
+ 'required',
+ 'properties',
+ 'additionalProperties',
+ 'items',
+ 'const',
+ 'enum',
+ 'oneOf',
+ 'anyOf',
+]);
+
+export interface SchemaValidationResult {
+ valid: boolean;
+ errors: string[];
+}
+
+function isType(value: unknown, expectedType: string): boolean {
+ switch (expectedType) {
+ case 'array':
+ return Array.isArray(value);
+ case 'object':
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+ case 'string':
+ return typeof value === 'string';
+ case 'number':
+ return typeof value === 'number' && Number.isFinite(value);
+ case 'integer':
+ return typeof value === 'number' && Number.isInteger(value);
+ case 'boolean':
+ return typeof value === 'boolean';
+ case 'null':
+ return value === null;
+ default:
+ return true;
+ }
+}
+
+function validateInternal(schema: JsonSchema, value: unknown, path: string, errors: string[]): void {
+ let hasUnsupportedKeyword = false;
+ for (const key of Object.keys(schema)) {
+ if (SUPPORTED_SCHEMA_KEYWORDS.has(key)) continue;
+ errors.push(`${path}: unsupported schema keyword "${key}"`);
+ hasUnsupportedKeyword = true;
+ }
+
+ if (hasUnsupportedKeyword) return;
+
+ if (schema.const !== undefined && value !== schema.const) {
+ errors.push(`${path}: expected const ${JSON.stringify(schema.const)}`);
+ return;
+ }
+
+ if (schema.enum && !schema.enum.includes(value)) {
+ errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);
+ return;
+ }
+
+ if (schema.oneOf) {
+ let matchCount = 0;
+ for (const nested of schema.oneOf) {
+ const nestedErrors: string[] = [];
+ validateInternal(nested, value, path, nestedErrors);
+ if (nestedErrors.length === 0) matchCount += 1;
+ }
+ if (matchCount !== 1) {
+ errors.push(`${path}: expected exactly one oneOf schema match`);
+ }
+ return;
+ }
+
+ if (schema.anyOf) {
+ let matched = false;
+ for (const nested of schema.anyOf) {
+ const nestedErrors: string[] = [];
+ validateInternal(nested, value, path, nestedErrors);
+ if (nestedErrors.length === 0) {
+ matched = true;
+ break;
+ }
+ }
+ if (!matched) {
+ errors.push(`${path}: expected at least one anyOf schema match`);
+ }
+ return;
+ }
+
+ if (schema.type) {
+ const expectedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
+ const hasTypeMatch = expectedTypes.some((expectedType) => isType(value, expectedType));
+ if (!hasTypeMatch) {
+ errors.push(`${path}: expected type ${expectedTypes.join('|')}`);
+ return;
+ }
+ }
+
+ const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
+ if (types.includes('array') && schema.items && Array.isArray(value)) {
+ value.forEach((item, index) => {
+ validateInternal(schema.items as JsonSchema, item, `${path}[${index}]`, errors);
+ });
+ return;
+ }
+
+ const isObjectSchema = schema.type === 'object' || (schema.properties && typeof value === 'object');
+ if (!isObjectSchema || typeof value !== 'object' || value === null || Array.isArray(value)) return;
+
+ const objectValue = value as Record;
+ if (schema.required) {
+ for (const key of schema.required) {
+ if (!(key in objectValue) || objectValue[key] === undefined) {
+ errors.push(`${path}: missing required property "${key}"`);
+ }
+ }
+ }
+
+ if (schema.properties) {
+ for (const [key, nestedSchema] of Object.entries(schema.properties)) {
+ if (!(key in objectValue) || objectValue[key] === undefined) continue;
+ validateInternal(nestedSchema, objectValue[key], `${path}.${key}`, errors);
+ }
+ }
+
+ if (schema.additionalProperties === false && schema.properties) {
+ const allowed = new Set(Object.keys(schema.properties));
+ for (const key of Object.keys(objectValue)) {
+ if (!allowed.has(key)) {
+ errors.push(`${path}: unexpected property "${key}"`);
+ }
+ }
+ }
+}
+
+export function validateJsonSchema(schema: JsonSchema, value: unknown): SchemaValidationResult {
+ const errors: string[] = [];
+ validateInternal(schema, value, '$', errors);
+ return { valid: errors.length === 0, errors };
+}
diff --git a/packages/super-editor/src/document-api-adapters/assemble-adapters.test.ts b/packages/super-editor/src/document-api-adapters/assemble-adapters.test.ts
new file mode 100644
index 0000000000..2734680432
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/assemble-adapters.test.ts
@@ -0,0 +1,52 @@
+import { describe, expect, it } from 'vitest';
+import { assembleDocumentApiAdapters } from './assemble-adapters.js';
+import type { Editor } from '../core/Editor.js';
+
+function makeEditor(): Editor {
+ return {
+ state: { doc: { content: { size: 0 } } },
+ commands: {},
+ schema: { marks: {} },
+ options: {},
+ } as unknown as Editor;
+}
+
+describe('assembleDocumentApiAdapters', () => {
+ it('returns an object with all expected adapter namespaces', () => {
+ const adapters = assembleDocumentApiAdapters(makeEditor());
+
+ expect(adapters).toHaveProperty('find.find');
+ expect(adapters).toHaveProperty('getNode.getNode');
+ expect(adapters).toHaveProperty('getNode.getNodeById');
+ expect(adapters).toHaveProperty('getText.getText');
+ expect(adapters).toHaveProperty('info.info');
+ expect(adapters).toHaveProperty('comments');
+ expect(adapters).toHaveProperty('write.write');
+ expect(adapters).toHaveProperty('format.bold');
+ expect(adapters).toHaveProperty('trackChanges.list');
+ expect(adapters).toHaveProperty('trackChanges.get');
+ expect(adapters).toHaveProperty('trackChanges.accept');
+ expect(adapters).toHaveProperty('trackChanges.reject');
+ expect(adapters).toHaveProperty('trackChanges.acceptAll');
+ expect(adapters).toHaveProperty('trackChanges.rejectAll');
+ expect(adapters).toHaveProperty('create.paragraph');
+ expect(adapters).toHaveProperty('lists.list');
+ expect(adapters).toHaveProperty('lists.get');
+ expect(adapters).toHaveProperty('lists.insert');
+ expect(adapters).toHaveProperty('lists.setType');
+ expect(adapters).toHaveProperty('lists.indent');
+ expect(adapters).toHaveProperty('lists.outdent');
+ expect(adapters).toHaveProperty('lists.restart');
+ expect(adapters).toHaveProperty('lists.exit');
+ });
+
+ it('returns functions for all adapter methods', () => {
+ const adapters = assembleDocumentApiAdapters(makeEditor());
+
+ expect(typeof adapters.find.find).toBe('function');
+ expect(typeof adapters.write.write).toBe('function');
+ expect(typeof adapters.format.bold).toBe('function');
+ expect(typeof adapters.create.paragraph).toBe('function');
+ expect(typeof adapters.lists.insert).toBe('function');
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts
new file mode 100644
index 0000000000..bacb4660b0
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts
@@ -0,0 +1,84 @@
+import type { DocumentApiAdapters } from '@superdoc/document-api';
+import type { Editor } from '../core/Editor.js';
+import { findAdapter } from './find-adapter.js';
+import { getNodeAdapter, getNodeByIdAdapter } from './get-node-adapter.js';
+import { getTextAdapter } from './get-text-adapter.js';
+import { infoAdapter } from './info-adapter.js';
+import { getDocumentApiCapabilities } from './capabilities-adapter.js';
+import { createCommentsAdapter } from './comments-adapter.js';
+import { writeAdapter } from './write-adapter.js';
+import { formatBoldAdapter } from './format-adapter.js';
+import {
+ trackChangesListAdapter,
+ trackChangesGetAdapter,
+ trackChangesAcceptAdapter,
+ trackChangesRejectAdapter,
+ trackChangesAcceptAllAdapter,
+ trackChangesRejectAllAdapter,
+} from './track-changes-adapter.js';
+import { createParagraphAdapter } from './create-adapter.js';
+import {
+ listsListAdapter,
+ listsGetAdapter,
+ listsInsertAdapter,
+ listsSetTypeAdapter,
+ listsIndentAdapter,
+ listsOutdentAdapter,
+ listsRestartAdapter,
+ listsExitAdapter,
+} from './lists-adapter.js';
+
+/**
+ * Assembles all document-api adapters for the given editor instance.
+ *
+ * @param editor - The editor instance to bind adapters to.
+ * @returns A {@link DocumentApiAdapters} object ready to pass to `createDocumentApi()`.
+ */
+export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters {
+ return {
+ find: {
+ find: (query) => findAdapter(editor, query),
+ },
+ getNode: {
+ getNode: (address) => getNodeAdapter(editor, address),
+ getNodeById: (input) => getNodeByIdAdapter(editor, input),
+ },
+ getText: {
+ getText: (input) => getTextAdapter(editor, input),
+ },
+ info: {
+ info: (input) => infoAdapter(editor, input),
+ },
+ capabilities: {
+ get: () => getDocumentApiCapabilities(editor),
+ },
+ comments: createCommentsAdapter(editor),
+ write: {
+ write: (request, options) => writeAdapter(editor, request, options),
+ },
+ format: {
+ bold: (input, options) => formatBoldAdapter(editor, input, options),
+ },
+ trackChanges: {
+ list: (input) => trackChangesListAdapter(editor, input),
+ get: (input) => trackChangesGetAdapter(editor, input),
+ accept: (input) => trackChangesAcceptAdapter(editor, input),
+ reject: (input) => trackChangesRejectAdapter(editor, input),
+ acceptAll: (input) => trackChangesAcceptAllAdapter(editor, input),
+ rejectAll: (input) => trackChangesRejectAllAdapter(editor, input),
+ },
+ create: {
+ paragraph: (input, options) => createParagraphAdapter(editor, input, options),
+ },
+ lists: {
+ list: (query) => listsListAdapter(editor, query),
+ get: (input) => listsGetAdapter(editor, input),
+ insert: (input, options) => listsInsertAdapter(editor, input, options),
+ setType: (input, options) => listsSetTypeAdapter(editor, input, options),
+ indent: (input, options) => listsIndentAdapter(editor, input, options),
+ outdent: (input, options) => listsOutdentAdapter(editor, input, options),
+ restart: (input, options) => listsRestartAdapter(editor, input, options),
+ exit: (input, options) => listsExitAdapter(editor, input, options),
+ },
+ };
+}
diff --git a/packages/super-editor/src/document-api-adapters/capabilities-adapter.test.ts b/packages/super-editor/src/document-api-adapters/capabilities-adapter.test.ts
new file mode 100644
index 0000000000..0515f30704
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/capabilities-adapter.test.ts
@@ -0,0 +1,208 @@
+import { describe, expect, it, vi } from 'vitest';
+import type { Editor } from '../core/Editor.js';
+import { OPERATION_IDS } from '@superdoc/document-api';
+import { TrackFormatMarkName } from '../extensions/track-changes/constants.js';
+import { getDocumentApiCapabilities } from './capabilities-adapter.js';
+
+function makeEditor(overrides: Partial = {}): Editor {
+ const defaultCommands = {
+ insertParagraphAt: vi.fn(() => true),
+ insertListItemAt: vi.fn(() => true),
+ setListTypeAt: vi.fn(() => true),
+ setTextSelection: vi.fn(() => true),
+ increaseListIndent: vi.fn(() => true),
+ decreaseListIndent: vi.fn(() => true),
+ restartNumbering: vi.fn(() => true),
+ exitListItemAt: vi.fn(() => true),
+ addComment: vi.fn(() => true),
+ editComment: vi.fn(() => true),
+ addCommentReply: vi.fn(() => true),
+ moveComment: vi.fn(() => true),
+ resolveComment: vi.fn(() => true),
+ removeComment: vi.fn(() => true),
+ setCommentInternal: vi.fn(() => true),
+ setActiveComment: vi.fn(() => true),
+ setCursorById: vi.fn(() => true),
+ insertTrackedChange: vi.fn(() => true),
+ acceptTrackedChangeById: vi.fn(() => true),
+ rejectTrackedChangeById: vi.fn(() => true),
+ acceptAllTrackedChanges: vi.fn(() => true),
+ rejectAllTrackedChanges: vi.fn(() => true),
+ };
+
+ const defaultMarks = {
+ bold: {
+ create: vi.fn(() => ({ type: 'bold' })),
+ },
+ [TrackFormatMarkName]: {
+ create: vi.fn(() => ({ type: TrackFormatMarkName })),
+ },
+ };
+
+ const overrideCommands = (overrides.commands ?? {}) as Partial;
+
+ const commands = {
+ ...defaultCommands,
+ ...overrideCommands,
+ };
+
+ // When the caller explicitly passes `schema: undefined`, respect that instead
+ // of constructing a default schema with marks.
+ const explicitUndefinedSchema = 'schema' in overrides && overrides.schema === undefined;
+ const overrideSchema = (overrides.schema ?? {}) as Partial;
+ const overrideMarks = (overrideSchema.marks ?? {}) as Record;
+
+ const schema = explicitUndefinedSchema
+ ? undefined
+ : {
+ ...overrideSchema,
+ marks: {
+ ...defaultMarks,
+ ...overrideMarks,
+ },
+ };
+
+ const defaultOptions = {
+ user: { name: 'Test User', email: 'test@example.com' },
+ };
+
+ return {
+ options: defaultOptions,
+ ...overrides,
+ commands,
+ schema,
+ } as unknown as Editor;
+}
+
+describe('getDocumentApiCapabilities', () => {
+ it('returns deterministic per-operation coverage for the full operation inventory', () => {
+ const capabilities = getDocumentApiCapabilities(makeEditor());
+ const operationKeys = Object.keys(capabilities.operations).sort();
+ expect(operationKeys).toEqual([...OPERATION_IDS].sort());
+ });
+
+ it('marks namespaces as unavailable when required commands are missing', () => {
+ const editor = makeEditor({
+ commands: {
+ addComment: undefined,
+ setListTypeAt: undefined,
+ insertTrackedChange: undefined,
+ } as unknown as Editor['commands'],
+ schema: {
+ marks: {
+ bold: undefined,
+ [TrackFormatMarkName]: {},
+ },
+ } as unknown as Editor['schema'],
+ });
+
+ const capabilities = getDocumentApiCapabilities(editor);
+
+ expect(capabilities.global.comments.enabled).toBe(false);
+ expect(capabilities.global.lists.enabled).toBe(false);
+ expect(capabilities.global.trackChanges.enabled).toBe(false);
+ expect(capabilities.operations['comments.add'].available).toBe(false);
+ expect(capabilities.operations['lists.setType'].available).toBe(false);
+ expect(capabilities.operations.insert.tracked).toBe(false);
+ expect(capabilities.operations['format.bold'].available).toBe(false);
+ });
+
+ it('exposes tracked + dryRun flags in line with command catalog capabilities', () => {
+ const capabilities = getDocumentApiCapabilities(makeEditor());
+
+ expect(capabilities.operations.insert.tracked).toBe(true);
+ expect(capabilities.operations.insert.dryRun).toBe(true);
+ expect(capabilities.operations['lists.setType'].tracked).toBe(false);
+ expect(capabilities.operations['lists.setType'].dryRun).toBe(true);
+ expect(capabilities.operations['trackChanges.accept'].dryRun).toBe(false);
+ expect(capabilities.operations['create.paragraph'].dryRun).toBe(true);
+ });
+
+ it('advertises dryRun for list mutators that implement dry-run behavior', () => {
+ const capabilities = getDocumentApiCapabilities(makeEditor());
+ const listMutations = [
+ 'lists.insert',
+ 'lists.setType',
+ 'lists.indent',
+ 'lists.outdent',
+ 'lists.restart',
+ 'lists.exit',
+ ] as const;
+
+ for (const operationId of listMutations) {
+ expect(capabilities.operations[operationId].dryRun, `${operationId} should advertise dryRun support`).toBe(true);
+ }
+ });
+
+ it('reports tracked mode unavailable when no editor user is configured', () => {
+ const capabilities = getDocumentApiCapabilities(
+ makeEditor({
+ options: { user: null } as unknown as Editor['options'],
+ }),
+ );
+
+ expect(capabilities.operations.insert.available).toBe(true);
+ expect(capabilities.operations.insert.tracked).toBe(false);
+ expect(capabilities.operations.insert.reasons).toContain('TRACKED_MODE_UNAVAILABLE');
+ expect(capabilities.operations['create.paragraph'].tracked).toBe(false);
+ expect(capabilities.operations['create.paragraph'].reasons).toContain('TRACKED_MODE_UNAVAILABLE');
+ });
+
+ it('never reports tracked=true when the operation is unavailable', () => {
+ const capabilities = getDocumentApiCapabilities(
+ makeEditor({
+ commands: {
+ insertTrackedChange: vi.fn(() => true),
+ insertParagraphAt: undefined,
+ } as unknown as Editor['commands'],
+ }),
+ );
+
+ expect(capabilities.operations['create.paragraph'].available).toBe(false);
+ expect(capabilities.operations['create.paragraph'].tracked).toBe(false);
+ });
+
+ it('does not emit unavailable reasons for modes that are unsupported by design', () => {
+ const capabilities = getDocumentApiCapabilities(makeEditor());
+ const setTypeReasons = capabilities.operations['lists.setType'].reasons ?? [];
+ const acceptAllReasons = capabilities.operations['trackChanges.acceptAll'].reasons ?? [];
+
+ expect(setTypeReasons).not.toContain('TRACKED_MODE_UNAVAILABLE');
+ expect(setTypeReasons).not.toContain('DRY_RUN_UNAVAILABLE');
+ expect(acceptAllReasons).not.toContain('DRY_RUN_UNAVAILABLE');
+ });
+
+ it('handles an editor with undefined schema gracefully', () => {
+ const editor = makeEditor({
+ schema: undefined as unknown as Editor['schema'],
+ });
+
+ const capabilities = getDocumentApiCapabilities(editor);
+
+ expect(capabilities.operations['format.bold'].available).toBe(false);
+ // insert.tracked remains true because the default insertTrackedChange command
+ // is still present — tracked mode for insert depends on commands, not schema.
+ expect(capabilities.operations.insert.tracked).toBe(true);
+ // Smoke-test: every operation has a defined entry
+ for (const id of OPERATION_IDS) {
+ expect(capabilities.operations[id]).toBeDefined();
+ }
+ });
+
+ it('uses OPERATION_UNAVAILABLE without COMMAND_UNAVAILABLE for non-command-backed availability failures', () => {
+ const capabilities = getDocumentApiCapabilities(
+ makeEditor({
+ schema: {
+ marks: {
+ bold: undefined,
+ [TrackFormatMarkName]: {},
+ },
+ } as unknown as Editor['schema'],
+ }),
+ );
+
+ const formatReasons = capabilities.operations['format.bold'].reasons ?? [];
+ expect(formatReasons).toContain('OPERATION_UNAVAILABLE');
+ expect(formatReasons).not.toContain('COMMAND_UNAVAILABLE');
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts
new file mode 100644
index 0000000000..5ebfa8976e
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/capabilities-adapter.ts
@@ -0,0 +1,184 @@
+import type { Editor } from '../core/Editor.js';
+import {
+ CAPABILITY_REASON_CODES,
+ COMMAND_CATALOG,
+ type CapabilityReasonCode,
+ type DocumentApiCapabilities,
+ type OperationId,
+ OPERATION_IDS,
+} from '@superdoc/document-api';
+import { TrackFormatMarkName } from '../extensions/track-changes/constants.js';
+
+type EditorCommandName = string;
+
+// Singleton write operations (insert, replace, delete) have no entry here because
+// they are backed by writeAdapter which is always available when the editor exists.
+// Read-only operations (find, getNode, getText, info, etc.) similarly need no commands.
+const REQUIRED_COMMANDS: Partial> = {
+ 'create.paragraph': ['insertParagraphAt'],
+ 'lists.insert': ['insertListItemAt'],
+ 'lists.setType': ['setListTypeAt'],
+ 'lists.indent': ['setTextSelection', 'increaseListIndent'],
+ 'lists.outdent': ['setTextSelection', 'decreaseListIndent'],
+ 'lists.restart': ['setTextSelection', 'restartNumbering'],
+ 'lists.exit': ['exitListItemAt'],
+ 'comments.add': ['addComment', 'setTextSelection'],
+ 'comments.edit': ['editComment'],
+ 'comments.reply': ['addCommentReply'],
+ 'comments.move': ['moveComment'],
+ 'comments.resolve': ['resolveComment'],
+ 'comments.remove': ['removeComment'],
+ 'comments.setInternal': ['setCommentInternal'],
+ 'comments.setActive': ['setActiveComment'],
+ 'comments.goTo': ['setCursorById'],
+ 'trackChanges.accept': ['acceptTrackedChangeById'],
+ 'trackChanges.reject': ['rejectTrackedChangeById'],
+ 'trackChanges.acceptAll': ['acceptAllTrackedChanges'],
+ 'trackChanges.rejectAll': ['rejectAllTrackedChanges'],
+};
+
+/** Runtime guard — ensures only canonical reason codes are emitted even if the set grows. */
+const VALID_CAPABILITY_REASON_CODES = new Set(CAPABILITY_REASON_CODES);
+
+function hasCommand(editor: Editor, command: EditorCommandName): boolean {
+ return typeof (editor.commands as Record | undefined)?.[command] === 'function';
+}
+
+function hasAllCommands(editor: Editor, operationId: OperationId): boolean {
+ const required = REQUIRED_COMMANDS[operationId];
+ if (!required || required.length === 0) return true;
+ return required.every((command) => hasCommand(editor, command));
+}
+
+function hasBoldCapability(editor: Editor): boolean {
+ return Boolean(editor.schema?.marks?.bold);
+}
+
+function hasTrackedModeCapability(editor: Editor, operationId: OperationId): boolean {
+ if (!hasCommand(editor, 'insertTrackedChange')) return false;
+ // ensureTrackedCapability (mutation-helpers.ts) requires editor.options.user;
+ // report tracked mode as unavailable when no user is configured so capability-
+ // gated clients don't offer tracked actions that would deterministically fail.
+ if (!editor.options?.user) return false;
+ if (operationId === 'format.bold') {
+ return Boolean(editor.schema?.marks?.[TrackFormatMarkName]);
+ }
+ return true;
+}
+
+function getNamespaceOperationIds(prefix: string): OperationId[] {
+ return (Object.keys(REQUIRED_COMMANDS) as OperationId[]).filter((id) => id.startsWith(`${prefix}.`));
+}
+
+function isCommentsNamespaceEnabled(editor: Editor): boolean {
+ return getNamespaceOperationIds('comments').every((id) => hasAllCommands(editor, id));
+}
+
+function isListsNamespaceEnabled(editor: Editor): boolean {
+ return getNamespaceOperationIds('lists').every((id) => hasAllCommands(editor, id));
+}
+
+function isTrackChangesEnabled(editor: Editor): boolean {
+ return (
+ hasCommand(editor, 'insertTrackedChange') &&
+ hasCommand(editor, 'acceptTrackedChangeById') &&
+ hasCommand(editor, 'rejectTrackedChangeById') &&
+ hasCommand(editor, 'acceptAllTrackedChanges') &&
+ hasCommand(editor, 'rejectAllTrackedChanges')
+ );
+}
+
+function getNamespaceReason(enabled: boolean): CapabilityReasonCode[] | undefined {
+ return enabled ? undefined : ['NAMESPACE_UNAVAILABLE'];
+}
+
+function pushReason(reasons: CapabilityReasonCode[], reason: CapabilityReasonCode): void {
+ if (!VALID_CAPABILITY_REASON_CODES.has(reason)) return;
+ if (!reasons.includes(reason)) reasons.push(reason);
+}
+
+function isOperationAvailable(editor: Editor, operationId: OperationId): boolean {
+ if (operationId === 'format.bold') {
+ return hasBoldCapability(editor);
+ }
+
+ return hasAllCommands(editor, operationId);
+}
+
+function isCommandBackedAvailability(operationId: OperationId): boolean {
+ return operationId !== 'format.bold';
+}
+
+function buildOperationCapabilities(editor: Editor): DocumentApiCapabilities['operations'] {
+ const operations = {} as DocumentApiCapabilities['operations'];
+
+ for (const operationId of OPERATION_IDS) {
+ const metadata = COMMAND_CATALOG[operationId];
+ const available = isOperationAvailable(editor, operationId);
+ const tracked = available && metadata.supportsTrackedMode && hasTrackedModeCapability(editor, operationId);
+ // dryRun is only meaningful for an operation that is currently executable.
+ const dryRun = metadata.supportsDryRun && available;
+ const reasons: CapabilityReasonCode[] = [];
+
+ if (!available) {
+ if (isCommandBackedAvailability(operationId)) {
+ pushReason(reasons, 'COMMAND_UNAVAILABLE');
+ }
+ pushReason(reasons, 'OPERATION_UNAVAILABLE');
+ }
+
+ if (metadata.supportsTrackedMode && !tracked) {
+ pushReason(reasons, 'TRACKED_MODE_UNAVAILABLE');
+ }
+
+ if (metadata.supportsDryRun && !dryRun) {
+ pushReason(reasons, 'DRY_RUN_UNAVAILABLE');
+ }
+
+ operations[operationId] = {
+ available,
+ tracked,
+ dryRun,
+ reasons: reasons.length > 0 ? reasons : undefined,
+ };
+ }
+
+ return operations;
+}
+
+/**
+ * Builds a {@link DocumentApiCapabilities} snapshot by introspecting the editor's
+ * registered commands and schema marks.
+ *
+ * @param editor - The ProseMirror-backed editor instance to introspect.
+ * @returns A complete capability snapshot covering global flags and per-operation details.
+ */
+export function getDocumentApiCapabilities(editor: Editor): DocumentApiCapabilities {
+ const operations = buildOperationCapabilities(editor);
+ const commentsEnabled = isCommentsNamespaceEnabled(editor);
+ const listsEnabled = isListsNamespaceEnabled(editor);
+ const trackChangesEnabled = isTrackChangesEnabled(editor);
+ const dryRunEnabled = OPERATION_IDS.some((operationId) => operations[operationId].dryRun);
+
+ return {
+ global: {
+ trackChanges: {
+ enabled: trackChangesEnabled,
+ reasons: getNamespaceReason(trackChangesEnabled),
+ },
+ comments: {
+ enabled: commentsEnabled,
+ reasons: getNamespaceReason(commentsEnabled),
+ },
+ lists: {
+ enabled: listsEnabled,
+ reasons: getNamespaceReason(listsEnabled),
+ },
+ dryRun: {
+ enabled: dryRunEnabled,
+ reasons: dryRunEnabled ? undefined : ['DRY_RUN_UNAVAILABLE'],
+ },
+ },
+ operations,
+ };
+}
diff --git a/packages/super-editor/src/document-api-adapters/comments-adapter.test.ts b/packages/super-editor/src/document-api-adapters/comments-adapter.test.ts
new file mode 100644
index 0000000000..9ec502f18d
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/comments-adapter.test.ts
@@ -0,0 +1,693 @@
+import type { Node as ProseMirrorNode } from 'prosemirror-model';
+import { Schema } from 'prosemirror-model';
+import { EditorState } from 'prosemirror-state';
+import type { Editor } from '../core/Editor.js';
+import { CommentMarkName } from '../extensions/comment/comments-constants.js';
+import { createCommentsAdapter } from './comments-adapter.js';
+
+type NodeOptions = {
+ attrs?: Record;
+ text?: string;
+ isInline?: boolean;
+ isBlock?: boolean;
+ isLeaf?: boolean;
+ inlineContent?: boolean;
+ nodeSize?: number;
+};
+
+function createNode(typeName: string, children: ProseMirrorNode[] = [], options: NodeOptions = {}): ProseMirrorNode {
+ const attrs = options.attrs ?? {};
+ const text = options.text ?? '';
+ const isText = typeName === 'text';
+ const isInline = options.isInline ?? isText;
+ const isBlock = options.isBlock ?? (!isInline && typeName !== 'doc');
+ const inlineContent = options.inlineContent ?? isBlock;
+ const isLeaf = options.isLeaf ?? (isInline && !isText && children.length === 0);
+
+ const contentSize = children.reduce((sum, child) => sum + child.nodeSize, 0);
+ const nodeSize = isText ? text.length : options.nodeSize != null ? options.nodeSize : isLeaf ? 1 : contentSize + 2;
+
+ return {
+ type: { name: typeName },
+ attrs,
+ text: isText ? text : undefined,
+ nodeSize,
+ isText,
+ isInline,
+ isBlock,
+ inlineContent,
+ isTextblock: inlineContent,
+ isLeaf,
+ childCount: children.length,
+ child(index: number) {
+ return children[index]!;
+ },
+ descendants(callback: (node: ProseMirrorNode, pos: number) => void) {
+ let offset = 0;
+ for (const child of children) {
+ callback(child, offset);
+ offset += child.nodeSize;
+ }
+ },
+ } as unknown as ProseMirrorNode;
+}
+
+function makeEditor(docNode: ProseMirrorNode, commands: Record): Editor {
+ return {
+ state: { doc: docNode },
+ commands,
+ } as unknown as Editor;
+}
+
+describe('addCommentAdapter', () => {
+ it('adds a comment when commands and range are valid', () => {
+ const textNode = createNode('text', [], { text: 'Hello' });
+ const paragraph = createNode('paragraph', [textNode], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const setTextSelection = vi.fn(() => true);
+ const commands: Record = { setTextSelection };
+ const editor = makeEditor(doc, commands);
+ const addComment = vi.fn(() => {
+ (editor as unknown as { converter?: { comments?: Array> } }).converter = {
+ comments: [
+ {
+ commentId: 'new-comment-id',
+ commentText: 'Review this',
+ createdTime: Date.now(),
+ },
+ ],
+ };
+ return true;
+ });
+ commands.addComment = addComment;
+
+ const receipt = createCommentsAdapter(editor).add({
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'Review this',
+ });
+
+ expect(setTextSelection).toHaveBeenCalledWith({ from: 1, to: 6 });
+ expect(addComment).toHaveBeenCalledWith(expect.objectContaining({ content: 'Review this', isInternal: false }));
+ const passedCommentId = addComment.mock.calls[0]?.[0]?.commentId;
+ expect(typeof passedCommentId).toBe('string');
+ expect(receipt.success).toBe(true);
+ expect(receipt.inserted?.[0]?.entityType).toBe('comment');
+ expect(receipt.inserted?.[0]?.entityId).toBe(passedCommentId);
+ });
+
+ it('reads addComment from a fresh command snapshot after applying selection', () => {
+ const textNode = createNode('text', [], { text: 'Hello' });
+ const paragraph = createNode('paragraph', [textNode], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const editor = {
+ state: { doc },
+ converter: {
+ comments: [] as Array>,
+ },
+ options: {
+ documentId: 'doc-1',
+ user: {
+ name: 'Test User',
+ email: 'test.user@example.com',
+ },
+ },
+ } as unknown as Editor & {
+ converter: {
+ comments: Array>;
+ };
+ };
+
+ let activeSelection = { from: 0, to: 0 };
+ const setTextSelection = vi.fn(({ from, to }: { from: number; to: number }) => {
+ activeSelection = { from, to };
+ return true;
+ });
+ const addCommentWithSnapshot = vi.fn(
+ (
+ selectionSnapshot: { from: number; to: number },
+ options: { content: string; isInternal: boolean; commentId?: string },
+ ) => {
+ if (selectionSnapshot.from === selectionSnapshot.to) return false;
+
+ editor.converter.comments.push({
+ commentId: options.commentId ?? 'fresh-command-id',
+ commentText: options.content,
+ createdTime: Date.now(),
+ });
+ return true;
+ },
+ );
+
+ Object.defineProperty(editor, 'commands', {
+ configurable: true,
+ get() {
+ const selectionSnapshot = { ...activeSelection };
+ return {
+ setTextSelection,
+ addComment: (options: { content: string; isInternal: boolean; commentId?: string }) =>
+ addCommentWithSnapshot(selectionSnapshot, options),
+ };
+ },
+ });
+
+ const receipt = createCommentsAdapter(editor).add({
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'Review this',
+ });
+
+ expect(setTextSelection).toHaveBeenCalledWith({ from: 1, to: 6 });
+ expect(addCommentWithSnapshot).toHaveBeenCalledWith(
+ { from: 1, to: 6 },
+ expect.objectContaining({ content: 'Review this', isInternal: false }),
+ );
+ const passedId = addCommentWithSnapshot.mock.calls[0]?.[1]?.commentId;
+ expect(typeof passedId).toBe('string');
+ expect(receipt.success).toBe(true);
+ expect(receipt.inserted?.[0]).toMatchObject({ entityType: 'comment', entityId: passedId });
+ });
+
+ it('returns false when commands are missing', () => {
+ const doc = createNode('doc', [], { isBlock: false });
+ const editor = makeEditor(doc, {});
+
+ expect(() =>
+ createCommentsAdapter(editor).add({
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 1 } },
+ text: 'No commands',
+ }),
+ ).toThrow('command is not available');
+ });
+
+ it('returns false when blockId is not found', () => {
+ const textNode = createNode('text', [], { text: 'Hi' });
+ const paragraph = createNode('paragraph', [textNode], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const setTextSelection = vi.fn(() => true);
+ const addComment = vi.fn(() => true);
+ const editor = makeEditor(doc, { setTextSelection, addComment });
+
+ expect(() =>
+ createCommentsAdapter(editor).add({
+ target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 1 } },
+ text: 'Missing',
+ }),
+ ).toThrow('Comment target could not be resolved.');
+ });
+
+ it('returns false for empty ranges', () => {
+ const textNode = createNode('text', [], { text: 'Hi' });
+ const paragraph = createNode('paragraph', [textNode], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const setTextSelection = vi.fn(() => true);
+ const addComment = vi.fn(() => true);
+ const editor = makeEditor(doc, { setTextSelection, addComment });
+
+ const receipt = createCommentsAdapter(editor).add({
+ target: { kind: 'text', blockId: 'p1', range: { start: 1, end: 1 } },
+ text: 'Empty',
+ });
+
+ expect(receipt.success).toBe(false);
+ expect(receipt.failure).toMatchObject({
+ code: 'INVALID_TARGET',
+ });
+ });
+
+ it('returns false for out-of-range offsets', () => {
+ const textNode = createNode('text', [], { text: 'Hi' });
+ const paragraph = createNode('paragraph', [textNode], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const setTextSelection = vi.fn(() => true);
+ const addComment = vi.fn(() => true);
+ const editor = makeEditor(doc, { setTextSelection, addComment });
+
+ expect(() =>
+ createCommentsAdapter(editor).add({
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'Out of range',
+ }),
+ ).toThrow('Comment target could not be resolved.');
+ });
+
+ it('returns INVALID_TARGET when text selection cannot be applied', () => {
+ const textNode = createNode('text', [], { text: 'Hi' });
+ const paragraph = createNode('paragraph', [textNode], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const setTextSelection = vi.fn(() => false);
+ const addComment = vi.fn(() => true);
+ const editor = makeEditor(doc, { setTextSelection, addComment });
+
+ const receipt = createCommentsAdapter(editor).add({
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } },
+ text: 'Selection failure',
+ });
+
+ expect(receipt.success).toBe(false);
+ expect(receipt.failure).toMatchObject({
+ code: 'INVALID_TARGET',
+ });
+ });
+
+ it('returns NO_OP when addComment does not apply a comment', () => {
+ const textNode = createNode('text', [], { text: 'Hi' });
+ const paragraph = createNode('paragraph', [textNode], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const setTextSelection = vi.fn(() => true);
+ const addComment = vi.fn(() => false);
+ const editor = makeEditor(doc, { setTextSelection, addComment });
+
+ const receipt = createCommentsAdapter(editor).add({
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 2 } },
+ text: 'Insert failure',
+ });
+
+ expect(receipt.success).toBe(false);
+ expect(receipt.failure).toMatchObject({
+ code: 'NO_OP',
+ });
+ });
+});
+
+function createCommentSchema(): Schema {
+ return new Schema({
+ nodes: {
+ doc: { content: 'block+' },
+ paragraph: {
+ attrs: { paraId: { default: null }, sdBlockId: { default: null } },
+ content: 'inline*',
+ group: 'block',
+ toDOM: () => ['p', 0],
+ parseDOM: [{ tag: 'p' }],
+ },
+ text: { group: 'inline' },
+ commentRangeStart: {
+ inline: true,
+ group: 'inline',
+ atom: true,
+ attrs: { 'w:id': {} },
+ toDOM: () => ['commentRangeStart'],
+ parseDOM: [{ tag: 'commentRangeStart' }],
+ },
+ commentRangeEnd: {
+ inline: true,
+ group: 'inline',
+ atom: true,
+ attrs: { 'w:id': {} },
+ toDOM: () => ['commentRangeEnd'],
+ parseDOM: [{ tag: 'commentRangeEnd' }],
+ },
+ },
+ marks: {
+ [CommentMarkName]: {
+ attrs: { commentId: {}, importedId: { default: null }, internal: { default: false } },
+ inclusive: false,
+ toDOM: () => [CommentMarkName],
+ parseDOM: [{ tag: CommentMarkName }],
+ },
+ },
+ });
+}
+
+function createPmEditor(
+ doc: ProseMirrorNode,
+ commands: Record = {},
+ comments: Array> = [],
+): Editor {
+ const state = EditorState.create({
+ schema: doc.type.schema,
+ doc,
+ });
+
+ return {
+ state,
+ commands,
+ converter: {
+ comments,
+ },
+ options: {
+ documentId: 'doc-1',
+ user: {
+ name: 'Test User',
+ email: 'test.user@example.com',
+ },
+ },
+ } as unknown as Editor;
+}
+
+describe('commentsAdapter additional operations', () => {
+ it('edits a comment text and returns updated receipt', () => {
+ const schema = createCommentSchema();
+ const mark = schema.marks[CommentMarkName].create({ commentId: 'c1', internal: false });
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello', [mark])]);
+ const doc = schema.node('doc', null, [paragraph]);
+ const editComment = vi.fn(() => true);
+ const editor = createPmEditor(doc, { editComment }, [{ commentId: 'c1', commentText: 'Before' }]);
+
+ const receipt = createCommentsAdapter(editor).edit({ commentId: 'c1', text: 'After' });
+
+ expect(editComment).toHaveBeenCalledWith({ commentId: 'c1', importedId: undefined, content: 'After' });
+ expect(receipt.success).toBe(true);
+ expect(receipt.updated?.[0]).toMatchObject({ entityType: 'comment', entityId: 'c1' });
+ expect(
+ (editor as unknown as { converter: { comments: Array<{ commentText?: string }> } }).converter.comments[0]
+ ?.commentText,
+ ).toBe('After');
+ });
+
+ it('replies to a comment and returns inserted receipt', () => {
+ const schema = createCommentSchema();
+ const parentMark = schema.marks[CommentMarkName].create({ commentId: 'c1', internal: false });
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello', [parentMark])]);
+ const doc = schema.node('doc', null, [paragraph]);
+ const addCommentReply = vi.fn(() => true);
+ const editor = createPmEditor(doc, { addCommentReply }, [{ commentId: 'c1', commentText: 'Root comment' }]);
+
+ const receipt = createCommentsAdapter(editor).reply({ parentCommentId: 'c1', text: 'Reply body' });
+
+ expect(addCommentReply).toHaveBeenCalledWith(
+ expect.objectContaining({
+ parentId: 'c1',
+ content: 'Reply body',
+ }),
+ );
+ expect(receipt.success).toBe(true);
+ expect(receipt.inserted?.[0]).toMatchObject({ entityType: 'comment' });
+ });
+
+ it('throws TARGET_NOT_FOUND when replying to a missing parent comment', () => {
+ const schema = createCommentSchema();
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello')]);
+ const doc = schema.node('doc', null, [paragraph]);
+ const addCommentReply = vi.fn(() => true);
+ const editor = createPmEditor(doc, { addCommentReply }, []);
+
+ expect(() =>
+ createCommentsAdapter(editor).reply({
+ parentCommentId: 'missing-parent',
+ text: 'Reply body',
+ }),
+ ).toThrow('Comment target could not be resolved.');
+ expect(addCommentReply).not.toHaveBeenCalled();
+ });
+
+ it('moves a comment to a new target range', () => {
+ const schema = createCommentSchema();
+ const mark = schema.marks[CommentMarkName].create({ commentId: 'c1', internal: false });
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello', [mark])]);
+ const doc = schema.node('doc', null, [paragraph]);
+ const moveComment = vi.fn(() => true);
+ const editor = createPmEditor(doc, { moveComment }, [{ commentId: 'c1', commentText: 'Move me' }]);
+
+ const receipt = createCommentsAdapter(editor).move({
+ commentId: 'c1',
+ target: { kind: 'text', blockId: 'p1', range: { start: 1, end: 4 } },
+ });
+
+ expect(moveComment).toHaveBeenCalledWith({ commentId: 'c1', from: 2, to: 5 });
+ expect(receipt.success).toBe(true);
+ expect(receipt.updated?.[0]).toMatchObject({ entityType: 'comment', entityId: 'c1' });
+ });
+
+ it('returns NO_OP when move command resolves but does not apply changes', () => {
+ const schema = createCommentSchema();
+ const mark = schema.marks[CommentMarkName].create({ commentId: 'c1', internal: false });
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello', [mark])]);
+ const doc = schema.node('doc', null, [paragraph]);
+ const moveComment = vi.fn(() => false);
+ const editor = createPmEditor(doc, { moveComment }, [{ commentId: 'c1', commentText: 'Move me' }]);
+
+ const receipt = createCommentsAdapter(editor).move({
+ commentId: 'c1',
+ target: { kind: 'text', blockId: 'p1', range: { start: 1, end: 4 } },
+ });
+
+ expect(moveComment).toHaveBeenCalledWith({ commentId: 'c1', from: 2, to: 5 });
+ expect(receipt.success).toBe(false);
+ expect(receipt.failure).toMatchObject({ code: 'NO_OP' });
+ });
+
+ it('resolves and removes comments, including replies', () => {
+ const schema = createCommentSchema();
+ const mark = schema.marks[CommentMarkName].create({ commentId: 'c1', internal: false });
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello', [mark])]);
+ const doc = schema.node('doc', null, [paragraph]);
+ const resolveComment = vi.fn(() => true);
+ const removeComment = vi.fn(() => true);
+ const editor = createPmEditor(doc, { resolveComment, removeComment }, [
+ { commentId: 'c1', commentText: 'Root', isDone: false },
+ { commentId: 'c2', parentCommentId: 'c1', commentText: 'Child' },
+ ]);
+
+ const api = createCommentsAdapter(editor);
+ const resolveReceipt = api.resolve({ commentId: 'c1' });
+ const removeReceipt = api.remove({ commentId: 'c1' });
+
+ expect(resolveComment).toHaveBeenCalledWith({ commentId: 'c1', importedId: undefined });
+ expect(resolveReceipt.success).toBe(true);
+ expect(resolveReceipt.updated?.[0]).toMatchObject({ entityId: 'c1' });
+
+ expect(removeComment).toHaveBeenCalledWith({ commentId: 'c1', importedId: undefined });
+ expect(removeReceipt.success).toBe(true);
+ const removedIds = (removeReceipt.removed ?? []).map((entry) => entry.entityId).sort();
+ expect(removedIds).toEqual(['c1', 'c2']);
+ });
+
+ it('returns NO_OP when resolve command resolves but does not apply changes', () => {
+ const schema = createCommentSchema();
+ const mark = schema.marks[CommentMarkName].create({ commentId: 'c1', internal: false });
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello', [mark])]);
+ const doc = schema.node('doc', null, [paragraph]);
+ const resolveComment = vi.fn(() => false);
+ const editor = createPmEditor(doc, { resolveComment }, [{ commentId: 'c1', commentText: 'Root', isDone: false }]);
+
+ const receipt = createCommentsAdapter(editor).resolve({ commentId: 'c1' });
+
+ expect(resolveComment).toHaveBeenCalledWith({ commentId: 'c1', importedId: undefined });
+ expect(receipt.success).toBe(false);
+ expect(receipt.failure).toMatchObject({ code: 'NO_OP' });
+ });
+
+ it('returns NO_OP when remove command does not apply and no records are removed', () => {
+ const schema = createCommentSchema();
+ const mark = schema.marks[CommentMarkName].create({ commentId: 'c1', internal: false });
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello', [mark])]);
+ const doc = schema.node('doc', null, [paragraph]);
+ const removeComment = vi.fn(() => false);
+ const editor = createPmEditor(doc, { removeComment }, []);
+
+ const receipt = createCommentsAdapter(editor).remove({ commentId: 'c1' });
+
+ expect(removeComment).toHaveBeenCalledWith({ commentId: 'c1', importedId: undefined });
+ expect(receipt.success).toBe(false);
+ expect(receipt.failure).toMatchObject({ code: 'NO_OP' });
+ });
+
+ it('removes anchorless reply records even when remove command is not applied', () => {
+ const schema = createCommentSchema();
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello')]);
+ const doc = schema.node('doc', null, [paragraph]);
+ const removeComment = vi.fn(() => false);
+ const editor = createPmEditor(doc, { removeComment }, [
+ { commentId: 'reply-1', parentCommentId: 'c1', commentText: 'Reply' },
+ ]);
+
+ const receipt = createCommentsAdapter(editor).remove({ commentId: 'reply-1' });
+
+ expect(removeComment).toHaveBeenCalledWith({ commentId: 'reply-1', importedId: undefined });
+ expect(receipt.success).toBe(true);
+ expect((receipt.removed ?? []).map((entry) => entry.entityId)).toEqual(['reply-1']);
+ });
+
+ it('updates internal metadata for anchorless comments via entity store mutation', () => {
+ const schema = createCommentSchema();
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello')]);
+ const doc = schema.node('doc', null, [paragraph]);
+ const setCommentInternal = vi.fn(() => false);
+ const editor = createPmEditor(doc, { setCommentInternal }, [
+ { commentId: 'c1', commentText: 'Root', isInternal: false },
+ ]);
+
+ const receipt = createCommentsAdapter(editor).setInternal({ commentId: 'c1', isInternal: true });
+
+ expect(setCommentInternal).not.toHaveBeenCalled();
+ expect(receipt.success).toBe(true);
+ const updated = (
+ editor as unknown as { converter: { comments: Array<{ commentId: string; isInternal?: boolean }> } }
+ ).converter.comments.find((comment) => comment.commentId === 'c1');
+ expect(updated?.isInternal).toBe(true);
+ });
+
+ it('sets internal, active, and cursor target comment operations', () => {
+ const schema = createCommentSchema();
+ const mark = schema.marks[CommentMarkName].create({ commentId: 'c1', internal: false });
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello', [mark])]);
+ const doc = schema.node('doc', null, [paragraph]);
+ const setCommentInternal = vi.fn(() => true);
+ const setActiveComment = vi.fn(() => true);
+ const setCursorById = vi.fn(() => true);
+ const editor = createPmEditor(
+ doc,
+ {
+ setCommentInternal,
+ setActiveComment,
+ setCursorById,
+ },
+ [{ commentId: 'c1', commentText: 'Root', isInternal: false }],
+ );
+
+ const api = createCommentsAdapter(editor);
+
+ const internalReceipt = api.setInternal({ commentId: 'c1', isInternal: true });
+ const activeReceipt = api.setActive({ commentId: 'c1' });
+ const clearActiveReceipt = api.setActive({ commentId: null });
+ const goToReceipt = api.goTo({ commentId: 'c1' });
+
+ expect(setCommentInternal).toHaveBeenCalledWith({ commentId: 'c1', importedId: undefined, isInternal: true });
+ expect(internalReceipt.success).toBe(true);
+ expect(activeReceipt.success).toBe(true);
+ expect(clearActiveReceipt.success).toBe(true);
+ expect(goToReceipt.success).toBe(true);
+ expect(setActiveComment).toHaveBeenNthCalledWith(1, { commentId: 'c1' });
+ expect(setActiveComment).toHaveBeenNthCalledWith(2, { commentId: null });
+ expect(setCursorById).toHaveBeenCalledWith('c1');
+ });
+
+ it('gets and lists comments across open and resolved anchors', () => {
+ const schema = createCommentSchema();
+ const openMark = schema.marks[CommentMarkName].create({ commentId: 'c1', internal: true });
+ const openParagraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Open comment', [openMark])]);
+ const resolvedParagraph = schema.node('paragraph', { paraId: 'p2' }, [
+ schema.nodes.commentRangeStart.create({ 'w:id': 'c2' }),
+ schema.text('Resolved comment'),
+ schema.nodes.commentRangeEnd.create({ 'w:id': 'c2' }),
+ ]);
+ const doc = schema.node('doc', null, [openParagraph, resolvedParagraph]);
+
+ const editor = createPmEditor(doc, {}, [
+ { commentId: 'c1', commentText: 'Open body', isDone: false, isInternal: true },
+ { commentId: 'c2', commentText: 'Resolved body', isDone: true },
+ ]);
+ const api = createCommentsAdapter(editor);
+
+ const open = api.get({ commentId: 'c1' });
+ const resolved = api.get({ commentId: 'c2' });
+ const openOnly = api.list({ includeResolved: false });
+ const all = api.list();
+
+ expect(open.status).toBe('open');
+ expect(open.commentId).toBe('c1');
+ expect(resolved.status).toBe('resolved');
+ expect(resolved.commentId).toBe('c2');
+ expect(openOnly.matches.map((comment) => comment.commentId)).toEqual(['c1']);
+ expect(all.total).toBeGreaterThanOrEqual(2);
+ });
+});
+
+describe('invariant: imported comment ID normalization', () => {
+ // These tests verify that comments with both a canonical commentId and an
+ // importedId (the w:id from DOCX) are treated as a single identity throughout
+ // the adapter. The import pipeline (prepareCommentsForImport) guarantees this
+ // today; these tests guard against regressions if a new code path creates
+ // marks or store entries with inconsistent IDs.
+
+ it('invariant: list() returns one record when mark carries both commentId and importedId', () => {
+ const schema = createCommentSchema();
+ const mark = schema.marks[CommentMarkName].create({
+ commentId: 'canonical-uuid',
+ importedId: 'imported-5',
+ internal: false,
+ });
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello', [mark])]);
+ const doc = schema.node('doc', null, [paragraph]);
+
+ const editor = createPmEditor(doc, {}, [
+ { commentId: 'canonical-uuid', importedId: 'imported-5', commentText: 'Body' },
+ ]);
+ const api = createCommentsAdapter(editor);
+ const result = api.list();
+
+ const matchingRecords = result.matches.filter(
+ (c) => c.commentId === 'canonical-uuid' || c.importedId === 'imported-5',
+ );
+ expect(matchingRecords).toHaveLength(1);
+ expect(matchingRecords[0]!.commentId).toBe('canonical-uuid');
+ });
+
+ it('invariant: get() by importedId returns the canonical record', () => {
+ const schema = createCommentSchema();
+ const mark = schema.marks[CommentMarkName].create({
+ commentId: 'canonical-uuid',
+ importedId: 'imported-5',
+ internal: false,
+ });
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello', [mark])]);
+ const doc = schema.node('doc', null, [paragraph]);
+
+ const editor = createPmEditor(doc, {}, [
+ { commentId: 'canonical-uuid', importedId: 'imported-5', commentText: 'Body' },
+ ]);
+ const api = createCommentsAdapter(editor);
+ const info = api.get({ commentId: 'imported-5' });
+
+ expect(info.commentId).toBe('canonical-uuid');
+ expect(info.target).toBeTruthy();
+ });
+
+ it('invariant: move() passes canonical commentId to moveComment command for imported comments', () => {
+ const schema = createCommentSchema();
+ const mark = schema.marks[CommentMarkName].create({
+ commentId: 'canonical-uuid',
+ importedId: 'imported-5',
+ internal: false,
+ });
+ const paragraph = schema.node('paragraph', { paraId: 'p1' }, [schema.text('Hello', [mark])]);
+ const doc = schema.node('doc', null, [paragraph]);
+ const moveComment = vi.fn(() => true);
+ const editor = createPmEditor(doc, { moveComment }, [
+ { commentId: 'canonical-uuid', importedId: 'imported-5', commentText: 'Move me' },
+ ]);
+
+ const receipt = createCommentsAdapter(editor).move({
+ commentId: 'imported-5',
+ target: { kind: 'text', blockId: 'p1', range: { start: 1, end: 4 } },
+ });
+
+ expect(receipt.success).toBe(true);
+ expect(moveComment).toHaveBeenCalledWith(expect.objectContaining({ commentId: 'canonical-uuid' }));
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/comments-adapter.ts b/packages/super-editor/src/document-api-adapters/comments-adapter.ts
new file mode 100644
index 0000000000..a9f376da4c
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/comments-adapter.ts
@@ -0,0 +1,734 @@
+import type { Editor } from '../core/Editor.js';
+import type {
+ AddCommentInput,
+ CommentInfo,
+ CommentsAdapter,
+ CommentsListQuery,
+ CommentsListResult,
+ EditCommentInput,
+ GetCommentInput,
+ GoToCommentInput,
+ MoveCommentInput,
+ Receipt,
+ RemoveCommentInput,
+ ReplyToCommentInput,
+ ResolveCommentInput,
+ SetCommentActiveInput,
+ SetCommentInternalInput,
+} from '@superdoc/document-api';
+import { TextSelection } from 'prosemirror-state';
+import { v4 as uuidv4 } from 'uuid';
+import { DocumentApiAdapterError } from './errors.js';
+import { requireEditorCommand } from './helpers/mutation-helpers.js';
+import { clearIndexCache } from './helpers/index-cache.js';
+import { resolveTextTarget } from './helpers/adapter-utils.js';
+import {
+ buildCommentJsonFromText,
+ extractCommentText,
+ findCommentEntity,
+ getCommentEntityStore,
+ isCommentResolved,
+ removeCommentEntityTree,
+ toCommentInfo,
+ upsertCommentEntity,
+} from './helpers/comment-entity-store.js';
+import { listCommentAnchors, resolveCommentAnchorsById } from './helpers/comment-target-resolver.js';
+import { toNonEmptyString } from './helpers/value-utils.js';
+
+type EditorUserIdentity = {
+ name?: string;
+ email?: string;
+ image?: string;
+};
+
+function toCommentAddress(commentId: string): { kind: 'entity'; entityType: 'comment'; entityId: string } {
+ return {
+ kind: 'entity',
+ entityType: 'comment',
+ entityId: commentId,
+ };
+}
+
+function toNotFoundError(input: unknown): DocumentApiAdapterError {
+ return new DocumentApiAdapterError('TARGET_NOT_FOUND', 'Comment target could not be resolved.', {
+ target: input,
+ });
+}
+
+function isSameTarget(
+ left: { blockId: string; range: { start: number; end: number } },
+ right: { blockId: string; range: { start: number; end: number } },
+): boolean {
+ return left.blockId === right.blockId && left.range.start === right.range.start && left.range.end === right.range.end;
+}
+
+/**
+ * Attempts to list comment anchors, returning an empty array on failure.
+ *
+ * listCommentAnchors walks the ProseMirror document tree and can throw when
+ * the document is in a transient or inconsistent state (e.g. mid-transaction,
+ * partially-loaded). Since this is only used by read-path aggregation
+ * (buildCommentInfos), returning an empty array is a safe degradation —
+ * callers will simply see fewer anchors rather than crashing the entire
+ * list/get flow.
+ */
+function listCommentAnchorsSafe(editor: Editor): ReturnType {
+ try {
+ return listCommentAnchors(editor);
+ } catch {
+ return [];
+ }
+}
+
+function applyTextSelection(editor: Editor, from: number, to: number): boolean {
+ const setTextSelection = editor.commands?.setTextSelection;
+ if (typeof setTextSelection === 'function') {
+ if (setTextSelection({ from, to }) === true) return true;
+ }
+
+ if (editor.state?.tr && typeof editor.dispatch === 'function') {
+ try {
+ const tr = editor.state.tr
+ .setSelection(TextSelection.create(editor.state.doc, from, to))
+ .setMeta('inputType', 'programmatic');
+ editor.dispatch(tr);
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ return false;
+}
+
+function resolveCommentIdentity(
+ editor: Editor,
+ commentId: string,
+): {
+ commentId: string;
+ importedId?: string;
+ anchors: ReturnType;
+} {
+ const store = getCommentEntityStore(editor);
+ const record = findCommentEntity(store, commentId);
+ const canonicalCommentIdFromRecord = toNonEmptyString(record?.commentId);
+ const importedIdFromRecord = toNonEmptyString(record?.importedId);
+
+ const anchorCandidates = [
+ ...resolveCommentAnchorsById(editor, commentId),
+ ...(canonicalCommentIdFromRecord && canonicalCommentIdFromRecord !== commentId
+ ? resolveCommentAnchorsById(editor, canonicalCommentIdFromRecord)
+ : []),
+ ...(importedIdFromRecord &&
+ importedIdFromRecord !== commentId &&
+ importedIdFromRecord !== canonicalCommentIdFromRecord
+ ? resolveCommentAnchorsById(editor, importedIdFromRecord)
+ : []),
+ ];
+
+ const seen = new Set();
+ const anchors = anchorCandidates.filter((anchor) => {
+ const key = `${anchor.commentId}|${anchor.importedId ?? ''}|${anchor.pos}|${anchor.end}`;
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+
+ const canonicalCommentId = canonicalCommentIdFromRecord ?? anchors[0]?.commentId;
+
+ if (!canonicalCommentId) {
+ throw toNotFoundError({ commentId });
+ }
+
+ const importedId = importedIdFromRecord ?? anchors[0]?.importedId;
+
+ return {
+ commentId: canonicalCommentId,
+ importedId,
+ anchors,
+ };
+}
+
+/**
+ * Merges document anchor data into a partially-built CommentInfo map.
+ *
+ * Grouping by anchor.commentId is safe because prepareCommentsForImport always
+ * sets the canonical commentId on marks (comments-helpers.js:650) and rewrites
+ * w:id on resolved range nodes (comments-helpers.js:621,639).
+ * resolveCommentIdFromAttrs returns canonical commentId first, so
+ * anchor.commentId matches the entity store key. If a non-import path ever
+ * creates marks without a canonical commentId attr, this grouping would need
+ * alias-merging by importedId.
+ */
+function mergeAnchorData(infosById: Map, anchors: ReturnType): void {
+ const grouped = new Map();
+ for (const anchor of anchors) {
+ const group = grouped.get(anchor.commentId) ?? [];
+ group.push(anchor);
+ grouped.set(anchor.commentId, group);
+ }
+
+ for (const [commentId, commentAnchors] of grouped.entries()) {
+ const sorted = [...commentAnchors].sort((a, b) => (a.pos === b.pos ? a.end - b.end : a.pos - b.pos));
+ const primary = sorted[0];
+ const status = sorted.every((anchor) => anchor.status === 'resolved') ? 'resolved' : 'open';
+ const existing = infosById.get(commentId);
+
+ if (existing) {
+ if (!existing.target) existing.target = primary.target;
+ if (!existing.importedId && primary.importedId) existing.importedId = primary.importedId;
+ if (existing.isInternal == null && primary.isInternal != null) existing.isInternal = primary.isInternal;
+ if (status === 'open') existing.status = 'open';
+ continue;
+ }
+
+ infosById.set(
+ commentId,
+ toCommentInfo(
+ {
+ commentId,
+ importedId: primary.importedId,
+ isInternal: primary.isInternal,
+ isDone: status === 'resolved',
+ },
+ {
+ target: primary.target,
+ status,
+ },
+ ),
+ );
+ }
+}
+
+function buildCommentInfos(editor: Editor): CommentInfo[] {
+ const store = getCommentEntityStore(editor);
+ const infosById = new Map();
+
+ for (const entry of store) {
+ const commentId = toNonEmptyString(entry.commentId) ?? toNonEmptyString(entry.importedId) ?? null;
+ if (!commentId) continue;
+ infosById.set(commentId, toCommentInfo({ ...entry, commentId }));
+ }
+
+ mergeAnchorData(infosById, listCommentAnchorsSafe(editor));
+
+ const infos = Array.from(infosById.values());
+ infos.sort((left, right) => {
+ const leftCreated = left.createdTime ?? 0;
+ const rightCreated = right.createdTime ?? 0;
+ if (leftCreated !== rightCreated) return leftCreated - rightCreated;
+
+ const leftStart = left.target?.range.start ?? Number.MAX_SAFE_INTEGER;
+ const rightStart = right.target?.range.start ?? Number.MAX_SAFE_INTEGER;
+ if (leftStart !== rightStart) return leftStart - rightStart;
+
+ return left.commentId.localeCompare(right.commentId);
+ });
+
+ return infos;
+}
+
+/**
+ * Adds a comment to the document at the specified text range.
+ *
+ * @param editor - The editor instance.
+ * @param input - The comment target and text.
+ * @returns A receipt indicating success and the created entity address.
+ */
+function addCommentHandler(editor: Editor, input: AddCommentInput): Receipt {
+ requireEditorCommand(editor.commands?.addComment, 'comments.add (addComment)');
+
+ if (input.target.range.start === input.target.range.end) {
+ return {
+ success: false,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Comment target range must be non-collapsed.',
+ },
+ };
+ }
+
+ const resolved = resolveTextTarget(editor, input.target);
+ if (!resolved) {
+ throw new DocumentApiAdapterError('TARGET_NOT_FOUND', 'Comment target could not be resolved.', {
+ target: input.target,
+ });
+ }
+ if (resolved.from === resolved.to) {
+ return {
+ success: false,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Comment target range must be non-collapsed.',
+ },
+ };
+ }
+
+ const commentId = uuidv4();
+
+ if (!applyTextSelection(editor, resolved.from, resolved.to)) {
+ return {
+ success: false,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Comment target selection could not be applied.',
+ details: { target: input.target },
+ },
+ };
+ }
+
+ // Re-read after selection so the command closure captures the updated selection snapshot.
+ const addComment = requireEditorCommand(editor.commands?.addComment, 'comments.add (addComment)');
+
+ const didInsert =
+ addComment({
+ content: input.text,
+ isInternal: false,
+ commentId,
+ }) === true;
+
+ if (!didInsert) {
+ return {
+ success: false,
+ failure: {
+ code: 'NO_OP',
+ message: 'Comment insertion produced no change.',
+ },
+ };
+ }
+
+ clearIndexCache(editor);
+
+ const store = getCommentEntityStore(editor);
+ const now = Date.now();
+ const user = (editor.options?.user ?? {}) as EditorUserIdentity;
+ upsertCommentEntity(store, commentId, {
+ commentId,
+ commentText: input.text,
+ commentJSON: buildCommentJsonFromText(input.text),
+ parentCommentId: undefined,
+ createdTime: now,
+ creatorName: user.name,
+ creatorEmail: user.email,
+ creatorImage: user.image,
+ isDone: false,
+ isInternal: false,
+ fileId: editor.options?.documentId,
+ documentId: editor.options?.documentId,
+ });
+
+ return {
+ success: true,
+ inserted: [toCommentAddress(commentId)],
+ };
+}
+
+function editCommentHandler(editor: Editor, input: EditCommentInput): Receipt {
+ const editComment = requireEditorCommand(editor.commands?.editComment, 'comments.edit (editComment)');
+
+ const store = getCommentEntityStore(editor);
+ const identity = resolveCommentIdentity(editor, input.commentId);
+ const existing = findCommentEntity(store, identity.commentId);
+ const existingText = existing ? extractCommentText(existing) : undefined;
+ if (existingText === input.text) {
+ return {
+ success: false,
+ failure: {
+ code: 'NO_OP',
+ message: 'Comment edit produced no change.',
+ },
+ };
+ }
+
+ const didEdit = editComment({
+ commentId: identity.commentId,
+ importedId: identity.importedId,
+ content: input.text,
+ });
+ if (!didEdit) {
+ return {
+ success: false,
+ failure: {
+ code: 'NO_OP',
+ message: 'Comment edit produced no change.',
+ },
+ };
+ }
+
+ upsertCommentEntity(store, identity.commentId, {
+ commentText: input.text,
+ commentJSON: buildCommentJsonFromText(input.text),
+ importedId: identity.importedId,
+ });
+
+ return {
+ success: true,
+ updated: [toCommentAddress(identity.commentId)],
+ };
+}
+
+function replyToCommentHandler(editor: Editor, input: ReplyToCommentInput): Receipt {
+ const addCommentReply = requireEditorCommand(editor.commands?.addCommentReply, 'comments.reply (addCommentReply)');
+
+ if (!input.parentCommentId) {
+ return {
+ success: false,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Reply target requires a non-empty parent comment id.',
+ },
+ };
+ }
+
+ const parentIdentity = resolveCommentIdentity(editor, input.parentCommentId);
+ const replyId = uuidv4();
+ const didReply = addCommentReply({
+ parentId: parentIdentity.commentId,
+ content: input.text,
+ commentId: replyId,
+ });
+ if (!didReply) {
+ return {
+ success: false,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Comment reply could not be applied.',
+ },
+ };
+ }
+
+ const now = Date.now();
+ const user = (editor.options?.user ?? {}) as EditorUserIdentity;
+ const store = getCommentEntityStore(editor);
+ upsertCommentEntity(store, replyId, {
+ commentId: replyId,
+ parentCommentId: parentIdentity.commentId,
+ commentText: input.text,
+ commentJSON: buildCommentJsonFromText(input.text),
+ createdTime: now,
+ creatorName: user.name,
+ creatorEmail: user.email,
+ creatorImage: user.image,
+ isDone: false,
+ isInternal: false,
+ fileId: editor.options?.documentId,
+ documentId: editor.options?.documentId,
+ });
+
+ return {
+ success: true,
+ inserted: [toCommentAddress(replyId)],
+ };
+}
+
+function moveCommentHandler(editor: Editor, input: MoveCommentInput): Receipt {
+ const moveComment = requireEditorCommand(editor.commands?.moveComment, 'comments.move (moveComment)');
+
+ if (input.target.range.start === input.target.range.end) {
+ return {
+ success: false,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Comment target range must be non-collapsed.',
+ },
+ };
+ }
+
+ const resolved = resolveTextTarget(editor, input.target);
+ if (!resolved) {
+ throw toNotFoundError(input.target);
+ }
+ if (resolved.from === resolved.to) {
+ return {
+ success: false,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Comment target range must be non-collapsed.',
+ },
+ };
+ }
+
+ const identity = resolveCommentIdentity(editor, input.commentId);
+ if (!identity.anchors.length) {
+ return {
+ success: false,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Comment cannot be moved because it has no resolvable anchor.',
+ },
+ };
+ }
+
+ if (identity.anchors.length > 1) {
+ return {
+ success: false,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Comment move target is ambiguous for comments with multiple anchors.',
+ },
+ };
+ }
+
+ const currentTarget = identity.anchors[0]?.target;
+ if (currentTarget && isSameTarget(currentTarget, input.target)) {
+ return {
+ success: false,
+ failure: {
+ code: 'NO_OP',
+ message: 'Comment move produced no change.',
+ },
+ };
+ }
+
+ // NOTE: Passing canonical commentId is sufficient because findRangeById checks
+ // marks by commentId || importedId (comments-plugin.js:1058) and resolved range
+ // nodes have w:id rewritten to canonical id during import (comments-helpers.js:621,639).
+ // If a non-import path ever creates anchors keyed only by importedId, this would
+ // need to fall back to identity.importedId.
+ const didMove = moveComment({
+ commentId: identity.commentId,
+ from: resolved.from,
+ to: resolved.to,
+ });
+
+ if (!didMove) {
+ return {
+ success: false,
+ failure: {
+ code: 'NO_OP',
+ message: 'Comment move produced no change.',
+ },
+ };
+ }
+
+ return {
+ success: true,
+ updated: [toCommentAddress(identity.commentId)],
+ };
+}
+
+function resolveCommentHandler(editor: Editor, input: ResolveCommentInput): Receipt {
+ const resolveComment = requireEditorCommand(editor.commands?.resolveComment, 'comments.resolve (resolveComment)');
+
+ const store = getCommentEntityStore(editor);
+ const identity = resolveCommentIdentity(editor, input.commentId);
+ const existing = findCommentEntity(store, identity.commentId);
+ const alreadyResolved =
+ (existing ? isCommentResolved(existing) : false) ||
+ (identity.anchors.length > 0 && identity.anchors.every((a) => a.status === 'resolved'));
+ if (alreadyResolved) {
+ return {
+ success: false,
+ failure: {
+ code: 'NO_OP',
+ message: 'Comment is already resolved.',
+ },
+ };
+ }
+
+ const didResolve = resolveComment({
+ commentId: identity.commentId,
+ importedId: identity.importedId,
+ });
+ if (!didResolve) {
+ return {
+ success: false,
+ failure: {
+ code: 'NO_OP',
+ message: 'Comment resolve produced no change.',
+ },
+ };
+ }
+
+ upsertCommentEntity(store, identity.commentId, {
+ importedId: identity.importedId,
+ isDone: true,
+ resolvedTime: Date.now(),
+ });
+
+ return {
+ success: true,
+ updated: [toCommentAddress(identity.commentId)],
+ };
+}
+
+function removeCommentHandler(editor: Editor, input: RemoveCommentInput): Receipt {
+ const removeComment = requireEditorCommand(editor.commands?.removeComment, 'comments.remove (removeComment)');
+
+ const store = getCommentEntityStore(editor);
+ const identity = resolveCommentIdentity(editor, input.commentId);
+
+ const didRemove =
+ removeComment({
+ commentId: identity.commentId,
+ importedId: identity.importedId,
+ }) === true;
+
+ const removedRecords = removeCommentEntityTree(store, identity.commentId);
+ if (!didRemove && removedRecords.length === 0) {
+ return {
+ success: false,
+ failure: {
+ code: 'NO_OP',
+ message: 'Comment remove produced no change.',
+ },
+ };
+ }
+
+ const removedIds = new Set();
+ for (const record of removedRecords) {
+ const removedId = toNonEmptyString(record.commentId);
+ if (removedId) {
+ removedIds.add(removedId);
+ }
+ }
+ if (!removedIds.size && didRemove) {
+ removedIds.add(identity.commentId);
+ }
+
+ return {
+ success: true,
+ removed: Array.from(removedIds).map((id) => toCommentAddress(id)),
+ };
+}
+
+function setCommentInternalHandler(editor: Editor, input: SetCommentInternalInput): Receipt {
+ const setCommentInternal = requireEditorCommand(
+ editor.commands?.setCommentInternal,
+ 'comments.setInternal (setCommentInternal)',
+ );
+
+ const store = getCommentEntityStore(editor);
+ const identity = resolveCommentIdentity(editor, input.commentId);
+ const existing = findCommentEntity(store, identity.commentId);
+ const currentInternal =
+ (typeof existing?.isInternal === 'boolean' ? existing.isInternal : undefined) ?? identity.anchors[0]?.isInternal;
+
+ if (typeof currentInternal === 'boolean' && currentInternal === input.isInternal) {
+ return {
+ success: false,
+ failure: {
+ code: 'NO_OP',
+ message: 'Comment internal state is already set to the requested value.',
+ },
+ };
+ }
+
+ const hasOpenAnchor = identity.anchors.some((anchor) => anchor.status === 'open');
+ if (hasOpenAnchor) {
+ const didApply = setCommentInternal({
+ commentId: identity.commentId,
+ importedId: identity.importedId,
+ isInternal: input.isInternal,
+ });
+ if (!didApply) {
+ return {
+ success: false,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Comment internal state could not be updated on the current anchor.',
+ },
+ };
+ }
+ }
+
+ upsertCommentEntity(store, identity.commentId, {
+ importedId: identity.importedId,
+ isInternal: input.isInternal,
+ });
+
+ return {
+ success: true,
+ updated: [toCommentAddress(identity.commentId)],
+ };
+}
+
+function setCommentActiveHandler(editor: Editor, input: SetCommentActiveInput): Receipt {
+ const setActiveComment = requireEditorCommand(
+ editor.commands?.setActiveComment,
+ 'comments.setActive (setActiveComment)',
+ );
+
+ let resolvedCommentId: string | null = null;
+ if (input.commentId != null) {
+ resolvedCommentId = resolveCommentIdentity(editor, input.commentId).commentId;
+ }
+
+ const didSet = setActiveComment({ commentId: resolvedCommentId });
+ if (!didSet) {
+ return {
+ success: false,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Active comment could not be updated.',
+ },
+ };
+ }
+
+ return {
+ success: true,
+ updated: resolvedCommentId ? [toCommentAddress(resolvedCommentId)] : undefined,
+ };
+}
+
+function goToCommentHandler(editor: Editor, input: GoToCommentInput): Receipt {
+ const setCursorById = requireEditorCommand(editor.commands?.setCursorById, 'comments.goTo (setCursorById)');
+
+ const identity = resolveCommentIdentity(editor, input.commentId);
+ let didSetCursor = setCursorById(identity.commentId);
+ if (!didSetCursor && identity.importedId && identity.importedId !== identity.commentId) {
+ didSetCursor = setCursorById(identity.importedId);
+ }
+ if (!didSetCursor) {
+ throw toNotFoundError({ commentId: identity.commentId });
+ }
+
+ return {
+ success: true,
+ updated: [toCommentAddress(identity.commentId)],
+ };
+}
+
+function getCommentHandler(editor: Editor, input: GetCommentInput): CommentInfo {
+ const comments = buildCommentInfos(editor);
+ const found = comments.find(
+ (comment) => comment.commentId === input.commentId || comment.importedId === input.commentId,
+ );
+ if (!found) {
+ throw toNotFoundError({ commentId: input.commentId });
+ }
+ return found;
+}
+
+function listCommentsHandler(editor: Editor, query?: CommentsListQuery): CommentsListResult {
+ const comments = buildCommentInfos(editor);
+ const includeResolved = query?.includeResolved ?? true;
+ const matches = includeResolved ? comments : comments.filter((comment) => comment.status !== 'resolved');
+
+ return {
+ matches,
+ total: matches.length,
+ };
+}
+
+/**
+ * Creates the comments adapter namespace for the Document API.
+ *
+ * @param editor - The editor instance to bind comment operations to.
+ * @returns A {@link CommentsAdapter} that delegates to editor commands.
+ */
+export function createCommentsAdapter(editor: Editor): CommentsAdapter {
+ return {
+ add: (input: AddCommentInput) => addCommentHandler(editor, input),
+ edit: (input: EditCommentInput) => editCommentHandler(editor, input),
+ reply: (input: ReplyToCommentInput) => replyToCommentHandler(editor, input),
+ move: (input: MoveCommentInput) => moveCommentHandler(editor, input),
+ resolve: (input: ResolveCommentInput) => resolveCommentHandler(editor, input),
+ remove: (input: RemoveCommentInput) => removeCommentHandler(editor, input),
+ setInternal: (input: SetCommentInternalInput) => setCommentInternalHandler(editor, input),
+ setActive: (input: SetCommentActiveInput) => setCommentActiveHandler(editor, input),
+ goTo: (input: GoToCommentInput) => goToCommentHandler(editor, input),
+ get: (input: GetCommentInput) => getCommentHandler(editor, input),
+ list: (query?: CommentsListQuery) => listCommentsHandler(editor, query),
+ };
+}
diff --git a/packages/super-editor/src/document-api-adapters/create-adapter.test.ts b/packages/super-editor/src/document-api-adapters/create-adapter.test.ts
new file mode 100644
index 0000000000..0cfba93001
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/create-adapter.test.ts
@@ -0,0 +1,387 @@
+import { describe, expect, it, vi } from 'vitest';
+import type { Node as ProseMirrorNode } from 'prosemirror-model';
+import type { Editor } from '../core/Editor.js';
+import { createParagraphAdapter } from './create-adapter.js';
+import * as trackedChangeResolver from './helpers/tracked-change-resolver.js';
+
+type MockNode = ProseMirrorNode & {
+ _children?: MockNode[];
+ marks?: Array<{ type: { name: string }; attrs?: Record }>;
+};
+
+function createTextNode(text: string, marks: MockNode['marks'] = []): MockNode {
+ return {
+ type: { name: 'text' },
+ text,
+ marks,
+ nodeSize: text.length,
+ isText: true,
+ isInline: true,
+ isBlock: false,
+ isLeaf: false,
+ inlineContent: false,
+ isTextblock: false,
+ childCount: 0,
+ child() {
+ throw new Error('text node has no children');
+ },
+ descendants() {
+ return undefined;
+ },
+ } as unknown as MockNode;
+}
+
+function createParagraphNode(
+ id: string,
+ text = '',
+ tracked = false,
+ extraAttrs: Record = {},
+): MockNode {
+ const marks =
+ tracked && text.length > 0
+ ? [
+ {
+ type: { name: 'trackInsert' },
+ attrs: { id: `tc-${id}` },
+ },
+ ]
+ : [];
+ const children = text.length > 0 ? [createTextNode(text, marks)] : [];
+ const contentSize = children.reduce((sum, child) => sum + child.nodeSize, 0);
+
+ return {
+ type: { name: 'paragraph' },
+ attrs: { sdBlockId: id, ...extraAttrs },
+ _children: children,
+ nodeSize: contentSize + 2,
+ isText: false,
+ isInline: false,
+ isBlock: true,
+ isLeaf: false,
+ inlineContent: true,
+ isTextblock: true,
+ childCount: children.length,
+ child(index: number) {
+ return children[index] as unknown as ProseMirrorNode;
+ },
+ descendants(callback: (node: ProseMirrorNode, pos: number) => void) {
+ let offset = 1;
+ for (const child of children) {
+ callback(child as unknown as ProseMirrorNode, offset);
+ offset += child.nodeSize;
+ }
+ return undefined;
+ },
+ } as unknown as MockNode;
+}
+
+function createDocNode(children: MockNode[]): MockNode {
+ const node = {
+ type: { name: 'doc' },
+ _children: children,
+ isText: false,
+ isInline: false,
+ isBlock: false,
+ isLeaf: false,
+ inlineContent: false,
+ isTextblock: false,
+ childCount: children.length,
+ child(index: number) {
+ return children[index] as unknown as ProseMirrorNode;
+ },
+ get nodeSize() {
+ return this.content.size + 2;
+ },
+ get content() {
+ return {
+ size: children.reduce((sum, child) => sum + child.nodeSize, 0),
+ };
+ },
+ descendants(callback: (node: ProseMirrorNode, pos: number) => void) {
+ let pos = 0;
+ for (const child of children) {
+ callback(child as unknown as ProseMirrorNode, pos);
+ let offset = 1;
+ for (const grandChild of child._children ?? []) {
+ callback(grandChild as unknown as ProseMirrorNode, pos + offset);
+ offset += grandChild.nodeSize;
+ }
+ pos += child.nodeSize;
+ }
+ return undefined;
+ },
+ nodesBetween(this: MockNode, from: number, to: number, callback: (node: ProseMirrorNode) => void) {
+ const size = this.content.size;
+ if (!Number.isFinite(size)) {
+ throw new Error('nodesBetween called without document context');
+ }
+ let pos = 0;
+ for (const child of children) {
+ const childStart = pos;
+ const childEnd = pos + child.nodeSize;
+ if (childEnd < from || childStart > to) {
+ pos += child.nodeSize;
+ continue;
+ }
+
+ callback(child as unknown as ProseMirrorNode);
+ for (const grandChild of child._children ?? []) {
+ callback(grandChild as unknown as ProseMirrorNode);
+ }
+ pos += child.nodeSize;
+ }
+ },
+ } as unknown as MockNode;
+
+ return node;
+}
+
+function insertChildAtPos(doc: MockNode, child: MockNode, pos: number): boolean {
+ const children = doc._children ?? [];
+ let cursor = 0;
+
+ for (let index = 0; index <= children.length; index += 1) {
+ if (cursor === pos) {
+ children.splice(index, 0, child);
+ doc.childCount = children.length;
+ return true;
+ }
+
+ if (index < children.length) {
+ cursor += children[index]!.nodeSize;
+ }
+ }
+
+ return false;
+}
+
+function makeEditor({
+ withTrackedCommand = true,
+ insertReturns = true,
+ insertedParagraphAttrs,
+ user,
+}: {
+ withTrackedCommand?: boolean;
+ insertReturns?: boolean;
+ insertedParagraphAttrs?: Record;
+ user?: { name: string };
+} = {}): {
+ editor: Editor;
+ insertParagraphAt: ReturnType;
+} {
+ const doc = createDocNode([createParagraphNode('p1', 'Hello')]);
+
+ const insertParagraphAt = vi.fn((options: { pos: number; text?: string; sdBlockId?: string; tracked?: boolean }) => {
+ if (!insertReturns) return false;
+ const nodeId = options.sdBlockId ?? 'new-paragraph';
+ const paragraph = createParagraphNode(nodeId, options.text ?? '', options.tracked === true, insertedParagraphAttrs);
+ return insertChildAtPos(doc, paragraph, options.pos);
+ });
+
+ const editor = {
+ state: {
+ doc,
+ },
+ commands: {
+ insertParagraphAt,
+ insertTrackedChange: withTrackedCommand ? vi.fn(() => true) : undefined,
+ },
+ can: () => ({
+ insertParagraphAt: () => insertReturns,
+ }),
+ options: { user },
+ } as unknown as Editor;
+
+ return { editor, insertParagraphAt };
+}
+
+describe('createParagraphAdapter', () => {
+ it('creates a paragraph at the document end by default', () => {
+ const { editor, insertParagraphAt } = makeEditor();
+
+ const result = createParagraphAdapter(editor, { text: 'New paragraph' }, { changeMode: 'direct' });
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.paragraph.kind).toBe('block');
+ expect(result.paragraph.nodeType).toBe('paragraph');
+ expect(result.insertionPoint.kind).toBe('text');
+ expect(result.insertionPoint.range).toEqual({ start: 0, end: 0 });
+ }
+
+ expect(insertParagraphAt).toHaveBeenCalledTimes(1);
+ expect(insertParagraphAt.mock.calls[0]?.[0]).toMatchObject({
+ text: 'New paragraph',
+ tracked: false,
+ });
+ });
+
+ it('creates a paragraph before a target block', () => {
+ const { editor, insertParagraphAt } = makeEditor();
+
+ const result = createParagraphAdapter(
+ editor,
+ {
+ at: {
+ kind: 'before',
+ target: { kind: 'block', nodeType: 'paragraph', nodeId: 'p1' },
+ },
+ },
+ { changeMode: 'direct' },
+ );
+
+ expect(result.success).toBe(true);
+ expect(insertParagraphAt.mock.calls[0]?.[0]?.pos).toBe(0);
+ });
+
+ it('throws TARGET_NOT_FOUND when a before/after target cannot be resolved', () => {
+ const { editor } = makeEditor();
+
+ expect(() =>
+ createParagraphAdapter(
+ editor,
+ {
+ at: {
+ kind: 'after',
+ target: { kind: 'block', nodeType: 'paragraph', nodeId: 'missing' },
+ },
+ },
+ { changeMode: 'direct' },
+ ),
+ ).toThrow('target block was not found');
+ });
+
+ it('throws CAPABILITY_UNAVAILABLE when tracked create is requested without tracked capability', () => {
+ const { editor } = makeEditor({ withTrackedCommand: false });
+
+ expect(() => createParagraphAdapter(editor, { text: 'Tracked' }, { changeMode: 'tracked' })).toThrow(
+ 'requires the insertTrackedChange command',
+ );
+ });
+
+ it('creates tracked paragraphs without losing nodesBetween context', () => {
+ const resolverSpy = vi.spyOn(trackedChangeResolver, 'buildTrackedChangeCanonicalIdMap').mockReturnValue(new Map());
+
+ const { editor } = makeEditor({ user: { name: 'Test' } });
+
+ const result = createParagraphAdapter(editor, { text: 'Tracked paragraph' }, { changeMode: 'tracked' });
+
+ expect(result.success).toBe(true);
+ if (!result.success) return;
+ expect(result.trackedChangeRefs?.length).toBeGreaterThan(0);
+ expect(result.trackedChangeRefs?.[0]).toMatchObject({
+ kind: 'entity',
+ entityType: 'trackedChange',
+ });
+ expect(resolverSpy).toHaveBeenCalledTimes(1);
+ resolverSpy.mockRestore();
+ });
+
+ it('returns INVALID_TARGET failure when command cannot apply the insertion', () => {
+ const { editor } = makeEditor({ insertReturns: false });
+
+ const result = createParagraphAdapter(editor, { text: 'No-op' }, { changeMode: 'direct' });
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.failure.code).toBe('INVALID_TARGET');
+ }
+ });
+
+ it('dry-run returns placeholder success without mutating the document', () => {
+ const { editor, insertParagraphAt } = makeEditor();
+
+ const result = createParagraphAdapter(editor, { text: 'Dry run text' }, { changeMode: 'direct', dryRun: true });
+
+ expect(result.success).toBe(true);
+ if (!result.success) return;
+ expect(result.paragraph).toEqual({ kind: 'block', nodeType: 'paragraph', nodeId: '(dry-run)' });
+ expect(result.insertionPoint).toEqual({ kind: 'text', blockId: '(dry-run)', range: { start: 0, end: 0 } });
+ expect(insertParagraphAt).not.toHaveBeenCalled();
+ });
+
+ it('dry-run returns INVALID_TARGET when insertion cannot be applied', () => {
+ const { editor } = makeEditor({ insertReturns: false });
+
+ const result = createParagraphAdapter(editor, { text: 'Dry run text' }, { changeMode: 'direct', dryRun: true });
+
+ expect(result.success).toBe(false);
+ if (result.success) return;
+ expect(result.failure.code).toBe('INVALID_TARGET');
+ });
+
+ it('dry-run still throws TARGET_NOT_FOUND when target block does not exist', () => {
+ const { editor } = makeEditor();
+
+ expect(() =>
+ createParagraphAdapter(
+ editor,
+ {
+ at: {
+ kind: 'before',
+ target: { kind: 'block', nodeType: 'paragraph', nodeId: 'missing' },
+ },
+ },
+ { changeMode: 'direct', dryRun: true },
+ ),
+ ).toThrow('target block was not found');
+ });
+
+ it('dry-run still throws CAPABILITY_UNAVAILABLE when tracked capability is missing', () => {
+ const { editor } = makeEditor({ withTrackedCommand: false });
+
+ expect(() =>
+ createParagraphAdapter(editor, { text: 'Tracked dry run' }, { changeMode: 'tracked', dryRun: true }),
+ ).toThrow('requires the insertTrackedChange command');
+ });
+
+ it('resolves created paragraph when block index identity prefers paraId over sdBlockId', () => {
+ const { editor } = makeEditor({
+ insertedParagraphAttrs: {
+ paraId: 'pm-para-id',
+ },
+ });
+
+ const result = createParagraphAdapter(editor, { text: 'Inserted paragraph' }, { changeMode: 'direct' });
+
+ expect(result.success).toBe(true);
+ if (!result.success) return;
+ expect(result.paragraph.nodeType).toBe('paragraph');
+ expect(result.paragraph.nodeId).toBe('pm-para-id');
+ expect(result.insertionPoint.blockId).toBe('pm-para-id');
+ });
+
+ it('returns success with generated ID when post-apply paragraph resolution fails', () => {
+ const { editor } = makeEditor({
+ insertedParagraphAttrs: {
+ sdBlockId: undefined,
+ },
+ });
+
+ const result = createParagraphAdapter(editor, { text: 'Inserted paragraph' }, { changeMode: 'direct' });
+
+ // Contract: success:false means no mutation was applied.
+ // The mutation DID apply, so we must return success with the generated ID.
+ expect(result.success).toBe(true);
+ if (!result.success) return;
+ expect(result.paragraph.nodeType).toBe('paragraph');
+ expect(typeof result.paragraph.nodeId).toBe('string');
+ expect(result.paragraph.nodeId).not.toBe('(dry-run)');
+ });
+
+ it('throws CAPABILITY_UNAVAILABLE for tracked dry-run without a configured user', () => {
+ const { editor } = makeEditor();
+
+ expect(() => createParagraphAdapter(editor, { text: 'Tracked' }, { changeMode: 'tracked', dryRun: true })).toThrow(
+ 'requires a user to be configured',
+ );
+ });
+
+ it('throws same error for tracked non-dry-run without a configured user', () => {
+ const { editor } = makeEditor();
+
+ expect(() => createParagraphAdapter(editor, { text: 'Tracked' }, { changeMode: 'tracked' })).toThrow(
+ 'requires a user to be configured',
+ );
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/create-adapter.ts b/packages/super-editor/src/document-api-adapters/create-adapter.ts
new file mode 100644
index 0000000000..ec081a0d0d
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/create-adapter.ts
@@ -0,0 +1,168 @@
+import { v4 as uuidv4 } from 'uuid';
+import type { Editor } from '../core/Editor.js';
+import type {
+ CreateParagraphInput,
+ CreateParagraphResult,
+ CreateParagraphSuccessResult,
+ MutationOptions,
+} from '@superdoc/document-api';
+import { clearIndexCache, getBlockIndex } from './helpers/index-cache.js';
+import { findBlockById, type BlockCandidate } from './helpers/node-address-resolver.js';
+import { collectTrackInsertRefsInRange } from './helpers/tracked-change-refs.js';
+import { DocumentApiAdapterError } from './errors.js';
+import { requireEditorCommand, ensureTrackedCapability } from './helpers/mutation-helpers.js';
+
+type InsertParagraphAtCommandOptions = {
+ pos: number;
+ text?: string;
+ sdBlockId?: string;
+ tracked?: boolean;
+};
+
+type InsertParagraphAtCommand = (options: InsertParagraphAtCommandOptions) => boolean;
+
+function resolveParagraphInsertPosition(editor: Editor, input: CreateParagraphInput): number {
+ const location = input.at ?? { kind: 'documentEnd' };
+
+ if (location.kind === 'documentStart') return 0;
+ if (location.kind === 'documentEnd') return editor.state.doc.content.size;
+
+ const index = getBlockIndex(editor);
+ const target = findBlockById(index, location.target);
+ if (!target) {
+ throw new DocumentApiAdapterError('TARGET_NOT_FOUND', 'Create paragraph target block was not found.', {
+ target: location.target,
+ });
+ }
+
+ return location.kind === 'before' ? target.pos : target.end;
+}
+
+function resolveCreatedParagraph(editor: Editor, paragraphId: string): BlockCandidate {
+ const index = getBlockIndex(editor);
+ const resolved = index.byId.get(`paragraph:${paragraphId}`);
+
+ if (resolved) return resolved;
+
+ // Paragraph addresses may currently prefer imported paraId over sdBlockId.
+ // After insertion, resolve by sdBlockId as a deterministic fallback so a
+ // successful insert cannot be reported as a failure solely due to ID
+ // projection differences.
+ const bySdBlockId = index.candidates.find((candidate) => {
+ if (candidate.nodeType !== 'paragraph') return false;
+ const attrs = (candidate.node as { attrs?: { sdBlockId?: unknown } }).attrs;
+ return typeof attrs?.sdBlockId === 'string' && attrs.sdBlockId === paragraphId;
+ });
+ if (bySdBlockId) return bySdBlockId;
+
+ const fallback = index.candidates.find(
+ (candidate) => candidate.nodeType === 'paragraph' && candidate.nodeId === paragraphId,
+ );
+ if (fallback) return fallback;
+
+ throw new DocumentApiAdapterError('TARGET_NOT_FOUND', 'Created paragraph could not be resolved after insertion.', {
+ paragraphId,
+ });
+}
+
+function buildParagraphCreateSuccess(
+ paragraphNodeId: string,
+ trackedChangeRefs?: CreateParagraphSuccessResult['trackedChangeRefs'],
+): CreateParagraphSuccessResult {
+ return {
+ success: true,
+ paragraph: {
+ kind: 'block',
+ nodeType: 'paragraph',
+ nodeId: paragraphNodeId,
+ },
+ insertionPoint: {
+ kind: 'text',
+ blockId: paragraphNodeId,
+ range: { start: 0, end: 0 },
+ },
+ trackedChangeRefs,
+ };
+}
+
+export function createParagraphAdapter(
+ editor: Editor,
+ input: CreateParagraphInput,
+ options?: MutationOptions,
+): CreateParagraphResult {
+ const insertParagraphAt = requireEditorCommand(
+ editor.commands?.insertParagraphAt,
+ 'create.paragraph',
+ ) as InsertParagraphAtCommand;
+ const mode = options?.changeMode ?? 'direct';
+
+ if (mode === 'tracked') {
+ ensureTrackedCapability(editor, { operation: 'create.paragraph' });
+ }
+
+ const insertAt = resolveParagraphInsertPosition(editor, input);
+
+ if (options?.dryRun) {
+ const canInsert = editor.can().insertParagraphAt?.({
+ pos: insertAt,
+ text: input.text,
+ tracked: mode === 'tracked',
+ });
+
+ if (!canInsert) {
+ return {
+ success: false,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Paragraph creation could not be applied at the requested location.',
+ },
+ };
+ }
+
+ return {
+ success: true,
+ paragraph: {
+ kind: 'block',
+ nodeType: 'paragraph',
+ nodeId: '(dry-run)',
+ },
+ insertionPoint: {
+ kind: 'text',
+ blockId: '(dry-run)',
+ range: { start: 0, end: 0 },
+ },
+ };
+ }
+
+ const paragraphId = uuidv4();
+
+ const didApply = insertParagraphAt({
+ pos: insertAt,
+ text: input.text,
+ sdBlockId: paragraphId,
+ tracked: mode === 'tracked',
+ });
+
+ if (!didApply) {
+ return {
+ success: false,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Paragraph creation could not be applied at the requested location.',
+ },
+ };
+ }
+
+ clearIndexCache(editor);
+ try {
+ const paragraph = resolveCreatedParagraph(editor, paragraphId);
+ const trackedChangeRefs =
+ mode === 'tracked' ? collectTrackInsertRefsInRange(editor, paragraph.pos, paragraph.end) : undefined;
+
+ return buildParagraphCreateSuccess(paragraph.nodeId, trackedChangeRefs);
+ } catch {
+ // Mutation already applied — contract requires success: true.
+ // Fall back to the generated ID we assigned to the command.
+ return buildParagraphCreateSuccess(paragraphId);
+ }
+}
diff --git a/packages/super-editor/src/document-api-adapters/errors.test.ts b/packages/super-editor/src/document-api-adapters/errors.test.ts
new file mode 100644
index 0000000000..2e8180081b
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/errors.test.ts
@@ -0,0 +1,59 @@
+import { DocumentApiAdapterError, isDocumentApiAdapterError } from './errors.js';
+
+describe('DocumentApiAdapterError', () => {
+ it('extends Error with name, code, and message', () => {
+ const error = new DocumentApiAdapterError('TARGET_NOT_FOUND', 'Node not found.');
+
+ expect(error).toBeInstanceOf(Error);
+ expect(error).toBeInstanceOf(DocumentApiAdapterError);
+ expect(error.name).toBe('DocumentApiAdapterError');
+ expect(error.code).toBe('TARGET_NOT_FOUND');
+ expect(error.message).toBe('Node not found.');
+ expect(error.details).toBeUndefined();
+ });
+
+ it('stores optional details payload', () => {
+ const details = { nodeId: 'p1', nodeType: 'paragraph' };
+ const error = new DocumentApiAdapterError('INVALID_TARGET', 'Bad target.', details);
+
+ expect(error.details).toEqual(details);
+ });
+
+ it('supports all error codes', () => {
+ const codes = ['TARGET_NOT_FOUND', 'INVALID_TARGET', 'CAPABILITY_UNAVAILABLE'] as const;
+
+ for (const code of codes) {
+ const error = new DocumentApiAdapterError(code, `Error: ${code}`);
+ expect(error.code).toBe(code);
+ }
+ });
+
+ it('is caught by instanceof checks after setPrototypeOf', () => {
+ const error = new DocumentApiAdapterError('CAPABILITY_UNAVAILABLE', 'test');
+
+ try {
+ throw error;
+ } catch (caught) {
+ expect(caught instanceof DocumentApiAdapterError).toBe(true);
+ }
+ });
+});
+
+describe('isDocumentApiAdapterError', () => {
+ it('returns true for DocumentApiAdapterError instances', () => {
+ const error = new DocumentApiAdapterError('TARGET_NOT_FOUND', 'test');
+ expect(isDocumentApiAdapterError(error)).toBe(true);
+ });
+
+ it('returns false for plain Error', () => {
+ expect(isDocumentApiAdapterError(new Error('test'))).toBe(false);
+ });
+
+ it('returns false for non-error values', () => {
+ expect(isDocumentApiAdapterError(null)).toBe(false);
+ expect(isDocumentApiAdapterError(undefined)).toBe(false);
+ expect(isDocumentApiAdapterError('string')).toBe(false);
+ expect(isDocumentApiAdapterError(42)).toBe(false);
+ expect(isDocumentApiAdapterError({ code: 'TARGET_NOT_FOUND', message: 'fake' })).toBe(false);
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/errors.ts b/packages/super-editor/src/document-api-adapters/errors.ts
new file mode 100644
index 0000000000..20186c1bce
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/errors.ts
@@ -0,0 +1,32 @@
+/** Error codes used by {@link DocumentApiAdapterError} to classify adapter failures. */
+export type DocumentApiAdapterErrorCode = 'TARGET_NOT_FOUND' | 'INVALID_TARGET' | 'CAPABILITY_UNAVAILABLE';
+
+/**
+ * Structured error thrown by document-api adapter functions.
+ *
+ * @param code - Machine-readable error classification.
+ * @param message - Human-readable description.
+ * @param details - Optional payload with additional context.
+ */
+export class DocumentApiAdapterError extends Error {
+ readonly code: DocumentApiAdapterErrorCode;
+ readonly details?: unknown;
+
+ constructor(code: DocumentApiAdapterErrorCode, message: string, details?: unknown) {
+ super(message);
+ this.name = 'DocumentApiAdapterError';
+ this.code = code;
+ this.details = details;
+ Object.setPrototypeOf(this, DocumentApiAdapterError.prototype);
+ }
+}
+
+/**
+ * Type guard that narrows an unknown value to {@link DocumentApiAdapterError}.
+ *
+ * @param error - The value to test.
+ * @returns `true` if the value is a `DocumentApiAdapterError` instance.
+ */
+export function isDocumentApiAdapterError(error: unknown): error is DocumentApiAdapterError {
+ return error instanceof DocumentApiAdapterError;
+}
diff --git a/packages/super-editor/src/document-api-adapters/find-adapter.test.ts b/packages/super-editor/src/document-api-adapters/find-adapter.test.ts
new file mode 100644
index 0000000000..1413274bbc
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/find-adapter.test.ts
@@ -0,0 +1,824 @@
+import type { Node as ProseMirrorNode } from 'prosemirror-model';
+import type { Editor } from '../core/Editor.js';
+import type { Query } from '@superdoc/document-api';
+import { findAdapter } from './find-adapter.js';
+
+// ---------------------------------------------------------------------------
+// Helpers — lightweight ProseMirror-like stubs
+// ---------------------------------------------------------------------------
+
+/**
+ * Creates a minimal ProseMirrorNode stub.
+ *
+ * `textContent` is an optional flat string representing the text in the doc.
+ * `textBetween(from, to, blockSep)` slices from it, inserting `blockSep` at
+ * every position where a child boundary is crossed. This is a simplified model
+ * sufficient for testing snippet generation.
+ */
+function makeNode(
+ typeName: string,
+ attrs: Record = {},
+ nodeSize = 10,
+ children: Array<{ node: ProseMirrorNode; offset: number }> = [],
+ textContent = '',
+): ProseMirrorNode {
+ const inlineTypes = new Set([
+ 'text',
+ 'run',
+ 'image',
+ 'tab',
+ 'lineBreak',
+ 'hardBreak',
+ 'bookmarkStart',
+ 'bookmarkEnd',
+ 'commentRangeStart',
+ 'commentRangeEnd',
+ 'commentReference',
+ 'structuredContent',
+ 'footnoteReference',
+ ]);
+ const isText = typeName === 'text';
+ const isInline = inlineTypes.has(typeName);
+ const isBlock = typeName !== 'doc' && !isInline;
+ const inlineContent = isBlock && typeName === 'paragraph';
+ const computedNodeSize = isText ? textContent.length : nodeSize;
+ const contentSize = children.reduce((max, child) => Math.max(max, child.offset + child.node.nodeSize), 0);
+
+ // Collect boundary positions where block separators should be inserted.
+ const boundaries = new Set();
+ for (const child of children) {
+ boundaries.add(child.offset);
+ boundaries.add(child.offset + (child.node as unknown as { nodeSize: number }).nodeSize);
+ }
+
+ return {
+ type: { name: typeName },
+ attrs,
+ nodeSize: computedNodeSize,
+ content: { size: contentSize },
+ textContent,
+ text: isText ? textContent : undefined,
+ isText,
+ isLeaf: isText || (isInline && children.length === 0),
+ isInline,
+ isBlock,
+ inlineContent,
+ isTextblock: inlineContent,
+ marks: (attrs.__marks ?? []) as unknown as ProseMirrorNode['marks'],
+ textBetween(from: number, to: number, blockSep = '') {
+ // Build text character-by-character, inserting blockSep at boundaries
+ let result = '';
+ for (let i = from; i < to; i++) {
+ if (i > from && boundaries.has(i) && blockSep) {
+ result += blockSep;
+ }
+ if (i < textContent.length) {
+ result += textContent[i];
+ }
+ }
+ return result;
+ },
+ descendants(callback: (node: ProseMirrorNode, pos: number) => void) {
+ for (const child of children) {
+ callback(child.node, child.offset);
+ }
+ },
+ childCount: children.length,
+ child(index: number) {
+ return children[index]!.node;
+ },
+ forEach(callback: (node: ProseMirrorNode, offset: number) => void) {
+ for (const child of children) {
+ callback(child.node, child.offset);
+ }
+ },
+ } as unknown as ProseMirrorNode;
+}
+
+type SearchFn = (pattern: string | RegExp, options?: Record) => unknown[];
+
+function makeEditor(docNode: ProseMirrorNode, search?: SearchFn): Editor {
+ return {
+ state: { doc: docNode },
+ commands: search ? { search } : {},
+ } as unknown as Editor;
+}
+
+/** Builds a doc with paragraph children at specified offsets. */
+function buildDoc(
+ ...entries: Array<{ typeName: string; attrs?: Record; nodeSize?: number; offset: number }>
+): ProseMirrorNode;
+function buildDoc(
+ textContent: string,
+ ...entries: Array<{ typeName: string; attrs?: Record; nodeSize?: number; offset: number }>
+): ProseMirrorNode;
+function buildDoc(...args: unknown[]): ProseMirrorNode {
+ let textContent = '';
+ let entries: Array<{ typeName: string; attrs?: Record; nodeSize?: number; offset: number }>;
+ if (typeof args[0] === 'string') {
+ textContent = args[0] as string;
+ entries = args.slice(1) as typeof entries;
+ } else {
+ entries = args as typeof entries;
+ }
+ const children = entries.map((e) => ({
+ node: makeNode(e.typeName, e.attrs ?? {}, e.nodeSize ?? 10),
+ offset: e.offset,
+ }));
+ const totalSize = entries.reduce((max, e) => Math.max(max, e.offset + (e.nodeSize ?? 10)), 0) + 2;
+ return makeNode('doc', {}, totalSize, children, textContent);
+}
+
+// ---------------------------------------------------------------------------
+// Block selector queries
+// ---------------------------------------------------------------------------
+
+describe('findAdapter — block selectors', () => {
+ it('returns all paragraphs when select.type is "paragraph"', () => {
+ const doc = buildDoc(
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, offset: 0 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p2' }, offset: 12 },
+ { typeName: 'image', attrs: { sdBlockId: 'img1' }, offset: 24 },
+ );
+ const editor = makeEditor(doc);
+ const query: Query = { select: { type: 'node', nodeType: 'paragraph' } };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(2);
+ expect(result.matches).toEqual([
+ { kind: 'block', nodeType: 'paragraph', nodeId: 'p1' },
+ { kind: 'block', nodeType: 'paragraph', nodeId: 'p2' },
+ ]);
+ expect(result.diagnostics).toBeUndefined();
+ });
+
+ it('returns headings for paragraphs with heading styleId', () => {
+ const doc = buildDoc(
+ { typeName: 'paragraph', attrs: { sdBlockId: 'h1', paragraphProperties: { styleId: 'Heading1' } }, offset: 0 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, offset: 12 },
+ );
+ const editor = makeEditor(doc);
+ const query: Query = { select: { type: 'node', nodeType: 'heading' } };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(1);
+ expect(result.matches[0]).toEqual({ kind: 'block', nodeType: 'heading', nodeId: 'h1' });
+ });
+
+ it('uses node selector with kind filter', () => {
+ const doc = buildDoc(
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, offset: 0 },
+ { typeName: 'table', attrs: { sdBlockId: 't1' }, offset: 12 },
+ );
+ const editor = makeEditor(doc);
+ const query: Query = { select: { type: 'node', kind: 'block' } };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(2);
+ });
+
+ it('uses node selector with nodeType filter', () => {
+ const doc = buildDoc(
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, offset: 0 },
+ { typeName: 'table', attrs: { sdBlockId: 't1' }, offset: 12 },
+ );
+ const editor = makeEditor(doc);
+ const query: Query = { select: { type: 'node', nodeType: 'table' } };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(1);
+ expect(result.matches[0].nodeId).toBe('t1');
+ });
+
+ it('emits diagnostic for includeUnknown', () => {
+ const doc = buildDoc(
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, offset: 0 },
+ { typeName: 'mysteryBlock', attrs: { sdBlockId: 'm1' }, offset: 12 },
+ );
+ const editor = makeEditor(doc);
+ const query: Query = { select: { type: 'node', nodeType: 'paragraph' }, includeUnknown: true };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.matches).toEqual([{ kind: 'block', nodeType: 'paragraph', nodeId: 'p1' }]);
+ expect(result.total).toBe(1);
+ expect(result.diagnostics).toBeDefined();
+ expect(result.diagnostics![0].message).toContain('Unknown block node type');
+ expect(result.diagnostics![0].message).not.toContain('position');
+ expect(result.diagnostics![0].hint).toContain('stable id "m1"');
+ });
+
+ it('emits actionable diagnostics for unknown inline nodes without raw positions', () => {
+ const doc = buildDoc(
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p-inline' }, nodeSize: 12, offset: 0 },
+ { typeName: 'commentReference', nodeSize: 1, offset: 3 },
+ );
+ const editor = makeEditor(doc);
+ const query: Query = { select: { type: 'node', nodeType: 'paragraph' }, includeUnknown: true };
+
+ const result = findAdapter(editor, query);
+
+ const diagnostic = result.diagnostics?.find((entry) => entry.message.includes('Unknown inline node type'));
+ expect(diagnostic).toBeDefined();
+ expect(diagnostic!.message).not.toContain('position');
+ expect(diagnostic!.address).toEqual({
+ kind: 'block',
+ nodeType: 'paragraph',
+ nodeId: 'p-inline',
+ });
+ expect(diagnostic!.hint).toContain('p-inline');
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Within scope
+// ---------------------------------------------------------------------------
+
+describe('findAdapter — within scope', () => {
+ it('limits block results to within a parent node', () => {
+ const doc = buildDoc(
+ { typeName: 'table', attrs: { sdBlockId: 'tbl1' }, nodeSize: 50, offset: 0 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p-inside' }, nodeSize: 10, offset: 5 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p-outside' }, nodeSize: 10, offset: 60 },
+ );
+ const editor = makeEditor(doc);
+ const query: Query = {
+ select: { type: 'node', nodeType: 'paragraph' },
+ within: { kind: 'block', nodeType: 'table', nodeId: 'tbl1' },
+ };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(1);
+ expect(result.matches[0].nodeId).toBe('p-inside');
+ });
+
+ it('returns empty when within target is not found', () => {
+ const doc = buildDoc({ typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, offset: 0 });
+ const editor = makeEditor(doc);
+ const query: Query = {
+ select: { type: 'node', nodeType: 'paragraph' },
+ within: { kind: 'block', nodeType: 'table', nodeId: 'no-such-table' },
+ };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.matches).toEqual([]);
+ expect(result.diagnostics).toBeDefined();
+ expect(result.diagnostics![0].message).toContain('was not found');
+ });
+
+ it('returns empty with diagnostic for inline within scope', () => {
+ const doc = buildDoc({ typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, offset: 0 });
+ const editor = makeEditor(doc);
+ const query: Query = {
+ select: { type: 'node', nodeType: 'paragraph' },
+ within: {
+ kind: 'inline',
+ nodeType: 'run',
+ anchor: { start: { blockId: 'p1', offset: 0 }, end: { blockId: 'p1', offset: 5 } },
+ },
+ };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.matches).toEqual([]);
+ expect(result.diagnostics![0].message).toContain('Inline');
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Pagination
+// ---------------------------------------------------------------------------
+
+describe('findAdapter — pagination', () => {
+ function buildThreeParagraphs() {
+ return buildDoc(
+ { typeName: 'paragraph', attrs: { sdBlockId: 'a' }, offset: 0 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'b' }, offset: 12 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'c' }, offset: 24 },
+ );
+ }
+
+ it('limits results with limit', () => {
+ const editor = makeEditor(buildThreeParagraphs());
+ const query: Query = { select: { type: 'node', nodeType: 'paragraph' }, limit: 2 };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(3);
+ expect(result.matches).toHaveLength(2);
+ expect(result.matches[0].nodeId).toBe('a');
+ expect(result.matches[1].nodeId).toBe('b');
+ });
+
+ it('skips results with offset', () => {
+ const editor = makeEditor(buildThreeParagraphs());
+ const query: Query = { select: { type: 'node', nodeType: 'paragraph' }, offset: 1 };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(3);
+ expect(result.matches).toHaveLength(2);
+ expect(result.matches[0].nodeId).toBe('b');
+ });
+
+ it('combines offset and limit', () => {
+ const editor = makeEditor(buildThreeParagraphs());
+ const query: Query = { select: { type: 'node', nodeType: 'paragraph' }, offset: 1, limit: 1 };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(3);
+ expect(result.matches).toHaveLength(1);
+ expect(result.matches[0].nodeId).toBe('b');
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Inline selectors
+// ---------------------------------------------------------------------------
+
+describe('findAdapter — inline selectors', () => {
+ it('returns run matches', () => {
+ const runText = makeNode('text', {}, 2, [], 'Hi');
+ const runNode = makeNode('run', { runProperties: { bold: true } }, 4, [{ node: runText, offset: 0 }]);
+ const paragraph = makeNode('paragraph', { sdBlockId: 'p-run' }, 6, [{ node: runNode, offset: 0 }]);
+ const doc = makeNode('doc', {}, 8, [{ node: paragraph, offset: 0 }]);
+ const editor = makeEditor(doc);
+
+ const result = findAdapter(editor, { select: { type: 'node', nodeType: 'run' } });
+
+ expect(result.total).toBe(1);
+ expect(result.matches[0]).toEqual({
+ kind: 'inline',
+ nodeType: 'run',
+ anchor: { start: { blockId: 'p-run', offset: 0 }, end: { blockId: 'p-run', offset: 2 } },
+ });
+ });
+
+ it('returns hyperlink matches from inline marks', () => {
+ const linkMark = {
+ type: { name: 'link' },
+ attrs: { href: 'https://example.com' },
+ } as unknown as ProseMirrorNode['marks'][number];
+ const textNode = makeNode('text', { __marks: [linkMark] }, 2, [], 'Hi');
+ const imageNode = makeNode('image', {}, 1, []);
+ const paragraph = makeNode('paragraph', { sdBlockId: 'p1' }, 5, [
+ { node: textNode, offset: 0 },
+ { node: imageNode, offset: 2 },
+ ]);
+ const doc = makeNode('doc', {}, 7, [{ node: paragraph, offset: 0 }]);
+ const editor = makeEditor(doc);
+
+ const result = findAdapter(editor, { select: { type: 'node', nodeType: 'hyperlink' } });
+
+ expect(result.total).toBe(1);
+ expect(result.matches[0]).toEqual({
+ kind: 'inline',
+ nodeType: 'hyperlink',
+ anchor: { start: { blockId: 'p1', offset: 0 }, end: { blockId: 'p1', offset: 2 } },
+ });
+ });
+
+ it('returns inline image matches', () => {
+ const textNode = makeNode('text', {}, 2, [], 'Hi');
+ const imageNode = makeNode('image', { src: 'x' }, 1, []);
+ const paragraph = makeNode('paragraph', { sdBlockId: 'p2' }, 5, [
+ { node: textNode, offset: 0 },
+ { node: imageNode, offset: 2 },
+ ]);
+ const doc = makeNode('doc', {}, 7, [{ node: paragraph, offset: 0 }]);
+ const editor = makeEditor(doc);
+
+ const result = findAdapter(editor, { select: { type: 'node', nodeType: 'image' } });
+
+ expect(result.total).toBe(1);
+ expect(result.matches[0]).toEqual({
+ kind: 'inline',
+ nodeType: 'image',
+ anchor: { start: { blockId: 'p2', offset: 2 }, end: { blockId: 'p2', offset: 3 } },
+ });
+ });
+
+ it('returns both block and inline sdts when kind is omitted', () => {
+ const inlineSdt = makeNode('structuredContent', { tag: 'inline-sdt' }, 1, []);
+ const paragraph = makeNode('paragraph', { sdBlockId: 'p-sdt' }, 5, [{ node: inlineSdt, offset: 0 }]);
+ const blockSdt = makeNode('structuredContentBlock', { sdBlockId: 'sdt-block' }, 4, []);
+ const doc = makeNode('doc', {}, 20, [
+ { node: paragraph, offset: 0 },
+ { node: blockSdt, offset: 10 },
+ ]);
+ const editor = makeEditor(doc);
+
+ const shorthand = findAdapter(editor, { select: { type: 'node', nodeType: 'sdt' } });
+ expect(shorthand.total).toBe(2);
+ expect(shorthand.matches).toEqual(
+ expect.arrayContaining([
+ { kind: 'block', nodeType: 'sdt', nodeId: 'sdt-block' },
+ {
+ kind: 'inline',
+ nodeType: 'sdt',
+ anchor: { start: { blockId: 'p-sdt', offset: 0 }, end: { blockId: 'p-sdt', offset: 1 } },
+ },
+ ]),
+ );
+
+ const nodeSelector = findAdapter(editor, { select: { type: 'node', nodeType: 'sdt' } });
+ expect(nodeSelector.total).toBe(2);
+ expect(nodeSelector.matches).toEqual(
+ expect.arrayContaining([
+ { kind: 'block', nodeType: 'sdt', nodeId: 'sdt-block' },
+ {
+ kind: 'inline',
+ nodeType: 'sdt',
+ anchor: { start: { blockId: 'p-sdt', offset: 0 }, end: { blockId: 'p-sdt', offset: 1 } },
+ },
+ ]),
+ );
+ });
+
+ it('respects explicit kind for sdt node selector', () => {
+ const inlineSdt = makeNode('structuredContent', { tag: 'inline-sdt' }, 1, []);
+ const paragraph = makeNode('paragraph', { sdBlockId: 'p-sdt' }, 5, [{ node: inlineSdt, offset: 0 }]);
+ const blockSdt = makeNode('structuredContentBlock', { sdBlockId: 'sdt-block' }, 4, []);
+ const doc = makeNode('doc', {}, 20, [
+ { node: paragraph, offset: 0 },
+ { node: blockSdt, offset: 10 },
+ ]);
+ const editor = makeEditor(doc);
+
+ const blockResult = findAdapter(editor, { select: { type: 'node', kind: 'block', nodeType: 'sdt' } });
+ expect(blockResult.total).toBe(1);
+ expect(blockResult.matches[0]).toEqual({ kind: 'block', nodeType: 'sdt', nodeId: 'sdt-block' });
+
+ const inlineResult = findAdapter(editor, { select: { type: 'node', kind: 'inline', nodeType: 'sdt' } });
+ expect(inlineResult.total).toBe(1);
+ expect(inlineResult.matches[0]).toEqual({
+ kind: 'inline',
+ nodeType: 'sdt',
+ anchor: { start: { blockId: 'p-sdt', offset: 0 }, end: { blockId: 'p-sdt', offset: 1 } },
+ });
+ });
+
+ it('returns mapped nodes when includeNodes is true', () => {
+ const runText = makeNode('text', {}, 2, [], 'Hi');
+ const runNode = makeNode('run', { runProperties: { bold: true } }, 4, [{ node: runText, offset: 0 }]);
+ const paragraph = makeNode('paragraph', { sdBlockId: 'p-run' }, 6, [{ node: runNode, offset: 0 }]);
+ const doc = makeNode('doc', {}, 8, [{ node: paragraph, offset: 0 }]);
+ const editor = makeEditor(doc);
+
+ const result = findAdapter(editor, { select: { type: 'node', nodeType: 'run' }, includeNodes: true });
+
+ expect(result.total).toBe(1);
+ expect(result.nodes).toHaveLength(1);
+ expect(result.nodes![0]).toMatchObject({
+ nodeType: 'run',
+ kind: 'inline',
+ properties: { bold: true },
+ });
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Text selector queries
+// ---------------------------------------------------------------------------
+
+describe('findAdapter — text selectors', () => {
+ // Pad textContent to 102 chars so textBetween returns something for any position in the two paragraphs.
+ const defaultText = 'a'.repeat(102);
+
+ function makeSearchableEditor(
+ searchResults: Array<{ from: number; to: number; text: string }>,
+ textContent = defaultText,
+ ) {
+ const doc = buildDoc(
+ textContent,
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, nodeSize: 50, offset: 0 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p2' }, nodeSize: 50, offset: 52 },
+ );
+ const search: SearchFn = () => searchResults;
+ return makeEditor(doc, search);
+ }
+
+ it('returns text matches with context', () => {
+ // Place "hello" at positions 5-10 in the text content
+ const text = ' hello' + 'a'.repeat(92);
+ const editor = makeSearchableEditor([{ from: 5, to: 10, text: 'hello' }], text);
+ const query: Query = { select: { type: 'text', pattern: 'hello' } };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(1);
+ expect(result.matches[0]).toEqual({ kind: 'block', nodeType: 'paragraph', nodeId: 'p1' });
+ expect(result.context).toBeDefined();
+ expect(result.context![0].snippet).toContain('hello');
+ expect(result.context![0].textRanges).toEqual([{ kind: 'text', blockId: 'p1', range: { start: 4, end: 9 } }]);
+ });
+
+ it('maps matches to their containing blocks', () => {
+ const editor = makeSearchableEditor([
+ { from: 5, to: 10, text: 'first' },
+ { from: 60, to: 65, text: 'second' },
+ ]);
+ const query: Query = { select: { type: 'text', pattern: 'test' } };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(2);
+ expect(result.matches[0].nodeId).toBe('p1');
+ expect(result.matches[1].nodeId).toBe('p2');
+ });
+
+ it('returns empty with diagnostic for empty pattern', () => {
+ const editor = makeSearchableEditor([]);
+ const query: Query = { select: { type: 'text', pattern: '' } };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.matches).toEqual([]);
+ expect(result.diagnostics).toBeDefined();
+ expect(result.diagnostics![0].message).toContain('non-empty');
+ });
+
+ it('returns empty with diagnostic for invalid regex', () => {
+ const editor = makeSearchableEditor([]);
+ const query: Query = { select: { type: 'text', pattern: '[invalid', mode: 'regex' } };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.matches).toEqual([]);
+ expect(result.diagnostics).toBeDefined();
+ expect(result.diagnostics![0].message).toContain('Invalid text query regex');
+ });
+
+ it('passes regex pattern to search for regex mode', () => {
+ let capturedPattern: string | RegExp | undefined;
+ const doc = buildDoc('a'.repeat(52), {
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'p1' },
+ nodeSize: 50,
+ offset: 0,
+ });
+ const search: SearchFn = (pattern) => {
+ capturedPattern = pattern;
+ return [{ from: 5, to: 10, text: 'hello' }];
+ };
+ const editor = makeEditor(doc, search);
+ const query: Query = { select: { type: 'text', pattern: 'hel+o', mode: 'regex' } };
+
+ findAdapter(editor, query);
+
+ expect(capturedPattern).toBeInstanceOf(RegExp);
+ expect((capturedPattern as RegExp).source).toBe('hel+o');
+ expect((capturedPattern as RegExp).flags).toContain('i');
+ });
+
+ it('passes case-sensitive regex for regex mode', () => {
+ let capturedPattern: string | RegExp | undefined;
+ const doc = buildDoc({ typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, nodeSize: 50, offset: 0 });
+ const search: SearchFn = (pattern) => {
+ capturedPattern = pattern;
+ return [];
+ };
+ const editor = makeEditor(doc, search);
+ const query: Query = { select: { type: 'text', pattern: 'Hello', mode: 'regex', caseSensitive: true } };
+
+ findAdapter(editor, query);
+
+ expect(capturedPattern).toBeInstanceOf(RegExp);
+ expect((capturedPattern as RegExp).flags).not.toContain('i');
+ });
+
+ it('passes escaped RegExp for default contains mode', () => {
+ let capturedPattern: string | RegExp | undefined;
+ const doc = buildDoc({ typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, nodeSize: 50, offset: 0 });
+ const search: SearchFn = (pattern) => {
+ capturedPattern = pattern;
+ return [];
+ };
+ const editor = makeEditor(doc, search);
+ const query: Query = { select: { type: 'text', pattern: 'hello' } };
+
+ findAdapter(editor, query);
+
+ expect(capturedPattern).toBeInstanceOf(RegExp);
+ expect((capturedPattern as RegExp).source).toBe('hello');
+ expect((capturedPattern as RegExp).flags).toContain('i');
+ });
+
+ it('treats slash-delimited contains patterns as literal text', () => {
+ const text = 'foo /foo/ foo';
+ const doc = buildDoc(text, {
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'p1' },
+ nodeSize: text.length + 4,
+ offset: 0,
+ });
+ const search: SearchFn = (pattern, options) => {
+ const caseSensitive = (options as { caseSensitive?: boolean })?.caseSensitive ?? false;
+ let effectivePattern: RegExp;
+
+ if (pattern instanceof RegExp) {
+ const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`;
+ effectivePattern = new RegExp(pattern.source, flags);
+ } else if (typeof pattern === 'string' && /^\/(.+)\/([gimsuy]*)$/.test(pattern)) {
+ const [, body, flags] = pattern.match(/^\/(.+)\/([gimsuy]*)$/) as RegExpMatchArray;
+ effectivePattern = new RegExp(body, flags.includes('g') ? flags : `${flags}g`);
+ } else {
+ const escaped = String(pattern).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ effectivePattern = new RegExp(escaped, caseSensitive ? 'g' : 'gi');
+ }
+
+ return Array.from(text.matchAll(effectivePattern)).map((match) => {
+ const from = match.index ?? 0;
+ return {
+ from,
+ to: from + match[0].length,
+ text: match[0],
+ };
+ });
+ };
+ const editor = makeEditor(doc, search);
+ const query: Query = { select: { type: 'text', pattern: '/foo/' } };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(1);
+ expect(result.context).toBeDefined();
+ const context = result.context![0];
+ expect(context.snippet.slice(context.highlightRange.start, context.highlightRange.end)).toBe('/foo/');
+ });
+
+ it('passes case-sensitive escaped RegExp for contains mode', () => {
+ let capturedPattern: string | RegExp | undefined;
+ const doc = buildDoc({ typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, nodeSize: 50, offset: 0 });
+ const search: SearchFn = (pattern) => {
+ capturedPattern = pattern;
+ return [];
+ };
+ const editor = makeEditor(doc, search);
+ const query: Query = { select: { type: 'text', pattern: 'Hello', caseSensitive: true } };
+
+ findAdapter(editor, query);
+
+ expect(capturedPattern).toBeInstanceOf(RegExp);
+ expect((capturedPattern as RegExp).source).toBe('Hello');
+ expect((capturedPattern as RegExp).flags).not.toContain('i');
+ });
+
+ it('forwards caseSensitive option to search command for contains mode', () => {
+ let capturedOptions: Record | undefined;
+ const doc = buildDoc({ typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, nodeSize: 50, offset: 0 });
+ const search: SearchFn = (_pattern, options) => {
+ capturedOptions = options as Record;
+ return [];
+ };
+ const editor = makeEditor(doc, search);
+ const query: Query = { select: { type: 'text', pattern: 'Hello', caseSensitive: true } };
+
+ findAdapter(editor, query);
+
+ expect(capturedOptions).toBeDefined();
+ expect(capturedOptions!.caseSensitive).toBe(true);
+ });
+
+ it('bounds maxMatches when pagination requests a small page', () => {
+ let capturedOptions: Record | undefined;
+ const doc = buildDoc({ typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, nodeSize: 50, offset: 0 });
+ const search: SearchFn = (_pattern, options) => {
+ capturedOptions = options as Record;
+ return [];
+ };
+ const editor = makeEditor(doc, search);
+ const query: Query = { select: { type: 'text', pattern: 'a' }, offset: 0, limit: 2 };
+
+ findAdapter(editor, query);
+
+ expect(capturedOptions).toBeDefined();
+ expect(typeof capturedOptions!.maxMatches).toBe('number');
+ expect(capturedOptions!.maxMatches as number).toBeLessThanOrEqual(1000);
+ });
+
+ it('throws when editor has no search command', () => {
+ const doc = buildDoc({ typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, nodeSize: 50, offset: 0 });
+ const editor = makeEditor(doc); // no search command
+ const query: Query = { select: { type: 'text', pattern: 'hello' } };
+
+ expect(() => findAdapter(editor, query)).toThrow('command is not available');
+ });
+
+ it('paginates text results and contexts together', () => {
+ const editor = makeSearchableEditor([
+ { from: 5, to: 10, text: 'aaa' },
+ { from: 15, to: 20, text: 'bbb' },
+ { from: 25, to: 30, text: 'ccc' },
+ ]);
+ const query: Query = { select: { type: 'text', pattern: 'test' }, offset: 1, limit: 1 };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(3);
+ expect(result.matches).toHaveLength(1);
+ expect(result.context).toHaveLength(1);
+ // The second match (from 15-20) should be the one returned
+ expect(result.context![0].snippet).toBeDefined();
+ });
+
+ it('reports true total for paginated text queries (not capped by page window)', () => {
+ // Build a search that respects maxMatches to expose the capping bug
+ const allMatches = [
+ { from: 5, to: 8, text: 'a' },
+ { from: 15, to: 18, text: 'b' },
+ { from: 25, to: 28, text: 'c' },
+ { from: 35, to: 38, text: 'd' },
+ { from: 45, to: 48, text: 'e' },
+ ];
+ const doc = buildDoc(
+ 'a'.repeat(102),
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, nodeSize: 50, offset: 0 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p2' }, nodeSize: 50, offset: 52 },
+ );
+ const search: SearchFn = (_pattern, opts) => {
+ const max = (opts as { maxMatches?: number })?.maxMatches ?? Infinity;
+ return allMatches.slice(0, max);
+ };
+ const editor = makeEditor(doc, search);
+ const query: Query = { select: { type: 'text', pattern: 'test' }, offset: 0, limit: 2 };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.matches).toHaveLength(2);
+ expect(result.total).toBe(5); // must be 5, not 2
+ });
+
+ it('filters text matches by within scope', () => {
+ const doc = buildDoc(
+ 'a'.repeat(70),
+ { typeName: 'table', attrs: { sdBlockId: 'tbl1' }, nodeSize: 40, offset: 0 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p-in' }, nodeSize: 10, offset: 5 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p-out' }, nodeSize: 10, offset: 50 },
+ );
+ const search: SearchFn = () => [
+ { from: 8, to: 13, text: 'inside' },
+ { from: 55, to: 60, text: 'outside' },
+ ];
+ const editor = makeEditor(doc, search);
+ const query: Query = {
+ select: { type: 'text', pattern: 'test' },
+ within: { kind: 'block', nodeType: 'table', nodeId: 'tbl1' },
+ };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(1);
+ expect(result.matches[0].nodeId).toBe('p-in');
+ });
+
+ it('skips matches whose position does not resolve to a block', () => {
+ const doc = buildDoc('a'.repeat(22), {
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'p1' },
+ nodeSize: 10,
+ offset: 10,
+ });
+ // Match at pos 0 is before any block candidate (paragraph starts at 10)
+ const search: SearchFn = () => [
+ { from: 0, to: 5, text: 'ghost' },
+ { from: 12, to: 17, text: 'real' },
+ ];
+ const editor = makeEditor(doc, search);
+ const query: Query = { select: { type: 'text', pattern: 'test' } };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.total).toBe(1);
+ expect(result.matches[0].nodeId).toBe('p1');
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Context / snippet building
+// ---------------------------------------------------------------------------
+
+describe('findAdapter — snippet context', () => {
+ it('includes highlight range in context', () => {
+ // Text: 40 chars of padding, then "hello" at positions 40-45, then more padding
+ const text = 'a'.repeat(40) + 'hello' + 'a'.repeat(55);
+ const doc = buildDoc(text, { typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, nodeSize: 100, offset: 0 });
+ const search: SearchFn = () => [{ from: 40, to: 45, text: 'hello' }];
+ const editor = makeEditor(doc, search);
+ const query: Query = { select: { type: 'text', pattern: 'hello' } };
+
+ const result = findAdapter(editor, query);
+
+ expect(result.context).toBeDefined();
+ const ctx = result.context![0];
+ // The snippet should contain the match, and the highlight range should point to it
+ expect(ctx.snippet.slice(ctx.highlightRange.start, ctx.highlightRange.end)).toBe('hello');
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/find-adapter.ts b/packages/super-editor/src/document-api-adapters/find-adapter.ts
new file mode 100644
index 0000000000..ecf3a69dc2
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/find-adapter.ts
@@ -0,0 +1,50 @@
+import type { Editor } from '../core/Editor.js';
+import type { Query, QueryResult, UnknownNodeDiagnostic } from '@superdoc/document-api';
+import { dedupeDiagnostics } from './helpers/adapter-utils.js';
+import { getBlockIndex } from './helpers/index-cache.js';
+import { resolveIncludedNodes } from './helpers/node-info-resolver.js';
+import { collectUnknownNodeDiagnostics, isInlineQuery, shouldQueryBothKinds } from './find/common.js';
+import { executeBlockSelector } from './find/block-strategy.js';
+import { executeDualKindSelector } from './find/dual-kind-strategy.js';
+import { executeInlineSelector } from './find/inline-strategy.js';
+import { executeTextSelector } from './find/text-strategy.js';
+
+/**
+ * Executes a document query against the editor's current state.
+ *
+ * Supports block-node selectors (by type) and text selectors (literal/regex)
+ * with optional `within` scoping and offset/limit pagination.
+ *
+ * @param editor - The editor instance to query.
+ * @param query - The query specifying what to find.
+ * @returns Query result with matches, total count, and any diagnostics.
+ * @throws {Error} If the editor's search command is unavailable (text queries only).
+ */
+export function findAdapter(editor: Editor, query: Query): QueryResult {
+ const diagnostics: UnknownNodeDiagnostic[] = [];
+ const index = getBlockIndex(editor);
+ if (query.includeUnknown) {
+ collectUnknownNodeDiagnostics(editor, index, diagnostics);
+ }
+
+ const isInlineSelector = query.select.type !== 'text' && isInlineQuery(query.select);
+ const isDualKindSelector = query.select.type !== 'text' && shouldQueryBothKinds(query.select);
+
+ const result =
+ query.select.type === 'text'
+ ? executeTextSelector(editor, index, query, diagnostics)
+ : isDualKindSelector
+ ? executeDualKindSelector(editor, index, query, diagnostics)
+ : isInlineSelector
+ ? executeInlineSelector(editor, index, query, diagnostics)
+ : executeBlockSelector(index, query, diagnostics);
+
+ const uniqueDiagnostics = dedupeDiagnostics(diagnostics);
+ const includedNodes = query.includeNodes ? resolveIncludedNodes(editor, index, result.matches) : undefined;
+
+ return {
+ ...result,
+ nodes: includedNodes?.length ? includedNodes : undefined,
+ diagnostics: uniqueDiagnostics.length ? uniqueDiagnostics : undefined,
+ };
+}
diff --git a/packages/super-editor/src/document-api-adapters/find/block-strategy.ts b/packages/super-editor/src/document-api-adapters/find/block-strategy.ts
new file mode 100644
index 0000000000..607af2e969
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/find/block-strategy.ts
@@ -0,0 +1,50 @@
+import type { Query, QueryResult, UnknownNodeDiagnostic } from '@superdoc/document-api';
+import { addDiagnostic, paginate, resolveWithinScope, scopeByRange } from '../helpers/adapter-utils.js';
+import type { BlockCandidate, BlockIndex } from '../helpers/node-address-resolver.js';
+
+/**
+ * Executes a block-level node selector against the block index.
+ *
+ * @param index - Pre-built block index to search.
+ * @param query - The query with a node selector and optional pagination/scope.
+ * @param diagnostics - Mutable array to collect diagnostics into.
+ * @returns Paginated query result containing block-kind matches.
+ */
+export function executeBlockSelector(
+ index: BlockIndex,
+ query: Query,
+ diagnostics: UnknownNodeDiagnostic[],
+): QueryResult {
+ const scope = resolveWithinScope(index, query, diagnostics);
+ if (!scope.ok) return { matches: [], total: 0 };
+
+ const scoped = scopeByRange(index.candidates, scope.range);
+ const select = query.select;
+ let filtered: BlockCandidate[] = [];
+
+ if (select.type === 'node') {
+ if (select.kind && select.kind !== 'block') {
+ addDiagnostic(diagnostics, 'Only block nodes are supported by the current adapter.');
+ } else {
+ filtered = scoped.filter((candidate) => {
+ if (select.nodeType) {
+ if (candidate.nodeType !== select.nodeType) return false;
+ }
+ return true;
+ });
+ }
+ }
+ // text selectors are handled by text-strategy, not block
+
+ const addresses = filtered.map((candidate) => ({
+ kind: 'block' as const,
+ nodeType: candidate.nodeType,
+ nodeId: candidate.nodeId,
+ }));
+ const paged = paginate(addresses, query.offset, query.limit);
+
+ return {
+ matches: paged.items,
+ total: paged.total,
+ };
+}
diff --git a/packages/super-editor/src/document-api-adapters/find/common.ts b/packages/super-editor/src/document-api-adapters/find/common.ts
new file mode 100644
index 0000000000..75dd6a3a44
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/find/common.ts
@@ -0,0 +1,219 @@
+import type { Editor } from '../../core/Editor.js';
+import type {
+ MatchContext,
+ NodeAddress,
+ NodeType,
+ Query,
+ TextAddress,
+ UnknownNodeDiagnostic,
+} from '@superdoc/document-api';
+import { toId } from '../helpers/value-utils.js';
+import { getInlineIndex } from '../helpers/index-cache.js';
+import {
+ findBlockById,
+ toBlockAddress,
+ type BlockCandidate,
+ type BlockIndex,
+} from '../helpers/node-address-resolver.js';
+import { findInlineByAnchor, isInlineQueryType } from '../helpers/inline-address-resolver.js';
+import { findCandidateByPos } from '../helpers/adapter-utils.js';
+
+/** Characters of document text to include before and after a match in snippet context. */
+const SNIPPET_PADDING = 30;
+const DUAL_KIND_TYPES = new Set(['sdt', 'image']);
+
+const KNOWN_BLOCK_PM_NODE_TYPES = new Set([
+ 'paragraph',
+ 'table',
+ 'tableRow',
+ 'tableCell',
+ 'tableHeader',
+ 'structuredContentBlock',
+ 'sdt',
+ 'image',
+]);
+
+const KNOWN_INLINE_PM_NODE_TYPES = new Set([
+ 'text',
+ 'run',
+ 'structuredContent',
+ 'image',
+ 'tab',
+ 'lineBreak',
+ 'hardBreak',
+ 'hard_break',
+ 'footnoteReference',
+ 'bookmarkStart',
+ 'bookmarkEnd',
+ 'commentRangeStart',
+ 'commentRangeEnd',
+]);
+
+function resolveUnknownBlockId(attrs: Record | undefined): string | undefined {
+ if (!attrs) return undefined;
+ return toId(attrs.sdBlockId) ?? toId(attrs.paraId) ?? toId(attrs.blockId) ?? toId(attrs.id) ?? toId(attrs.uuid);
+}
+
+function isDualKindType(nodeType: NodeType | undefined): boolean {
+ return Boolean(nodeType && DUAL_KIND_TYPES.has(nodeType));
+}
+
+function getAddressStartPos(editor: Editor, index: BlockIndex, address: NodeAddress): number {
+ if (address.kind === 'block') {
+ const block = findBlockById(index, address);
+ return block?.pos ?? Number.MAX_SAFE_INTEGER;
+ }
+
+ const inlineIndex = getInlineIndex(editor);
+ const inline = findInlineByAnchor(inlineIndex, address);
+ return inline?.pos ?? Number.MAX_SAFE_INTEGER;
+}
+
+/**
+ * Builds a snippet context for a text match, including surrounding text and highlight offsets.
+ *
+ * @param editor - The editor instance.
+ * @param address - The address of the block containing the match.
+ * @param matchFrom - Absolute document position of the match start.
+ * @param matchTo - Absolute document position of the match end.
+ * @param textRanges - Optional block-relative text ranges for the match.
+ * @returns A {@link MatchContext} with snippet, highlight range, and text ranges.
+ */
+export function buildTextContext(
+ editor: Editor,
+ address: NodeAddress,
+ matchFrom: number,
+ matchTo: number,
+ textRanges?: TextAddress[],
+): MatchContext {
+ const docSize = editor.state.doc.content.size;
+ const snippetFrom = Math.max(0, matchFrom - SNIPPET_PADDING);
+ const snippetTo = Math.min(docSize, matchTo + SNIPPET_PADDING);
+ const rawSnippet = editor.state.doc.textBetween(snippetFrom, snippetTo, ' ');
+ const snippet = rawSnippet.replace(/ {2,}/g, ' ');
+
+ const rawPrefix = editor.state.doc.textBetween(snippetFrom, matchFrom, ' ');
+ const rawMatch = editor.state.doc.textBetween(matchFrom, matchTo, ' ');
+ const prefix = rawPrefix.replace(/ {2,}/g, ' ');
+ const matchNormalized = rawMatch.replace(/ {2,}/g, ' ');
+
+ return {
+ address,
+ snippet,
+ highlightRange: {
+ start: prefix.length,
+ end: prefix.length + matchNormalized.length,
+ },
+ textRanges: textRanges?.length ? textRanges : undefined,
+ };
+}
+
+/**
+ * Converts an absolute document range to a block-relative {@link TextAddress}.
+ *
+ * @param editor - The editor instance.
+ * @param block - The block candidate containing the range.
+ * @param range - Absolute document positions.
+ * @returns A text address, or `undefined` if the range falls outside the block.
+ */
+export function toTextAddress(
+ editor: Editor,
+ block: BlockCandidate,
+ range: { from: number; to: number },
+): TextAddress | undefined {
+ const blockStart = block.pos + 1;
+ const blockEnd = block.end - 1;
+ if (range.from < blockStart || range.to > blockEnd) return undefined;
+
+ const start = editor.state.doc.textBetween(blockStart, range.from, '\n', '\ufffc').length;
+ const end = editor.state.doc.textBetween(blockStart, range.to, '\n', '\ufffc').length;
+
+ return {
+ kind: 'text',
+ blockId: block.nodeId,
+ range: { start, end },
+ };
+}
+
+/**
+ * Returns `true` if the selector targets a node type that exists as both block and inline
+ * and no explicit `kind` is specified, requiring a dual-kind query.
+ *
+ * @param select - The query selector.
+ */
+export function shouldQueryBothKinds(select: Query['select']): boolean {
+ if (select.type === 'node') {
+ return !select.kind && isDualKindType(select.nodeType);
+ }
+ return false;
+}
+
+/**
+ * Sorts node addresses by their absolute document position (ascending).
+ * Block addresses are ordered before inline addresses at the same position.
+ *
+ * @param editor - The editor instance.
+ * @param index - Pre-built block index for position lookup.
+ * @param addresses - The addresses to sort.
+ * @returns A new sorted array.
+ */
+export function sortAddressesByPosition(editor: Editor, index: BlockIndex, addresses: NodeAddress[]): NodeAddress[] {
+ return [...addresses].sort((a, b) => {
+ const aPos = getAddressStartPos(editor, index, a);
+ const bPos = getAddressStartPos(editor, index, b);
+ if (aPos !== bPos) return aPos - bPos;
+ if (a.kind === b.kind) return 0;
+ return a.kind === 'block' ? -1 : 1;
+ });
+}
+
+/**
+ * Walks the document and pushes diagnostics for block/inline nodes that are
+ * not part of the stable Document API match set.
+ *
+ * @param editor - The editor instance.
+ * @param index - Pre-built block index (used to resolve containing blocks for inline nodes).
+ * @param diagnostics - Mutable array to push diagnostics into.
+ */
+export function collectUnknownNodeDiagnostics(
+ editor: Editor,
+ index: BlockIndex,
+ diagnostics: UnknownNodeDiagnostic[],
+): void {
+ editor.state.doc.descendants((node, pos) => {
+ if (node.isBlock && !KNOWN_BLOCK_PM_NODE_TYPES.has(node.type.name)) {
+ const blockId = resolveUnknownBlockId((node.attrs ?? {}) as Record);
+ diagnostics.push({
+ message: `Unknown block node type "${node.type.name}" is not part of the stable Document API match set.`,
+ hint: blockId
+ ? `Skipped unknown block with stable id "${blockId}".`
+ : 'Skipped unknown block with no stable id available.',
+ });
+ return;
+ }
+
+ if (node.isInline && !KNOWN_INLINE_PM_NODE_TYPES.has(node.type.name)) {
+ const container = findCandidateByPos(index.candidates, pos);
+ diagnostics.push({
+ message: `Unknown inline node type "${node.type.name}" is not part of the stable Document API match set.`,
+ address: container ? toBlockAddress(container) : undefined,
+ hint: container
+ ? `Skipped unknown inline node inside block "${container.nodeType}" with id "${container.nodeId}".`
+ : 'Skipped unknown inline node outside resolvable block scope.',
+ });
+ }
+ });
+}
+
+/**
+ * Returns `true` if the selector exclusively targets inline nodes.
+ *
+ * @param select - The query selector.
+ */
+export function isInlineQuery(select: Query['select']): boolean {
+ if (select.type === 'node') {
+ if (select.kind) return select.kind === 'inline';
+ return Boolean(select.nodeType && isInlineQueryType(select.nodeType));
+ }
+ return false;
+}
diff --git a/packages/super-editor/src/document-api-adapters/find/dual-kind-strategy.ts b/packages/super-editor/src/document-api-adapters/find/dual-kind-strategy.ts
new file mode 100644
index 0000000000..50e74fd56e
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/find/dual-kind-strategy.ts
@@ -0,0 +1,42 @@
+import type { Editor } from '../../core/Editor.js';
+import type { Query, QueryResult, UnknownNodeDiagnostic } from '@superdoc/document-api';
+import { paginate } from '../helpers/adapter-utils.js';
+import type { BlockIndex } from '../helpers/node-address-resolver.js';
+import { sortAddressesByPosition } from './common.js';
+import { executeBlockSelector } from './block-strategy.js';
+import { executeInlineSelector } from './inline-strategy.js';
+
+/**
+ * Executes a selector for node types that exist as both block and inline
+ * (e.g. `sdt`, `image`). Merges and sorts results from both strategies.
+ *
+ * @param editor - The editor instance.
+ * @param index - Pre-built block index.
+ * @param query - The query to execute.
+ * @param diagnostics - Mutable array to collect diagnostics into.
+ * @returns Paginated query result with merged block and inline matches.
+ */
+export function executeDualKindSelector(
+ editor: Editor,
+ index: BlockIndex,
+ query: Query,
+ diagnostics: UnknownNodeDiagnostic[],
+): QueryResult {
+ const queryWithoutPagination: Query = {
+ ...query,
+ offset: undefined,
+ limit: undefined,
+ };
+
+ const blockResult = executeBlockSelector(index, queryWithoutPagination, diagnostics);
+ const inlineResult = executeInlineSelector(editor, index, queryWithoutPagination, diagnostics);
+
+ const mergedMatches = [...blockResult.matches, ...inlineResult.matches];
+ const sortedMatches = sortAddressesByPosition(editor, index, mergedMatches);
+ const paged = paginate(sortedMatches, query.offset, query.limit);
+
+ return {
+ matches: paged.items,
+ total: paged.total,
+ };
+}
diff --git a/packages/super-editor/src/document-api-adapters/find/inline-strategy.ts b/packages/super-editor/src/document-api-adapters/find/inline-strategy.ts
new file mode 100644
index 0000000000..e6ecadc8bb
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/find/inline-strategy.ts
@@ -0,0 +1,67 @@
+import type { Editor } from '../../core/Editor.js';
+import type { InlineNodeType, NodeAddress, Query, QueryResult, UnknownNodeDiagnostic } from '@superdoc/document-api';
+import { getInlineIndex } from '../helpers/index-cache.js';
+import { findInlineByType, isInlineQueryType, type InlineCandidate } from '../helpers/inline-address-resolver.js';
+import { addDiagnostic, paginate, resolveWithinScope, scopeByRange } from '../helpers/adapter-utils.js';
+import type { BlockIndex } from '../helpers/node-address-resolver.js';
+
+function toInlineAddress(candidate: InlineCandidate, nodeTypeOverride?: InlineNodeType): NodeAddress {
+ return {
+ kind: 'inline',
+ nodeType: nodeTypeOverride ?? candidate.nodeType,
+ anchor: candidate.anchor,
+ };
+}
+
+/**
+ * Executes an inline-level node selector against the inline index.
+ *
+ * @param editor - The editor instance.
+ * @param index - Pre-built block index (used for within-scope resolution).
+ * @param query - The query with an inline node selector.
+ * @param diagnostics - Mutable array to collect diagnostics into.
+ * @returns Paginated query result containing inline-kind matches.
+ */
+export function executeInlineSelector(
+ editor: Editor,
+ index: BlockIndex,
+ query: Query,
+ diagnostics: UnknownNodeDiagnostic[],
+): QueryResult {
+ const scope = resolveWithinScope(index, query, diagnostics);
+ if (!scope.ok) return { matches: [], total: 0 };
+
+ const inlineIndex = getInlineIndex(editor);
+ const select = query.select;
+ let requestedType: InlineNodeType | undefined;
+ let addressType: InlineNodeType | undefined;
+
+ if (select.type === 'node') {
+ if (select.kind && select.kind !== 'inline') {
+ addDiagnostic(diagnostics, 'Only inline nodes are supported by the current inline adapter.');
+ return { matches: [], total: 0 };
+ }
+ if (select.nodeType) {
+ if (!isInlineQueryType(select.nodeType)) {
+ addDiagnostic(diagnostics, `Node type "${select.nodeType}" is not an inline type.`);
+ return { matches: [], total: 0 };
+ }
+ requestedType = select.nodeType;
+ addressType = select.nodeType;
+ }
+ } else {
+ // text selectors are handled by text-strategy, not inline
+ return { matches: [], total: 0 };
+ }
+
+ let candidates = requestedType ? findInlineByType(inlineIndex, requestedType) : inlineIndex.candidates;
+ candidates = scopeByRange(candidates, scope.range);
+
+ const addresses = candidates.map((candidate) => toInlineAddress(candidate, addressType));
+ const paged = paginate(addresses, query.offset, query.limit);
+
+ return {
+ matches: paged.items,
+ total: paged.total,
+ };
+}
diff --git a/packages/super-editor/src/document-api-adapters/find/text-strategy.ts b/packages/super-editor/src/document-api-adapters/find/text-strategy.ts
new file mode 100644
index 0000000000..f6af42ac0e
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/find/text-strategy.ts
@@ -0,0 +1,146 @@
+import type { Editor } from '../../core/Editor.js';
+import type {
+ MatchContext,
+ NodeAddress,
+ Query,
+ QueryResult,
+ TextAddress,
+ TextSelector,
+ UnknownNodeDiagnostic,
+} from '@superdoc/document-api';
+import {
+ findBlockByPos,
+ isTextBlockCandidate,
+ toBlockAddress,
+ type BlockCandidate,
+ type BlockIndex,
+} from '../helpers/node-address-resolver.js';
+import { addDiagnostic, findCandidateByPos, paginate, resolveWithinScope } from '../helpers/adapter-utils.js';
+import { buildTextContext, toTextAddress } from './common.js';
+import { DocumentApiAdapterError } from '../errors.js';
+import { requireEditorCommand } from '../helpers/mutation-helpers.js';
+
+/** Shape returned by `editor.commands.search`. */
+type SearchMatch = {
+ from: number;
+ to: number;
+ text: string;
+ ranges?: Array<{ from: number; to: number }>;
+};
+
+function compileRegex(selector: TextSelector, diagnostics: UnknownNodeDiagnostic[]): RegExp | null {
+ const flags = selector.caseSensitive ? 'g' : 'gi';
+ try {
+ return new RegExp(selector.pattern, flags);
+ } catch (error) {
+ const reason = error instanceof Error ? error.message : String(error);
+ addDiagnostic(diagnostics, `Invalid text query regex: ${reason}`);
+ return null;
+ }
+}
+
+function buildSearchPattern(selector: TextSelector, diagnostics: UnknownNodeDiagnostic[]): RegExp | null {
+ const mode = selector.mode ?? 'contains';
+ if (mode === 'regex') {
+ return compileRegex(selector, diagnostics);
+ }
+ // Compile as an escaped RegExp to guarantee literal matching. Passing a raw
+ // string can be reinterpreted by the search command (e.g. slash-delimited
+ // strings like "/foo/" are parsed as regex syntax by some implementations).
+ const escaped = selector.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const flags = selector.caseSensitive ? 'g' : 'gi';
+ return new RegExp(escaped, flags);
+}
+
+/**
+ * Executes a text-based search query using the editor's search command.
+ *
+ * @param editor - The editor instance (must expose `commands.search`).
+ * @param index - Pre-built block index for position resolution.
+ * @param query - The query with a text selector.
+ * @param diagnostics - Mutable array to collect diagnostics into.
+ * @returns Paginated query result with block matches and snippet context.
+ * @throws {DocumentApiAdapterError} If the editor's search command is unavailable.
+ */
+export function executeTextSelector(
+ editor: Editor,
+ index: BlockIndex,
+ query: Query,
+ diagnostics: UnknownNodeDiagnostic[],
+): QueryResult {
+ if (query.select.type !== 'text') {
+ addDiagnostic(diagnostics, `Text strategy received a non-text selector (type="${query.select.type}").`);
+ return { matches: [], total: 0 };
+ }
+
+ const selector: TextSelector = query.select;
+ if (!selector.pattern.length) {
+ addDiagnostic(diagnostics, 'Text query pattern must be non-empty.');
+ return { matches: [], total: 0 };
+ }
+
+ const scope = resolveWithinScope(index, query, diagnostics);
+ if (!scope.ok) return { matches: [], total: 0 };
+
+ const pattern = buildSearchPattern(selector, diagnostics);
+ if (!pattern) return { matches: [], total: 0 };
+
+ const search = requireEditorCommand(editor.commands?.search, 'find (search)');
+
+ // Cap materialized matches to avoid memory pressure on high-frequency queries
+ // (e.g. single-character patterns). Pagination is applied after filtering.
+ const MAX_SEARCH_MATCHES = 1000;
+ const rawResult = search(pattern, {
+ highlight: false,
+ maxMatches: MAX_SEARCH_MATCHES,
+ caseSensitive: selector.caseSensitive ?? false,
+ });
+
+ if (!Array.isArray(rawResult)) {
+ throw new DocumentApiAdapterError(
+ 'CAPABILITY_UNAVAILABLE',
+ 'Editor search command returned an unexpected result format.',
+ );
+ }
+ const allMatches = rawResult as SearchMatch[];
+
+ const scopeRange = scope.range;
+ const matches = scopeRange
+ ? allMatches.filter((m) => m.from >= scopeRange.start && m.to <= scopeRange.end)
+ : allMatches;
+
+ const textBlocks = index.candidates.filter(isTextBlockCandidate);
+ const contexts: MatchContext[] = [];
+ const addresses: NodeAddress[] = [];
+
+ for (const match of matches) {
+ const ranges = match.ranges?.length ? match.ranges : [{ from: match.from, to: match.to }];
+ let source: BlockCandidate | undefined;
+ const textRanges = ranges
+ .map((range) => {
+ const block = findCandidateByPos(textBlocks, range.from);
+ if (!block) return undefined;
+ if (!source) source = block;
+ return toTextAddress(editor, block, range);
+ })
+ .filter((range): range is TextAddress => Boolean(range));
+
+ if (!source) {
+ source = findCandidateByPos(textBlocks, match.from) ?? findBlockByPos(index, match.from);
+ }
+ if (!source) continue;
+
+ const address = toBlockAddress(source);
+ addresses.push(address);
+ contexts.push(buildTextContext(editor, address, match.from, match.to, textRanges));
+ }
+
+ const paged = paginate(addresses, query.offset, query.limit);
+ const pagedContexts = paginate(contexts, query.offset, query.limit).items;
+
+ return {
+ matches: paged.items,
+ total: paged.total,
+ context: pagedContexts.length ? pagedContexts : undefined,
+ };
+}
diff --git a/packages/super-editor/src/document-api-adapters/format-adapter.test.ts b/packages/super-editor/src/document-api-adapters/format-adapter.test.ts
new file mode 100644
index 0000000000..d3ed0518f4
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/format-adapter.test.ts
@@ -0,0 +1,286 @@
+import { describe, expect, it, vi } from 'vitest';
+import type { Node as ProseMirrorNode } from 'prosemirror-model';
+import type { Editor } from '../core/Editor.js';
+import { TrackFormatMarkName } from '../extensions/track-changes/constants.js';
+import { formatBoldAdapter } from './format-adapter.js';
+
+type NodeOptions = {
+ attrs?: Record;
+ text?: string;
+ isInline?: boolean;
+ isBlock?: boolean;
+ isLeaf?: boolean;
+ inlineContent?: boolean;
+ nodeSize?: number;
+};
+
+function createNode(typeName: string, children: ProseMirrorNode[] = [], options: NodeOptions = {}): ProseMirrorNode {
+ const attrs = options.attrs ?? {};
+ const text = options.text ?? '';
+ const isText = typeName === 'text';
+ const isInline = options.isInline ?? isText;
+ const isBlock = options.isBlock ?? (!isInline && typeName !== 'doc');
+ const inlineContent = options.inlineContent ?? isBlock;
+ const isLeaf = options.isLeaf ?? (isInline && !isText && children.length === 0);
+
+ const contentSize = children.reduce((sum, child) => sum + child.nodeSize, 0);
+ const nodeSize = isText ? text.length : options.nodeSize != null ? options.nodeSize : isLeaf ? 1 : contentSize + 2;
+
+ return {
+ type: { name: typeName },
+ attrs,
+ text: isText ? text : undefined,
+ nodeSize,
+ isText,
+ isInline,
+ isBlock,
+ inlineContent,
+ isTextblock: inlineContent,
+ isLeaf,
+ childCount: children.length,
+ child(index: number) {
+ return children[index]!;
+ },
+ descendants(callback: (node: ProseMirrorNode, pos: number) => void) {
+ let offset = 0;
+ for (const child of children) {
+ callback(child, offset);
+ offset += child.nodeSize;
+ }
+ },
+ } as unknown as ProseMirrorNode;
+}
+
+function makeEditor(
+ text = 'Hello',
+ options: { user?: { name: string } } = {},
+): {
+ editor: Editor;
+ dispatch: ReturnType;
+ insertTrackedChange: ReturnType;
+ textBetween: ReturnType;
+ tr: {
+ addMark: ReturnType;
+ setMeta: ReturnType;
+ };
+} {
+ const textNode = createNode('text', [], { text });
+ const paragraph = createNode('paragraph', [textNode], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const tr = {
+ addMark: vi.fn(),
+ setMeta: vi.fn(),
+ };
+ tr.addMark.mockReturnValue(tr);
+ tr.setMeta.mockReturnValue(tr);
+
+ const dispatch = vi.fn();
+ const insertTrackedChange = vi.fn(() => true);
+ const textBetween = vi.fn((from: number, to: number) => {
+ const start = Math.max(0, from - 1);
+ const end = Math.max(start, to - 1);
+ return text.slice(start, end);
+ });
+
+ const editor = {
+ state: {
+ doc: {
+ ...doc,
+ textBetween,
+ },
+ tr,
+ },
+ schema: {
+ marks: {
+ bold: {
+ create: vi.fn(() => ({ type: 'bold' })),
+ },
+ [TrackFormatMarkName]: {
+ create: vi.fn(() => ({ type: TrackFormatMarkName })),
+ },
+ },
+ },
+ commands: {
+ insertTrackedChange,
+ },
+ options: { user: options.user },
+ dispatch,
+ } as unknown as Editor;
+
+ return { editor, dispatch, insertTrackedChange, textBetween, tr };
+}
+
+describe('formatBoldAdapter', () => {
+ it('applies direct bold formatting', () => {
+ const { editor, dispatch, tr } = makeEditor();
+ const receipt = formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } },
+ { changeMode: 'direct' },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(receipt.resolution).toMatchObject({
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ range: { from: 1, to: 6 },
+ text: 'Hello',
+ });
+ expect(tr.addMark).toHaveBeenCalledTimes(1);
+ expect(tr.setMeta).toHaveBeenCalledWith('inputType', 'programmatic');
+ expect(dispatch).toHaveBeenCalledTimes(1);
+ });
+
+ it('sets skipTrackChanges meta in direct mode to preserve operation-scoped semantics', () => {
+ const { editor, tr } = makeEditor();
+ const receipt = formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } },
+ { changeMode: 'direct' },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(tr.setMeta).toHaveBeenCalledWith('skipTrackChanges', true);
+ expect(tr.setMeta).not.toHaveBeenCalledWith('forceTrackChanges', true);
+ });
+
+ it('sets forceTrackChanges meta in tracked mode', () => {
+ const { editor, tr } = makeEditor('Hello', { user: { name: 'Test' } });
+ const receipt = formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } },
+ { changeMode: 'tracked' },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(tr.setMeta).toHaveBeenCalledWith('forceTrackChanges', true);
+ });
+
+ it('throws when target cannot be resolved', () => {
+ const { editor } = makeEditor();
+ expect(() =>
+ formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 5 } } },
+ { changeMode: 'direct' },
+ ),
+ ).toThrow('Format target could not be resolved.');
+ });
+
+ it('returns INVALID_TARGET for collapsed target ranges', () => {
+ const { editor } = makeEditor();
+ const receipt = formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'p1', range: { start: 2, end: 2 } } },
+ { changeMode: 'direct' },
+ );
+
+ expect(receipt.success).toBe(false);
+ expect(receipt.failure).toMatchObject({
+ code: 'INVALID_TARGET',
+ });
+ expect(receipt.resolution.range).toEqual({ from: 3, to: 3 });
+ });
+
+ it('throws when bold mark is unavailable', () => {
+ const { editor } = makeEditor();
+ delete (editor.schema?.marks as Record)?.bold;
+
+ expect(() =>
+ formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } },
+ { changeMode: 'direct' },
+ ),
+ ).toThrow('requires the "bold" mark');
+ });
+
+ it('throws when tracked format capability is unavailable', () => {
+ const { editor } = makeEditor();
+ delete (editor.commands as Record)?.insertTrackedChange;
+
+ expect(() =>
+ formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } },
+ { changeMode: 'tracked' },
+ ),
+ ).toThrow('requires the insertTrackedChange command');
+ });
+
+ it('supports direct dry-run without building a transaction', () => {
+ const { editor, dispatch, tr } = makeEditor();
+ const receipt = formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } },
+ { changeMode: 'direct', dryRun: true },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(receipt.resolution.range).toEqual({ from: 1, to: 6 });
+ expect(tr.addMark).not.toHaveBeenCalled();
+ expect(dispatch).not.toHaveBeenCalled();
+ });
+
+ it('supports tracked dry-run without building a transaction', () => {
+ const { editor, dispatch, tr } = makeEditor('Hello', { user: { name: 'Test' } });
+ const receipt = formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } },
+ { changeMode: 'tracked', dryRun: true },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(receipt.resolution.range).toEqual({ from: 1, to: 6 });
+ expect(tr.addMark).not.toHaveBeenCalled();
+ expect(tr.setMeta).not.toHaveBeenCalledWith('forceTrackChanges', true);
+ expect(dispatch).not.toHaveBeenCalled();
+ });
+
+ it('keeps direct and tracked bold operations deterministic for the same target', () => {
+ const { editor, tr } = makeEditor('Hello', { user: { name: 'Test' } });
+
+ const direct = formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } },
+ { changeMode: 'direct' },
+ );
+ expect(direct.success).toBe(true);
+
+ const tracked = formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } },
+ { changeMode: 'tracked' },
+ );
+ expect(tracked.success).toBe(true);
+ expect(tr.setMeta).toHaveBeenCalledWith('forceTrackChanges', true);
+ });
+
+ it('throws CAPABILITY_UNAVAILABLE for tracked dry-run without a configured user', () => {
+ const { editor } = makeEditor();
+
+ expect(() =>
+ formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } },
+ { changeMode: 'tracked', dryRun: true },
+ ),
+ ).toThrow('requires a user to be configured');
+ });
+
+ it('throws same error for tracked non-dry-run without a configured user', () => {
+ const { editor } = makeEditor();
+
+ expect(() =>
+ formatBoldAdapter(
+ editor,
+ { target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } } },
+ { changeMode: 'tracked' },
+ ),
+ ).toThrow('requires a user to be configured');
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/format-adapter.ts b/packages/super-editor/src/document-api-adapters/format-adapter.ts
new file mode 100644
index 0000000000..45673ef162
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/format-adapter.ts
@@ -0,0 +1,56 @@
+import type { Editor } from '../core/Editor.js';
+import type { FormatBoldInput, MutationOptions, TextMutationReceipt } from '@superdoc/document-api';
+import { TrackFormatMarkName } from '../extensions/track-changes/constants.js';
+import { DocumentApiAdapterError } from './errors.js';
+import { requireSchemaMark, ensureTrackedCapability } from './helpers/mutation-helpers.js';
+import { applyDirectMutationMeta, applyTrackedMutationMeta } from './helpers/transaction-meta.js';
+import { resolveTextTarget } from './helpers/adapter-utils.js';
+import { buildTextMutationResolution, readTextAtResolvedRange } from './helpers/text-mutation-resolution.js';
+
+export function formatBoldAdapter(
+ editor: Editor,
+ input: FormatBoldInput,
+ options?: MutationOptions,
+): TextMutationReceipt {
+ const range = resolveTextTarget(editor, input.target);
+ if (!range) {
+ throw new DocumentApiAdapterError('TARGET_NOT_FOUND', 'Format target could not be resolved.', {
+ target: input.target,
+ });
+ }
+
+ const resolution = buildTextMutationResolution({
+ requestedTarget: input.target,
+ target: input.target,
+ range,
+ text: readTextAtResolvedRange(editor, range),
+ });
+
+ if (range.from === range.to) {
+ return {
+ success: false,
+ resolution,
+ failure: {
+ code: 'INVALID_TARGET',
+ message: 'Bold formatting requires a non-collapsed target range.',
+ },
+ };
+ }
+
+ const boldMark = requireSchemaMark(editor, 'bold', 'format.bold');
+
+ const mode = options?.changeMode ?? 'direct';
+ if (mode === 'tracked')
+ ensureTrackedCapability(editor, { operation: 'format.bold', requireMarks: [TrackFormatMarkName] });
+
+ if (options?.dryRun) {
+ return { success: true, resolution };
+ }
+
+ const tr = editor.state.tr.addMark(range.from, range.to, boldMark.create());
+ if (mode === 'tracked') applyTrackedMutationMeta(tr);
+ else applyDirectMutationMeta(tr);
+
+ editor.dispatch(tr);
+ return { success: true, resolution };
+}
diff --git a/packages/super-editor/src/document-api-adapters/get-node-adapter.test.ts b/packages/super-editor/src/document-api-adapters/get-node-adapter.test.ts
new file mode 100644
index 0000000000..1247bda870
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/get-node-adapter.test.ts
@@ -0,0 +1,218 @@
+import type { Node as ProseMirrorNode, Mark as ProseMirrorMark } from 'prosemirror-model';
+import type { Editor } from '../core/Editor.js';
+import type { BlockIndex } from './helpers/node-address-resolver.js';
+import { buildInlineIndex, findInlineByType } from './helpers/inline-address-resolver.js';
+import { getNodeAdapter, getNodeByIdAdapter } from './get-node-adapter.js';
+
+function makeMark(name: string, attrs: Record = {}): ProseMirrorMark {
+ return { type: { name }, attrs } as unknown as ProseMirrorMark;
+}
+
+type NodeOptions = {
+ attrs?: Record;
+ marks?: ProseMirrorMark[];
+ text?: string;
+ isInline?: boolean;
+ isBlock?: boolean;
+ isLeaf?: boolean;
+ inlineContent?: boolean;
+};
+
+function createNode(typeName: string, children: ProseMirrorNode[] = [], options: NodeOptions = {}): ProseMirrorNode {
+ const attrs = options.attrs ?? {};
+ const marks = options.marks ?? [];
+ const text = options.text ?? '';
+ const isText = typeName === 'text';
+ const isInline = options.isInline ?? isText;
+ const isBlock = options.isBlock ?? (!isInline && typeName !== 'doc');
+ const inlineContent = options.inlineContent ?? isBlock;
+ const isLeaf = options.isLeaf ?? (isInline && children.length === 0 && !isText);
+
+ const contentSize = children.reduce((sum, child) => sum + child.nodeSize, 0);
+ const nodeSize = isText ? text.length : isLeaf ? 1 : contentSize + 2;
+
+ return {
+ type: { name: typeName },
+ attrs,
+ marks,
+ text: isText ? text : undefined,
+ nodeSize,
+ content: { size: contentSize },
+ isText,
+ isInline,
+ isBlock,
+ inlineContent,
+ isTextblock: inlineContent,
+ isLeaf,
+ childCount: children.length,
+ child(index: number) {
+ return children[index]!;
+ },
+ forEach(callback: (node: ProseMirrorNode, offset: number) => void) {
+ let offset = 0;
+ for (const child of children) {
+ callback(child, offset);
+ offset += child.nodeSize;
+ }
+ },
+ descendants(callback: (node: ProseMirrorNode, pos: number) => void) {
+ let offset = 0;
+ for (const child of children) {
+ callback(child, offset);
+ offset += child.nodeSize;
+ }
+ },
+ } as unknown as ProseMirrorNode;
+}
+
+function makeEditor(docNode: ProseMirrorNode): Editor {
+ return { state: { doc: docNode } } as unknown as Editor;
+}
+
+function buildBlockIndexFromParagraph(paragraph: ProseMirrorNode, nodeId: string): BlockIndex {
+ const candidate = {
+ node: paragraph,
+ pos: 0,
+ end: paragraph.nodeSize,
+ nodeType: 'paragraph' as const,
+ nodeId,
+ };
+ const byId = new Map();
+ byId.set(`paragraph:${nodeId}`, candidate);
+ return { candidates: [candidate], byId };
+}
+
+describe('getNodeAdapter — inline', () => {
+ it('resolves inline images by anchor', () => {
+ const textNode = createNode('text', [], { text: 'Hi' });
+ const imageNode = createNode('image', [], { isInline: true, isLeaf: true, attrs: { src: 'x' } });
+ const paragraph = createNode('paragraph', [textNode, imageNode], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const editor = makeEditor(doc);
+ const blockIndex = buildBlockIndexFromParagraph(paragraph, 'p1');
+ const inlineIndex = buildInlineIndex(editor, blockIndex);
+ const imageCandidate = findInlineByType(inlineIndex, 'image')[0];
+ if (!imageCandidate) throw new Error('Expected image candidate');
+
+ const result = getNodeAdapter(editor, {
+ kind: 'inline',
+ nodeType: 'image',
+ anchor: imageCandidate.anchor,
+ });
+
+ expect(result.nodeType).toBe('image');
+ expect(result.kind).toBe('inline');
+ });
+
+ it('resolves hyperlink marks by anchor', () => {
+ const linkMark = makeMark('link', { href: 'https://example.com' });
+ const textNode = createNode('text', [], { text: 'Hi', marks: [linkMark] });
+ const paragraph = createNode('paragraph', [textNode], {
+ attrs: { sdBlockId: 'p2' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const editor = makeEditor(doc);
+ const blockIndex = buildBlockIndexFromParagraph(paragraph, 'p2');
+ const inlineIndex = buildInlineIndex(editor, blockIndex);
+ const hyperlink = findInlineByType(inlineIndex, 'hyperlink')[0];
+ if (!hyperlink) throw new Error('Expected hyperlink candidate');
+
+ const result = getNodeAdapter(editor, {
+ kind: 'inline',
+ nodeType: 'hyperlink',
+ anchor: hyperlink.anchor,
+ });
+
+ expect(result.nodeType).toBe('hyperlink');
+ expect(result.kind).toBe('inline');
+ });
+});
+
+describe('getNodeAdapter — block', () => {
+ it('throws when a block address matches multiple nodes with the same type and id', () => {
+ const first = createNode('paragraph', [], { attrs: { sdBlockId: 'dup' }, isBlock: true, inlineContent: true });
+ const second = createNode('paragraph', [], { attrs: { sdBlockId: 'dup' }, isBlock: true, inlineContent: true });
+ const doc = createNode('doc', [first, second], { isBlock: false });
+ const editor = makeEditor(doc);
+
+ expect(() =>
+ getNodeAdapter(editor, {
+ kind: 'block',
+ nodeType: 'paragraph',
+ nodeId: 'dup',
+ }),
+ ).toThrow('Multiple nodes share paragraph id "dup".');
+ });
+});
+
+describe('getNodeByIdAdapter', () => {
+ it('resolves a block node by id without nodeType', () => {
+ const textNode = createNode('text', [], { text: 'Hi' });
+ const paragraph = createNode('paragraph', [textNode], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const editor = makeEditor(doc);
+ const result = getNodeByIdAdapter(editor, { nodeId: 'p1' });
+
+ expect(result.nodeType).toBe('paragraph');
+ expect(result.kind).toBe('block');
+ });
+
+ it('resolves a block node by id with nodeType', () => {
+ const paragraph = createNode('paragraph', [], { attrs: { sdBlockId: 'p2' }, isBlock: true, inlineContent: true });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+ const editor = makeEditor(doc);
+
+ const result = getNodeByIdAdapter(editor, { nodeId: 'p2', nodeType: 'paragraph' });
+
+ expect(result.nodeType).toBe('paragraph');
+ });
+
+ it('throws when nodeId is missing', () => {
+ const paragraph = createNode('paragraph', [], { attrs: { sdBlockId: 'p3' }, isBlock: true, inlineContent: true });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+ const editor = makeEditor(doc);
+
+ expect(() => getNodeByIdAdapter(editor, { nodeId: 'missing' })).toThrow();
+ });
+
+ it('throws when nodeId is ambiguous without nodeType', () => {
+ const paragraph = createNode('paragraph', [], { attrs: { sdBlockId: 'dup' }, isBlock: true, inlineContent: true });
+ const table = createNode('table', [], { attrs: { sdBlockId: 'dup' }, isBlock: true });
+ const doc = createNode('doc', [paragraph, table], { isBlock: false });
+ const editor = makeEditor(doc);
+
+ expect(() => getNodeByIdAdapter(editor, { nodeId: 'dup' })).toThrow();
+ });
+
+ it('throws when nodeId is ambiguous for the same nodeType', () => {
+ const first = createNode('paragraph', [], {
+ attrs: { sdBlockId: 'dup-typed' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const second = createNode('paragraph', [], {
+ attrs: { sdBlockId: 'dup-typed' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [first, second], { isBlock: false });
+ const editor = makeEditor(doc);
+
+ expect(() => getNodeByIdAdapter(editor, { nodeId: 'dup-typed', nodeType: 'paragraph' })).toThrow(
+ 'Multiple nodes share paragraph id "dup-typed".',
+ );
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/get-node-adapter.ts b/packages/super-editor/src/document-api-adapters/get-node-adapter.ts
new file mode 100644
index 0000000000..888a774c2a
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/get-node-adapter.ts
@@ -0,0 +1,99 @@
+import type { Editor } from '../core/Editor.js';
+import type { BlockNodeType, GetNodeByIdInput, NodeAddress, NodeInfo } from '@superdoc/document-api';
+import type { BlockCandidate, BlockIndex } from './helpers/node-address-resolver.js';
+import { getBlockIndex, getInlineIndex } from './helpers/index-cache.js';
+import { findInlineByAnchor } from './helpers/inline-address-resolver.js';
+import { mapNodeInfo } from './helpers/node-info-mapper.js';
+import { DocumentApiAdapterError } from './errors.js';
+
+function findBlocksByTypeAndId(blockIndex: BlockIndex, nodeType: BlockNodeType, nodeId: string): BlockCandidate[] {
+ return blockIndex.candidates.filter((candidate) => candidate.nodeType === nodeType && candidate.nodeId === nodeId);
+}
+
+/**
+ * Resolves a {@link NodeAddress} to full {@link NodeInfo} by looking up the
+ * node in the editor's current document state.
+ *
+ * @param editor - The editor instance to query.
+ * @param address - The node address to resolve.
+ * @returns Detailed node information with typed properties.
+ * @throws {DocumentApiAdapterError} If no node is found for the given address.
+ */
+export function getNodeAdapter(editor: Editor, address: NodeAddress): NodeInfo {
+ const blockIndex = getBlockIndex(editor);
+
+ if (address.kind === 'block') {
+ const matches = findBlocksByTypeAndId(blockIndex, address.nodeType, address.nodeId);
+ if (matches.length === 0) {
+ throw new DocumentApiAdapterError(
+ 'TARGET_NOT_FOUND',
+ `Node "${address.nodeType}" not found for id "${address.nodeId}".`,
+ );
+ }
+ if (matches.length > 1) {
+ throw new DocumentApiAdapterError(
+ 'TARGET_NOT_FOUND',
+ `Multiple nodes share ${address.nodeType} id "${address.nodeId}".`,
+ );
+ }
+
+ return mapNodeInfo(matches[0]!, address.nodeType);
+ }
+
+ const inlineIndex = getInlineIndex(editor);
+ const candidate = findInlineByAnchor(inlineIndex, address);
+ if (!candidate) {
+ throw new DocumentApiAdapterError(
+ 'TARGET_NOT_FOUND',
+ `Inline node "${address.nodeType}" not found for the provided anchor.`,
+ );
+ }
+
+ return mapNodeInfo(candidate, address.nodeType);
+}
+
+function resolveBlockById(
+ editor: Editor,
+ nodeId: string,
+ nodeType?: BlockNodeType,
+): { candidate: BlockCandidate; resolvedType: BlockNodeType } {
+ const blockIndex = getBlockIndex(editor);
+ if (nodeType) {
+ const matches = findBlocksByTypeAndId(blockIndex, nodeType, nodeId);
+ if (matches.length === 0) {
+ throw new DocumentApiAdapterError('TARGET_NOT_FOUND', `Node "${nodeType}" not found for id "${nodeId}".`);
+ }
+ if (matches.length > 1) {
+ throw new DocumentApiAdapterError('TARGET_NOT_FOUND', `Multiple nodes share ${nodeType} id "${nodeId}".`);
+ }
+ return { candidate: matches[0]!, resolvedType: nodeType };
+ }
+
+ const matches = blockIndex.candidates.filter((candidate) => candidate.nodeId === nodeId);
+ if (matches.length === 0) {
+ throw new DocumentApiAdapterError('TARGET_NOT_FOUND', `Node not found for id "${nodeId}".`);
+ }
+ if (matches.length > 1) {
+ throw new DocumentApiAdapterError(
+ 'TARGET_NOT_FOUND',
+ `Multiple nodes share id "${nodeId}". Provide nodeType to disambiguate.`,
+ );
+ }
+
+ return { candidate: matches[0]!, resolvedType: matches[0]!.nodeType };
+}
+
+/**
+ * Resolves a block node by its ID (and optional type) to full {@link NodeInfo}.
+ *
+ * @param editor - The editor instance to query.
+ * @param input - The block node id input payload.
+ * @returns Detailed node information with typed properties.
+ * @throws {DocumentApiAdapterError} If no node matches or multiple nodes match without a type disambiguator.
+ */
+export function getNodeByIdAdapter(editor: Editor, input: GetNodeByIdInput): NodeInfo {
+ const { nodeId, nodeType } = input;
+ const { candidate, resolvedType } = resolveBlockById(editor, nodeId, nodeType);
+ const displayType = nodeType ?? resolvedType;
+ return mapNodeInfo(candidate, displayType);
+}
diff --git a/packages/super-editor/src/document-api-adapters/get-text-adapter.test.ts b/packages/super-editor/src/document-api-adapters/get-text-adapter.test.ts
new file mode 100644
index 0000000000..499216c574
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/get-text-adapter.test.ts
@@ -0,0 +1,43 @@
+import { describe, expect, it, vi } from 'vitest';
+import type { Editor } from '../core/Editor.js';
+import { getTextAdapter } from './get-text-adapter.js';
+
+function makeEditor(textContent: string): Editor {
+ return {
+ state: {
+ doc: {
+ textContent,
+ content: { size: textContent.length },
+ textBetween: () => textContent,
+ },
+ },
+ } as unknown as Editor;
+}
+
+describe('getTextAdapter', () => {
+ it('returns the document text content', () => {
+ const editor = makeEditor('Hello world');
+ expect(getTextAdapter(editor, {})).toBe('Hello world');
+ });
+
+ it('returns an empty string for an empty document', () => {
+ const editor = makeEditor('');
+ expect(getTextAdapter(editor, {})).toBe('');
+ });
+
+ it('preserves block separators when reading full document text', () => {
+ const textBetween = vi.fn(() => 'Hello\nworld');
+ const editor = {
+ state: {
+ doc: {
+ textContent: 'Helloworld',
+ content: { size: 10 },
+ textBetween,
+ },
+ },
+ } as unknown as Editor;
+
+ expect(getTextAdapter(editor, {})).toBe('Hello\nworld');
+ expect(textBetween).toHaveBeenCalledWith(0, 10, '\n', '\n');
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/get-text-adapter.ts b/packages/super-editor/src/document-api-adapters/get-text-adapter.ts
new file mode 100644
index 0000000000..3fa957dcb8
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/get-text-adapter.ts
@@ -0,0 +1,13 @@
+import type { Editor } from '../core/Editor.js';
+import type { GetTextInput } from '@superdoc/document-api';
+
+/**
+ * Return the full document text content from the ProseMirror document.
+ *
+ * @param editor - The editor instance.
+ * @returns Plain text content of the document.
+ */
+export function getTextAdapter(editor: Editor, _input: GetTextInput): string {
+ const doc = editor.state.doc;
+ return doc.textBetween(0, doc.content.size, '\n', '\n');
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/adapter-utils.test.ts b/packages/super-editor/src/document-api-adapters/helpers/adapter-utils.test.ts
new file mode 100644
index 0000000000..a06d5abfe3
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/adapter-utils.test.ts
@@ -0,0 +1,211 @@
+import type { UnknownNodeDiagnostic } from '@superdoc/document-api';
+import { addDiagnostic, dedupeDiagnostics, findCandidateByPos, paginate, scopeByRange } from './adapter-utils.js';
+
+// ---------------------------------------------------------------------------
+// paginate
+// ---------------------------------------------------------------------------
+
+describe('paginate', () => {
+ const items = ['a', 'b', 'c', 'd', 'e'];
+
+ it('returns all items when no offset or limit is provided', () => {
+ const result = paginate(items);
+ expect(result).toEqual({ total: 5, items: ['a', 'b', 'c', 'd', 'e'] });
+ });
+
+ it('applies offset', () => {
+ const result = paginate(items, 2);
+ expect(result).toEqual({ total: 5, items: ['c', 'd', 'e'] });
+ });
+
+ it('applies limit', () => {
+ const result = paginate(items, 0, 3);
+ expect(result).toEqual({ total: 5, items: ['a', 'b', 'c'] });
+ });
+
+ it('combines offset and limit', () => {
+ const result = paginate(items, 1, 2);
+ expect(result).toEqual({ total: 5, items: ['b', 'c'] });
+ });
+
+ it('returns empty when offset exceeds length', () => {
+ const result = paginate(items, 10);
+ expect(result).toEqual({ total: 5, items: [] });
+ });
+
+ it('returns empty for limit 0', () => {
+ const result = paginate(items, 0, 0);
+ expect(result).toEqual({ total: 5, items: [] });
+ });
+
+ it('clamps negative offset to 0', () => {
+ const result = paginate(items, -5, 2);
+ expect(result).toEqual({ total: 5, items: ['a', 'b'] });
+ });
+
+ it('clamps negative limit to 0', () => {
+ const result = paginate(items, 0, -1);
+ expect(result).toEqual({ total: 5, items: [] });
+ });
+
+ it('handles empty array', () => {
+ const result = paginate([]);
+ expect(result).toEqual({ total: 0, items: [] });
+ });
+});
+
+// ---------------------------------------------------------------------------
+// dedupeDiagnostics
+// ---------------------------------------------------------------------------
+
+describe('dedupeDiagnostics', () => {
+ it('removes duplicate diagnostics by message', () => {
+ const diagnostics: UnknownNodeDiagnostic[] = [
+ { message: 'error A' },
+ { message: 'error A' },
+ { message: 'error B' },
+ ];
+
+ const result = dedupeDiagnostics(diagnostics);
+
+ expect(result).toEqual([{ message: 'error A' }, { message: 'error B' }]);
+ });
+
+ it('considers hint in deduplication key', () => {
+ const diagnostics: UnknownNodeDiagnostic[] = [
+ { message: 'same', hint: 'hint1' },
+ { message: 'same', hint: 'hint2' },
+ ];
+
+ const result = dedupeDiagnostics(diagnostics);
+
+ expect(result).toHaveLength(2);
+ });
+
+ it('considers address in deduplication key', () => {
+ const diagnostics: UnknownNodeDiagnostic[] = [
+ { message: 'same', address: { kind: 'block', nodeType: 'paragraph', nodeId: 'p1' } },
+ { message: 'same', address: { kind: 'block', nodeType: 'paragraph', nodeId: 'p2' } },
+ ];
+
+ const result = dedupeDiagnostics(diagnostics);
+
+ expect(result).toHaveLength(2);
+ });
+
+ it('preserves insertion order', () => {
+ const diagnostics: UnknownNodeDiagnostic[] = [{ message: 'first' }, { message: 'second' }, { message: 'first' }];
+
+ const result = dedupeDiagnostics(diagnostics);
+
+ expect(result[0].message).toBe('first');
+ expect(result[1].message).toBe('second');
+ });
+
+ it('returns empty array for empty input', () => {
+ expect(dedupeDiagnostics([])).toEqual([]);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// addDiagnostic
+// ---------------------------------------------------------------------------
+
+describe('addDiagnostic', () => {
+ it('pushes a diagnostic with the given message', () => {
+ const diagnostics: UnknownNodeDiagnostic[] = [];
+ addDiagnostic(diagnostics, 'Something went wrong.');
+
+ expect(diagnostics).toEqual([{ message: 'Something went wrong.' }]);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// scopeByRange
+// ---------------------------------------------------------------------------
+
+describe('scopeByRange', () => {
+ const candidates = [
+ { pos: 0, end: 10 },
+ { pos: 15, end: 25 },
+ { pos: 30, end: 40 },
+ ];
+
+ it('returns all candidates when range is undefined', () => {
+ const result = scopeByRange(candidates, undefined);
+ expect(result).toHaveLength(3);
+ });
+
+ it('filters to candidates fully within the range', () => {
+ const result = scopeByRange(candidates, { start: 0, end: 30 });
+ expect(result).toHaveLength(2);
+ expect(result[0].pos).toBe(0);
+ expect(result[1].pos).toBe(15);
+ });
+
+ it('excludes candidates partially outside the range', () => {
+ const result = scopeByRange(candidates, { start: 5, end: 40 });
+ expect(result).toHaveLength(2);
+ expect(result[0].pos).toBe(15);
+ expect(result[1].pos).toBe(30);
+ });
+
+ it('returns empty when no candidates fit', () => {
+ const result = scopeByRange(candidates, { start: 11, end: 14 });
+ expect(result).toHaveLength(0);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// findCandidateByPos
+// ---------------------------------------------------------------------------
+
+describe('findCandidateByPos', () => {
+ // Candidates: [0, 10), [15, 25), [30, 40)
+ const candidates = [
+ { pos: 0, end: 10, id: 'a' },
+ { pos: 15, end: 25, id: 'b' },
+ { pos: 30, end: 40, id: 'c' },
+ ];
+
+ it('finds a candidate at start of range', () => {
+ expect(findCandidateByPos(candidates, 0)?.id).toBe('a');
+ });
+
+ it('finds a candidate in the middle of range', () => {
+ expect(findCandidateByPos(candidates, 5)?.id).toBe('a');
+ });
+
+ it('treats end as exclusive (pos at end returns undefined for gap)', () => {
+ // pos 10 is at candidate.end, which is exclusive — should not match 'a'
+ expect(findCandidateByPos(candidates, 10)).toBeUndefined();
+ });
+
+ it('finds the middle candidate', () => {
+ expect(findCandidateByPos(candidates, 20)?.id).toBe('b');
+ });
+
+ it('finds the last candidate', () => {
+ expect(findCandidateByPos(candidates, 35)?.id).toBe('c');
+ });
+
+ it('returns undefined for position in a gap', () => {
+ expect(findCandidateByPos(candidates, 12)).toBeUndefined();
+ });
+
+ it('returns undefined for position beyond all candidates', () => {
+ expect(findCandidateByPos(candidates, 50)).toBeUndefined();
+ });
+
+ it('returns undefined for empty array', () => {
+ expect(findCandidateByPos([], 5)).toBeUndefined();
+ });
+
+ it('finds the only candidate in a single-element array', () => {
+ const single = [{ pos: 5, end: 15, id: 'only' }];
+ expect(findCandidateByPos(single, 5)?.id).toBe('only');
+ expect(findCandidateByPos(single, 10)?.id).toBe('only');
+ expect(findCandidateByPos(single, 4)).toBeUndefined();
+ expect(findCandidateByPos(single, 15)).toBeUndefined();
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/helpers/adapter-utils.ts b/packages/super-editor/src/document-api-adapters/helpers/adapter-utils.ts
new file mode 100644
index 0000000000..e1c3ea43a5
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/adapter-utils.ts
@@ -0,0 +1,225 @@
+import type { Query, TextAddress, UnknownNodeDiagnostic } from '@superdoc/document-api';
+import { getBlockIndex } from './index-cache.js';
+import { findBlockById, isTextBlockCandidate, type BlockCandidate, type BlockIndex } from './node-address-resolver.js';
+import { resolveTextRangeInBlock } from './text-offset-resolver.js';
+import type { Editor } from '../../core/Editor.js';
+import { DocumentApiAdapterError } from '../errors.js';
+
+export type WithinResult = { ok: true; range: { start: number; end: number } | undefined } | { ok: false };
+export type ResolvedTextTarget = { from: number; to: number };
+
+function findTextBlockCandidates(index: BlockIndex, blockId: string): BlockCandidate[] {
+ return index.candidates.filter((candidate) => candidate.nodeId === blockId && isTextBlockCandidate(candidate));
+}
+
+function assertUnambiguous(matches: BlockCandidate[], blockId: string): void {
+ if (matches.length > 1) {
+ throw new DocumentApiAdapterError(
+ 'INVALID_TARGET',
+ `Block ID "${blockId}" is ambiguous: matched ${matches.length} text blocks.`,
+ {
+ blockId,
+ matchCount: matches.length,
+ },
+ );
+ }
+}
+
+function findInlineWithinTextBlock(index: BlockIndex, blockId: string): BlockCandidate | undefined {
+ const matches = findTextBlockCandidates(index, blockId);
+ assertUnambiguous(matches, blockId);
+ return matches[0];
+}
+
+/**
+ * Resolves a {@link TextAddress} to absolute ProseMirror positions.
+ *
+ * @param editor - The editor instance.
+ * @param target - The text address to resolve.
+ * @returns Absolute `{ from, to }` positions, or `null` if the target block cannot be found.
+ * @throws {DocumentApiAdapterError} `INVALID_TARGET` when multiple text blocks share the same blockId.
+ */
+export function resolveTextTarget(editor: Editor, target: TextAddress): ResolvedTextTarget | null {
+ const index = getBlockIndex(editor);
+ const matches = findTextBlockCandidates(index, target.blockId);
+ assertUnambiguous(matches, target.blockId);
+ const block = matches[0];
+ if (!block) return null;
+ return resolveTextRangeInBlock(block.node, block.pos, target.range);
+}
+
+/**
+ * Resolves the deterministic default insertion target for insert-without-target calls.
+ *
+ * Priority:
+ * 1) First paragraph block in document order.
+ * 2) First editable text block in document order.
+ */
+export function resolveDefaultInsertTarget(editor: Editor): { target: TextAddress; range: ResolvedTextTarget } | null {
+ const index = getBlockIndex(editor);
+ const firstParagraph = index.candidates.find(
+ (candidate) => candidate.nodeType === 'paragraph' && isTextBlockCandidate(candidate),
+ );
+ const firstTextBlock = firstParagraph ?? index.candidates.find((candidate) => isTextBlockCandidate(candidate));
+ if (!firstTextBlock) return null;
+
+ const range = resolveTextRangeInBlock(firstTextBlock.node, firstTextBlock.pos, { start: 0, end: 0 });
+ if (!range) return null;
+
+ return {
+ target: {
+ kind: 'text',
+ blockId: firstTextBlock.nodeId,
+ range: { start: 0, end: 0 },
+ },
+ range,
+ };
+}
+
+/**
+ * Appends a diagnostic message to the mutable diagnostics array.
+ *
+ * @param diagnostics - Array to push the diagnostic into.
+ * @param message - Human-readable diagnostic message.
+ */
+export function addDiagnostic(diagnostics: UnknownNodeDiagnostic[], message: string): void {
+ diagnostics.push({ message });
+}
+
+/**
+ * Applies offset/limit pagination to an array, returning the total count and the sliced page.
+ *
+ * @param items - The full result array.
+ * @param offset - Number of items to skip (default `0`).
+ * @param limit - Maximum items to return (default: all remaining).
+ * @returns An object with `total` (pre-pagination count) and `items` (the sliced page).
+ */
+export function paginate(items: T[], offset = 0, limit = items.length): { total: number; items: T[] } {
+ const total = items.length;
+ const safeOffset = Math.max(0, offset ?? 0);
+ const safeLimit = Math.max(0, limit ?? total);
+ return { total, items: items.slice(safeOffset, safeOffset + safeLimit) };
+}
+
+/**
+ * Deduplicates diagnostics by message + hint + address, preserving insertion order.
+ *
+ * @param diagnostics - The diagnostics to deduplicate.
+ * @returns A new array with unique diagnostics.
+ */
+export function dedupeDiagnostics(diagnostics: UnknownNodeDiagnostic[]): UnknownNodeDiagnostic[] {
+ const seen = new Set();
+ const unique: UnknownNodeDiagnostic[] = [];
+
+ for (const diagnostic of diagnostics) {
+ const key = `${diagnostic.message}|${diagnostic.hint ?? ''}|${
+ diagnostic.address ? JSON.stringify(diagnostic.address) : ''
+ }`;
+ if (seen.has(key)) continue;
+ seen.add(key);
+ unique.push(diagnostic);
+ }
+
+ return unique;
+}
+
+/**
+ * Resolves the `within` scope of a query to an absolute position range.
+ *
+ * @param index - Pre-built block index.
+ * @param query - The query whose `within` clause should be resolved.
+ * @param diagnostics - Mutable array to collect diagnostics into.
+ * @returns `{ ok: true, range }` on success (range is `undefined` when no scope), or `{ ok: false }` with a diagnostic.
+ */
+export function resolveWithinScope(
+ index: BlockIndex,
+ query: Query,
+ diagnostics: UnknownNodeDiagnostic[],
+): WithinResult {
+ if (!query.within) return { ok: true, range: undefined };
+
+ if (query.within.kind === 'block') {
+ const within = findBlockById(index, query.within);
+ if (!within) {
+ addDiagnostic(
+ diagnostics,
+ `Within block "${query.within.nodeType}" with id "${query.within.nodeId}" was not found in the document.`,
+ );
+ return { ok: false };
+ }
+ return { ok: true, range: { start: within.pos, end: within.end } };
+ }
+
+ if (query.within.anchor.start.blockId !== query.within.anchor.end.blockId) {
+ addDiagnostic(diagnostics, 'Inline within anchors that span multiple blocks are not supported.');
+ return { ok: false };
+ }
+
+ const block = findInlineWithinTextBlock(index, query.within.anchor.start.blockId);
+ if (!block) {
+ addDiagnostic(
+ diagnostics,
+ `Within inline anchor block "${query.within.anchor.start.blockId}" was not found in the document.`,
+ );
+ return { ok: false };
+ }
+
+ const resolved = resolveTextRangeInBlock(block.node, block.pos, {
+ start: query.within.anchor.start.offset,
+ end: query.within.anchor.end.offset,
+ });
+ if (!resolved) {
+ addDiagnostic(diagnostics, 'Inline within anchor offsets could not be resolved in the target block.');
+ return { ok: false };
+ }
+
+ return { ok: true, range: { start: resolved.from, end: resolved.to } };
+}
+
+/**
+ * Filters candidates to those fully contained within the given position range.
+ * Returns the full array unchanged when `range` is `undefined`.
+ *
+ * @param candidates - Candidates with `pos` and `end` fields.
+ * @param range - Optional absolute position range to filter by.
+ * @returns Filtered candidates.
+ */
+export function scopeByRange(
+ candidates: T[],
+ range: { start: number; end: number } | undefined,
+): T[] {
+ if (!range) return candidates;
+ return candidates.filter((candidate) => candidate.pos >= range.start && candidate.end <= range.end);
+}
+
+/**
+ * Binary-searches a sorted candidate array for the entry containing `pos`.
+ * Uses half-open interval `[candidate.pos, candidate.end)`.
+ *
+ * @param candidates - Sorted array of candidates with `pos` and `end` fields.
+ * @param pos - The absolute document position to look up.
+ * @returns The matching candidate, or `undefined` if no candidate contains the position.
+ */
+export function findCandidateByPos(
+ candidates: T[],
+ pos: number,
+): T | undefined {
+ let low = 0;
+ let high = candidates.length - 1;
+
+ while (low <= high) {
+ const mid = (low + high) >> 1;
+ const candidate = candidates[mid];
+ if (pos < candidate.pos) {
+ high = mid - 1;
+ continue;
+ }
+ if (pos >= candidate.end) {
+ low = mid + 1;
+ continue;
+ }
+ return candidate;
+ }
+
+ return undefined;
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/comment-entity-store.test.ts b/packages/super-editor/src/document-api-adapters/helpers/comment-entity-store.test.ts
new file mode 100644
index 0000000000..48ce78752a
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/comment-entity-store.test.ts
@@ -0,0 +1,243 @@
+import { describe, expect, it } from 'vitest';
+import type { Editor } from '../../core/Editor.js';
+import {
+ buildCommentJsonFromText,
+ extractCommentText,
+ findCommentEntity,
+ getCommentEntityStore,
+ isCommentResolved,
+ removeCommentEntityTree,
+ toCommentInfo,
+ upsertCommentEntity,
+ type CommentEntityRecord,
+} from './comment-entity-store.js';
+
+function makeEditorWithConverter(comments: CommentEntityRecord[] = []): Editor {
+ return { converter: { comments } } as unknown as Editor;
+}
+
+function makeEditorWithoutConverter(): Editor {
+ return {} as unknown as Editor;
+}
+
+describe('getCommentEntityStore', () => {
+ it('returns converter.comments when converter exists', () => {
+ const comments: CommentEntityRecord[] = [{ commentId: 'c1' }];
+ const editor = makeEditorWithConverter(comments);
+ expect(getCommentEntityStore(editor)).toBe(comments);
+ });
+
+ it('initializes converter.comments as empty array when undefined', () => {
+ const editor = { converter: {} } as unknown as Editor;
+ const store = getCommentEntityStore(editor);
+ expect(store).toEqual([]);
+ expect(Array.isArray(store)).toBe(true);
+ });
+
+ it('uses fallback storage when converter is missing', () => {
+ const editor = makeEditorWithoutConverter();
+ const store = getCommentEntityStore(editor);
+ expect(store).toEqual([]);
+ // Subsequent calls return the same array
+ expect(getCommentEntityStore(editor)).toBe(store);
+ });
+});
+
+describe('findCommentEntity', () => {
+ it('finds by commentId', () => {
+ const store: CommentEntityRecord[] = [{ commentId: 'c1', commentText: 'Hello' }];
+ expect(findCommentEntity(store, 'c1')?.commentText).toBe('Hello');
+ });
+
+ it('finds by importedId', () => {
+ const store: CommentEntityRecord[] = [{ commentId: 'c1', importedId: 'imp-1' }];
+ expect(findCommentEntity(store, 'imp-1')?.commentId).toBe('c1');
+ });
+
+ it('returns undefined when not found', () => {
+ const store: CommentEntityRecord[] = [{ commentId: 'c1' }];
+ expect(findCommentEntity(store, 'missing')).toBeUndefined();
+ });
+});
+
+describe('upsertCommentEntity', () => {
+ it('creates a new entry when none exists', () => {
+ const store: CommentEntityRecord[] = [];
+ const result = upsertCommentEntity(store, 'c1', { commentText: 'New' });
+ expect(result.commentId).toBe('c1');
+ expect(result.commentText).toBe('New');
+ expect(store).toHaveLength(1);
+ });
+
+ it('updates an existing entry preserving its commentId', () => {
+ const store: CommentEntityRecord[] = [{ commentId: 'c1', commentText: 'Old' }];
+ const result = upsertCommentEntity(store, 'c1', { commentText: 'Updated' });
+ expect(result.commentText).toBe('Updated');
+ expect(result.commentId).toBe('c1');
+ expect(store).toHaveLength(1);
+ });
+
+ it('resolves to the provided commentId when existing entry has no commentId', () => {
+ const store: CommentEntityRecord[] = [{ importedId: 'imp-1', commentText: 'Old' }];
+ const result = upsertCommentEntity(store, 'imp-1', { commentText: 'Updated' });
+ expect(result.commentId).toBe('imp-1');
+ });
+});
+
+describe('removeCommentEntityTree', () => {
+ it('removes a root comment and its children', () => {
+ const store: CommentEntityRecord[] = [
+ { commentId: 'c1', commentText: 'Root' },
+ { commentId: 'c2', parentCommentId: 'c1', commentText: 'Reply' },
+ { commentId: 'c3', parentCommentId: 'c2', commentText: 'Nested reply' },
+ ];
+
+ const removed = removeCommentEntityTree(store, 'c1');
+ expect(removed.map((r) => r.commentId).sort()).toEqual(['c1', 'c2', 'c3']);
+ expect(store).toHaveLength(0);
+ });
+
+ it('returns empty array when comment is not found', () => {
+ const store: CommentEntityRecord[] = [{ commentId: 'c1' }];
+ const removed = removeCommentEntityTree(store, 'missing');
+ expect(removed).toEqual([]);
+ expect(store).toHaveLength(1);
+ });
+
+ it('preserves unrelated comments', () => {
+ const store: CommentEntityRecord[] = [
+ { commentId: 'c1', commentText: 'Root' },
+ { commentId: 'c2', parentCommentId: 'c1', commentText: 'Reply' },
+ { commentId: 'c3', commentText: 'Unrelated' },
+ ];
+
+ removeCommentEntityTree(store, 'c1');
+ expect(store).toHaveLength(1);
+ expect(store[0]?.commentId).toBe('c3');
+ });
+
+ it('returns empty array when root has empty commentId', () => {
+ const store: CommentEntityRecord[] = [{ commentId: '', importedId: 'imp-1' }];
+ const removed = removeCommentEntityTree(store, 'imp-1');
+ expect(removed).toEqual([]);
+ });
+});
+
+describe('extractCommentText', () => {
+ it('returns commentText when available', () => {
+ expect(extractCommentText({ commentText: 'Hello' })).toBe('Hello');
+ });
+
+ it('extracts text from commentJSON structure', () => {
+ const entry: CommentEntityRecord = {
+ commentJSON: [{ type: 'paragraph', content: [{ type: 'text', text: 'From JSON' }] }],
+ };
+ expect(extractCommentText(entry)).toBe('From JSON');
+ });
+
+ it('extracts text from elements structure', () => {
+ const entry: CommentEntityRecord = {
+ elements: [{ text: 'From elements' }],
+ };
+ expect(extractCommentText(entry)).toBe('From elements');
+ });
+
+ it('returns undefined when no text source exists', () => {
+ expect(extractCommentText({})).toBeUndefined();
+ });
+
+ it('returns undefined for empty commentJSON', () => {
+ expect(extractCommentText({ commentJSON: [] })).toBeUndefined();
+ });
+});
+
+describe('buildCommentJsonFromText', () => {
+ it('creates paragraph/run/text structure from plain text', () => {
+ const result = buildCommentJsonFromText('Hello world');
+ expect(result).toEqual([
+ {
+ type: 'paragraph',
+ content: [
+ {
+ type: 'run',
+ content: [{ type: 'text', text: 'Hello world' }],
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('strips HTML tags from input', () => {
+ const result = buildCommentJsonFromText('Bold text');
+ expect(result[0]).toMatchObject({
+ content: [{ content: [{ text: 'Bold text' }] }],
+ });
+ });
+
+ it('replaces with spaces', () => {
+ const result = buildCommentJsonFromText('Hello world');
+ expect(result[0]).toMatchObject({
+ content: [{ content: [{ text: 'Hello world' }] }],
+ });
+ });
+});
+
+describe('isCommentResolved', () => {
+ it('returns true when isDone is true', () => {
+ expect(isCommentResolved({ isDone: true })).toBe(true);
+ });
+
+ it('returns true when resolvedTime is set', () => {
+ expect(isCommentResolved({ resolvedTime: Date.now() })).toBe(true);
+ });
+
+ it('returns false when neither isDone nor resolvedTime is set', () => {
+ expect(isCommentResolved({})).toBe(false);
+ });
+
+ it('returns false when resolvedTime is null', () => {
+ expect(isCommentResolved({ resolvedTime: null })).toBe(false);
+ });
+});
+
+describe('toCommentInfo', () => {
+ it('builds CommentInfo from a record', () => {
+ const info = toCommentInfo({
+ commentId: 'c1',
+ importedId: 'imp-1',
+ commentText: 'Hello',
+ isInternal: true,
+ createdTime: 1000,
+ creatorName: 'Ada',
+ creatorEmail: 'ada@example.com',
+ });
+
+ expect(info.commentId).toBe('c1');
+ expect(info.importedId).toBe('imp-1');
+ expect(info.text).toBe('Hello');
+ expect(info.isInternal).toBe(true);
+ expect(info.status).toBe('open');
+ expect(info.createdTime).toBe(1000);
+ });
+
+ it('respects explicit status override', () => {
+ const info = toCommentInfo({ commentId: 'c1' }, { status: 'resolved' });
+ expect(info.status).toBe('resolved');
+ });
+
+ it('derives resolved status from isDone', () => {
+ const info = toCommentInfo({ commentId: 'c1', isDone: true });
+ expect(info.status).toBe('resolved');
+ });
+
+ it('falls back to importedId when commentId is missing', () => {
+ const info = toCommentInfo({ importedId: 'imp-1' });
+ expect(info.commentId).toBe('imp-1');
+ });
+
+ it('includes target when provided', () => {
+ const target = { kind: 'text' as const, blockId: 'p1', range: { start: 0, end: 5 } };
+ const info = toCommentInfo({ commentId: 'c1' }, { target });
+ expect(info.target).toBe(target);
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/helpers/comment-entity-store.ts b/packages/super-editor/src/document-api-adapters/helpers/comment-entity-store.ts
new file mode 100644
index 0000000000..28109ac653
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/comment-entity-store.ts
@@ -0,0 +1,212 @@
+import type { Editor } from '../../core/Editor.js';
+import type { CommentInfo, CommentStatus, TextAddress } from '@superdoc/document-api';
+
+const FALLBACK_STORE_KEY = '__documentApiComments';
+
+export interface CommentEntityRecord {
+ commentId?: string;
+ importedId?: string;
+ parentCommentId?: string;
+ commentText?: string;
+ commentJSON?: unknown;
+ elements?: unknown;
+ isInternal?: boolean;
+ isDone?: boolean;
+ resolvedTime?: number | null;
+ resolvedByEmail?: string | null;
+ resolvedByName?: string | null;
+ creatorName?: string;
+ creatorEmail?: string;
+ creatorImage?: string;
+ createdTime?: number;
+ [key: string]: unknown;
+}
+
+type ConverterWithComments = {
+ comments?: CommentEntityRecord[];
+};
+
+type EditorWithCommentStorage = Editor & {
+ converter?: ConverterWithComments;
+ storage?: Record;
+};
+
+function ensureFallbackStore(editor: EditorWithCommentStorage): CommentEntityRecord[] {
+ if (!editor.storage) {
+ (editor as unknown as Record).storage = {};
+ }
+ const storage = editor.storage as Record;
+
+ if (!Array.isArray(storage[FALLBACK_STORE_KEY])) {
+ storage[FALLBACK_STORE_KEY] = [];
+ }
+
+ return storage[FALLBACK_STORE_KEY] as CommentEntityRecord[];
+}
+
+export function getCommentEntityStore(editor: Editor): CommentEntityRecord[] {
+ const mutableEditor = editor as EditorWithCommentStorage;
+ const converter = mutableEditor.converter as ConverterWithComments | undefined;
+
+ if (converter) {
+ if (!Array.isArray(converter.comments)) {
+ converter.comments = [];
+ }
+ return converter.comments as CommentEntityRecord[];
+ }
+
+ return ensureFallbackStore(mutableEditor);
+}
+
+export function findCommentEntity(store: CommentEntityRecord[], commentId: string): CommentEntityRecord | undefined {
+ return store.find((entry) => entry.commentId === commentId || entry.importedId === commentId);
+}
+
+export function upsertCommentEntity(
+ store: CommentEntityRecord[],
+ commentId: string,
+ patch: Partial,
+): CommentEntityRecord {
+ const existing = findCommentEntity(store, commentId);
+ if (existing) {
+ const resolvedId =
+ typeof existing.commentId === 'string' && existing.commentId.length > 0 ? existing.commentId : commentId;
+ Object.assign(existing, patch, { commentId: resolvedId });
+ return existing;
+ }
+
+ const created: CommentEntityRecord = {
+ ...patch,
+ commentId,
+ };
+ store.push(created);
+ return created;
+}
+
+export function removeCommentEntityTree(store: CommentEntityRecord[], commentId: string): CommentEntityRecord[] {
+ const root = findCommentEntity(store, commentId);
+ if (!root || typeof root.commentId !== 'string' || root.commentId.length === 0) return [];
+
+ const removeIds = new Set([root.commentId]);
+ let changed = true;
+
+ while (changed) {
+ changed = false;
+ for (const entry of store) {
+ if (typeof entry.commentId !== 'string' || entry.commentId.length === 0) continue;
+ if (typeof entry.parentCommentId !== 'string' || entry.parentCommentId.length === 0) continue;
+ if (removeIds.has(entry.parentCommentId) && !removeIds.has(entry.commentId)) {
+ removeIds.add(entry.commentId);
+ changed = true;
+ }
+ }
+ }
+
+ const removed = store.filter((entry) => typeof entry.commentId === 'string' && removeIds.has(entry.commentId));
+ const kept = store.filter((entry) => !(typeof entry.commentId === 'string' && removeIds.has(entry.commentId)));
+
+ store.splice(0, store.length, ...kept);
+ return removed;
+}
+
+/**
+ * Strips HTML tags from a comment text string using simple regex replacement.
+ *
+ * This is only intended for normalizing comment content that was already authored
+ * within the editor. It is NOT a security sanitizer and must not be used to
+ * neutralize untrusted or user-supplied HTML.
+ */
+function stripHtmlToText(value: string): string {
+ return value
+ .replace(/<[^>]+>/g, ' ')
+ .replace(/ /gi, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+}
+
+function collectTextFragments(value: unknown, sink: string[]): void {
+ if (!value) return;
+
+ if (typeof value === 'string') {
+ if (value.length > 0) sink.push(value);
+ return;
+ }
+
+ if (Array.isArray(value)) {
+ for (const item of value) collectTextFragments(item, sink);
+ return;
+ }
+
+ if (typeof value !== 'object') return;
+ const record = value as Record;
+ if (typeof record.text === 'string' && record.text.length > 0) sink.push(record.text);
+
+ if (record.content) collectTextFragments(record.content, sink);
+ if (record.elements) collectTextFragments(record.elements, sink);
+ if (record.nodes) collectTextFragments(record.nodes, sink);
+}
+
+export function extractCommentText(entry: CommentEntityRecord): string | undefined {
+ if (typeof entry.commentText === 'string') return entry.commentText;
+
+ const fragments: string[] = [];
+ if (entry.commentJSON) collectTextFragments(entry.commentJSON, fragments);
+ if (entry.elements) collectTextFragments(entry.elements, fragments);
+
+ if (!fragments.length) return undefined;
+ return fragments.join('').trim();
+}
+
+export function buildCommentJsonFromText(text: string): unknown[] {
+ const normalized = stripHtmlToText(text);
+
+ return [
+ {
+ type: 'paragraph',
+ content: [
+ {
+ type: 'run',
+ content: [
+ {
+ type: 'text',
+ text: normalized,
+ },
+ ],
+ },
+ ],
+ },
+ ];
+}
+
+export function isCommentResolved(entry: CommentEntityRecord): boolean {
+ return Boolean(entry.isDone || entry.resolvedTime);
+}
+
+export function toCommentInfo(
+ entry: CommentEntityRecord,
+ options: {
+ target?: TextAddress;
+ status?: CommentStatus;
+ } = {},
+): CommentInfo {
+ const resolvedId = typeof entry.commentId === 'string' ? entry.commentId : String(entry.importedId ?? '');
+ const status = options.status ?? (isCommentResolved(entry) ? 'resolved' : 'open');
+
+ return {
+ address: {
+ kind: 'entity',
+ entityType: 'comment',
+ entityId: resolvedId,
+ },
+ commentId: resolvedId,
+ importedId: typeof entry.importedId === 'string' ? entry.importedId : undefined,
+ parentCommentId: typeof entry.parentCommentId === 'string' ? entry.parentCommentId : undefined,
+ text: extractCommentText(entry),
+ isInternal: typeof entry.isInternal === 'boolean' ? entry.isInternal : undefined,
+ status,
+ target: options.target,
+ createdTime: typeof entry.createdTime === 'number' ? entry.createdTime : undefined,
+ creatorName: typeof entry.creatorName === 'string' ? entry.creatorName : undefined,
+ creatorEmail: typeof entry.creatorEmail === 'string' ? entry.creatorEmail : undefined,
+ };
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/comment-target-resolver.ts b/packages/super-editor/src/document-api-adapters/helpers/comment-target-resolver.ts
new file mode 100644
index 0000000000..21581aa4d8
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/comment-target-resolver.ts
@@ -0,0 +1,83 @@
+import type { Editor } from '../../core/Editor.js';
+import type { TextAddress } from '@superdoc/document-api';
+import { getInlineIndex } from './index-cache.js';
+import type { InlineCandidate } from './inline-address-resolver.js';
+import { resolveCommentIdFromAttrs, toNonEmptyString } from './value-utils.js';
+
+export type CommentAnchorStatus = 'open' | 'resolved';
+
+export interface CommentAnchor {
+ commentId: string;
+ importedId?: string;
+ status: CommentAnchorStatus;
+ target: TextAddress;
+ isInternal?: boolean;
+ pos: number;
+ end: number;
+ attrs: Record;
+}
+
+function resolveCommentId(candidate: InlineCandidate): string | undefined {
+ return resolveCommentIdFromAttrs(candidate.attrs ?? {});
+}
+
+function resolveImportedId(candidate: InlineCandidate): string | undefined {
+ const attrs = candidate.attrs ?? {};
+ return toNonEmptyString(attrs.importedId);
+}
+
+function toTextAddress(candidate: InlineCandidate): TextAddress | null {
+ const { start, end } = candidate.anchor;
+ if (start.blockId !== end.blockId) return null;
+
+ return {
+ kind: 'text',
+ blockId: start.blockId,
+ range: {
+ start: start.offset,
+ end: end.offset,
+ },
+ };
+}
+
+export function listCommentAnchors(editor: Editor): CommentAnchor[] {
+ const inlineIndex = getInlineIndex(editor);
+ const candidates = inlineIndex.byType.get('comment') ?? [];
+ const anchors: CommentAnchor[] = [];
+ const seen = new Set();
+
+ for (const candidate of candidates) {
+ const commentId = resolveCommentId(candidate);
+ if (!commentId) continue;
+
+ const target = toTextAddress(candidate);
+ if (!target) continue;
+
+ const dedupeKey = `${commentId}|${target.blockId}:${target.range.start}:${target.range.end}`;
+ if (seen.has(dedupeKey)) continue;
+ seen.add(dedupeKey);
+
+ const attrs = (candidate.attrs ?? {}) as Record;
+ const isInternal = typeof attrs.internal === 'boolean' ? attrs.internal : undefined;
+ const status: CommentAnchorStatus = candidate.mark ? 'open' : 'resolved';
+
+ anchors.push({
+ commentId,
+ importedId: resolveImportedId(candidate),
+ status,
+ target,
+ isInternal,
+ pos: candidate.pos,
+ end: candidate.end,
+ attrs,
+ });
+ }
+
+ return anchors;
+}
+
+export function resolveCommentAnchorsById(editor: Editor, commentId: string): CommentAnchor[] {
+ return listCommentAnchors(editor).filter(
+ (anchor) => anchor.commentId === commentId || anchor.importedId === commentId,
+ );
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/index-cache.test.ts b/packages/super-editor/src/document-api-adapters/helpers/index-cache.test.ts
new file mode 100644
index 0000000000..e19b2baa3f
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/index-cache.test.ts
@@ -0,0 +1,122 @@
+import type { Node as ProseMirrorNode } from 'prosemirror-model';
+import type { Editor } from '../../core/Editor.js';
+import { getBlockIndex, getInlineIndex } from './index-cache.js';
+
+function createTextNode(text: string): ProseMirrorNode {
+ return {
+ type: { name: 'text' },
+ attrs: {},
+ marks: [],
+ text,
+ nodeSize: text.length,
+ content: { size: 0 },
+ isText: true,
+ isInline: true,
+ isBlock: false,
+ isLeaf: true,
+ childCount: 0,
+ child() {
+ throw new Error('Text nodes do not have children.');
+ },
+ forEach() {
+ // Text nodes do not expose children.
+ },
+ } as unknown as ProseMirrorNode;
+}
+
+function createParagraphNode(nodeId: string, text = 'Hello'): ProseMirrorNode {
+ const textNode = createTextNode(text);
+ return {
+ type: { name: 'paragraph' },
+ attrs: { sdBlockId: nodeId },
+ marks: [],
+ nodeSize: textNode.nodeSize + 2,
+ content: { size: textNode.nodeSize },
+ isText: false,
+ isInline: false,
+ isBlock: true,
+ inlineContent: true,
+ isTextblock: true,
+ isLeaf: false,
+ childCount: 1,
+ child(index: number) {
+ if (index !== 0) throw new Error('Paragraph has only one child.');
+ return textNode;
+ },
+ forEach(callback: (node: ProseMirrorNode, offset: number) => void) {
+ callback(textNode, 0);
+ },
+ } as unknown as ProseMirrorNode;
+}
+
+function createDocNode(paragraph: ProseMirrorNode): ProseMirrorNode {
+ return {
+ type: { name: 'doc' },
+ attrs: {},
+ marks: [],
+ nodeSize: paragraph.nodeSize + 2,
+ content: { size: paragraph.nodeSize },
+ isText: false,
+ isInline: false,
+ isBlock: false,
+ isLeaf: false,
+ childCount: 1,
+ child(index: number) {
+ if (index !== 0) throw new Error('Doc has only one child.');
+ return paragraph;
+ },
+ forEach(callback: (node: ProseMirrorNode, offset: number) => void) {
+ callback(paragraph, 0);
+ },
+ descendants(callback: (node: ProseMirrorNode, pos: number) => void) {
+ callback(paragraph, 0);
+ },
+ } as unknown as ProseMirrorNode;
+}
+
+function makeEditor(doc: ProseMirrorNode): Editor {
+ return {
+ state: {
+ doc,
+ },
+ } as unknown as Editor;
+}
+
+describe('index-cache', () => {
+ it('reuses block index for the same document snapshot', () => {
+ const editor = makeEditor(createDocNode(createParagraphNode('p1')));
+
+ const first = getBlockIndex(editor);
+ const second = getBlockIndex(editor);
+
+ expect(second).toBe(first);
+ });
+
+ it('lazily builds and reuses inline index for the same document snapshot', () => {
+ const editor = makeEditor(createDocNode(createParagraphNode('p1')));
+
+ const block = getBlockIndex(editor);
+ const firstInline = getInlineIndex(editor);
+ const secondInline = getInlineIndex(editor);
+
+ expect(secondInline).toBe(firstInline);
+ expect(getBlockIndex(editor)).toBe(block);
+ });
+
+ it('invalidates block and inline indexes when the document snapshot changes', () => {
+ const firstDoc = createDocNode(createParagraphNode('p1'));
+ const secondDoc = createDocNode(createParagraphNode('p2'));
+ const editor = makeEditor(firstDoc) as Editor & { state: { doc: ProseMirrorNode } };
+
+ const firstBlock = getBlockIndex(editor);
+ const firstInline = getInlineIndex(editor);
+
+ editor.state.doc = secondDoc;
+
+ const secondBlock = getBlockIndex(editor);
+ const secondInline = getInlineIndex(editor);
+
+ expect(secondBlock).not.toBe(firstBlock);
+ expect(secondInline).not.toBe(firstInline);
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/helpers/index-cache.ts b/packages/super-editor/src/document-api-adapters/helpers/index-cache.ts
new file mode 100644
index 0000000000..55c6aec7e4
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/index-cache.ts
@@ -0,0 +1,65 @@
+import type { Node as ProseMirrorNode } from 'prosemirror-model';
+import type { Editor } from '../../core/Editor.js';
+import { buildInlineIndex, type InlineIndex } from './inline-address-resolver.js';
+import { buildBlockIndex, type BlockIndex } from './node-address-resolver.js';
+
+type CacheEntry = {
+ doc: ProseMirrorNode;
+ blockIndex: BlockIndex;
+ inlineIndex: InlineIndex | null;
+};
+
+const cacheByEditor = new WeakMap();
+
+function createCacheEntry(editor: Editor): CacheEntry {
+ return {
+ doc: editor.state.doc,
+ blockIndex: buildBlockIndex(editor),
+ inlineIndex: null,
+ };
+}
+
+function getCacheEntry(editor: Editor): CacheEntry {
+ const doc = editor.state.doc;
+ const existing = cacheByEditor.get(editor);
+ if (existing && existing.doc === doc) return existing;
+
+ const next = createCacheEntry(editor);
+ cacheByEditor.set(editor, next);
+ return next;
+}
+
+/**
+ * Returns the cached block index for the editor's current document.
+ * Rebuilds automatically when the document snapshot changes.
+ *
+ * @param editor - The editor instance.
+ * @returns The block-level positional index.
+ */
+export function getBlockIndex(editor: Editor): BlockIndex {
+ return getCacheEntry(editor).blockIndex;
+}
+
+/**
+ * Returns the cached inline index for the editor's current document.
+ * Lazily built on first access; rebuilt when the document snapshot changes.
+ *
+ * @param editor - The editor instance.
+ * @returns The inline-level positional index.
+ */
+export function getInlineIndex(editor: Editor): InlineIndex {
+ const entry = getCacheEntry(editor);
+ if (!entry.inlineIndex) {
+ entry.inlineIndex = buildInlineIndex(editor, entry.blockIndex);
+ }
+ return entry.inlineIndex;
+}
+
+/**
+ * Removes cached indexes for the given editor instance.
+ *
+ * @param editor - The editor whose cache entry should be cleared.
+ */
+export function clearIndexCache(editor: Editor): void {
+ cacheByEditor.delete(editor);
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/inline-address-resolver.test.ts b/packages/super-editor/src/document-api-adapters/helpers/inline-address-resolver.test.ts
new file mode 100644
index 0000000000..3242947cf5
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/inline-address-resolver.test.ts
@@ -0,0 +1,229 @@
+import type { Node as ProseMirrorNode, Mark as ProseMirrorMark } from 'prosemirror-model';
+import type { Editor } from '../../core/Editor.js';
+import type { BlockIndex } from './node-address-resolver.js';
+import { buildInlineIndex, findInlineByType } from './inline-address-resolver.js';
+
+function makeMark(name: string, attrs: Record = {}): ProseMirrorMark {
+ return { type: { name }, attrs } as unknown as ProseMirrorMark;
+}
+
+type NodeOptions = {
+ attrs?: Record;
+ marks?: ProseMirrorMark[];
+ text?: string;
+ isInline?: boolean;
+ isBlock?: boolean;
+ isLeaf?: boolean;
+ inlineContent?: boolean;
+};
+
+function createNode(typeName: string, children: ProseMirrorNode[] = [], options: NodeOptions = {}): ProseMirrorNode {
+ const attrs = options.attrs ?? {};
+ const marks = options.marks ?? [];
+ const text = options.text ?? '';
+ const isText = typeName === 'text';
+ const isInline = options.isInline ?? isText;
+ const isBlock = options.isBlock ?? (!isInline && typeName !== 'doc');
+ const inlineContent = options.inlineContent ?? isBlock;
+ const isLeaf = options.isLeaf ?? (isInline && children.length === 0 && !isText);
+
+ const childEntries = children.map((child) => ({ node: child }));
+ let offset = 0;
+ const contentSize = children.reduce((sum, child) => sum + child.nodeSize, 0);
+ const nodeSize = isText ? text.length : isLeaf ? 1 : contentSize + 2;
+
+ return {
+ type: { name: typeName },
+ attrs,
+ marks,
+ text: isText ? text : undefined,
+ nodeSize,
+ content: { size: contentSize },
+ isText,
+ isInline,
+ isBlock,
+ inlineContent,
+ isTextblock: inlineContent,
+ isLeaf,
+ childCount: children.length,
+ child(index: number) {
+ return children[index]!;
+ },
+ forEach(callback: (node: ProseMirrorNode, offset: number) => void) {
+ offset = 0;
+ for (const child of childEntries) {
+ callback(child.node, offset);
+ offset += child.node.nodeSize;
+ }
+ },
+ } as unknown as ProseMirrorNode;
+}
+
+function makeEditor(docNode: ProseMirrorNode): Editor {
+ return { state: { doc: docNode } } as unknown as Editor;
+}
+
+function buildBlockIndexFromParagraph(paragraph: ProseMirrorNode, nodeId: string): BlockIndex {
+ const candidate = {
+ node: paragraph,
+ pos: 0,
+ end: paragraph.nodeSize,
+ nodeType: 'paragraph' as const,
+ nodeId,
+ };
+ const byId = new Map();
+ byId.set(`paragraph:${nodeId}`, candidate);
+ return { candidates: [candidate], byId };
+}
+
+describe('inline-address-resolver', () => {
+ it('builds inline candidates for marks and atoms', () => {
+ const linkMark = makeMark('link', { href: 'https://example.com' });
+ const textNode = createNode('text', [], { text: 'Hi', marks: [linkMark] });
+ const imageNode = createNode('image', [], { isInline: true, isLeaf: true, attrs: { src: 'x' } });
+ const paragraph = createNode('paragraph', [textNode, imageNode], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const editor = makeEditor(doc);
+ const blockIndex = buildBlockIndexFromParagraph(paragraph, 'p1');
+ const inlineIndex = buildInlineIndex(editor, blockIndex);
+
+ const hyperlinks = findInlineByType(inlineIndex, 'hyperlink');
+ expect(hyperlinks).toHaveLength(1);
+ expect(hyperlinks[0]!.anchor.start.offset).toBe(0);
+ expect(hyperlinks[0]!.anchor.end.offset).toBe(2);
+
+ const images = findInlineByType(inlineIndex, 'image');
+ expect(images).toHaveLength(1);
+ expect(images[0]!.anchor.start.offset).toBe(2);
+ expect(images[0]!.anchor.end.offset).toBe(3);
+ });
+
+ it('pairs bookmark start and end nodes into a single anchor', () => {
+ const bookmarkStart = createNode('bookmarkStart', [], {
+ isInline: true,
+ isLeaf: false,
+ attrs: { id: 'b1', name: 'bm' },
+ });
+ const textNode = createNode('text', [], { text: 'A' });
+ const bookmarkEnd = createNode('bookmarkEnd', [], { isInline: true, isLeaf: true, attrs: { id: 'b1' } });
+ const paragraph = createNode('paragraph', [bookmarkStart, textNode, bookmarkEnd], {
+ attrs: { sdBlockId: 'p2' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const editor = makeEditor(doc);
+ const blockIndex = buildBlockIndexFromParagraph(paragraph, 'p2');
+ const inlineIndex = buildInlineIndex(editor, blockIndex);
+
+ const bookmarks = findInlineByType(inlineIndex, 'bookmark');
+ expect(bookmarks).toHaveLength(1);
+ expect(bookmarks[0]!.anchor.start.offset).toBe(0);
+ expect(bookmarks[0]!.anchor.end.offset).toBe(1);
+ });
+
+ it('does not count comment range markers toward comment offsets', () => {
+ const commentStart = createNode('commentRangeStart', [], {
+ isInline: true,
+ isLeaf: true,
+ attrs: { 'w:id': 'c1' },
+ });
+ const textNode = createNode('text', [], { text: 'A' });
+ const commentEnd = createNode('commentRangeEnd', [], {
+ isInline: true,
+ isLeaf: true,
+ attrs: { 'w:id': 'c1' },
+ });
+ const paragraph = createNode('paragraph', [commentStart, textNode, commentEnd], {
+ attrs: { sdBlockId: 'p3' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const editor = makeEditor(doc);
+ const blockIndex = buildBlockIndexFromParagraph(paragraph, 'p3');
+ const inlineIndex = buildInlineIndex(editor, blockIndex);
+
+ const comments = findInlineByType(inlineIndex, 'comment');
+ expect(comments).toHaveLength(1);
+ expect(comments[0]!.anchor.start.offset).toBe(0);
+ expect(comments[0]!.anchor.end.offset).toBe(1);
+ });
+
+ it('does not count bookmark range markers toward subsequent offsets', () => {
+ const bookmarkStart = createNode('bookmarkStart', [], {
+ isInline: true,
+ isLeaf: false,
+ attrs: { id: 'b1', name: 'bm' },
+ });
+ const textA = createNode('text', [], { text: 'A' });
+ const bookmarkEnd = createNode('bookmarkEnd', [], { isInline: true, isLeaf: true, attrs: { id: 'b1' } });
+ const linkMark = makeMark('link', { href: 'https://example.com' });
+ const textB = createNode('text', [], { text: 'B', marks: [linkMark] });
+ const paragraph = createNode('paragraph', [bookmarkStart, textA, bookmarkEnd, textB], {
+ attrs: { sdBlockId: 'p4' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const editor = makeEditor(doc);
+ const blockIndex = buildBlockIndexFromParagraph(paragraph, 'p4');
+ const inlineIndex = buildInlineIndex(editor, blockIndex);
+
+ const bookmarks = findInlineByType(inlineIndex, 'bookmark');
+ expect(bookmarks).toHaveLength(1);
+ expect(bookmarks[0]!.anchor.start.offset).toBe(0);
+ expect(bookmarks[0]!.anchor.end.offset).toBe(1);
+
+ // "B" starts immediately after "A" at offset 1, not 2.
+ const hyperlinks = findInlineByType(inlineIndex, 'hyperlink');
+ expect(hyperlinks).toHaveLength(1);
+ expect(hyperlinks[0]!.anchor.start.offset).toBe(1);
+ expect(hyperlinks[0]!.anchor.end.offset).toBe(2);
+ });
+
+ it('does not count comment range markers toward subsequent offsets', () => {
+ const commentStart = createNode('commentRangeStart', [], {
+ isInline: true,
+ isLeaf: true,
+ attrs: { 'w:id': 'c1' },
+ });
+ const textA = createNode('text', [], { text: 'A' });
+ const commentEnd = createNode('commentRangeEnd', [], {
+ isInline: true,
+ isLeaf: true,
+ attrs: { 'w:id': 'c1' },
+ });
+ const linkMark = makeMark('link', { href: 'https://example.com' });
+ const textB = createNode('text', [], { text: 'B', marks: [linkMark] });
+ const paragraph = createNode('paragraph', [commentStart, textA, commentEnd, textB], {
+ attrs: { sdBlockId: 'p5' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const editor = makeEditor(doc);
+ const blockIndex = buildBlockIndexFromParagraph(paragraph, 'p5');
+ const inlineIndex = buildInlineIndex(editor, blockIndex);
+
+ const comments = findInlineByType(inlineIndex, 'comment');
+ expect(comments).toHaveLength(1);
+ expect(comments[0]!.anchor.start.offset).toBe(0);
+ expect(comments[0]!.anchor.end.offset).toBe(1);
+
+ // "B" starts immediately after "A" at offset 1, not 3.
+ const hyperlinks = findInlineByType(inlineIndex, 'hyperlink');
+ expect(hyperlinks).toHaveLength(1);
+ expect(hyperlinks[0]!.anchor.start.offset).toBe(1);
+ expect(hyperlinks[0]!.anchor.end.offset).toBe(2);
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/helpers/inline-address-resolver.ts b/packages/super-editor/src/document-api-adapters/helpers/inline-address-resolver.ts
new file mode 100644
index 0000000000..daa5d34ba6
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/inline-address-resolver.ts
@@ -0,0 +1,408 @@
+import type { Mark, Node as ProseMirrorNode } from 'prosemirror-model';
+import type { Editor } from '../../core/Editor.js';
+import type { BlockIndex } from './node-address-resolver.js';
+import type { InlineAnchor, InlineNodeType, NodeAddress, NodeType } from '@superdoc/document-api';
+import { CommentMarkName } from '../../extensions/comment/comments-constants.js';
+
+const LINK_MARK_NAME = 'link';
+const COMMENT_MARK_NAME = CommentMarkName;
+
+const SUPPORTED_INLINE_TYPES: ReadonlySet = new Set([
+ 'run',
+ 'bookmark',
+ 'comment',
+ 'hyperlink',
+ 'sdt',
+ 'image',
+ 'footnoteRef',
+ 'tab',
+ 'lineBreak',
+]);
+
+/** A single inline-level element (mark span, atom, or range marker) resolved to block-relative offsets. */
+export type InlineCandidate = {
+ nodeType: InlineNodeType;
+ anchor: InlineAnchor;
+ blockId: string;
+ pos: number;
+ end: number;
+ node?: ProseMirrorNode;
+ mark?: Mark;
+ attrs?: Record;
+};
+
+/** Position-sorted index of inline candidates with type and anchor lookup maps. */
+export type InlineIndex = {
+ candidates: InlineCandidate[];
+ byType: Map;
+ byKey: Map;
+};
+
+/**
+ * Returns `true` if `nodeType` is an inline type recognised by the inline adapter.
+ *
+ * @param nodeType - A node type string.
+ * @returns Whether the type is an {@link InlineNodeType}.
+ */
+export function isInlineQueryType(nodeType: NodeType): nodeType is InlineNodeType {
+ return SUPPORTED_INLINE_TYPES.has(nodeType as InlineNodeType);
+}
+
+function stableStringify(value: unknown): string {
+ if (value == null) return '';
+ if (typeof value !== 'object') return String(value);
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
+ const entries = Object.entries(value as Record).sort(([a], [b]) => a.localeCompare(b));
+ return `{${entries.map(([key, val]) => `${key}:${stableStringify(val)}`).join(',')}}`;
+}
+
+function markKey(mark: Mark): string {
+ return `${mark.type.name}:${stableStringify(mark.attrs ?? {})}`;
+}
+
+function mapInlineNodeType(node: ProseMirrorNode): InlineNodeType | undefined {
+ switch (node.type.name) {
+ case 'run':
+ return 'run';
+ case 'image':
+ return 'image';
+ case 'tab':
+ return 'tab';
+ case 'lineBreak':
+ case 'hardBreak':
+ case 'hard_break':
+ return 'lineBreak';
+ case 'footnoteReference':
+ return 'footnoteRef';
+ case 'structuredContent':
+ return 'sdt';
+ default:
+ return undefined;
+ }
+}
+
+function makeAnchor(blockId: string, start: number, end: number): InlineAnchor {
+ return {
+ start: { blockId, offset: start },
+ end: { blockId, offset: end },
+ };
+}
+
+function inlineKey(nodeType: InlineNodeType, anchor: InlineAnchor): string {
+ return `${nodeType}:${anchor.start.blockId}:${anchor.start.offset}:${anchor.end.offset}`;
+}
+
+type ActiveMark = {
+ mark: Mark;
+ startOffset: number;
+ startPos: number;
+};
+
+/**
+ * Mutable state carried through the block-content walker.
+ *
+ * **Purpose**: Walks ProseMirror block content to build an index of inline
+ * elements (marks, atoms, range markers) with block-relative text offsets.
+ *
+ * **Offset model**: Text nodes contribute their UTF-16 length. Leaf atoms
+ * (images, tabs, breaks) contribute 1. Block separators (between sibling
+ * blocks) contribute 1. Zero-width range delimiters (bookmarkEnd,
+ * commentRangeStart, commentRangeEnd) contribute 0. This mirrors ProseMirror's
+ * `textBetween(from, to, '\n', '\ufffc')` model.
+ *
+ * **Mark lifecycle**: `syncMarks()` opens/closes mark spans when the active
+ * mark set changes between nodes. All open marks auto-close at block
+ * boundaries via `closeAllMarks()`.
+ *
+ * **Range markers**: Bookmark and comment ranges use start/end element pairs.
+ * Starts are buffered in maps (`bookmarkStarts`, `commentRangeStarts`); ends
+ * close the range and emit a candidate. Unpaired starts/ends are discarded.
+ *
+ * **Deduplication**: Comments found via marks and via range markers are
+ * deduplicated via `commentIdsWithMarks` — marks take priority.
+ */
+export type BlockWalkState = {
+ blockId: string;
+ offset: number;
+ candidates: InlineCandidate[];
+ activeMarks: Map;
+ bookmarkStarts: Map }>;
+ commentRangeStarts: Map }>;
+ commentIdsWithMarks: Set;
+};
+
+function createBlockWalkState(blockId: string, candidates: InlineCandidate[]): BlockWalkState {
+ return {
+ blockId,
+ offset: 0,
+ candidates,
+ activeMarks: new Map(),
+ bookmarkStarts: new Map(),
+ commentRangeStarts: new Map(),
+ commentIdsWithMarks: new Set(),
+ };
+}
+
+function isInlineHost(node: ProseMirrorNode): boolean {
+ return (
+ Boolean((node as unknown as { inlineContent?: boolean }).inlineContent) ||
+ Boolean((node as unknown as { isTextblock?: boolean }).isTextblock)
+ );
+}
+
+function relevantMarks(marks: readonly Mark[] | null | undefined): Mark[] {
+ if (!marks?.length) return [];
+ return marks.filter((mark) => mark.type?.name === LINK_MARK_NAME || mark.type?.name === COMMENT_MARK_NAME);
+}
+
+function closeMarkSpan(state: BlockWalkState, key: string, endOffset: number, endPos: number): void {
+ const active = state.activeMarks.get(key);
+ if (!active) return;
+ state.activeMarks.delete(key);
+ if (endOffset <= active.startOffset) return;
+
+ const markType = active.mark.type?.name;
+ const nodeType: InlineNodeType | undefined =
+ markType === LINK_MARK_NAME ? 'hyperlink' : markType === COMMENT_MARK_NAME ? 'comment' : undefined;
+ if (!nodeType) return;
+
+ const attrs = (active.mark.attrs ?? {}) as Record;
+ if (nodeType === 'comment') {
+ const commentId =
+ typeof attrs.commentId === 'string'
+ ? attrs.commentId
+ : typeof attrs.importedId === 'string'
+ ? attrs.importedId
+ : undefined;
+ if (commentId) state.commentIdsWithMarks.add(commentId);
+ }
+
+ state.candidates.push({
+ nodeType,
+ anchor: makeAnchor(state.blockId, active.startOffset, endOffset),
+ blockId: state.blockId,
+ pos: active.startPos,
+ end: endPos,
+ mark: active.mark,
+ attrs,
+ });
+}
+
+function closeAllMarks(state: BlockWalkState, endPos: number): void {
+ for (const key of Array.from(state.activeMarks.keys())) {
+ closeMarkSpan(state, key, state.offset, endPos);
+ }
+}
+
+function syncMarks(state: BlockWalkState, marks: readonly Mark[] | null | undefined, docPos: number): void {
+ const marksOfInterest = relevantMarks(marks);
+ const nextKeys = new Set();
+ for (const mark of marksOfInterest) {
+ const key = markKey(mark);
+ nextKeys.add(key);
+ if (!state.activeMarks.has(key)) {
+ state.activeMarks.set(key, { mark, startOffset: state.offset, startPos: docPos });
+ }
+ }
+
+ for (const [key] of state.activeMarks.entries()) {
+ if (!nextKeys.has(key)) {
+ closeMarkSpan(state, key, state.offset, docPos);
+ }
+ }
+}
+
+function handleBookmarkStart(state: BlockWalkState, node: ProseMirrorNode, docPos: number): void {
+ const attrs = (node.attrs ?? {}) as Record;
+ const id = typeof attrs.id === 'string' ? attrs.id : typeof attrs.name === 'string' ? attrs.name : undefined;
+ if (!id) return;
+ state.bookmarkStarts.set(id, { offset: state.offset, pos: docPos, attrs });
+}
+
+function handleBookmarkEnd(state: BlockWalkState, node: ProseMirrorNode, docPos: number): void {
+ const attrs = (node.attrs ?? {}) as Record;
+ const id = typeof attrs.id === 'string' ? attrs.id : undefined;
+ if (!id) return;
+ const start = state.bookmarkStarts.get(id);
+ if (!start) return;
+ state.bookmarkStarts.delete(id);
+ if (state.offset < start.offset) return;
+ state.candidates.push({
+ nodeType: 'bookmark',
+ anchor: makeAnchor(state.blockId, start.offset, state.offset),
+ blockId: state.blockId,
+ pos: start.pos,
+ end: docPos,
+ attrs: start.attrs,
+ });
+}
+
+function handleCommentRangeStart(state: BlockWalkState, node: ProseMirrorNode, docPos: number): void {
+ const attrs = (node.attrs ?? {}) as Record;
+ const id = typeof attrs['w:id'] === 'string' ? (attrs['w:id'] as string) : undefined;
+ if (!id) return;
+ state.commentRangeStarts.set(id, { offset: state.offset, pos: docPos, attrs });
+}
+
+function handleCommentRangeEnd(state: BlockWalkState, node: ProseMirrorNode, docPos: number): void {
+ const attrs = (node.attrs ?? {}) as Record;
+ const id = typeof attrs['w:id'] === 'string' ? (attrs['w:id'] as string) : undefined;
+ if (!id) return;
+ if (state.commentIdsWithMarks.has(id)) return;
+ const start = state.commentRangeStarts.get(id);
+ if (!start) return;
+ state.commentRangeStarts.delete(id);
+ if (state.offset < start.offset) return;
+ state.candidates.push({
+ nodeType: 'comment',
+ anchor: makeAnchor(state.blockId, start.offset, state.offset),
+ blockId: state.blockId,
+ pos: start.pos,
+ end: docPos,
+ attrs: { ...start.attrs, ...attrs },
+ });
+}
+
+function walkNodeContent(state: BlockWalkState, node: ProseMirrorNode, contentStart: number): void {
+ let firstChild = true;
+ node.forEach((child: ProseMirrorNode, childOffset: number) => {
+ const childDocPos = contentStart + childOffset;
+ if (child.isBlock && !firstChild) {
+ closeAllMarks(state, childDocPos);
+ state.offset += 1;
+ }
+ walkNode(state, child, childDocPos);
+ firstChild = false;
+ });
+}
+
+function walkNode(state: BlockWalkState, node: ProseMirrorNode, docPos: number): void {
+ const isBookmarkStart = node.type?.name === 'bookmarkStart';
+ if (isBookmarkStart) {
+ handleBookmarkStart(state, node, docPos);
+ }
+
+ if (node.isText) {
+ const text = node.text ?? '';
+ syncMarks(state, node.marks, docPos);
+ state.offset += text.length;
+ return;
+ }
+
+ if (node.isLeaf) {
+ syncMarks(state, node.marks, docPos);
+
+ if (node.type?.name === 'bookmarkEnd') {
+ handleBookmarkEnd(state, node, docPos);
+ return; // Zero-width range delimiter — no text offset contribution.
+ } else if (node.type?.name === 'commentRangeStart') {
+ handleCommentRangeStart(state, node, docPos);
+ return; // Zero-width range delimiter — no text offset contribution.
+ } else if (node.type?.name === 'commentRangeEnd') {
+ handleCommentRangeEnd(state, node, docPos);
+ return; // Zero-width range delimiter — no text offset contribution.
+ } else if (!isBookmarkStart) {
+ const nodeType = mapInlineNodeType(node);
+ if (nodeType) {
+ state.candidates.push({
+ nodeType,
+ anchor: makeAnchor(state.blockId, state.offset, state.offset + 1),
+ blockId: state.blockId,
+ pos: docPos,
+ end: docPos + node.nodeSize,
+ node,
+ });
+ }
+ }
+
+ state.offset += 1;
+ return;
+ }
+
+ if (node.isInline) {
+ const nodeType = mapInlineNodeType(node);
+ const startOffset = state.offset;
+ const startPos = docPos;
+ walkNodeContent(state, node, docPos + 1);
+ const endOffset = state.offset;
+ const endPos = docPos + node.nodeSize;
+
+ if (nodeType) {
+ state.candidates.push({
+ nodeType,
+ anchor: makeAnchor(state.blockId, startOffset, endOffset),
+ blockId: state.blockId,
+ pos: startPos,
+ end: endPos,
+ node,
+ });
+ }
+ return;
+ }
+
+ walkNodeContent(state, node, docPos + 1);
+}
+
+function buildIndexMaps(candidates: InlineCandidate[]): InlineIndex {
+ const byType = new Map();
+ const byKey = new Map();
+
+ for (const candidate of candidates) {
+ if (!byType.has(candidate.nodeType)) {
+ byType.set(candidate.nodeType, []);
+ }
+ byType.get(candidate.nodeType)!.push(candidate);
+ byKey.set(inlineKey(candidate.nodeType, candidate.anchor), candidate);
+ }
+
+ return { candidates, byType, byKey };
+}
+
+/**
+ * Walks all inline-hosting blocks and builds an index of inline-level nodes
+ * (marks, atoms, and range markers).
+ *
+ * @param editor - The editor instance to inspect.
+ * @param blockIndex - A pre-built block index to iterate over.
+ * @returns An {@link InlineIndex} with sorted candidates and lookup maps.
+ */
+export function buildInlineIndex(editor: Editor, blockIndex: BlockIndex): InlineIndex {
+ const candidates: InlineCandidate[] = [];
+
+ for (const block of blockIndex.candidates) {
+ if (!isInlineHost(block.node)) continue;
+
+ const state = createBlockWalkState(block.nodeId, candidates);
+ walkNodeContent(state, block.node, block.pos + 1);
+ closeAllMarks(state, block.pos + block.node.nodeSize);
+ }
+
+ candidates.sort((a, b) => (a.pos === b.pos ? a.end - b.end : a.pos - b.pos));
+ return buildIndexMaps(candidates);
+}
+
+/**
+ * Looks up an inline candidate by its {@link NodeAddress} anchor.
+ *
+ * @param index - The inline index to search.
+ * @param address - The inline address to resolve.
+ * @returns The matching candidate, or `undefined` if not found.
+ */
+export function findInlineByAnchor(index: InlineIndex, address: NodeAddress): InlineCandidate | undefined {
+ if (address.kind !== 'inline') return undefined;
+ if (address.anchor.start.blockId !== address.anchor.end.blockId) return undefined;
+ const nodeType = address.nodeType as InlineNodeType;
+ return index.byKey.get(inlineKey(nodeType, address.anchor));
+}
+
+/**
+ * Returns all inline candidates matching a given type, or all candidates if no type is specified.
+ *
+ * @param index - The inline index to search.
+ * @param nodeType - Optional inline node type to filter by.
+ * @returns Matching inline candidates.
+ */
+export function findInlineByType(index: InlineIndex, nodeType?: InlineNodeType): InlineCandidate[] {
+ if (!nodeType) return index.candidates;
+ return index.byType.get(nodeType) ?? [];
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/list-item-resolver.test.ts b/packages/super-editor/src/document-api-adapters/helpers/list-item-resolver.test.ts
new file mode 100644
index 0000000000..9736d81d89
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/list-item-resolver.test.ts
@@ -0,0 +1,169 @@
+import { describe, expect, it } from 'vitest';
+import type { Editor } from '../../core/Editor.js';
+import { listListItems, resolveListItem } from './list-item-resolver.js';
+
+type MockParagraphOptions = {
+ id: string;
+ text?: string;
+ numId?: number;
+ ilvl?: number;
+ markerText?: string;
+ path?: number[];
+ numberingType?: string;
+};
+
+type MockNode = {
+ type: { name: string };
+ attrs: Record;
+ nodeSize: number;
+ isBlock: boolean;
+ textContent: string;
+};
+
+function makeParagraph(options: MockParagraphOptions): MockNode {
+ const text = options.text ?? '';
+ const numberingProperties =
+ options.numId != null
+ ? {
+ numId: options.numId,
+ ilvl: options.ilvl ?? 0,
+ }
+ : undefined;
+
+ return {
+ type: { name: 'paragraph' },
+ attrs: {
+ paraId: options.id,
+ paragraphProperties: numberingProperties ? { numberingProperties } : {},
+ listRendering:
+ options.numId != null
+ ? {
+ markerText: options.markerText ?? '',
+ path: options.path ?? [],
+ numberingType: options.numberingType,
+ }
+ : null,
+ },
+ nodeSize: Math.max(2, text.length + 2),
+ isBlock: true,
+ textContent: text,
+ };
+}
+
+function makeDoc(children: MockNode[]) {
+ return {
+ content: {
+ size: children.reduce((sum, child) => sum + child.nodeSize, 0),
+ },
+ descendants(callback: (node: MockNode, pos: number) => void) {
+ let pos = 0;
+ for (const child of children) {
+ callback(child, pos);
+ pos += child.nodeSize;
+ }
+ return undefined;
+ },
+ };
+}
+
+function makeEditor(children: MockNode[]): Editor {
+ return {
+ state: {
+ doc: makeDoc(children),
+ },
+ converter: {
+ numbering: { definitions: {}, abstracts: {} },
+ },
+ } as unknown as Editor;
+}
+
+describe('list-item-resolver', () => {
+ it('lists paragraph-based list items with paragraph node ids', () => {
+ const editor = makeEditor([
+ makeParagraph({
+ id: 'li-1',
+ text: 'First',
+ numId: 1,
+ ilvl: 0,
+ markerText: '1.',
+ path: [1],
+ numberingType: 'decimal',
+ }),
+ makeParagraph({
+ id: 'li-2',
+ text: 'Second',
+ numId: 1,
+ ilvl: 0,
+ markerText: '2.',
+ path: [2],
+ numberingType: 'decimal',
+ }),
+ makeParagraph({ id: 'p-3', text: 'Plain paragraph' }),
+ ]);
+
+ const result = listListItems(editor);
+ expect(result.total).toBe(2);
+ expect(result.matches.map((match) => match.nodeId)).toEqual(['li-1', 'li-2']);
+ expect(result.items[0]?.kind).toBe('ordered');
+ expect(result.items[0]?.ordinal).toBe(1);
+ expect(result.items[1]?.ordinal).toBe(2);
+ });
+
+ it('applies inclusive within scope when within itself is a list item', () => {
+ const editor = makeEditor([
+ makeParagraph({
+ id: 'li-1',
+ text: 'First',
+ numId: 1,
+ ilvl: 0,
+ markerText: '1.',
+ path: [1],
+ numberingType: 'decimal',
+ }),
+ makeParagraph({
+ id: 'li-2',
+ text: 'Second',
+ numId: 1,
+ ilvl: 0,
+ markerText: '2.',
+ path: [2],
+ numberingType: 'decimal',
+ }),
+ ]);
+
+ const result = listListItems(editor, {
+ within: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ });
+
+ expect(result.total).toBe(1);
+ expect(result.matches[0]?.nodeId).toBe('li-1');
+ });
+
+ it('throws TARGET_NOT_FOUND when resolving a stale list address', () => {
+ const editor = makeEditor([
+ makeParagraph({ id: 'li-1', numId: 1, markerText: '1.', path: [1], numberingType: 'decimal' }),
+ ]);
+
+ expect(() =>
+ resolveListItem(editor, {
+ kind: 'block',
+ nodeType: 'listItem',
+ nodeId: 'missing',
+ }),
+ ).toThrow('List item target was not found');
+ });
+
+ it('throws INVALID_TARGET for ambiguous list ids', () => {
+ const editor = makeEditor([
+ makeParagraph({ id: 'dup', numId: 1, markerText: '1.', path: [1], numberingType: 'decimal' }),
+ makeParagraph({ id: 'dup', numId: 2, markerText: '1.', path: [1], numberingType: 'decimal' }),
+ ]);
+
+ try {
+ resolveListItem(editor, { kind: 'block', nodeType: 'listItem', nodeId: 'dup' });
+ throw new Error('expected resolver to throw');
+ } catch (error) {
+ expect((error as { code?: string }).code).toBe('INVALID_TARGET');
+ }
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/helpers/list-item-resolver.ts b/packages/super-editor/src/document-api-adapters/helpers/list-item-resolver.ts
new file mode 100644
index 0000000000..43b8bcbfb7
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/list-item-resolver.ts
@@ -0,0 +1,233 @@
+import { ListHelpers } from '@helpers/list-numbering-helpers.js';
+import type { Editor } from '../../core/Editor.js';
+import type { BlockNodeAddress, ListItemAddress, ListItemInfo, ListKind, ListsListQuery } from '@superdoc/document-api';
+import { DocumentApiAdapterError } from '../errors.js';
+import { getBlockIndex } from './index-cache.js';
+import type { BlockCandidate, BlockIndex } from './node-address-resolver.js';
+import { toFiniteNumber } from './value-utils.js';
+
+export type ListItemProjection = {
+ candidate: BlockCandidate;
+ address: ListItemAddress;
+ numId?: number;
+ level?: number;
+ marker?: string;
+ path?: number[];
+ ordinal?: number;
+ kind?: ListKind;
+ text?: string;
+};
+
+function toPath(value: unknown): number[] | undefined {
+ if (!Array.isArray(value)) return undefined;
+ const parsed = value.map((entry) => toFiniteNumber(entry)).filter((entry): entry is number => entry != null);
+ return parsed.length > 0 ? parsed : undefined;
+}
+
+function getNumberingProperties(node: BlockCandidate['node']): { numId?: number; level?: number } {
+ const attrs = (node.attrs ?? {}) as {
+ paragraphProperties?: { numberingProperties?: { numId?: unknown; ilvl?: unknown } | null } | null;
+ numberingProperties?: { numId?: unknown; ilvl?: unknown } | null;
+ };
+
+ const paragraphNumbering = attrs.paragraphProperties?.numberingProperties ?? undefined;
+ const fallbackNumbering = attrs.numberingProperties ?? undefined;
+ const numId = toFiniteNumber(paragraphNumbering?.numId ?? fallbackNumbering?.numId);
+ const level = toFiniteNumber(paragraphNumbering?.ilvl ?? fallbackNumbering?.ilvl);
+ return { numId, level };
+}
+
+function deriveListKindFromDefinitions(editor: Editor, numId?: number, level?: number): ListKind | undefined {
+ if (numId == null || level == null || !editor.converter) return undefined;
+ try {
+ const details = ListHelpers.getListDefinitionDetails({ numId, level, editor });
+ const numberingType = typeof details?.listNumberingType === 'string' ? details.listNumberingType : undefined;
+ if (numberingType === 'bullet') return 'bullet';
+ if (typeof numberingType === 'string' && numberingType.length > 0) return 'ordered';
+ return undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+function deriveListKind(
+ editor: Editor,
+ candidate: BlockCandidate,
+ numId?: number,
+ level?: number,
+): ListKind | undefined {
+ const listRendering = (candidate.node.attrs ?? {}) as {
+ listRendering?: {
+ numberingType?: unknown;
+ } | null;
+ };
+ const numberingType = listRendering.listRendering?.numberingType;
+ if (numberingType === 'bullet') return 'bullet';
+ if (typeof numberingType === 'string' && numberingType.length > 0) return 'ordered';
+ return deriveListKindFromDefinitions(editor, numId, level);
+}
+
+function getListText(candidate: BlockCandidate): string | undefined {
+ const text = (candidate.node as { textContent?: unknown }).textContent;
+ return typeof text === 'string' ? text : undefined;
+}
+
+export function projectListItemCandidate(editor: Editor, candidate: BlockCandidate): ListItemProjection {
+ const attrs = (candidate.node.attrs ?? {}) as {
+ listRendering?: {
+ markerText?: unknown;
+ path?: unknown;
+ } | null;
+ };
+
+ const { numId, level } = getNumberingProperties(candidate.node);
+ const path = toPath(attrs.listRendering?.path);
+ const ordinal = path?.length ? path[path.length - 1] : undefined;
+ const marker = typeof attrs.listRendering?.markerText === 'string' ? attrs.listRendering.markerText : undefined;
+
+ return {
+ candidate,
+ address: {
+ kind: 'block',
+ nodeType: 'listItem',
+ nodeId: candidate.nodeId,
+ },
+ numId,
+ level,
+ kind: deriveListKind(editor, candidate, numId, level),
+ marker,
+ path,
+ ordinal,
+ text: getListText(candidate),
+ };
+}
+
+export function listItemProjectionToInfo(projection: ListItemProjection): ListItemInfo {
+ return {
+ address: projection.address,
+ marker: projection.marker,
+ ordinal: projection.ordinal,
+ path: projection.path,
+ level: projection.level,
+ kind: projection.kind,
+ text: projection.text,
+ };
+}
+
+function matchesListQuery(projection: ListItemProjection, query?: ListsListQuery): boolean {
+ if (!query) return true;
+ if (query.kind && projection.kind !== query.kind) return false;
+ if (query.level != null && projection.level !== query.level) return false;
+ if (query.ordinal != null && projection.ordinal !== query.ordinal) return false;
+ return true;
+}
+
+export function resolveBlockScopeRange(
+ index: BlockIndex,
+ within?: BlockNodeAddress,
+): { start: number; end: number } | undefined {
+ if (!within) return undefined;
+
+ const matches = index.candidates.filter(
+ (candidate) => candidate.nodeType === within.nodeType && candidate.nodeId === within.nodeId,
+ );
+ if (matches.length === 0) {
+ throw new DocumentApiAdapterError('TARGET_NOT_FOUND', 'List scope block was not found.', {
+ within,
+ });
+ }
+ if (matches.length > 1) {
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'List scope block id is ambiguous.', {
+ within,
+ count: matches.length,
+ });
+ }
+
+ return {
+ start: matches[0]!.pos,
+ end: matches[0]!.end,
+ };
+}
+
+function isWithinScope(candidate: BlockCandidate, scope: { start: number; end: number } | undefined): boolean {
+ if (!scope) return true;
+ return candidate.pos >= scope.start && candidate.end <= scope.end;
+}
+
+function listItemCandidatesInScope(
+ index: BlockIndex,
+ scope: { start: number; end: number } | undefined,
+): BlockCandidate[] {
+ return index.candidates.filter((candidate) => candidate.nodeType === 'listItem' && isWithinScope(candidate, scope));
+}
+
+export function buildListItemIndex(editor: Editor): { index: BlockIndex; items: ListItemProjection[] } {
+ const index = getBlockIndex(editor);
+ const items = index.candidates
+ .filter((candidate) => candidate.nodeType === 'listItem')
+ .map((candidate) => projectListItemCandidate(editor, candidate));
+ return { index, items };
+}
+
+export function listListItems(
+ editor: Editor,
+ query?: ListsListQuery,
+): { matches: ListItemAddress[]; total: number; items: ListItemInfo[] } {
+ if (query?.within && query.within.kind !== 'block') {
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'lists.list only supports block within scopes.', {
+ within: query.within,
+ });
+ }
+
+ const index = getBlockIndex(editor);
+ const scope = resolveBlockScopeRange(index, query?.within as BlockNodeAddress | undefined);
+ const candidates = listItemCandidatesInScope(index, scope);
+ const safeOffset = Math.max(0, query?.offset ?? 0);
+ const safeLimit = Math.max(0, query?.limit ?? Number.POSITIVE_INFINITY);
+ const pageEnd = safeOffset + safeLimit;
+
+ let total = 0;
+ const infos: ListItemInfo[] = [];
+ const matches: ListItemAddress[] = [];
+
+ for (const candidate of candidates) {
+ const projection = projectListItemCandidate(editor, candidate);
+ if (!matchesListQuery(projection, query)) continue;
+
+ const currentIndex = total;
+ total += 1;
+ if (currentIndex < safeOffset || currentIndex >= pageEnd) continue;
+
+ const info = listItemProjectionToInfo(projection);
+ infos.push(info);
+ matches.push(info.address);
+ }
+
+ return {
+ matches,
+ total,
+ items: infos,
+ };
+}
+
+export function resolveListItem(editor: Editor, address: ListItemAddress): ListItemProjection {
+ const index = getBlockIndex(editor);
+ const matches = index.candidates.filter(
+ (candidate) => candidate.nodeType === 'listItem' && candidate.nodeId === address.nodeId,
+ );
+
+ if (matches.length === 0) {
+ throw new DocumentApiAdapterError('TARGET_NOT_FOUND', 'List item target was not found.', {
+ target: address,
+ });
+ }
+
+ if (matches.length > 1) {
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'List item target id is ambiguous.', {
+ target: address,
+ count: matches.length,
+ });
+ }
+
+ return projectListItemCandidate(editor, matches[0]!);
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/mutation-helpers.test.ts b/packages/super-editor/src/document-api-adapters/helpers/mutation-helpers.test.ts
new file mode 100644
index 0000000000..38589f81a0
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/mutation-helpers.test.ts
@@ -0,0 +1,128 @@
+import { DocumentApiAdapterError } from '../errors.js';
+import {
+ requireEditorCommand,
+ requireSchemaMark,
+ ensureTrackedCapability,
+ rejectTrackedMode,
+} from './mutation-helpers.js';
+
+function makeEditor(overrides: Record = {}): any {
+ return {
+ commands: {},
+ schema: { marks: {} },
+ options: {},
+ ...overrides,
+ };
+}
+
+describe('requireEditorCommand', () => {
+ it('returns the command when present', () => {
+ const command = () => true;
+ expect(requireEditorCommand(command, 'test')).toBe(command);
+ });
+
+ it('throws CAPABILITY_UNAVAILABLE with reason: missing_command when absent', () => {
+ expect(() => requireEditorCommand(undefined, 'test.op')).toThrow(DocumentApiAdapterError);
+ try {
+ requireEditorCommand(undefined, 'test.op');
+ } catch (error) {
+ const err = error as DocumentApiAdapterError;
+ expect(err.code).toBe('CAPABILITY_UNAVAILABLE');
+ expect(err.details).toEqual({ reason: 'missing_command' });
+ expect(err.message).toContain('test.op');
+ }
+ });
+});
+
+describe('requireSchemaMark', () => {
+ it('returns the mark type when present', () => {
+ const boldMark = { name: 'bold' };
+ const editor = makeEditor({ schema: { marks: { bold: boldMark } } });
+ expect(requireSchemaMark(editor, 'bold', 'format.bold')).toBe(boldMark);
+ });
+
+ it('throws CAPABILITY_UNAVAILABLE with reason: missing_mark when absent', () => {
+ const editor = makeEditor();
+ expect(() => requireSchemaMark(editor, 'bold', 'format.bold')).toThrow(DocumentApiAdapterError);
+ try {
+ requireSchemaMark(editor, 'bold', 'format.bold');
+ } catch (error) {
+ const err = error as DocumentApiAdapterError;
+ expect(err.code).toBe('CAPABILITY_UNAVAILABLE');
+ expect(err.details).toEqual({ reason: 'missing_mark', markName: 'bold' });
+ }
+ });
+});
+
+describe('ensureTrackedCapability', () => {
+ it('does not throw when all prerequisites are met', () => {
+ const editor = makeEditor({
+ commands: { insertTrackedChange: () => true },
+ schema: { marks: { trackFormat: {} } },
+ options: { user: { name: 'test' } },
+ });
+ expect(() => ensureTrackedCapability(editor, { operation: 'test', requireMarks: ['trackFormat'] })).not.toThrow();
+ });
+
+ it('throws with reason: missing_command when insertTrackedChange is missing', () => {
+ const editor = makeEditor({ options: { user: { name: 'test' } } });
+ try {
+ ensureTrackedCapability(editor, { operation: 'test.op' });
+ throw new Error('expected throw');
+ } catch (error) {
+ const err = error as DocumentApiAdapterError;
+ expect(err.code).toBe('CAPABILITY_UNAVAILABLE');
+ expect(err.details).toEqual({ reason: 'missing_command' });
+ }
+ });
+
+ it('throws with reason: missing_mark when a required mark is missing', () => {
+ const editor = makeEditor({
+ commands: { insertTrackedChange: () => true },
+ options: { user: { name: 'test' } },
+ });
+ try {
+ ensureTrackedCapability(editor, { operation: 'test.op', requireMarks: ['trackFormat'] });
+ throw new Error('expected throw');
+ } catch (error) {
+ const err = error as DocumentApiAdapterError;
+ expect(err.code).toBe('CAPABILITY_UNAVAILABLE');
+ expect(err.details).toEqual({ reason: 'missing_mark', markName: 'trackFormat' });
+ }
+ });
+
+ it('throws with reason: missing_user when user is not configured', () => {
+ const editor = makeEditor({
+ commands: { insertTrackedChange: () => true },
+ });
+ try {
+ ensureTrackedCapability(editor, { operation: 'test.op' });
+ throw new Error('expected throw');
+ } catch (error) {
+ const err = error as DocumentApiAdapterError;
+ expect(err.code).toBe('CAPABILITY_UNAVAILABLE');
+ expect(err.details).toEqual({ reason: 'missing_user' });
+ }
+ });
+});
+
+describe('rejectTrackedMode', () => {
+ it('does not throw for direct mode', () => {
+ expect(() => rejectTrackedMode('test.op', { changeMode: 'direct' })).not.toThrow();
+ });
+
+ it('does not throw when options are undefined', () => {
+ expect(() => rejectTrackedMode('test.op')).not.toThrow();
+ });
+
+ it('throws CAPABILITY_UNAVAILABLE for tracked mode', () => {
+ try {
+ rejectTrackedMode('test.op', { changeMode: 'tracked' });
+ throw new Error('expected throw');
+ } catch (error) {
+ const err = error as DocumentApiAdapterError;
+ expect(err.code).toBe('CAPABILITY_UNAVAILABLE');
+ expect(err.details).toEqual({ reason: 'tracked_mode_unsupported' });
+ }
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/helpers/mutation-helpers.ts b/packages/super-editor/src/document-api-adapters/helpers/mutation-helpers.ts
new file mode 100644
index 0000000000..f5786f223a
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/mutation-helpers.ts
@@ -0,0 +1,79 @@
+import type { MarkType } from 'prosemirror-model';
+import type { MutationOptions } from '@superdoc/document-api';
+import type { Editor } from '../../core/Editor.js';
+import { DocumentApiAdapterError } from '../errors.js';
+
+/**
+ * Validates that an editor command exists and returns it.
+ *
+ * @throws {DocumentApiAdapterError} `CAPABILITY_UNAVAILABLE` with `reason: 'missing_command'`.
+ */
+export function requireEditorCommand(command: T | undefined, operationName: string): NonNullable {
+ if (typeof command === 'function') return command as NonNullable;
+ throw new DocumentApiAdapterError('CAPABILITY_UNAVAILABLE', `${operationName} command is not available.`, {
+ reason: 'missing_command',
+ });
+}
+
+/**
+ * Validates that a schema mark exists and returns it.
+ *
+ * @throws {DocumentApiAdapterError} `CAPABILITY_UNAVAILABLE` with `reason: 'missing_mark'`.
+ */
+export function requireSchemaMark(editor: Editor, markName: string, operationName: string): MarkType {
+ const mark = editor.schema?.marks?.[markName];
+ if (mark) return mark;
+ throw new DocumentApiAdapterError('CAPABILITY_UNAVAILABLE', `${operationName} requires the "${markName}" mark.`, {
+ reason: 'missing_mark',
+ markName,
+ });
+}
+
+/**
+ * Validates all tracked-mode prerequisites: insertTrackedChange command,
+ * optional required marks, and a configured user.
+ *
+ * @throws {DocumentApiAdapterError} `CAPABILITY_UNAVAILABLE` with a `reason` detail
+ * of `'missing_command'`, `'missing_mark'`, or `'missing_user'`.
+ */
+export function ensureTrackedCapability(editor: Editor, config: { operation: string; requireMarks?: string[] }): void {
+ if (typeof editor.commands?.insertTrackedChange !== 'function') {
+ throw new DocumentApiAdapterError(
+ 'CAPABILITY_UNAVAILABLE',
+ `${config.operation} requires the insertTrackedChange command.`,
+ { reason: 'missing_command' },
+ );
+ }
+
+ if (config.requireMarks) {
+ for (const markName of config.requireMarks) {
+ if (!editor.schema?.marks?.[markName]) {
+ throw new DocumentApiAdapterError(
+ 'CAPABILITY_UNAVAILABLE',
+ `${config.operation} requires the "${markName}" mark in the schema.`,
+ { reason: 'missing_mark', markName },
+ );
+ }
+ }
+ }
+
+ if (!editor.options.user) {
+ throw new DocumentApiAdapterError(
+ 'CAPABILITY_UNAVAILABLE',
+ `${config.operation} requires a user to be configured on the editor instance.`,
+ { reason: 'missing_user' },
+ );
+ }
+}
+
+/**
+ * Rejects tracked mode for adapters that do not support it yet.
+ *
+ * @throws {DocumentApiAdapterError} `CAPABILITY_UNAVAILABLE` with `reason: 'tracked_mode_unsupported'`.
+ */
+export function rejectTrackedMode(operation: string, options?: MutationOptions): void {
+ if ((options?.changeMode ?? 'direct') === 'direct') return;
+ throw new DocumentApiAdapterError('CAPABILITY_UNAVAILABLE', `${operation} does not support tracked mode.`, {
+ reason: 'tracked_mode_unsupported',
+ });
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/node-address-resolver.test.ts b/packages/super-editor/src/document-api-adapters/helpers/node-address-resolver.test.ts
new file mode 100644
index 0000000000..4c30e43e92
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/node-address-resolver.test.ts
@@ -0,0 +1,543 @@
+import type { Node as ProseMirrorNode } from 'prosemirror-model';
+import type { Editor } from '../../core/Editor.js';
+import type { NodeAddress } from '@superdoc/document-api';
+import {
+ buildBlockIndex,
+ findBlockById,
+ findBlockByPos,
+ isSupportedNodeType,
+ toBlockAddress,
+ type BlockCandidate,
+ type BlockIndex,
+} from './node-address-resolver.js';
+
+// ---------------------------------------------------------------------------
+// Helpers — lightweight ProseMirror-like stubs
+// ---------------------------------------------------------------------------
+
+/**
+ * Creates a minimal ProseMirrorNode stub.
+ *
+ * `children` is a flat list of `{ node, offset }` pairs where `offset` is the
+ * **absolute** document position of the child — matching how ProseMirror's
+ * `descendants` callback provides positions.
+ */
+function makeNode(
+ typeName: string,
+ attrs: Record = {},
+ nodeSize = 10,
+ children: Array<{ node: ProseMirrorNode; offset: number }> = [],
+): ProseMirrorNode {
+ const inlineTypes = new Set(['image', 'run', 'bookmarkStart', 'bookmarkEnd', 'commentRangeStart', 'commentRangeEnd']);
+ const isBlock = typeName !== 'doc' && !inlineTypes.has(typeName);
+ return {
+ type: { name: typeName },
+ attrs,
+ nodeSize,
+ isBlock,
+ descendants(callback: (node: ProseMirrorNode, pos: number) => void) {
+ for (const child of children) {
+ callback(child.node, child.offset);
+ }
+ },
+ } as unknown as ProseMirrorNode;
+}
+
+function makeEditor(docNode: ProseMirrorNode): Editor {
+ return { state: { doc: docNode } } as unknown as Editor;
+}
+
+function indexFromNodes(
+ ...entries: Array<{ typeName: string; attrs?: Record; nodeSize?: number; offset: number }>
+): BlockIndex {
+ const children = entries.map((e) => ({
+ node: makeNode(e.typeName, e.attrs ?? {}, e.nodeSize ?? 10),
+ offset: e.offset,
+ }));
+ const totalSize = entries.reduce((max, e) => Math.max(max, e.offset + (e.nodeSize ?? 10)), 0) + 2;
+ const doc = makeNode('doc', {}, totalSize, children);
+ return buildBlockIndex(makeEditor(doc));
+}
+
+// ---------------------------------------------------------------------------
+// isSupportedNodeType
+// ---------------------------------------------------------------------------
+
+describe('isSupportedNodeType', () => {
+ it.each(['paragraph', 'heading', 'listItem', 'table', 'tableRow', 'tableCell', 'image', 'sdt'] as const)(
+ 'returns true for supported block type "%s"',
+ (nodeType) => {
+ expect(isSupportedNodeType(nodeType)).toBe(true);
+ },
+ );
+
+ it.each(['text', 'run', 'field', 'bookmark', 'comment', 'hyperlink', 'footnoteRef', 'tab', 'lineBreak'] as const)(
+ 'returns false for unsupported type "%s"',
+ (nodeType) => {
+ expect(isSupportedNodeType(nodeType)).toBe(false);
+ },
+ );
+});
+
+// ---------------------------------------------------------------------------
+// toBlockAddress
+// ---------------------------------------------------------------------------
+
+describe('toBlockAddress', () => {
+ it('converts a BlockCandidate to a block NodeAddress', () => {
+ const candidate: BlockCandidate = {
+ node: makeNode('paragraph'),
+ pos: 5,
+ end: 15,
+ nodeType: 'paragraph',
+ nodeId: 'abc',
+ };
+
+ expect(toBlockAddress(candidate)).toEqual({
+ kind: 'block',
+ nodeType: 'paragraph',
+ nodeId: 'abc',
+ });
+ });
+
+ it('does not include pos/end/node in the address', () => {
+ const candidate: BlockCandidate = {
+ node: makeNode('table'),
+ pos: 0,
+ end: 50,
+ nodeType: 'table',
+ nodeId: 't1',
+ };
+
+ const address = toBlockAddress(candidate);
+ expect(Object.keys(address).sort()).toEqual(['kind', 'nodeId', 'nodeType']);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// buildBlockIndex — node type mapping
+// ---------------------------------------------------------------------------
+
+describe('buildBlockIndex', () => {
+ describe('paragraph type mapping', () => {
+ it('maps a plain paragraph to "paragraph"', () => {
+ const index = indexFromNodes({ typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, offset: 0 });
+ expect(index.candidates[0].nodeType).toBe('paragraph');
+ });
+
+ it('maps a paragraph with heading styleId to "heading"', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'p1', paragraphProperties: { styleId: 'Heading1' } },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeType).toBe('heading');
+ });
+
+ it.each(['heading 2', 'Heading3', 'heading 6', 'HEADING 4'])(
+ 'recognises heading styleId variation "%s"',
+ (styleId) => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'p1', paragraphProperties: { styleId } },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeType).toBe('heading');
+ },
+ );
+
+ it('does not treat non-heading styleId as heading', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'p1', paragraphProperties: { styleId: 'Normal' } },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeType).toBe('paragraph');
+ });
+
+ it('does not treat heading7+ as heading', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'p1', paragraphProperties: { styleId: 'heading7' } },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeType).toBe('paragraph');
+ });
+
+ it('maps paragraph with numberingProperties (numId + ilvl) to "listItem"', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'p1', paragraphProperties: { numberingProperties: { numId: 1, ilvl: 0 } } },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeType).toBe('listItem');
+ });
+
+ it('maps paragraph with numberingProperties (ilvl only) to "listItem"', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'p1', paragraphProperties: { numberingProperties: { ilvl: 2 } } },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeType).toBe('listItem');
+ });
+
+ it('maps paragraph with listRendering.markerText to "listItem"', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'p1', listRendering: { markerText: '1.' } },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeType).toBe('listItem');
+ });
+
+ it('maps paragraph with listRendering.path to "listItem"', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'p1', listRendering: { path: [0, 1] } },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeType).toBe('listItem');
+ });
+
+ it('does not map paragraph with empty listRendering.path to "listItem"', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'p1', listRendering: { path: [] } },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeType).toBe('paragraph');
+ });
+
+ it('heading takes priority over listItem when both are present', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: {
+ sdBlockId: 'p1',
+ paragraphProperties: {
+ styleId: 'Heading1',
+ numberingProperties: { numId: 1, ilvl: 0 },
+ },
+ },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeType).toBe('heading');
+ });
+ });
+
+ describe('non-paragraph type mapping', () => {
+ it.each([
+ ['table', 'table'],
+ ['tableRow', 'tableRow'],
+ ['tableCell', 'tableCell'],
+ ['tableHeader', 'tableCell'],
+ ['sdt', 'sdt'],
+ ['structuredContentBlock', 'sdt'],
+ ] as const)('maps PM node type "%s" to block type "%s"', (pmType, expectedBlockType) => {
+ const index = indexFromNodes({
+ typeName: pmType,
+ attrs: { sdBlockId: 'test-id' },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeType).toBe(expectedBlockType);
+ });
+
+ it('skips unsupported node types', () => {
+ const index = indexFromNodes({ typeName: 'hardBreak', offset: 0 });
+ expect(index.candidates).toHaveLength(0);
+ });
+
+ it('skips unknown node types', () => {
+ const index = indexFromNodes({ typeName: 'someCustomNode', offset: 0 });
+ expect(index.candidates).toHaveLength(0);
+ });
+ });
+
+ describe('ID resolution — paragraph nodes', () => {
+ it('prefers paraId over sdBlockId when both are present', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'sd1', paraId: 'p1' },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeId).toBe('p1');
+ });
+
+ it('falls back to paraId when sdBlockId is absent', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { paraId: 'p1' },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeId).toBe('p1');
+ });
+
+ it('falls back to paraId when sdBlockId is null', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { sdBlockId: null, paraId: 'p1' },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeId).toBe('p1');
+ });
+
+ it('skips paragraph candidates when no explicit id attrs exist', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: {},
+ offset: 7,
+ });
+ expect(index.candidates).toHaveLength(0);
+ });
+ });
+
+ describe('ID resolution — non-paragraph nodes', () => {
+ it('prefers imported IDs over sdBlockId when both are present', () => {
+ const index = indexFromNodes({
+ typeName: 'table',
+ attrs: { sdBlockId: 'sd1', blockId: 'b1', id: 'i1', paraId: 'p1', uuid: 'u1' },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeId).toBe('b1');
+ });
+
+ it('uses sdBlockId when imported IDs are absent', () => {
+ const index = indexFromNodes({
+ typeName: 'table',
+ attrs: { sdBlockId: 'sd1' },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeId).toBe('sd1');
+ });
+
+ it('falls back to blockId', () => {
+ const index = indexFromNodes({
+ typeName: 'table',
+ attrs: { blockId: 'b1', id: 'i1' },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeId).toBe('b1');
+ });
+
+ it('falls back to id', () => {
+ const index = indexFromNodes({
+ typeName: 'table',
+ attrs: { id: 'i1', paraId: 'p1' },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeId).toBe('i1');
+ });
+
+ it('falls back to paraId', () => {
+ const index = indexFromNodes({
+ typeName: 'table',
+ attrs: { paraId: 'p1', uuid: 'u1' },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeId).toBe('p1');
+ });
+
+ it('falls back to uuid', () => {
+ const index = indexFromNodes({
+ typeName: 'table',
+ attrs: { uuid: 'u1' },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeId).toBe('u1');
+ });
+
+ it('skips non-paragraph candidates when no id attrs exist', () => {
+ const index = indexFromNodes({
+ typeName: 'table',
+ attrs: {},
+ offset: 3,
+ });
+ expect(index.candidates).toHaveLength(0);
+ });
+
+ it('ignores empty string attrs', () => {
+ const index = indexFromNodes({
+ typeName: 'table',
+ attrs: { sdBlockId: '', blockId: '', id: 'real' },
+ offset: 0,
+ });
+ expect(index.candidates[0].nodeId).toBe('real');
+ });
+ });
+
+ describe('index structure', () => {
+ it('populates byId with "nodeType:nodeId" keys', () => {
+ const index = indexFromNodes(
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, offset: 0 },
+ { typeName: 'table', attrs: { sdBlockId: 't1' }, offset: 12 },
+ );
+ expect(index.byId.has('paragraph:p1')).toBe(true);
+ expect(index.byId.has('table:t1')).toBe(true);
+ });
+
+ it('sets end = pos + nodeSize on each candidate', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'p1' },
+ nodeSize: 15,
+ offset: 5,
+ });
+ expect(index.candidates[0].pos).toBe(5);
+ expect(index.candidates[0].end).toBe(20);
+ });
+
+ it('preserves insertion order in candidates array', () => {
+ const index = indexFromNodes(
+ { typeName: 'paragraph', attrs: { sdBlockId: 'a' }, offset: 0 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'b' }, offset: 12 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'c' }, offset: 24 },
+ );
+ expect(index.candidates.map((c) => c.nodeId)).toEqual(['a', 'b', 'c']);
+ });
+
+ it('excludes ambiguous composite keys from byId to prevent silent arbitrary resolution', () => {
+ const p1 = makeNode('paragraph', { sdBlockId: 'dup' }, 10);
+ const p2 = makeNode('paragraph', { sdBlockId: 'dup' }, 10);
+ const doc = makeNode('doc', {}, 24, [
+ { node: p1, offset: 0 },
+ { node: p2, offset: 12 },
+ ]);
+ const index = buildBlockIndex(makeEditor(doc));
+
+ // Ambiguous keys are excluded from byId to prevent silent arbitrary resolution.
+ // Both candidates still appear in the ordered candidates array.
+ expect(index.byId.get('paragraph:dup')).toBeUndefined();
+ expect(index.candidates.filter((c) => c.nodeId === 'dup')).toHaveLength(2);
+ });
+
+ it('returns empty index for a document with no block nodes', () => {
+ const doc = makeNode('doc', {}, 2);
+ const index = buildBlockIndex(makeEditor(doc));
+ expect(index.candidates).toHaveLength(0);
+ expect(index.byId.size).toBe(0);
+ });
+ });
+});
+
+// ---------------------------------------------------------------------------
+// findBlockById
+// ---------------------------------------------------------------------------
+
+describe('findBlockById', () => {
+ function buildMultiTypeIndex(): BlockIndex {
+ return indexFromNodes(
+ { typeName: 'paragraph', attrs: { sdBlockId: 'p1' }, offset: 0 },
+ { typeName: 'table', attrs: { sdBlockId: 't1' }, offset: 12 },
+ );
+ }
+
+ it('returns the candidate matching a block address', () => {
+ const index = buildMultiTypeIndex();
+ const result = findBlockById(index, { kind: 'block', nodeType: 'paragraph', nodeId: 'p1' });
+ expect(result).toBeDefined();
+ expect(result!.nodeId).toBe('p1');
+ expect(result!.nodeType).toBe('paragraph');
+ });
+
+ it('returns undefined for a non-existent nodeId', () => {
+ const index = buildMultiTypeIndex();
+ expect(findBlockById(index, { kind: 'block', nodeType: 'paragraph', nodeId: 'nope' })).toBeUndefined();
+ });
+
+ it('returns undefined when nodeId matches but nodeType does not', () => {
+ const index = buildMultiTypeIndex();
+ // 'p1' exists as paragraph, not as table
+ expect(findBlockById(index, { kind: 'block', nodeType: 'table', nodeId: 'p1' })).toBeUndefined();
+ });
+
+ it('returns undefined for an inline address', () => {
+ const index = buildMultiTypeIndex();
+ const address: NodeAddress = {
+ kind: 'inline',
+ nodeType: 'run',
+ anchor: { start: { blockId: 'p1', offset: 0 }, end: { blockId: 'p1', offset: 5 } },
+ };
+ expect(findBlockById(index, address)).toBeUndefined();
+ });
+
+ it('treats duplicate nodeType:nodeId matches as ambiguous and does not resolve an arbitrary block', () => {
+ const index = indexFromNodes(
+ { typeName: 'paragraph', attrs: { sdBlockId: 'dup' }, offset: 0 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'dup' }, offset: 12 },
+ );
+
+ expect(findBlockById(index, { kind: 'block', nodeType: 'paragraph', nodeId: 'dup' })).toBeUndefined();
+ });
+});
+
+// ---------------------------------------------------------------------------
+// findBlockByPos
+// ---------------------------------------------------------------------------
+
+describe('findBlockByPos', () => {
+ // Three non-overlapping paragraphs with gaps between them:
+ // a: [0, 10] gap: (10, 15) b: [15, 25] gap: (25, 30) c: [30, 40]
+ function buildGappedIndex(): BlockIndex {
+ return indexFromNodes(
+ { typeName: 'paragraph', attrs: { sdBlockId: 'a' }, nodeSize: 10, offset: 0 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'b' }, nodeSize: 10, offset: 15 },
+ { typeName: 'paragraph', attrs: { sdBlockId: 'c' }, nodeSize: 10, offset: 30 },
+ );
+ }
+
+ it('finds the first block at its start position', () => {
+ const index = buildGappedIndex();
+ expect(findBlockByPos(index, 0)?.nodeId).toBe('a');
+ });
+
+ it('finds the first block at its end position (inclusive)', () => {
+ const index = buildGappedIndex();
+ // end = pos + nodeSize = 0 + 10 = 10; comparison is pos > candidate.end → 10 > 10 is false → found
+ expect(findBlockByPos(index, 10)?.nodeId).toBe('a');
+ });
+
+ it('finds the middle block at a position within its range', () => {
+ const index = buildGappedIndex();
+ expect(findBlockByPos(index, 20)?.nodeId).toBe('b');
+ });
+
+ it('finds the last block at its start position', () => {
+ const index = buildGappedIndex();
+ expect(findBlockByPos(index, 30)?.nodeId).toBe('c');
+ });
+
+ it('finds the last block at its end position', () => {
+ const index = buildGappedIndex();
+ expect(findBlockByPos(index, 40)?.nodeId).toBe('c');
+ });
+
+ it('returns undefined for a position in a gap between blocks', () => {
+ const index = buildGappedIndex();
+ expect(findBlockByPos(index, 12)).toBeUndefined();
+ });
+
+ it('returns undefined for a position beyond all blocks', () => {
+ const index = buildGappedIndex();
+ expect(findBlockByPos(index, 100)).toBeUndefined();
+ });
+
+ it('returns undefined for an empty index', () => {
+ const doc = makeNode('doc', {}, 2);
+ const index = buildBlockIndex(makeEditor(doc));
+ expect(findBlockByPos(index, 0)).toBeUndefined();
+ });
+
+ it('finds the only block in a single-element index', () => {
+ const index = indexFromNodes({
+ typeName: 'paragraph',
+ attrs: { sdBlockId: 'solo' },
+ nodeSize: 10,
+ offset: 5,
+ });
+ expect(findBlockByPos(index, 5)?.nodeId).toBe('solo');
+ expect(findBlockByPos(index, 10)?.nodeId).toBe('solo');
+ expect(findBlockByPos(index, 15)?.nodeId).toBe('solo');
+ expect(findBlockByPos(index, 4)).toBeUndefined();
+ expect(findBlockByPos(index, 16)).toBeUndefined();
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/helpers/node-address-resolver.ts b/packages/super-editor/src/document-api-adapters/helpers/node-address-resolver.ts
new file mode 100644
index 0000000000..3bf9c210c5
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/node-address-resolver.ts
@@ -0,0 +1,231 @@
+import type { Node as ProseMirrorNode } from 'prosemirror-model';
+import type { Editor } from '../../core/Editor.js';
+import type { BlockNodeAttributes } from '../../core/types/NodeCategories.js';
+import type { BlockNodeAddress, BlockNodeType, NodeAddress, NodeType } from '@superdoc/document-api';
+import type { ParagraphAttrs } from '../../extensions/types/node-attributes.js';
+import { toId } from './value-utils.js';
+
+/** Superset of all possible ID attributes across block node types. */
+type BlockIdAttrs = BlockNodeAttributes & {
+ blockId?: string | null;
+ id?: string | null;
+ paraId?: string | null;
+ uuid?: string | null;
+};
+
+/** A block-level node found during document traversal, with its position and resolved identity. */
+export type BlockCandidate = {
+ node: ProseMirrorNode;
+ pos: number;
+ end: number;
+ nodeType: BlockNodeType;
+ nodeId: string;
+};
+
+/**
+ * Positional index of all block-level nodes in the document.
+ *
+ * Built by {@link buildBlockIndex}. The index is a snapshot — it must be
+ * rebuilt after any document mutation.
+ */
+export type BlockIndex = {
+ candidates: BlockCandidate[];
+ byId: Map;
+};
+
+// Keep in sync with BlockNodeType in document-api/types/node.ts
+const SUPPORTED_BLOCK_NODE_TYPES: ReadonlySet = new Set([
+ 'paragraph',
+ 'heading',
+ 'listItem',
+ 'table',
+ 'tableRow',
+ 'tableCell',
+ 'image',
+ 'sdt',
+]);
+
+/**
+ * Returns `true` if `nodeType` is a block-level type supported by the adapter index.
+ *
+ * @param nodeType - A node type string (block, inline, or the literal `'text'`).
+ * @returns Whether the type is a supported {@link BlockNodeType}.
+ */
+export function isSupportedNodeType(nodeType: NodeType | 'text'): nodeType is BlockNodeType {
+ return SUPPORTED_BLOCK_NODE_TYPES.has(nodeType as BlockNodeType);
+}
+
+function isListItem(attrs: ParagraphAttrs | null | undefined): boolean {
+ const numbering = attrs?.paragraphProperties?.numberingProperties;
+ if (numbering && (numbering.numId != null || numbering.ilvl != null)) return true;
+ const listRendering = attrs?.listRendering;
+ if (listRendering?.markerText) return true;
+ if (Array.isArray(listRendering?.path) && listRendering.path.length > 0) return true;
+ return false;
+}
+
+/**
+ * Extracts the heading level (1–6) from an OOXML styleId string.
+ *
+ * @param styleId - A paragraph styleId (e.g. `"Heading1"`, `"heading 3"`).
+ * @returns The heading level, or `undefined` if the styleId is not a heading.
+ */
+export function getHeadingLevel(styleId?: string | null): number | undefined {
+ if (!styleId) return undefined;
+ const match = /heading\s*([1-6])/i.exec(styleId);
+ if (!match) return undefined;
+ return Number(match[1]);
+}
+
+function mapBlockNodeType(node: ProseMirrorNode): BlockNodeType | undefined {
+ if (!node.isBlock) return undefined;
+ switch (node.type.name) {
+ case 'paragraph': {
+ const attrs = node.attrs as ParagraphAttrs | undefined;
+ const styleId = attrs?.paragraphProperties?.styleId ?? undefined;
+ if (getHeadingLevel(styleId) != null) return 'heading';
+ if (isListItem(attrs)) return 'listItem';
+ return 'paragraph';
+ }
+ case 'table':
+ return 'table';
+ case 'tableRow':
+ return 'tableRow';
+ case 'tableCell':
+ case 'tableHeader':
+ return 'tableCell';
+ case 'image':
+ return 'image';
+ case 'structuredContentBlock':
+ case 'sdt':
+ return 'sdt';
+ default:
+ return undefined;
+ }
+}
+
+function resolveBlockNodeId(node: ProseMirrorNode): string | undefined {
+ if (node.type.name === 'paragraph') {
+ const attrs = node.attrs as ParagraphAttrs | undefined;
+ // NOTE: Migration surface for the stable-addresses plan.
+ // Today we preserve DOCX-import identity precedence (`paraId` first) for
+ // paragraph nodes. Any future switch to `sdBlockId` canonical precedence
+ // must be handled as an explicit compatibility migration.
+ return toId(attrs?.paraId) ?? toId(attrs?.sdBlockId);
+ }
+
+ const attrs = (node.attrs ?? {}) as BlockIdAttrs;
+ // NOTE: Migration surface for the stable-addresses plan.
+ // Imported IDs currently win over `sdBlockId` to preserve historical
+ // identity during DOCX round-trips.
+ return toId(attrs.blockId) ?? toId(attrs.id) ?? toId(attrs.paraId) ?? toId(attrs.uuid) ?? toId(attrs.sdBlockId);
+}
+
+/**
+ * Converts a {@link BlockCandidate} into a stable {@link NodeAddress}.
+ *
+ * @param candidate - The block candidate to convert.
+ * @returns A block-kind node address.
+ */
+export function toBlockAddress(candidate: BlockCandidate): BlockNodeAddress {
+ return {
+ kind: 'block',
+ nodeType: candidate.nodeType,
+ nodeId: candidate.nodeId,
+ };
+}
+
+/**
+ * Walks the editor document and builds a positional index of all recognised
+ * block-level nodes.
+ *
+ * The returned index is a **snapshot** tied to the current document state.
+ * It must be rebuilt after any transaction that mutates the document.
+ *
+ * @param editor - The editor whose document will be indexed.
+ * @returns A {@link BlockIndex} containing ordered candidates and a lookup map.
+ */
+export function buildBlockIndex(editor: Editor): BlockIndex {
+ const candidates: BlockCandidate[] = [];
+ const byId = new Map();
+ const ambiguous = new Set();
+
+ // This traversal is a hot path for adapter workflows (for example find ->
+ // getNode). Keep this pure snapshot builder so a transaction-invalidated
+ // cache can be layered on later without API changes.
+ editor.state.doc.descendants((node, pos) => {
+ const nodeType = mapBlockNodeType(node);
+ if (!nodeType) return;
+ const nodeId = resolveBlockNodeId(node);
+ if (!nodeId) return;
+
+ const candidate: BlockCandidate = {
+ node,
+ pos,
+ end: pos + node.nodeSize,
+ nodeType,
+ nodeId,
+ };
+
+ candidates.push(candidate);
+ const key = `${candidate.nodeType}:${candidate.nodeId}`;
+ if (byId.has(key)) {
+ ambiguous.add(key);
+ byId.delete(key);
+ } else if (!ambiguous.has(key)) {
+ byId.set(key, candidate);
+ }
+ });
+
+ return { candidates, byId };
+}
+
+/**
+ * Looks up a block candidate by its {@link NodeAddress}.
+ *
+ * @param index - The block index to search.
+ * @param address - The address to resolve. Non-block addresses return `undefined`.
+ * @returns The matching candidate, or `undefined` if not found.
+ */
+export function findBlockById(index: BlockIndex, address: NodeAddress): BlockCandidate | undefined {
+ if (address.kind !== 'block') return undefined;
+ return index.byId.get(`${address.nodeType}:${address.nodeId}`);
+}
+
+/**
+ * Returns true for block candidates that accept inline text content.
+ */
+export function isTextBlockCandidate(candidate: BlockCandidate): boolean {
+ const node = candidate.node as unknown as { inlineContent?: boolean; isTextblock?: boolean };
+ return Boolean(node?.inlineContent || node?.isTextblock);
+}
+
+/**
+ * Finds a block candidate whose range contains the given position.
+ *
+ * Note: nested blocks (e.g. table > row > cell > paragraph) produce overlapping
+ * candidates. This returns whichever the binary search lands on first, not
+ * necessarily the innermost. This is sufficient for resolving a containing block
+ * for match context but callers needing the most specific block should filter further.
+ */
+export function findBlockByPos(index: BlockIndex, pos: number): BlockCandidate | undefined {
+ const candidates = index.candidates;
+ let low = 0;
+ let high = candidates.length - 1;
+
+ while (low <= high) {
+ const mid = (low + high) >> 1;
+ const candidate = candidates[mid];
+ if (pos < candidate.pos) {
+ high = mid - 1;
+ continue;
+ }
+ if (pos > candidate.end) {
+ low = mid + 1;
+ continue;
+ }
+ return candidate;
+ }
+
+ return undefined;
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.test.ts b/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.test.ts
new file mode 100644
index 0000000000..15b29b368a
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.test.ts
@@ -0,0 +1,335 @@
+import type { Node as ProseMirrorNode, Mark as ProseMirrorMark } from 'prosemirror-model';
+import type { BlockCandidate } from './node-address-resolver.js';
+import type { InlineCandidate } from './inline-address-resolver.js';
+import type { InlineAnchor, InlineNodeType, NodeType } from '@superdoc/document-api';
+import { mapNodeInfo } from './node-info-mapper.js';
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+function makeBlockNode(typeName: string, attrs: Record = {}): ProseMirrorNode {
+ return {
+ type: { name: typeName },
+ attrs,
+ isBlock: true,
+ isInline: false,
+ isText: false,
+ } as unknown as ProseMirrorNode;
+}
+
+function makeBlockCandidate(
+ nodeType: BlockCandidate['nodeType'],
+ nodeId: string,
+ attrs: Record = {},
+ typeName?: string,
+): BlockCandidate {
+ return {
+ node: makeBlockNode(typeName ?? nodeType, attrs),
+ pos: 0,
+ end: 10,
+ nodeType,
+ nodeId,
+ };
+}
+
+function makeAnchor(blockId: string, start = 0, end = 1): InlineAnchor {
+ return {
+ start: { blockId, offset: start },
+ end: { blockId, offset: end },
+ };
+}
+
+function makeInlineCandidate(
+ nodeType: InlineNodeType,
+ options: {
+ blockId?: string;
+ attrs?: Record;
+ markAttrs?: Record;
+ markName?: string;
+ nodeAttrs?: Record;
+ } = {},
+): InlineCandidate {
+ const blockId = options.blockId ?? 'p1';
+ return {
+ nodeType,
+ anchor: makeAnchor(blockId),
+ blockId,
+ pos: 0,
+ end: 1,
+ attrs: options.attrs,
+ mark: options.markAttrs
+ ? ({ type: { name: options.markName ?? nodeType }, attrs: options.markAttrs } as unknown as ProseMirrorMark)
+ : undefined,
+ node: options.nodeAttrs
+ ? ({ type: { name: nodeType }, attrs: options.nodeAttrs } as unknown as ProseMirrorNode)
+ : undefined,
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Block node mapping
+// ---------------------------------------------------------------------------
+
+describe('mapNodeInfo — block nodes', () => {
+ it('maps paragraph with properties', () => {
+ const result = mapNodeInfo(
+ makeBlockCandidate('paragraph', 'p1', {
+ paragraphProperties: { styleId: 'Normal', justification: 'center' },
+ }),
+ );
+
+ expect(result.nodeType).toBe('paragraph');
+ expect(result.kind).toBe('block');
+ expect(result.properties).toMatchObject({
+ styleId: 'Normal',
+ alignment: 'center',
+ });
+ });
+
+ it('maps heading with level from styleId', () => {
+ const result = mapNodeInfo(
+ makeBlockCandidate('heading', 'h1', {
+ paragraphProperties: { styleId: 'Heading2' },
+ }),
+ );
+
+ expect(result.nodeType).toBe('heading');
+ expect(result.properties).toMatchObject({ headingLevel: 2 });
+ });
+
+ it('throws for heading without valid level', () => {
+ expect(() =>
+ mapNodeInfo(
+ makeBlockCandidate('heading', 'h1', {
+ paragraphProperties: { styleId: 'Normal' },
+ }),
+ ),
+ ).toThrow('does not have a valid heading level');
+ });
+
+ it('maps listItem with numbering', () => {
+ const result = mapNodeInfo(
+ makeBlockCandidate('listItem', 'li1', {
+ listRendering: { markerText: '1.', path: [1] },
+ }),
+ );
+
+ expect(result.nodeType).toBe('listItem');
+ expect(result.properties).toMatchObject({
+ numbering: { marker: '1.', path: [1], ordinal: 1 },
+ });
+ });
+
+ it('maps table with layout and width', () => {
+ const result = mapNodeInfo(
+ makeBlockCandidate('table', 't1', {
+ tableProperties: { tableLayout: 'fixed', tableWidth: 5000, justification: 'center' },
+ }),
+ );
+
+ expect(result.nodeType).toBe('table');
+ expect(result.properties).toMatchObject({
+ layout: 'fixed',
+ width: 5000,
+ alignment: 'center',
+ });
+ });
+
+ it('maps tableRow with empty properties', () => {
+ const result = mapNodeInfo(makeBlockCandidate('tableRow', 'tr1'));
+
+ expect(result.nodeType).toBe('tableRow');
+ expect(result.properties).toEqual({});
+ });
+
+ it('maps tableCell with width and shading', () => {
+ const result = mapNodeInfo(
+ makeBlockCandidate('tableCell', 'tc1', {
+ tableCellProperties: { cellWidth: 2500, shading: { fill: '#FF0000' } },
+ }),
+ );
+
+ expect(result.nodeType).toBe('tableCell');
+ expect(result.properties).toMatchObject({ width: 2500, shading: '#FF0000' });
+ });
+});
+
+// ---------------------------------------------------------------------------
+// overrideType behavior
+// ---------------------------------------------------------------------------
+
+describe('mapNodeInfo — overrideType', () => {
+ it('uses overrideType="sdt" for sdt block candidates', () => {
+ const candidate = makeBlockCandidate('sdt', 'sdt2', { tag: 'MyTag' }, 'structuredContentBlock');
+ const result = mapNodeInfo(candidate, 'sdt');
+
+ expect(result.nodeType).toBe('sdt');
+ expect(result.properties).toMatchObject({ tag: 'MyTag' });
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Inline node mapping
+// ---------------------------------------------------------------------------
+
+describe('mapNodeInfo — inline nodes', () => {
+ it('maps hyperlink from mark attrs', () => {
+ const result = mapNodeInfo(
+ makeInlineCandidate('hyperlink', {
+ markAttrs: { href: 'https://example.com', tooltip: 'Click me' },
+ markName: 'link',
+ }),
+ );
+
+ expect(result.nodeType).toBe('hyperlink');
+ expect(result.kind).toBe('inline');
+ expect(result.properties).toMatchObject({
+ href: 'https://example.com',
+ tooltip: 'Click me',
+ });
+ });
+
+ it('maps comment from mark attrs', () => {
+ const result = mapNodeInfo(
+ makeInlineCandidate('comment', {
+ markAttrs: { commentId: 'c42' },
+ markName: 'comment',
+ }),
+ );
+
+ expect(result.nodeType).toBe('comment');
+ expect(result.properties).toMatchObject({ commentId: 'c42' });
+ });
+
+ it('maps comment with importedId fallback', () => {
+ const result = mapNodeInfo(
+ makeInlineCandidate('comment', {
+ markAttrs: { importedId: 'imp1' },
+ markName: 'comment',
+ }),
+ );
+
+ expect(result.properties).toMatchObject({ commentId: 'imp1' });
+ });
+
+ it('maps bookmark from attrs', () => {
+ const result = mapNodeInfo(
+ makeInlineCandidate('bookmark', {
+ attrs: { name: 'Bookmark1', id: 'bk1' },
+ }),
+ );
+
+ expect(result.nodeType).toBe('bookmark');
+ expect(result.properties).toMatchObject({ name: 'Bookmark1', bookmarkId: 'bk1' });
+ });
+
+ it('maps footnoteRef from node attrs', () => {
+ const result = mapNodeInfo(
+ makeInlineCandidate('footnoteRef', {
+ nodeAttrs: { id: 'fn1' },
+ }),
+ );
+
+ expect(result.nodeType).toBe('footnoteRef');
+ expect(result.properties).toMatchObject({ noteId: 'fn1' });
+ });
+
+ it('maps tab with empty properties', () => {
+ const result = mapNodeInfo(makeInlineCandidate('tab'));
+ expect(result).toEqual({ nodeType: 'tab', kind: 'inline', properties: {} });
+ });
+
+ it('maps lineBreak with empty properties', () => {
+ const result = mapNodeInfo(makeInlineCandidate('lineBreak'));
+ expect(result).toEqual({ nodeType: 'lineBreak', kind: 'inline', properties: {} });
+ });
+
+ it('maps image with properties', () => {
+ const result = mapNodeInfo(
+ makeInlineCandidate('image', {
+ nodeAttrs: { src: 'pic.png', alt: 'A picture', size: { width: 100, height: 50 } },
+ }),
+ );
+
+ expect(result.nodeType).toBe('image');
+ expect(result.kind).toBe('inline');
+ expect(result.properties).toMatchObject({
+ src: 'pic.png',
+ alt: 'A picture',
+ size: { width: 100, height: 50 },
+ });
+ });
+
+ it('maps run with text-style properties', () => {
+ const result = mapNodeInfo(
+ makeInlineCandidate('run', {
+ nodeAttrs: {
+ runProperties: {
+ bold: true,
+ italic: true,
+ underline: { val: 'single' },
+ rFonts: { ascii: 'Calibri' },
+ sz: 24,
+ color: { val: 'FF0000' },
+ highlight: 'yellow',
+ rStyle: 'Strong',
+ lang: { val: 'en-US' },
+ },
+ },
+ }),
+ );
+
+ expect(result.nodeType).toBe('run');
+ expect(result.kind).toBe('inline');
+ expect(result.properties).toMatchObject({
+ bold: true,
+ italic: true,
+ underline: true,
+ font: 'Calibri',
+ size: 24,
+ color: 'FF0000',
+ highlight: 'yellow',
+ styleId: 'Strong',
+ language: 'en-US',
+ });
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Kind mismatch errors
+// ---------------------------------------------------------------------------
+
+describe('mapNodeInfo — kind mismatch errors', () => {
+ const blockOnlyTypes = ['paragraph', 'heading', 'listItem', 'table', 'tableRow', 'tableCell'] as const;
+
+ for (const nodeType of blockOnlyTypes) {
+ it(`throws when ${nodeType} is mapped from an inline candidate`, () => {
+ const inlineCandidate = makeInlineCandidate('hyperlink');
+ expect(() => mapNodeInfo(inlineCandidate, nodeType)).toThrow();
+ });
+ }
+
+ const inlineOnlyTypes = ['hyperlink', 'comment', 'bookmark', 'footnoteRef'] as const;
+
+ for (const nodeType of inlineOnlyTypes) {
+ it(`throws when ${nodeType} is mapped from a block candidate`, () => {
+ const blockCandidate = makeBlockCandidate('paragraph', 'p1');
+ expect(() => mapNodeInfo(blockCandidate, nodeType)).toThrow();
+ });
+ }
+});
+
+// ---------------------------------------------------------------------------
+// Unknown type
+// ---------------------------------------------------------------------------
+
+describe('mapNodeInfo — unknown type', () => {
+ it('throws for unimplemented node type', () => {
+ const candidate = makeBlockCandidate('paragraph', 'p1');
+ // Force an unknown type via overrideType
+ expect(() => mapNodeInfo(candidate, 'not-a-real-node-type' as unknown as NodeType)).toThrow(
+ 'Node type "not-a-real-node-type" is not implemented yet.',
+ );
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.ts b/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.ts
new file mode 100644
index 0000000000..bcbfbebbad
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/node-info-mapper.ts
@@ -0,0 +1,422 @@
+import { getHeadingLevel, type BlockCandidate } from './node-address-resolver.js';
+import type { InlineCandidate } from './inline-address-resolver.js';
+import { resolveCommentIdFromAttrs, toFiniteNumber } from './value-utils.js';
+import { DocumentApiAdapterError } from '../errors.js';
+import type {
+ BookmarkNodeInfo,
+ CommentNodeInfo,
+ FootnoteRefNodeInfo,
+ HeadingNodeInfo,
+ HeadingProperties,
+ HyperlinkNodeInfo,
+ ImageNodeInfo,
+ LineBreakNodeInfo,
+ ListItemNodeInfo,
+ ListItemProperties,
+ ListNumbering,
+ NodeInfo,
+ NodeType,
+ ParagraphNodeInfo,
+ ParagraphProperties,
+ RunNodeInfo,
+ SdtNodeInfo,
+ TabNodeInfo,
+ TableCellNodeInfo,
+ TableNodeInfo,
+ TableRowNodeInfo,
+} from '@superdoc/document-api';
+import type {
+ ImageAttrs,
+ ParagraphAttrs,
+ StructuredContentBlockAttrs,
+ TableAttrs,
+ TableCellAttrs,
+ TableMeasurement,
+} from '../../extensions/types/node-attributes.js';
+
+function resolveMeasurement(value: number | TableMeasurement | null | undefined): number | undefined {
+ if (typeof value === 'number') return value;
+ if (value && typeof value === 'object' && typeof value.value === 'number') return value.value;
+ return undefined;
+}
+
+function mapTableAlignment(
+ justification: TableAttrs['tableProperties'] extends { justification?: infer J } ? J : never,
+): TableNodeInfo['properties']['alignment'] {
+ switch (justification) {
+ case 'start':
+ return 'left';
+ case 'end':
+ return 'right';
+ case 'left':
+ case 'center':
+ case 'right':
+ return justification;
+ default:
+ return undefined;
+ }
+}
+
+function mapParagraphProperties(attrs: ParagraphAttrs | null | undefined): ParagraphProperties {
+ const props = attrs?.paragraphProperties ?? undefined;
+ const indentation = props?.indentation
+ ? {
+ left: props.indentation.left,
+ right: props.indentation.right,
+ firstLine: props.indentation.firstLine,
+ hanging: props.indentation.hanging,
+ }
+ : undefined;
+
+ const spacing = props?.spacing
+ ? {
+ before: props.spacing.before,
+ after: props.spacing.after,
+ line: props.spacing.line,
+ }
+ : undefined;
+
+ const justification = props?.justification;
+ const alignment = justification === 'both' ? 'justify' : justification;
+
+ const paragraphNumbering = props?.numberingProperties
+ ? {
+ numId: toFiniteNumber(props.numberingProperties.numId),
+ level: toFiniteNumber(props.numberingProperties.ilvl),
+ }
+ : undefined;
+
+ return {
+ styleId: props?.styleId ?? undefined,
+ alignment: alignment ?? undefined,
+ indentation,
+ spacing,
+ keepWithNext: props?.keepNext ?? undefined,
+ outlineLevel: props?.outlineLevel ?? undefined,
+ paragraphNumbering,
+ };
+}
+
+function mapListNumbering(attrs: ParagraphAttrs | null | undefined): ListNumbering | undefined {
+ const listRendering = attrs?.listRendering ?? undefined;
+ if (!listRendering) return undefined;
+
+ const listNumbering: ListNumbering = {};
+ if (listRendering.markerText) listNumbering.marker = listRendering.markerText;
+ if (Array.isArray(listRendering.path)) listNumbering.path = listRendering.path;
+ if (Array.isArray(listRendering.path) && listRendering.path.length > 0) {
+ listNumbering.ordinal = listRendering.path[listRendering.path.length - 1];
+ }
+ return Object.keys(listNumbering).length ? listNumbering : undefined;
+}
+
+function mapParagraphNode(candidate: BlockCandidate): ParagraphNodeInfo {
+ const attrs = candidate.node.attrs as ParagraphAttrs | undefined;
+ const properties = mapParagraphProperties(attrs);
+ return {
+ nodeType: 'paragraph',
+ kind: 'block',
+ properties,
+ };
+}
+
+function mapHeadingNode(candidate: BlockCandidate): HeadingNodeInfo {
+ const attrs = candidate.node.attrs as ParagraphAttrs | undefined;
+ const baseProps = mapParagraphProperties(attrs);
+ const headingLevelCandidate =
+ getHeadingLevel(attrs?.paragraphProperties?.styleId) ??
+ (baseProps.outlineLevel != null ? baseProps.outlineLevel + 1 : undefined);
+
+ if (!headingLevelCandidate || headingLevelCandidate < 1 || headingLevelCandidate > 6) {
+ throw new DocumentApiAdapterError(
+ 'INVALID_TARGET',
+ `Node "${candidate.nodeId}" does not have a valid heading level.`,
+ );
+ }
+
+ const properties: HeadingProperties = {
+ ...baseProps,
+ headingLevel: headingLevelCandidate as HeadingProperties['headingLevel'],
+ };
+
+ return {
+ nodeType: 'heading',
+ kind: 'block',
+ properties,
+ };
+}
+
+function mapListItemNode(candidate: BlockCandidate): ListItemNodeInfo {
+ const attrs = candidate.node.attrs as ParagraphAttrs | undefined;
+ const baseProps = mapParagraphProperties(attrs);
+ const properties: ListItemProperties = {
+ ...baseProps,
+ numbering: mapListNumbering(attrs),
+ };
+
+ return {
+ nodeType: 'listItem',
+ kind: 'block',
+ properties,
+ };
+}
+
+function mapTableNode(candidate: BlockCandidate): TableNodeInfo {
+ const attrs = candidate.node.attrs as TableAttrs | undefined;
+ const tableProps = attrs?.tableProperties ?? undefined;
+ const properties = {
+ layout: tableProps?.tableLayout ?? undefined,
+ width: resolveMeasurement(tableProps?.tableWidth ?? null) ?? undefined,
+ alignment: mapTableAlignment(tableProps?.justification),
+ };
+
+ return {
+ nodeType: 'table',
+ kind: 'block',
+ properties,
+ };
+}
+
+function mapTableRowNode(): TableRowNodeInfo {
+ return {
+ nodeType: 'tableRow',
+ kind: 'block',
+ properties: {},
+ };
+}
+
+function mapTableCellNode(candidate: BlockCandidate): TableCellNodeInfo {
+ const attrs = candidate.node.attrs as TableCellAttrs | undefined;
+ const cellProps = attrs?.tableCellProperties ?? undefined;
+ const properties = {
+ width:
+ resolveMeasurement(cellProps?.cellWidth ?? null) ??
+ (Array.isArray(attrs?.colwidth) && attrs.colwidth.length > 0 ? attrs.colwidth[0] : undefined),
+ shading: cellProps?.shading?.fill ?? attrs?.background?.color ?? undefined,
+ vMerge:
+ cellProps?.vMerge === 'continue' || cellProps?.vMerge === 'restart'
+ ? true
+ : attrs?.rowspan && attrs.rowspan > 1
+ ? true
+ : undefined,
+ gridSpan: cellProps?.gridSpan ?? attrs?.colspan ?? undefined,
+ padding:
+ resolveMeasurement(cellProps?.cellMargins?.top ?? null) ?? resolveMeasurement(attrs?.cellMargins?.top ?? null),
+ };
+
+ return {
+ nodeType: 'tableCell',
+ kind: 'block',
+ properties,
+ };
+}
+
+function buildImageInfo(attrs: ImageAttrs | undefined, kind: 'block' | 'inline'): ImageNodeInfo {
+ const properties = {
+ src: attrs?.src ?? undefined,
+ alt: attrs?.alt ?? undefined,
+ size: attrs?.size
+ ? {
+ width: attrs.size.width,
+ height: attrs.size.height,
+ unit: undefined,
+ }
+ : undefined,
+ wrap: attrs?.wrap?.type ?? undefined,
+ };
+
+ return {
+ nodeType: 'image',
+ kind,
+ properties,
+ };
+}
+
+function buildSdtInfo(attrs: StructuredContentBlockAttrs | undefined, kind: 'block' | 'inline'): SdtNodeInfo {
+ const properties = {
+ tag: attrs?.tag ?? undefined,
+ alias: attrs?.alias ?? undefined,
+ };
+
+ return {
+ nodeType: 'sdt',
+ kind,
+ properties,
+ };
+}
+
+function mapHyperlinkNode(candidate: InlineCandidate): HyperlinkNodeInfo {
+ const attrs = (candidate.mark?.attrs ?? candidate.attrs ?? {}) as Record;
+ const properties = {
+ href: typeof attrs.href === 'string' ? attrs.href : undefined,
+ anchor:
+ typeof attrs.anchor === 'string'
+ ? attrs.anchor
+ : typeof attrs.docLocation === 'string'
+ ? attrs.docLocation
+ : undefined,
+ tooltip: typeof attrs.tooltip === 'string' ? attrs.tooltip : undefined,
+ };
+ return { nodeType: 'hyperlink', kind: 'inline', properties };
+}
+
+function mapCommentNode(candidate: InlineCandidate): CommentNodeInfo {
+ const attrs = (candidate.mark?.attrs ?? candidate.attrs ?? {}) as Record;
+ const commentId = resolveCommentIdFromAttrs(attrs);
+ if (!commentId) {
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'Comment node is missing a commentId attribute.');
+ }
+ const properties = {
+ commentId,
+ };
+ return { nodeType: 'comment', kind: 'inline', properties };
+}
+
+function mapBookmarkNode(candidate: InlineCandidate): BookmarkNodeInfo {
+ const attrs = (candidate.attrs ?? candidate.node?.attrs ?? {}) as Record;
+ const properties = {
+ name: typeof attrs.name === 'string' ? attrs.name : undefined,
+ bookmarkId: typeof attrs.id === 'string' ? attrs.id : undefined,
+ };
+ return { nodeType: 'bookmark', kind: 'inline', properties };
+}
+
+function mapFootnoteRefNode(candidate: InlineCandidate): FootnoteRefNodeInfo {
+ const attrs = (candidate.node?.attrs ?? candidate.attrs ?? {}) as Record;
+ const properties = {
+ noteId: typeof attrs.id === 'string' ? attrs.id : undefined,
+ };
+ return { nodeType: 'footnoteRef', kind: 'inline', properties };
+}
+
+function mapRunNode(candidate: InlineCandidate): RunNodeInfo {
+ const attrs = (candidate.node?.attrs ?? candidate.attrs ?? {}) as {
+ runProperties?: {
+ bold?: boolean;
+ italic?: boolean;
+ underline?: { val?: string } | boolean;
+ rFonts?: { ascii?: string; hAnsi?: string; eastAsia?: string; cs?: string };
+ sz?: number;
+ color?: { val?: string };
+ highlight?: string;
+ rStyle?: string;
+ lang?: { val?: string };
+ u?: { val?: string };
+ } | null;
+ };
+ const runProperties = attrs.runProperties ?? undefined;
+ const underline = Boolean(
+ runProperties?.underline === true ||
+ runProperties?.u?.val === 'single' ||
+ (typeof runProperties?.underline === 'object' &&
+ typeof runProperties?.underline?.val === 'string' &&
+ runProperties.underline.val !== 'none'),
+ );
+ const font =
+ runProperties?.rFonts?.ascii ??
+ runProperties?.rFonts?.hAnsi ??
+ runProperties?.rFonts?.eastAsia ??
+ runProperties?.rFonts?.cs;
+
+ return {
+ nodeType: 'run',
+ kind: 'inline',
+ properties: {
+ bold: runProperties?.bold ?? undefined,
+ italic: runProperties?.italic ?? undefined,
+ underline: underline || undefined,
+ font: typeof font === 'string' ? font : undefined,
+ size: typeof runProperties?.sz === 'number' ? runProperties.sz : undefined,
+ color: runProperties?.color?.val ?? undefined,
+ highlight: runProperties?.highlight ?? undefined,
+ styleId: runProperties?.rStyle ?? undefined,
+ language: runProperties?.lang?.val ?? undefined,
+ },
+ };
+}
+
+function mapTabNode(): TabNodeInfo {
+ return { nodeType: 'tab', kind: 'inline', properties: {} };
+}
+
+function mapLineBreakNode(): LineBreakNodeInfo {
+ return { nodeType: 'lineBreak', kind: 'inline', properties: {} };
+}
+
+function isInlineCandidate(candidate: BlockCandidate | InlineCandidate): candidate is InlineCandidate {
+ return 'anchor' in candidate;
+}
+
+/**
+ * Maps a block or inline candidate to its typed {@link NodeInfo} representation.
+ *
+ * @param candidate - The block or inline candidate to map.
+ * @param overrideType - Optional node type override.
+ * @returns Typed node information with properties populated from node attributes.
+ * @throws {Error} If the node type is not implemented or the candidate kind mismatches.
+ */
+export function mapNodeInfo(candidate: BlockCandidate | InlineCandidate, overrideType?: NodeType): NodeInfo {
+ const nodeType: NodeType = overrideType ?? candidate.nodeType;
+ const kind = isInlineCandidate(candidate) ? 'inline' : 'block';
+
+ switch (nodeType) {
+ case 'paragraph':
+ if (kind !== 'block')
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'Paragraph nodes can only be resolved as blocks.');
+ return mapParagraphNode(candidate as BlockCandidate);
+ case 'heading':
+ if (kind !== 'block')
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'Heading nodes can only be resolved as blocks.');
+ return mapHeadingNode(candidate as BlockCandidate);
+ case 'listItem':
+ if (kind !== 'block')
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'ListItem nodes can only be resolved as blocks.');
+ return mapListItemNode(candidate as BlockCandidate);
+ case 'table':
+ if (kind !== 'block')
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'Table nodes can only be resolved as blocks.');
+ return mapTableNode(candidate as BlockCandidate);
+ case 'tableRow':
+ if (kind !== 'block')
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'TableRow nodes can only be resolved as blocks.');
+ return mapTableRowNode();
+ case 'tableCell':
+ if (kind !== 'block')
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'TableCell nodes can only be resolved as blocks.');
+ return mapTableCellNode(candidate as BlockCandidate);
+ case 'image': {
+ const attrs = candidate.node?.attrs as ImageAttrs | undefined;
+ return buildImageInfo(attrs, kind);
+ }
+ case 'sdt': {
+ const attrs = candidate.node?.attrs as StructuredContentBlockAttrs | undefined;
+ return buildSdtInfo(attrs, kind);
+ }
+ case 'hyperlink':
+ if (!isInlineCandidate(candidate))
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'Hyperlink nodes can only be resolved inline.');
+ return mapHyperlinkNode(candidate);
+ case 'comment':
+ if (!isInlineCandidate(candidate))
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'Comment nodes can only be resolved inline.');
+ return mapCommentNode(candidate);
+ case 'run':
+ if (!isInlineCandidate(candidate))
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'Run nodes can only be resolved inline.');
+ return mapRunNode(candidate);
+ case 'bookmark':
+ if (!isInlineCandidate(candidate))
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'Bookmark nodes can only be resolved inline.');
+ return mapBookmarkNode(candidate);
+ case 'footnoteRef':
+ if (!isInlineCandidate(candidate))
+ throw new DocumentApiAdapterError('INVALID_TARGET', 'Footnote references can only be resolved inline.');
+ return mapFootnoteRefNode(candidate);
+ case 'tab':
+ return mapTabNode();
+ case 'lineBreak':
+ return mapLineBreakNode();
+ default:
+ throw new DocumentApiAdapterError('INVALID_TARGET', `Node type "${nodeType}" is not implemented yet.`);
+ }
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/node-info-resolver.ts b/packages/super-editor/src/document-api-adapters/helpers/node-info-resolver.ts
new file mode 100644
index 0000000000..76ce6b349a
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/node-info-resolver.ts
@@ -0,0 +1,61 @@
+import type { Editor } from '../../core/Editor.js';
+import type { NodeAddress, NodeInfo } from '@superdoc/document-api';
+import { findInlineByAnchor, type InlineIndex } from './inline-address-resolver.js';
+import { getInlineIndex } from './index-cache.js';
+import { findBlockById, type BlockIndex } from './node-address-resolver.js';
+import { mapNodeInfo } from './node-info-mapper.js';
+
+/**
+ * Resolves a single {@link NodeAddress} to its {@link NodeInfo} representation.
+ *
+ * @param editor - The editor instance.
+ * @param index - Pre-built block index.
+ * @param address - The address to resolve.
+ * @param inlineIndex - Optional pre-built inline index (built lazily if omitted).
+ * @returns The resolved node info, or `undefined` if the address cannot be found.
+ */
+export function resolveNodeInfoForAddress(
+ editor: Editor,
+ index: BlockIndex,
+ address: NodeAddress,
+ inlineIndex?: InlineIndex,
+): NodeInfo | undefined {
+ if (address.kind === 'block') {
+ const candidate = findBlockById(index, address);
+ if (!candidate) return undefined;
+ return mapNodeInfo(candidate, address.nodeType);
+ }
+
+ const resolvedInlineIndex = inlineIndex ?? getInlineIndex(editor);
+ const candidate = findInlineByAnchor(resolvedInlineIndex, address);
+ if (!candidate) return undefined;
+ return mapNodeInfo(candidate, address.nodeType);
+}
+
+/**
+ * Batch-resolves an array of addresses to their {@link NodeInfo} representations.
+ * Unresolvable addresses are silently skipped.
+ *
+ * @param editor - The editor instance.
+ * @param index - Pre-built block index.
+ * @param addresses - The addresses to resolve.
+ * @returns Array of resolved node infos (may be shorter than input if some addresses are missing).
+ */
+export function resolveIncludedNodes(editor: Editor, index: BlockIndex, addresses: NodeAddress[]): NodeInfo[] {
+ const included: NodeInfo[] = [];
+ let inlineIndex: InlineIndex | undefined;
+
+ for (const address of addresses) {
+ if (address.kind === 'inline') {
+ inlineIndex ??= getInlineIndex(editor);
+ const info = resolveNodeInfoForAddress(editor, index, address, inlineIndex);
+ if (info) included.push(info);
+ continue;
+ }
+
+ const info = resolveNodeInfoForAddress(editor, index, address);
+ if (info) included.push(info);
+ }
+
+ return included;
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/text-mutation-resolution.test.ts b/packages/super-editor/src/document-api-adapters/helpers/text-mutation-resolution.test.ts
new file mode 100644
index 0000000000..66c3caf491
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/text-mutation-resolution.test.ts
@@ -0,0 +1,82 @@
+import type { TextAddress } from '@superdoc/document-api';
+import { buildTextMutationResolution, readTextAtResolvedRange } from './text-mutation-resolution.js';
+import type { Editor } from '../../core/Editor.js';
+
+function makeEditor(text: string): Editor {
+ return {
+ state: {
+ doc: {
+ textBetween: vi.fn((_from: number, _to: number, _blockSep: string, _leafChar: string) => text),
+ },
+ },
+ } as unknown as Editor;
+}
+
+describe('readTextAtResolvedRange', () => {
+ it('delegates to textBetween with canonical separators', () => {
+ const editor = makeEditor('Hello');
+ const result = readTextAtResolvedRange(editor, { from: 1, to: 6 });
+
+ expect(result).toBe('Hello');
+ expect(editor.state.doc.textBetween).toHaveBeenCalledWith(1, 6, '\n', '\ufffc');
+ });
+
+ it('returns empty string for collapsed ranges', () => {
+ const editor = makeEditor('');
+ const result = readTextAtResolvedRange(editor, { from: 1, to: 1 });
+
+ expect(result).toBe('');
+ expect(editor.state.doc.textBetween).toHaveBeenCalledWith(1, 1, '\n', '\ufffc');
+ });
+});
+
+describe('buildTextMutationResolution', () => {
+ const target: TextAddress = { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } };
+
+ it('builds resolution with all fields', () => {
+ const requestedTarget: TextAddress = { kind: 'text', blockId: 'p1', range: { start: 0, end: 10 } };
+ const result = buildTextMutationResolution({
+ requestedTarget,
+ target,
+ range: { from: 1, to: 6 },
+ text: 'Hello',
+ });
+
+ expect(result).toEqual({
+ requestedTarget,
+ target,
+ range: { from: 1, to: 6 },
+ text: 'Hello',
+ });
+ });
+
+ it('omits requestedTarget when not provided', () => {
+ const result = buildTextMutationResolution({
+ target,
+ range: { from: 1, to: 6 },
+ text: 'Hello',
+ });
+
+ expect(result).toEqual({
+ target,
+ range: { from: 1, to: 6 },
+ text: 'Hello',
+ });
+ expect('requestedTarget' in result).toBe(false);
+ });
+
+ it('handles collapsed ranges with empty text', () => {
+ const collapsedTarget: TextAddress = { kind: 'text', blockId: 'p1', range: { start: 0, end: 0 } };
+ const result = buildTextMutationResolution({
+ target: collapsedTarget,
+ range: { from: 1, to: 1 },
+ text: '',
+ });
+
+ expect(result).toEqual({
+ target: collapsedTarget,
+ range: { from: 1, to: 1 },
+ text: '',
+ });
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/helpers/text-mutation-resolution.ts b/packages/super-editor/src/document-api-adapters/helpers/text-mutation-resolution.ts
new file mode 100644
index 0000000000..0ea253b2cd
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/text-mutation-resolution.ts
@@ -0,0 +1,40 @@
+import type { TextAddress, TextMutationResolution } from '@superdoc/document-api';
+import type { Editor } from '../../core/Editor.js';
+import type { ResolvedTextTarget } from './adapter-utils.js';
+
+/** Unicode Object Replacement Character — used as placeholder for leaf inline nodes in textBetween(). */
+const OBJECT_REPLACEMENT_CHAR = '\ufffc';
+
+/**
+ * Reads the canonical flattened text between two resolved document positions.
+ *
+ * Uses `\n` as the block separator and `\ufffc` (Object Replacement Character) as the
+ * leaf-inline placeholder, matching the offset model used by `TextAddress`.
+ *
+ * @param editor - The editor instance to read from.
+ * @param range - Resolved absolute document positions.
+ * @returns The text content between the resolved positions.
+ */
+export function readTextAtResolvedRange(editor: Editor, range: ResolvedTextTarget): string {
+ return editor.state.doc.textBetween(range.from, range.to, '\n', OBJECT_REPLACEMENT_CHAR);
+}
+
+/**
+ * Builds a `TextMutationResolution` from already-resolved adapter data.
+ *
+ * @param input - The resolved target, range, and text snapshot.
+ * @returns A `TextMutationResolution` suitable for inclusion in a `TextMutationReceipt`.
+ */
+export function buildTextMutationResolution(input: {
+ requestedTarget?: TextAddress;
+ target: TextAddress;
+ range: ResolvedTextTarget;
+ text: string;
+}): TextMutationResolution {
+ return {
+ ...(input.requestedTarget ? { requestedTarget: input.requestedTarget } : {}),
+ target: input.target,
+ range: { from: input.range.from, to: input.range.to },
+ text: input.text,
+ };
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/text-offset-resolver.test.ts b/packages/super-editor/src/document-api-adapters/helpers/text-offset-resolver.test.ts
new file mode 100644
index 0000000000..17f3dcaa40
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/text-offset-resolver.test.ts
@@ -0,0 +1,103 @@
+import type { Node as ProseMirrorNode } from 'prosemirror-model';
+import { resolveTextRangeInBlock } from './text-offset-resolver.js';
+
+type NodeOptions = {
+ text?: string;
+ isInline?: boolean;
+ isBlock?: boolean;
+ isLeaf?: boolean;
+ inlineContent?: boolean;
+ nodeSize?: number;
+};
+
+function createNode(typeName: string, children: ProseMirrorNode[] = [], options: NodeOptions = {}): ProseMirrorNode {
+ const text = options.text ?? '';
+ const isText = typeName === 'text';
+ const isInline = options.isInline ?? isText;
+ const isBlock = options.isBlock ?? (!isInline && typeName !== 'doc');
+ const inlineContent = options.inlineContent ?? isBlock;
+ const isLeaf = options.isLeaf ?? (isInline && !isText && children.length === 0);
+
+ const contentSize = children.reduce((sum, child) => sum + child.nodeSize, 0);
+ const nodeSize = isText ? text.length : options.nodeSize != null ? options.nodeSize : isLeaf ? 1 : contentSize + 2;
+
+ return {
+ type: { name: typeName },
+ text: isText ? text : undefined,
+ nodeSize,
+ isText,
+ isInline,
+ isBlock,
+ inlineContent,
+ isTextblock: inlineContent,
+ isLeaf,
+ childCount: children.length,
+ child(index: number) {
+ return children[index]!;
+ },
+ } as unknown as ProseMirrorNode;
+}
+
+describe('resolveTextRangeInBlock', () => {
+ it('resolves plain text offsets to absolute positions', () => {
+ const textNode = createNode('text', [], { text: 'Hello' });
+ const paragraph = createNode('paragraph', [textNode], { isBlock: true, inlineContent: true });
+
+ const result = resolveTextRangeInBlock(paragraph, 0, { start: 0, end: 5 });
+
+ expect(result).toEqual({ from: 1, to: 6 });
+ });
+
+ it('resolves offsets that target leaf atoms with nodeSize > 1', () => {
+ const textNode = createNode('text', [], { text: 'A' });
+ const imageNode = createNode('image', [], { isInline: true, isLeaf: true, nodeSize: 3 });
+ const paragraph = createNode('paragraph', [textNode, imageNode], { isBlock: true, inlineContent: true });
+
+ const result = resolveTextRangeInBlock(paragraph, 0, { start: 1, end: 2 });
+
+ expect(result).toEqual({ from: 2, to: 5 });
+ });
+
+ it('treats inline wrappers as transparent', () => {
+ const textNode = createNode('text', [], { text: 'Hi' });
+ const runNode = createNode('run', [textNode], { isInline: true, isLeaf: false });
+ const paragraph = createNode('paragraph', [runNode], { isBlock: true, inlineContent: true });
+
+ const result = resolveTextRangeInBlock(paragraph, 0, { start: 0, end: 2 });
+
+ expect(result).toEqual({ from: 2, to: 4 });
+ });
+
+ it('returns null for out-of-range offsets', () => {
+ const textNode = createNode('text', [], { text: 'Hi' });
+ const paragraph = createNode('paragraph', [textNode], { isBlock: true, inlineContent: true });
+
+ const result = resolveTextRangeInBlock(paragraph, 0, { start: 0, end: 5 });
+
+ expect(result).toBeNull();
+ });
+
+ it('resolves collapsed zero-offset ranges in empty text blocks', () => {
+ const paragraph = createNode('paragraph', [], { isBlock: true, inlineContent: true });
+
+ const result = resolveTextRangeInBlock(paragraph, 10, { start: 0, end: 0 });
+
+ expect(result).toEqual({ from: 11, to: 11 });
+ });
+
+ it('accounts for block separators inside container blocks', () => {
+ const paraA = createNode('paragraph', [createNode('text', [], { text: 'A' })], {
+ isBlock: true,
+ inlineContent: true,
+ });
+ const paraB = createNode('paragraph', [createNode('text', [], { text: 'B' })], {
+ isBlock: true,
+ inlineContent: true,
+ });
+ const cell = createNode('tableCell', [paraA, paraB], { isBlock: true, inlineContent: false });
+
+ const result = resolveTextRangeInBlock(cell, 0, { start: 2, end: 3 });
+
+ expect(result).toEqual({ from: 5, to: 6 });
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/helpers/text-offset-resolver.ts b/packages/super-editor/src/document-api-adapters/helpers/text-offset-resolver.ts
new file mode 100644
index 0000000000..1d677981cc
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/text-offset-resolver.ts
@@ -0,0 +1,107 @@
+import type { Node as ProseMirrorNode } from 'prosemirror-model';
+
+export type TextOffsetRange = {
+ start: number;
+ end: number;
+};
+
+export type ResolvedTextRange = {
+ from: number;
+ to: number;
+};
+
+function resolveSegmentPosition(
+ targetOffset: number,
+ segmentStart: number,
+ segmentLength: number,
+ docFrom: number,
+ docTo: number,
+): number {
+ if (segmentLength <= 1) {
+ return targetOffset <= segmentStart ? docFrom : docTo;
+ }
+ return docFrom + (targetOffset - segmentStart);
+}
+
+/**
+ * Resolves block-relative text offsets into absolute ProseMirror positions.
+ *
+ * Uses the same flattened text model as search:
+ * - Text contributes its length.
+ * - Leaf atoms contribute 1.
+ * - Inline wrappers contribute only their inner text.
+ * - Block separators contribute 1 between block children.
+ */
+export function resolveTextRangeInBlock(
+ blockNode: ProseMirrorNode,
+ blockPos: number,
+ range: TextOffsetRange,
+): ResolvedTextRange | null {
+ if (range.start < 0 || range.end < range.start) return null;
+
+ let offset = 0;
+ let fromPos: number | undefined;
+ let toPos: number | undefined;
+
+ const advanceSegment = (segmentLength: number, docFrom: number, docTo: number) => {
+ const segmentStart = offset;
+ const segmentEnd = offset + segmentLength;
+
+ if (fromPos == null && range.start <= segmentEnd) {
+ fromPos = resolveSegmentPosition(range.start, segmentStart, segmentLength, docFrom, docTo);
+ }
+ if (toPos == null && range.end <= segmentEnd) {
+ toPos = resolveSegmentPosition(range.end, segmentStart, segmentLength, docFrom, docTo);
+ }
+
+ offset = segmentEnd;
+ };
+
+ const walkNodeContent = (node: ProseMirrorNode, contentStart: number) => {
+ let isFirstChild = true;
+ let childOffset = 0;
+
+ for (let i = 0; i < node.childCount; i += 1) {
+ const child = node.child(i);
+ const childPos = contentStart + childOffset;
+
+ if (child.isBlock && !isFirstChild) {
+ advanceSegment(1, childPos, childPos + 1);
+ }
+
+ walkNode(child, childPos);
+ childOffset += child.nodeSize;
+ isFirstChild = false;
+ }
+ };
+
+ const walkNode = (node: ProseMirrorNode, docPos: number) => {
+ if (node.isText) {
+ const text = node.text ?? '';
+ if (text.length > 0) {
+ advanceSegment(text.length, docPos, docPos + text.length);
+ }
+ return;
+ }
+
+ if (node.isLeaf) {
+ advanceSegment(1, docPos, docPos + node.nodeSize);
+ return;
+ }
+
+ walkNodeContent(node, docPos + 1);
+ };
+
+ walkNodeContent(blockNode, blockPos + 1);
+
+ // Empty text blocks have no traversable segments. A collapsed 0..0 range
+ // should still resolve to the block start so inserts can target blank docs.
+ if (offset === 0 && range.start === 0 && range.end === 0) {
+ const anchor = blockPos + 1;
+ return { from: anchor, to: anchor };
+ }
+
+ if (range.end > offset) return null;
+ if (fromPos == null || toPos == null) return null;
+ return { from: fromPos, to: toPos };
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/tracked-change-refs.ts b/packages/super-editor/src/document-api-adapters/helpers/tracked-change-refs.ts
new file mode 100644
index 0000000000..2d0cf1a8ea
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/tracked-change-refs.ts
@@ -0,0 +1,41 @@
+import type { Editor } from '../../core/Editor.js';
+import { TrackInsertMarkName } from '../../extensions/track-changes/constants.js';
+import { buildTrackedChangeCanonicalIdMap } from './tracked-change-resolver.js';
+import { toNonEmptyString } from './value-utils.js';
+
+type ReceiptInsert = { kind: 'entity'; entityType: 'trackedChange'; entityId: string };
+
+type PmMarkLike = { readonly type: { readonly name: string }; readonly attrs?: Readonly> };
+
+/**
+ * Collects tracked-insert mark references within a document range.
+ *
+ * @param editor - The editor instance to query.
+ * @param from - Start position in the document.
+ * @param to - End position in the document.
+ * @returns Deduplicated tracked-change entity refs, or `undefined` if none found.
+ */
+export function collectTrackInsertRefsInRange(editor: Editor, from: number, to: number): ReceiptInsert[] | undefined {
+ if (to <= from) return undefined;
+
+ // ProseMirror Node exposes nodesBetween but the Editor type doesn't surface it directly.
+ const doc = editor.state.doc as {
+ nodesBetween?: (from: number, to: number, callback: (node: { marks?: readonly PmMarkLike[] }) => void) => void;
+ };
+ if (typeof doc.nodesBetween !== 'function') return undefined;
+
+ const canonicalIdByAlias = buildTrackedChangeCanonicalIdMap(editor);
+ const ids = new Set();
+ doc.nodesBetween(from, to, (node) => {
+ const marks = node.marks ?? [];
+ for (const mark of marks) {
+ if (mark.type.name !== TrackInsertMarkName) continue;
+ const id = toNonEmptyString(mark.attrs?.id);
+ if (!id) continue;
+ ids.add(canonicalIdByAlias.get(id) ?? id);
+ }
+ });
+
+ if (ids.size === 0) return undefined;
+ return Array.from(ids).map((id) => ({ kind: 'entity', entityType: 'trackedChange', entityId: id }));
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/tracked-change-resolver.test.ts b/packages/super-editor/src/document-api-adapters/helpers/tracked-change-resolver.test.ts
new file mode 100644
index 0000000000..6d46658e9b
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/tracked-change-resolver.test.ts
@@ -0,0 +1,230 @@
+import { describe, expect, it, vi, beforeEach } from 'vitest';
+import type { Editor } from '../../core/Editor.js';
+import {
+ TrackDeleteMarkName,
+ TrackFormatMarkName,
+ TrackInsertMarkName,
+} from '../../extensions/track-changes/constants.js';
+import { getTrackChanges } from '../../extensions/track-changes/trackChangesHelpers/getTrackChanges.js';
+import {
+ buildTrackedChangeCanonicalIdMap,
+ groupTrackedChanges,
+ resolveTrackedChange,
+ resolveTrackedChangeType,
+ toCanonicalTrackedChangeId,
+} from './tracked-change-resolver.js';
+
+vi.mock('../../extensions/track-changes/trackChangesHelpers/getTrackChanges.js', () => ({
+ getTrackChanges: vi.fn(),
+}));
+
+function makeEditor(): Editor {
+ return {
+ state: {
+ doc: {
+ content: { size: 100 },
+ textBetween: vi.fn((_from: number, _to: number) => 'excerpt'),
+ },
+ },
+ } as unknown as Editor;
+}
+
+function makeTrackMark(typeName: string, id: string, attrs: Record = {}) {
+ return {
+ mark: {
+ type: { name: typeName },
+ attrs: { id, ...attrs },
+ },
+ };
+}
+
+describe('resolveTrackedChangeType', () => {
+ it('returns insert when hasInsert is true', () => {
+ expect(resolveTrackedChangeType({ hasInsert: true, hasDelete: false, hasFormat: false })).toBe('insert');
+ });
+
+ it('returns delete when only hasDelete is true', () => {
+ expect(resolveTrackedChangeType({ hasInsert: false, hasDelete: true, hasFormat: false })).toBe('delete');
+ });
+
+ it('returns format when hasFormat is true', () => {
+ expect(resolveTrackedChangeType({ hasInsert: false, hasDelete: false, hasFormat: true })).toBe('format');
+ });
+
+ it('returns format over insert/delete when hasFormat is true', () => {
+ expect(resolveTrackedChangeType({ hasInsert: true, hasDelete: true, hasFormat: true })).toBe('format');
+ });
+
+ it('returns insert when both hasInsert and hasDelete are true (no format)', () => {
+ expect(resolveTrackedChangeType({ hasInsert: true, hasDelete: true, hasFormat: false })).toBe('insert');
+ });
+});
+
+describe('groupTrackedChanges', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('groups marks by raw id', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ { ...makeTrackMark(TrackInsertMarkName, 'tc-1'), from: 1, to: 5 },
+ { ...makeTrackMark(TrackDeleteMarkName, 'tc-1'), from: 5, to: 10 },
+ ] as never);
+
+ const editor = makeEditor();
+ const grouped = groupTrackedChanges(editor);
+
+ expect(grouped).toHaveLength(1);
+ expect(grouped[0]?.rawId).toBe('tc-1');
+ expect(grouped[0]?.from).toBe(1);
+ expect(grouped[0]?.to).toBe(10);
+ expect(grouped[0]?.hasInsert).toBe(true);
+ expect(grouped[0]?.hasDelete).toBe(true);
+ });
+
+ it('keeps separate entries for different raw ids', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ { ...makeTrackMark(TrackInsertMarkName, 'tc-1'), from: 1, to: 5 },
+ { ...makeTrackMark(TrackDeleteMarkName, 'tc-2'), from: 6, to: 10 },
+ ] as never);
+
+ const grouped = groupTrackedChanges(makeEditor());
+ expect(grouped).toHaveLength(2);
+ });
+
+ it('generates deterministic stable ids', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ { ...makeTrackMark(TrackInsertMarkName, 'tc-1', { author: 'Ada' }), from: 2, to: 5 },
+ ] as never);
+
+ const editor = makeEditor();
+ const first = groupTrackedChanges(editor);
+ // Force cache invalidation by changing doc reference
+ (editor.state as { doc: unknown }).doc = {
+ ...editor.state.doc,
+ textBetween: vi.fn(() => 'excerpt'),
+ };
+ const second = groupTrackedChanges(editor);
+
+ expect(first[0]?.id).toBe(second[0]?.id);
+ });
+
+ it('caches results by document reference', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ { ...makeTrackMark(TrackInsertMarkName, 'tc-1'), from: 1, to: 5 },
+ ] as never);
+
+ const editor = makeEditor();
+ const first = groupTrackedChanges(editor);
+ const second = groupTrackedChanges(editor);
+
+ expect(first).toBe(second);
+ expect(vi.mocked(getTrackChanges)).toHaveBeenCalledTimes(1);
+ });
+
+ it('returns empty array when no tracked marks exist', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([] as never);
+ expect(groupTrackedChanges(makeEditor())).toEqual([]);
+ });
+
+ it('skips marks without an id', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ { mark: { type: { name: TrackInsertMarkName }, attrs: {} }, from: 1, to: 5 },
+ ] as never);
+
+ expect(groupTrackedChanges(makeEditor())).toEqual([]);
+ });
+
+ it('detects format marks', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ { ...makeTrackMark(TrackFormatMarkName, 'tc-1'), from: 1, to: 5 },
+ ] as never);
+
+ const grouped = groupTrackedChanges(makeEditor());
+ expect(grouped[0]?.hasFormat).toBe(true);
+ expect(grouped[0]?.hasInsert).toBe(false);
+ expect(grouped[0]?.hasDelete).toBe(false);
+ });
+
+ it('sorts results by from position', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ { ...makeTrackMark(TrackInsertMarkName, 'tc-2'), from: 10, to: 15 },
+ { ...makeTrackMark(TrackDeleteMarkName, 'tc-1'), from: 1, to: 5 },
+ ] as never);
+
+ const grouped = groupTrackedChanges(makeEditor());
+ expect(grouped[0]?.from).toBeLessThan(grouped[1]?.from ?? 0);
+ });
+});
+
+describe('resolveTrackedChange', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('finds a grouped change by derived id', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ { ...makeTrackMark(TrackInsertMarkName, 'tc-1'), from: 1, to: 5 },
+ ] as never);
+
+ const editor = makeEditor();
+ const grouped = groupTrackedChanges(editor);
+ const id = grouped[0]?.id;
+ expect(id).toBeDefined();
+
+ const resolved = resolveTrackedChange(editor, id!);
+ expect(resolved?.rawId).toBe('tc-1');
+ });
+
+ it('returns null for unknown ids', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([] as never);
+ expect(resolveTrackedChange(makeEditor(), 'unknown')).toBeNull();
+ });
+});
+
+describe('toCanonicalTrackedChangeId', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('maps a raw id to its canonical derived id', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ { ...makeTrackMark(TrackInsertMarkName, 'tc-1'), from: 1, to: 5 },
+ ] as never);
+
+ const editor = makeEditor();
+ const canonical = toCanonicalTrackedChangeId(editor, 'tc-1');
+ expect(typeof canonical).toBe('string');
+ expect(canonical).not.toBe('tc-1');
+ });
+
+ it('returns null for unknown raw ids', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([] as never);
+ expect(toCanonicalTrackedChangeId(makeEditor(), 'missing')).toBeNull();
+ });
+});
+
+describe('buildTrackedChangeCanonicalIdMap', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('maps both raw id and canonical id to canonical id', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ { ...makeTrackMark(TrackInsertMarkName, 'tc-1'), from: 1, to: 5 },
+ ] as never);
+
+ const editor = makeEditor();
+ const map = buildTrackedChangeCanonicalIdMap(editor);
+ const grouped = groupTrackedChanges(editor);
+ const canonicalId = grouped[0]?.id;
+
+ expect(map.get('tc-1')).toBe(canonicalId);
+ expect(map.get(canonicalId!)).toBe(canonicalId);
+ });
+
+ it('returns empty map when no tracked changes exist', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([] as never);
+ expect(buildTrackedChangeCanonicalIdMap(makeEditor()).size).toBe(0);
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/helpers/tracked-change-resolver.ts b/packages/super-editor/src/document-api-adapters/helpers/tracked-change-resolver.ts
new file mode 100644
index 0000000000..77eaef8ff5
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/tracked-change-resolver.ts
@@ -0,0 +1,177 @@
+import type { Node as ProseMirrorNode } from 'prosemirror-model';
+import type { Editor } from '../../core/Editor.js';
+import type { TrackChangeType } from '@superdoc/document-api';
+import {
+ TrackDeleteMarkName,
+ TrackFormatMarkName,
+ TrackInsertMarkName,
+} from '../../extensions/track-changes/constants.js';
+import { getTrackChanges } from '../../extensions/track-changes/trackChangesHelpers/getTrackChanges.js';
+import { normalizeExcerpt, toNonEmptyString } from './value-utils.js';
+
+const DERIVED_ID_LENGTH = 24;
+
+type RawTrackedMark = {
+ mark: {
+ type: { name: string };
+ attrs?: Record;
+ };
+ from: number;
+ to: number;
+};
+
+export type GroupedTrackedChange = {
+ rawId: string;
+ id: string;
+ from: number;
+ to: number;
+ hasInsert: boolean;
+ hasDelete: boolean;
+ hasFormat: boolean;
+ attrs: Record;
+};
+
+type ChangeTypeInput = Pick;
+
+function getRawTrackedMarks(editor: Editor): RawTrackedMark[] {
+ try {
+ const marks = getTrackChanges(editor.state) as RawTrackedMark[];
+ return Array.isArray(marks) ? marks : [];
+ } catch {
+ return [];
+ }
+}
+
+/**
+ * Browser-safe hash producing a {@link DERIVED_ID_LENGTH}-char hex string.
+ *
+ * Uses FNV-1a-inspired mixing across three independent accumulators to produce
+ * a 96-bit (24-hex-char) digest. This is NOT cryptographic — it only needs to
+ * be deterministic with low collision probability for tracked-change IDs.
+ */
+function portableHash(input: string): string {
+ let h1 = 0x811c9dc5;
+ let h2 = 0x01000193;
+ let h3 = 0xdeadbeef;
+
+ for (let i = 0; i < input.length; i++) {
+ const c = input.charCodeAt(i);
+ h1 = Math.imul(h1 ^ c, 0x01000193);
+ h2 = Math.imul(h2 ^ c, 0x5bd1e995);
+ h3 = Math.imul(h3 ^ c, 0x1b873593);
+ }
+
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 0x85ebca6b);
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 0xcc9e2d51);
+ h3 = Math.imul(h3 ^ (h3 >>> 16), 0x1b873593);
+
+ return (
+ (h1 >>> 0).toString(16).padStart(8, '0') +
+ (h2 >>> 0).toString(16).padStart(8, '0') +
+ (h3 >>> 0).toString(16).padStart(8, '0')
+ ).slice(0, DERIVED_ID_LENGTH);
+}
+
+/**
+ * Derives a deterministic ID for a tracked change from the current document state.
+ *
+ * The ID is computed from the change type, ProseMirror positions, author,
+ * date, and a text excerpt. It is stable for a given document state but will
+ * change if the document is edited, since positions shift. These are NOT
+ * persistent identifiers — they are ephemeral keys valid only for the
+ * current transaction snapshot.
+ */
+function deriveTrackedChangeId(editor: Editor, change: Omit): string {
+ const type = resolveTrackedChangeType(change);
+ const excerpt = normalizeExcerpt(editor.state.doc.textBetween(change.from, change.to, ' ', '\ufffc')) ?? '';
+ const author = toNonEmptyString(change.attrs.author) ?? '';
+ const authorEmail = toNonEmptyString(change.attrs.authorEmail) ?? '';
+ const date = toNonEmptyString(change.attrs.date) ?? '';
+ const signature = `${type}|${change.from}|${change.to}|${author}|${authorEmail}|${date}|${excerpt}`;
+
+ return portableHash(signature);
+}
+
+export function resolveTrackedChangeType(change: ChangeTypeInput): TrackChangeType {
+ if (change.hasFormat) return 'format';
+ if (change.hasDelete && !change.hasInsert) return 'delete';
+ return 'insert';
+}
+
+const groupedCache = new WeakMap();
+
+export function groupTrackedChanges(editor: Editor): GroupedTrackedChange[] {
+ const currentDoc = editor.state.doc;
+ const cached = groupedCache.get(editor);
+ if (cached && cached.doc === currentDoc) return cached.grouped;
+
+ const marks = getRawTrackedMarks(editor);
+ const byRawId = new Map>();
+
+ for (const item of marks) {
+ const attrs = item.mark?.attrs ?? {};
+ const id = toNonEmptyString(attrs.id);
+ if (!id) continue;
+
+ const existing = byRawId.get(id);
+ const markType = item.mark.type.name;
+ const nextHasInsert = markType === TrackInsertMarkName;
+ const nextHasDelete = markType === TrackDeleteMarkName;
+ const nextHasFormat = markType === TrackFormatMarkName;
+
+ if (!existing) {
+ byRawId.set(id, {
+ rawId: id,
+ from: item.from,
+ to: item.to,
+ hasInsert: nextHasInsert,
+ hasDelete: nextHasDelete,
+ hasFormat: nextHasFormat,
+ attrs: { ...attrs },
+ });
+ continue;
+ }
+
+ existing.from = Math.min(existing.from, item.from);
+ existing.to = Math.max(existing.to, item.to);
+ existing.hasInsert = existing.hasInsert || nextHasInsert;
+ existing.hasDelete = existing.hasDelete || nextHasDelete;
+ existing.hasFormat = existing.hasFormat || nextHasFormat;
+ if (Object.keys(existing.attrs).length === 0 && Object.keys(attrs).length > 0) {
+ existing.attrs = { ...attrs };
+ }
+ }
+
+ const grouped = Array.from(byRawId.values())
+ .map((change) => ({
+ ...change,
+ id: deriveTrackedChangeId(editor, change),
+ }))
+ .sort((a, b) => {
+ if (a.from !== b.from) return a.from - b.from;
+ return a.id.localeCompare(b.id);
+ });
+
+ groupedCache.set(editor, { doc: currentDoc, grouped });
+ return grouped;
+}
+
+export function resolveTrackedChange(editor: Editor, id: string): GroupedTrackedChange | null {
+ const grouped = groupTrackedChanges(editor);
+ return grouped.find((item) => item.id === id) ?? null;
+}
+
+export function toCanonicalTrackedChangeId(editor: Editor, rawId: string): string | null {
+ const grouped = groupTrackedChanges(editor);
+ return grouped.find((item) => item.rawId === rawId)?.id ?? null;
+}
+
+export function buildTrackedChangeCanonicalIdMap(editor: Editor): Map {
+ const grouped = groupTrackedChanges(editor);
+ const map = new Map();
+ for (const change of grouped) {
+ map.set(change.rawId, change.id);
+ map.set(change.id, change.id);
+ }
+ return map;
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/transaction-meta.test.ts b/packages/super-editor/src/document-api-adapters/helpers/transaction-meta.test.ts
new file mode 100644
index 0000000000..a9a91ba766
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/transaction-meta.test.ts
@@ -0,0 +1,51 @@
+import { describe, expect, it, vi } from 'vitest';
+import { applyDirectMutationMeta, applyTrackedMutationMeta } from './transaction-meta.js';
+
+function makeFakeTransaction() {
+ const meta = new Map();
+ return {
+ setMeta: vi.fn((key: string, value: unknown) => meta.set(key, value)),
+ getMeta: (key: string) => meta.get(key),
+ _meta: meta,
+ };
+}
+
+describe('applyDirectMutationMeta', () => {
+ it('sets inputType to programmatic', () => {
+ const tr = makeFakeTransaction();
+ applyDirectMutationMeta(tr as any);
+ expect(tr.setMeta).toHaveBeenCalledWith('inputType', 'programmatic');
+ });
+
+ it('sets skipTrackChanges to true', () => {
+ const tr = makeFakeTransaction();
+ applyDirectMutationMeta(tr as any);
+ expect(tr.setMeta).toHaveBeenCalledWith('skipTrackChanges', true);
+ });
+
+ it('returns the same transaction', () => {
+ const tr = makeFakeTransaction();
+ const result = applyDirectMutationMeta(tr as any);
+ expect(result).toBe(tr);
+ });
+});
+
+describe('applyTrackedMutationMeta', () => {
+ it('sets inputType to programmatic', () => {
+ const tr = makeFakeTransaction();
+ applyTrackedMutationMeta(tr as any);
+ expect(tr.setMeta).toHaveBeenCalledWith('inputType', 'programmatic');
+ });
+
+ it('sets forceTrackChanges to true', () => {
+ const tr = makeFakeTransaction();
+ applyTrackedMutationMeta(tr as any);
+ expect(tr.setMeta).toHaveBeenCalledWith('forceTrackChanges', true);
+ });
+
+ it('returns the same transaction', () => {
+ const tr = makeFakeTransaction();
+ const result = applyTrackedMutationMeta(tr as any);
+ expect(result).toBe(tr);
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/helpers/transaction-meta.ts b/packages/super-editor/src/document-api-adapters/helpers/transaction-meta.ts
new file mode 100644
index 0000000000..7a67259733
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/transaction-meta.ts
@@ -0,0 +1,27 @@
+import type { Transaction } from 'prosemirror-state';
+
+/**
+ * Applies metadata required for direct (non-tracked) document-api mutations.
+ * This prevents active track-changes sessions from transforming direct writes.
+ *
+ * @param tr - The ProseMirror transaction to annotate
+ * @returns The same transaction, with `inputType` and `skipTrackChanges` meta set
+ */
+export function applyDirectMutationMeta(tr: Transaction): Transaction {
+ tr.setMeta('inputType', 'programmatic');
+ tr.setMeta('skipTrackChanges', true);
+ return tr;
+}
+
+/**
+ * Applies metadata required for tracked mutations implemented via raw transactions.
+ * Tracked write operations that call tracked commands directly do not use this helper.
+ *
+ * @param tr - The ProseMirror transaction to annotate
+ * @returns The same transaction, with `inputType` and `forceTrackChanges` meta set
+ */
+export function applyTrackedMutationMeta(tr: Transaction): Transaction {
+ tr.setMeta('inputType', 'programmatic');
+ tr.setMeta('forceTrackChanges', true);
+ return tr;
+}
diff --git a/packages/super-editor/src/document-api-adapters/helpers/value-utils.test.ts b/packages/super-editor/src/document-api-adapters/helpers/value-utils.test.ts
new file mode 100644
index 0000000000..3c7bb5b2b2
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/value-utils.test.ts
@@ -0,0 +1,123 @@
+import { toNonEmptyString, toFiniteNumber, toId, resolveCommentIdFromAttrs, normalizeExcerpt } from './value-utils.js';
+
+describe('toNonEmptyString', () => {
+ it('returns a non-empty string as-is', () => {
+ expect(toNonEmptyString('hello')).toBe('hello');
+ });
+
+ it('returns undefined for an empty string', () => {
+ expect(toNonEmptyString('')).toBeUndefined();
+ });
+
+ it('returns undefined for non-string values', () => {
+ expect(toNonEmptyString(null)).toBeUndefined();
+ expect(toNonEmptyString(undefined)).toBeUndefined();
+ expect(toNonEmptyString(42)).toBeUndefined();
+ expect(toNonEmptyString(true)).toBeUndefined();
+ expect(toNonEmptyString({})).toBeUndefined();
+ });
+});
+
+describe('toFiniteNumber', () => {
+ it('returns a finite number as-is', () => {
+ expect(toFiniteNumber(42)).toBe(42);
+ expect(toFiniteNumber(0)).toBe(0);
+ expect(toFiniteNumber(-3.14)).toBe(-3.14);
+ });
+
+ it('returns undefined for non-finite numbers', () => {
+ expect(toFiniteNumber(Infinity)).toBeUndefined();
+ expect(toFiniteNumber(-Infinity)).toBeUndefined();
+ expect(toFiniteNumber(NaN)).toBeUndefined();
+ });
+
+ it('parses numeric strings', () => {
+ expect(toFiniteNumber('42')).toBe(42);
+ expect(toFiniteNumber('3.14')).toBe(3.14);
+ expect(toFiniteNumber(' 7 ')).toBe(7);
+ });
+
+ it('returns undefined for non-numeric strings', () => {
+ expect(toFiniteNumber('abc')).toBeUndefined();
+ expect(toFiniteNumber('')).toBeUndefined();
+ expect(toFiniteNumber(' ')).toBeUndefined();
+ });
+
+ it('returns undefined for non-number/string values', () => {
+ expect(toFiniteNumber(null)).toBeUndefined();
+ expect(toFiniteNumber(undefined)).toBeUndefined();
+ expect(toFiniteNumber(true)).toBeUndefined();
+ expect(toFiniteNumber({})).toBeUndefined();
+ });
+});
+
+describe('toId', () => {
+ it('returns a non-empty string as-is', () => {
+ expect(toId('abc')).toBe('abc');
+ });
+
+ it('returns undefined for an empty string', () => {
+ expect(toId('')).toBeUndefined();
+ });
+
+ it('converts a finite number to a string', () => {
+ expect(toId(42)).toBe('42');
+ expect(toId(0)).toBe('0');
+ });
+
+ it('returns undefined for non-finite numbers', () => {
+ expect(toId(NaN)).toBeUndefined();
+ expect(toId(Infinity)).toBeUndefined();
+ });
+
+ it('returns undefined for other types', () => {
+ expect(toId(null)).toBeUndefined();
+ expect(toId(undefined)).toBeUndefined();
+ expect(toId(true)).toBeUndefined();
+ expect(toId({})).toBeUndefined();
+ });
+});
+
+describe('resolveCommentIdFromAttrs', () => {
+ it('prefers commentId over importedId and w:id', () => {
+ expect(resolveCommentIdFromAttrs({ commentId: 'c1', importedId: 'i1', 'w:id': 'w1' })).toBe('c1');
+ });
+
+ it('falls back to importedId when commentId is absent', () => {
+ expect(resolveCommentIdFromAttrs({ importedId: 'i1', 'w:id': 'w1' })).toBe('i1');
+ });
+
+ it('falls back to w:id when commentId and importedId are absent', () => {
+ expect(resolveCommentIdFromAttrs({ 'w:id': 'w1' })).toBe('w1');
+ });
+
+ it('returns undefined when no id attribute is present', () => {
+ expect(resolveCommentIdFromAttrs({})).toBeUndefined();
+ });
+
+ it('skips empty string values', () => {
+ expect(resolveCommentIdFromAttrs({ commentId: '', importedId: 'i1' })).toBe('i1');
+ });
+});
+
+describe('normalizeExcerpt', () => {
+ it('collapses multiple whitespace characters', () => {
+ expect(normalizeExcerpt('hello world')).toBe('hello world');
+ });
+
+ it('trims leading and trailing whitespace', () => {
+ expect(normalizeExcerpt(' hello ')).toBe('hello');
+ });
+
+ it('normalizes newlines and tabs', () => {
+ expect(normalizeExcerpt('hello\n\tworld')).toBe('hello world');
+ });
+
+ it('returns undefined for empty string', () => {
+ expect(normalizeExcerpt('')).toBeUndefined();
+ });
+
+ it('returns undefined for whitespace-only string', () => {
+ expect(normalizeExcerpt(' \n\t ')).toBeUndefined();
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/helpers/value-utils.ts b/packages/super-editor/src/document-api-adapters/helpers/value-utils.ts
new file mode 100644
index 0000000000..499bf0e58a
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/helpers/value-utils.ts
@@ -0,0 +1,60 @@
+/**
+ * Shared scalar utility functions for document-api adapters.
+ */
+
+/**
+ * Returns the value as a string if it is a non-empty string, otherwise `undefined`.
+ *
+ * @param value - The value to test.
+ */
+export function toNonEmptyString(value: unknown): string | undefined {
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
+}
+
+/**
+ * Coerces a value to a finite number. Accepts numbers and numeric strings.
+ *
+ * @param value - The value to coerce.
+ * @returns A finite number, or `undefined` if the value cannot be coerced.
+ */
+export function toFiniteNumber(value: unknown): number | undefined {
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
+ if (typeof value === 'string' && value.trim().length > 0) {
+ const parsed = Number(value);
+ if (Number.isFinite(parsed)) return parsed;
+ }
+ return undefined;
+}
+
+/**
+ * Coerces a value to a stable ID string. Accepts non-empty strings and finite numbers.
+ *
+ * @param value - The value to coerce.
+ * @returns A string ID, or `undefined` if the value is not a valid identifier.
+ */
+export function toId(value: unknown): string | undefined {
+ if (typeof value === 'string' && value.length > 0) return value;
+ if (typeof value === 'number' && Number.isFinite(value)) return String(value);
+ return undefined;
+}
+
+/**
+ * Extracts a comment ID from node attributes, checking `commentId`, `importedId`, and `w:id` in order.
+ *
+ * @param attrs - The attributes record to search.
+ * @returns The first non-empty comment ID found, or `undefined`.
+ */
+export function resolveCommentIdFromAttrs(attrs: Record): string | undefined {
+ return toNonEmptyString(attrs.commentId) ?? toNonEmptyString(attrs.importedId) ?? toNonEmptyString(attrs['w:id']);
+}
+
+/**
+ * Normalizes whitespace in a text excerpt and returns `undefined` for empty results.
+ *
+ * @param text - The raw text to normalize.
+ * @returns Trimmed text with collapsed whitespace, or `undefined` if empty.
+ */
+export function normalizeExcerpt(text: string): string | undefined {
+ const trimmed = text.replace(/\s+/g, ' ').trim();
+ return trimmed.length ? trimmed : undefined;
+}
diff --git a/packages/super-editor/src/document-api-adapters/index.ts b/packages/super-editor/src/document-api-adapters/index.ts
new file mode 100644
index 0000000000..753c919c52
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/index.ts
@@ -0,0 +1,97 @@
+import type {
+ DocumentApiAdapters,
+ GetNodeByIdInput,
+ GetTextInput,
+ InfoInput,
+ NodeAddress,
+ Query,
+ TrackChangesAcceptAllInput,
+ TrackChangesAcceptInput,
+ TrackChangesGetInput,
+ TrackChangesRejectAllInput,
+ TrackChangesRejectInput,
+} from '@superdoc/document-api';
+import type { Editor } from '../core/Editor.js';
+import { getDocumentApiCapabilities } from './capabilities-adapter.js';
+import { createCommentsAdapter } from './comments-adapter.js';
+import { createParagraphAdapter } from './create-adapter.js';
+import { findAdapter } from './find-adapter.js';
+import { formatBoldAdapter } from './format-adapter.js';
+import { getNodeAdapter, getNodeByIdAdapter } from './get-node-adapter.js';
+import { getTextAdapter } from './get-text-adapter.js';
+import { infoAdapter } from './info-adapter.js';
+import {
+ listsExitAdapter,
+ listsGetAdapter,
+ listsIndentAdapter,
+ listsInsertAdapter,
+ listsListAdapter,
+ listsOutdentAdapter,
+ listsRestartAdapter,
+ listsSetTypeAdapter,
+} from './lists-adapter.js';
+import {
+ trackChangesAcceptAdapter,
+ trackChangesAcceptAllAdapter,
+ trackChangesGetAdapter,
+ trackChangesListAdapter,
+ trackChangesRejectAdapter,
+ trackChangesRejectAllAdapter,
+} from './track-changes-adapter.js';
+import { writeAdapter } from './write-adapter.js';
+
+/**
+ * Creates the full set of Document API adapters backed by the given editor instance.
+ *
+ * @param editor - The editor instance to bind adapters to.
+ * @returns Adapter implementations for document query/mutation APIs.
+ */
+export function getDocumentApiAdapters(editor: Editor): DocumentApiAdapters {
+ return {
+ find: {
+ find: (query: Query) => findAdapter(editor, query),
+ },
+ getNode: {
+ getNode: (address: NodeAddress) => getNodeAdapter(editor, address),
+ getNodeById: (input: GetNodeByIdInput) => getNodeByIdAdapter(editor, input),
+ },
+ getText: {
+ getText: (input: GetTextInput) => getTextAdapter(editor, input),
+ },
+ info: {
+ info: (input: InfoInput) => infoAdapter(editor, input),
+ },
+ capabilities: {
+ get: () => getDocumentApiCapabilities(editor),
+ },
+ // Factory pattern — comments has 11 methods; inline lambdas would be unwieldy.
+ comments: createCommentsAdapter(editor),
+ write: {
+ write: (request, options) => writeAdapter(editor, request, options),
+ },
+ format: {
+ bold: (input, options) => formatBoldAdapter(editor, input, options),
+ },
+ trackChanges: {
+ list: (query) => trackChangesListAdapter(editor, query),
+ get: (input: TrackChangesGetInput) => trackChangesGetAdapter(editor, input),
+ accept: (input: TrackChangesAcceptInput) => trackChangesAcceptAdapter(editor, input),
+ reject: (input: TrackChangesRejectInput) => trackChangesRejectAdapter(editor, input),
+ acceptAll: (input: TrackChangesAcceptAllInput) => trackChangesAcceptAllAdapter(editor, input),
+ rejectAll: (input: TrackChangesRejectAllInput) => trackChangesRejectAllAdapter(editor, input),
+ },
+ create: {
+ paragraph: (input, options) => createParagraphAdapter(editor, input, options),
+ },
+ lists: {
+ list: (query) => listsListAdapter(editor, query),
+ get: (input) => listsGetAdapter(editor, input),
+ insert: (input, options) => listsInsertAdapter(editor, input, options),
+ setType: (input, options) => listsSetTypeAdapter(editor, input, options),
+ indent: (input, options) => listsIndentAdapter(editor, input, options),
+ outdent: (input, options) => listsOutdentAdapter(editor, input, options),
+ restart: (input, options) => listsRestartAdapter(editor, input, options),
+ exit: (input, options) => listsExitAdapter(editor, input, options),
+ },
+ };
+}
diff --git a/packages/super-editor/src/document-api-adapters/info-adapter.test.ts b/packages/super-editor/src/document-api-adapters/info-adapter.test.ts
new file mode 100644
index 0000000000..10da54fc4f
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/info-adapter.test.ts
@@ -0,0 +1,133 @@
+import type { Query, QueryResult } from '@superdoc/document-api';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Editor } from '../core/Editor.js';
+import { findAdapter } from './find-adapter.js';
+import { getTextAdapter } from './get-text-adapter.js';
+import { infoAdapter } from './info-adapter.js';
+
+vi.mock('./find-adapter.js', () => ({
+ findAdapter: vi.fn(),
+}));
+
+vi.mock('./get-text-adapter.js', () => ({
+ getTextAdapter: vi.fn(),
+}));
+
+const findAdapterMock = vi.mocked(findAdapter);
+const getTextAdapterMock = vi.mocked(getTextAdapter);
+
+function makeResult(result: Partial): QueryResult {
+ return {
+ matches: [],
+ total: 0,
+ ...result,
+ };
+}
+
+function resolveFindResult(query: Query): QueryResult {
+ if (query.select.type === 'text') {
+ throw new Error('infoAdapter should only perform node-type queries.');
+ }
+
+ switch (query.select.nodeType) {
+ case 'paragraph':
+ return makeResult({ total: 5 });
+ case 'heading':
+ return makeResult({
+ total: 2,
+ matches: [
+ { kind: 'block', nodeType: 'heading', nodeId: 'H1' },
+ { kind: 'block', nodeType: 'heading', nodeId: 'H2' },
+ ],
+ nodes: [
+ {
+ nodeType: 'heading',
+ kind: 'block',
+ properties: { headingLevel: 2 },
+ text: 'Overview',
+ },
+ {
+ nodeType: 'heading',
+ kind: 'block',
+ properties: { headingLevel: 6 },
+ summary: { text: 'Details' },
+ },
+ ],
+ });
+ case 'table':
+ return makeResult({ total: 1 });
+ case 'image':
+ return makeResult({ total: 3 });
+ case 'comment':
+ return makeResult({
+ total: 4,
+ nodes: [
+ {
+ nodeType: 'comment',
+ kind: 'inline',
+ properties: { commentId: 'c-1' },
+ },
+ {
+ nodeType: 'comment',
+ kind: 'inline',
+ properties: { commentId: 'c-1' },
+ },
+ {
+ nodeType: 'comment',
+ kind: 'inline',
+ properties: { commentId: 'c-2' },
+ },
+ ],
+ });
+ default:
+ return makeResult({});
+ }
+}
+
+describe('infoAdapter', () => {
+ beforeEach(() => {
+ findAdapterMock.mockReset();
+ getTextAdapterMock.mockReset();
+ });
+
+ it('computes counts and outline from find/get-text adapters', () => {
+ getTextAdapterMock.mockReturnValue('hello world from info adapter');
+ findAdapterMock.mockImplementation((editor: Editor, query: Query) => resolveFindResult(query));
+
+ const result = infoAdapter({} as Editor, {});
+
+ expect(result.counts).toEqual({
+ words: 5,
+ paragraphs: 5,
+ headings: 2,
+ tables: 1,
+ images: 3,
+ comments: 2,
+ });
+ expect(result.outline).toEqual([
+ { level: 2, text: 'Overview', nodeId: 'H1' },
+ { level: 6, text: 'Details', nodeId: 'H2' },
+ ]);
+ expect(result.capabilities).toEqual({
+ canFind: true,
+ canGetNode: true,
+ canComment: true,
+ canReplace: true,
+ });
+ });
+
+ it('falls back to total comment count when includeNodes does not return comment nodes', () => {
+ getTextAdapterMock.mockReturnValue('');
+ findAdapterMock.mockImplementation((editor: Editor, query: Query) => {
+ if (query.select.type === 'text') return makeResult({});
+ if (query.select.nodeType === 'comment') {
+ return makeResult({ total: 7, nodes: [] });
+ }
+ return makeResult({});
+ });
+
+ const result = infoAdapter({} as Editor, {});
+
+ expect(result.counts.comments).toBe(7);
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/info-adapter.ts b/packages/super-editor/src/document-api-adapters/info-adapter.ts
new file mode 100644
index 0000000000..d11ce2bbcd
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/info-adapter.ts
@@ -0,0 +1,108 @@
+import type { DocumentInfo, InfoInput, NodeInfo, NodeType, QueryResult } from '@superdoc/document-api';
+import type { Editor } from '../core/Editor.js';
+import { findAdapter } from './find-adapter.js';
+import { getTextAdapter } from './get-text-adapter.js';
+
+type HeadingNodeInfo = Extract;
+type CommentNodeInfo = Extract;
+
+function countWords(text: string): number {
+ const matches = text.trim().match(/\S+/g);
+ return matches ? matches.length : 0;
+}
+
+function clampHeadingLevel(value: unknown): number {
+ if (typeof value !== 'number' || !Number.isFinite(value)) return 1;
+ const rounded = Math.floor(value);
+ if (rounded < 1) return 1;
+ if (rounded > 6) return 6;
+ return rounded;
+}
+
+function isHeadingNodeInfo(node: NodeInfo | undefined): node is HeadingNodeInfo {
+ return node?.kind === 'block' && node.nodeType === 'heading';
+}
+
+function isCommentNodeInfo(node: NodeInfo | undefined): node is CommentNodeInfo {
+ return node?.kind === 'inline' && node.nodeType === 'comment';
+}
+
+function getHeadingText(node: HeadingNodeInfo | undefined): string {
+ if (!node) return '';
+ if (typeof node.text === 'string' && node.text.length > 0) return node.text;
+ if (typeof node.summary?.text === 'string' && node.summary.text.length > 0) return node.summary.text;
+ return '';
+}
+
+function buildOutline(result: QueryResult): DocumentInfo['outline'] {
+ const outline: DocumentInfo['outline'] = [];
+
+ for (const [index, match] of result.matches.entries()) {
+ if (match.kind !== 'block') continue;
+
+ const maybeHeading = isHeadingNodeInfo(result.nodes?.[index]) ? result.nodes[index] : undefined;
+ outline.push({
+ level: clampHeadingLevel(maybeHeading?.properties.headingLevel),
+ text: getHeadingText(maybeHeading),
+ nodeId: match.nodeId,
+ });
+ }
+
+ return outline;
+}
+
+function countDistinctCommentIds(result: QueryResult): number {
+ const commentIds = new Set();
+ for (const node of result.nodes ?? []) {
+ if (!isCommentNodeInfo(node)) continue;
+ if (typeof node.properties.commentId !== 'string' || node.properties.commentId.length === 0) continue;
+ commentIds.add(node.properties.commentId);
+ }
+
+ // When node data is available, deduplicate by commentId. Otherwise fall
+ // back to the query total (e.g. when includeNodes was not requested).
+ if (commentIds.size > 0) {
+ return commentIds.size;
+ }
+ return result.total;
+}
+
+function findByNodeType(editor: Editor, nodeType: NodeType, includeNodes = false): QueryResult {
+ return findAdapter(editor, {
+ select: { type: 'node', nodeType },
+ includeNodes,
+ });
+}
+
+/**
+ * Build `doc.info` payload from engine-backed find/getText adapters.
+ *
+ * This keeps `document-api` engine-agnostic while centralizing composition
+ * logic in the super-editor adapter layer.
+ */
+export function infoAdapter(editor: Editor, _input: InfoInput): DocumentInfo {
+ const text = getTextAdapter(editor, {});
+ const paragraphResult = findByNodeType(editor, 'paragraph');
+ const headingResult = findByNodeType(editor, 'heading', true);
+ const tableResult = findByNodeType(editor, 'table');
+ const imageResult = findByNodeType(editor, 'image');
+ const commentResult = findByNodeType(editor, 'comment', true);
+
+ return {
+ counts: {
+ words: countWords(text),
+ paragraphs: paragraphResult.total,
+ headings: headingResult.total,
+ tables: tableResult.total,
+ images: imageResult.total,
+ comments: countDistinctCommentIds(commentResult),
+ },
+ outline: buildOutline(headingResult),
+ capabilities: {
+ canFind: true,
+ canGetNode: true,
+ canComment: true,
+ canReplace: true,
+ },
+ };
+}
diff --git a/packages/super-editor/src/document-api-adapters/lists-adapter.test.ts b/packages/super-editor/src/document-api-adapters/lists-adapter.test.ts
new file mode 100644
index 0000000000..4bd5ee26d0
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/lists-adapter.test.ts
@@ -0,0 +1,585 @@
+import { describe, expect, it, vi, beforeEach } from 'vitest';
+import type { Editor } from '../core/Editor.js';
+import {
+ listsExitAdapter,
+ listsIndentAdapter,
+ listsInsertAdapter,
+ listsListAdapter,
+ listsOutdentAdapter,
+ listsRestartAdapter,
+ listsSetTypeAdapter,
+} from './lists-adapter.js';
+import { ListHelpers } from '../core/helpers/list-numbering-helpers.js';
+
+type MockTextNode = {
+ type: { name: 'text' };
+ marks?: Array<{ type: { name: string }; attrs?: Record }>;
+};
+
+type MockParagraphNode = {
+ type: { name: 'paragraph' };
+ attrs: Record;
+ nodeSize: number;
+ isBlock: true;
+ textContent: string;
+ _textNode?: MockTextNode;
+};
+
+function makeListParagraph(options: {
+ id: string;
+ text?: string;
+ numId?: number;
+ ilvl?: number;
+ markerText?: string;
+ path?: number[];
+ numberingType?: string;
+ sdBlockId?: string;
+ trackedMarkId?: string;
+}): MockParagraphNode {
+ const text = options.text ?? '';
+ const numberingProperties =
+ options.numId != null
+ ? {
+ numId: options.numId,
+ ilvl: options.ilvl ?? 0,
+ }
+ : undefined;
+
+ return {
+ type: { name: 'paragraph' },
+ attrs: {
+ paraId: options.id,
+ sdBlockId: options.sdBlockId ?? options.id,
+ paragraphProperties: numberingProperties ? { numberingProperties } : {},
+ listRendering:
+ options.numId != null
+ ? {
+ markerText: options.markerText ?? '',
+ path: options.path ?? [],
+ numberingType: options.numberingType ?? 'decimal',
+ }
+ : null,
+ },
+ nodeSize: Math.max(2, text.length + 2),
+ isBlock: true,
+ textContent: text,
+ _textNode:
+ options.trackedMarkId != null
+ ? {
+ type: { name: 'text' },
+ marks: [{ type: { name: 'trackInsert' }, attrs: { id: options.trackedMarkId } }],
+ }
+ : undefined,
+ };
+}
+
+function makeDoc(children: MockParagraphNode[]) {
+ return {
+ get content() {
+ return {
+ size: children.reduce((sum, child) => sum + child.nodeSize, 0),
+ };
+ },
+ nodeAt(pos: number) {
+ let cursor = 0;
+ for (const child of children) {
+ if (cursor === pos) return child;
+ cursor += child.nodeSize;
+ }
+ return null;
+ },
+ descendants(callback: (node: MockParagraphNode, pos: number) => void) {
+ let pos = 0;
+ for (const child of children) {
+ callback(child, pos);
+ pos += child.nodeSize;
+ }
+ return undefined;
+ },
+ nodesBetween(from: number, to: number, callback: (node: unknown) => void) {
+ let pos = 0;
+ for (const child of children) {
+ const end = pos + child.nodeSize;
+ if (end < from || pos > to) {
+ pos = end;
+ continue;
+ }
+ callback(child);
+ if (child._textNode) callback(child._textNode);
+ pos = end;
+ }
+ return undefined;
+ },
+ };
+}
+
+function makeEditor(
+ children: MockParagraphNode[],
+ commandOverrides: Record = {},
+ editorOptions: { user?: { name: string } } = {},
+): Editor {
+ const doc = makeDoc(children);
+ const baseCommands = {
+ insertListItemAt: vi.fn(
+ (options: {
+ pos: number;
+ position: 'before' | 'after';
+ sdBlockId?: string;
+ text?: string;
+ tracked?: boolean;
+ }) => {
+ const insertionId = options.sdBlockId ?? `inserted-${Date.now()}`;
+ let targetIndex = -1;
+ let cursor = 0;
+ for (let i = 0; i < children.length; i += 1) {
+ if (cursor === options.pos) {
+ targetIndex = i;
+ break;
+ }
+ cursor += children[i]!.nodeSize;
+ }
+ if (targetIndex < 0) return false;
+
+ const target = children[targetIndex]!;
+ const numbering = (
+ target.attrs.paragraphProperties as { numberingProperties?: { numId?: number; ilvl?: number } }
+ )?.numberingProperties;
+ if (!numbering) return false;
+
+ const inserted = makeListParagraph({
+ id: insertionId,
+ sdBlockId: insertionId,
+ text: options.text ?? '',
+ numId: numbering.numId,
+ ilvl: numbering.ilvl,
+ markerText: '',
+ path: [1],
+ numberingType: target.attrs?.listRendering?.numberingType as string | undefined,
+ trackedMarkId: options.tracked ? `tc-${insertionId}` : undefined,
+ });
+ const at = options.position === 'before' ? targetIndex : targetIndex + 1;
+ children.splice(at, 0, inserted);
+ return true;
+ },
+ ),
+ setListTypeAt: vi.fn(() => true),
+ setTextSelection: vi.fn(() => true),
+ increaseListIndent: vi.fn(() => true),
+ decreaseListIndent: vi.fn(() => true),
+ restartNumbering: vi.fn(() => true),
+ exitListItemAt: vi.fn(() => true),
+ insertTrackedChange: vi.fn(() => true),
+ };
+
+ return {
+ state: {
+ doc,
+ },
+ commands: {
+ ...baseCommands,
+ ...commandOverrides,
+ },
+ options: { user: editorOptions.user },
+ converter: {
+ numbering: { definitions: {}, abstracts: {} },
+ },
+ } as unknown as Editor;
+}
+
+describe('lists adapter', () => {
+ beforeEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('lists projected list items', () => {
+ const editor = makeEditor([
+ makeListParagraph({
+ id: 'li-1',
+ text: 'One',
+ numId: 1,
+ ilvl: 0,
+ markerText: '1.',
+ path: [1],
+ numberingType: 'decimal',
+ }),
+ makeListParagraph({
+ id: 'li-2',
+ text: 'Two',
+ numId: 1,
+ ilvl: 0,
+ markerText: '2.',
+ path: [2],
+ numberingType: 'decimal',
+ }),
+ ]);
+
+ const result = listsListAdapter(editor);
+ expect(result.total).toBe(2);
+ expect(result.matches.map((match) => match.nodeId)).toEqual(['li-1', 'li-2']);
+ });
+
+ it('inserts a list item with deterministic insertionPoint at offset 0', () => {
+ const editor = makeEditor([
+ makeListParagraph({
+ id: 'li-1',
+ text: 'One',
+ numId: 1,
+ ilvl: 0,
+ markerText: '1.',
+ path: [1],
+ numberingType: 'decimal',
+ }),
+ ]);
+
+ const result = listsInsertAdapter(
+ editor,
+ {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ position: 'after',
+ text: 'Inserted',
+ },
+ { changeMode: 'direct' },
+ );
+
+ expect(result.success).toBe(true);
+ if (!result.success) return;
+ expect(result.insertionPoint.range).toEqual({ start: 0, end: 0 });
+ });
+
+ it('throws CAPABILITY_UNAVAILABLE for direct-only tracked requests', () => {
+ const editor = makeEditor([
+ makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, markerText: '1.', path: [1], numberingType: 'decimal' }),
+ ]);
+
+ expect(() =>
+ listsSetTypeAdapter(
+ editor,
+ {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ kind: 'bullet',
+ },
+ { changeMode: 'tracked' },
+ ),
+ ).toThrow('does not support tracked mode');
+ });
+
+ it('returns NO_OP when setType already matches requested kind', () => {
+ const editor = makeEditor([
+ makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, markerText: '1.', path: [1], numberingType: 'bullet' }),
+ ]);
+
+ const result = listsSetTypeAdapter(editor, {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ kind: 'bullet',
+ });
+
+ expect(result.success).toBe(false);
+ if (result.success) return;
+ expect(result.failure.code).toBe('NO_OP');
+ });
+
+ it('returns NO_OP for outdent at level 0', () => {
+ const editor = makeEditor([
+ makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, markerText: '1.', path: [1], numberingType: 'decimal' }),
+ ]);
+
+ const result = listsOutdentAdapter(editor, {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ });
+
+ expect(result.success).toBe(false);
+ if (result.success) return;
+ expect(result.failure.code).toBe('NO_OP');
+ });
+
+ it('returns NO_OP for indent when list definition does not support next level', () => {
+ const hasDefinitionSpy = vi.spyOn(ListHelpers, 'hasListDefinition').mockReturnValue(false);
+ const editor = makeEditor([
+ makeListParagraph({
+ id: 'li-1',
+ numId: 1,
+ ilvl: 2,
+ markerText: 'iii.',
+ path: [1, 1, 3],
+ numberingType: 'lowerRoman',
+ }),
+ ]);
+
+ const result = listsIndentAdapter(editor, {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ });
+
+ expect(result.success).toBe(false);
+ if (result.success) return;
+ expect(result.failure.code).toBe('NO_OP');
+ expect(hasDefinitionSpy).toHaveBeenCalled();
+ });
+
+ it('returns NO_OP for restart when target is already effective start at 1 and run start', () => {
+ const editor = makeEditor([
+ makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, markerText: '1.', path: [1], numberingType: 'decimal' }),
+ ]);
+
+ const result = listsRestartAdapter(editor, {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ });
+
+ expect(result.success).toBe(false);
+ if (result.success) return;
+ expect(result.failure.code).toBe('NO_OP');
+ });
+
+ it('returns NO_OP for restart when a level-1 item starts after a level-0 item with same numId', () => {
+ const editor = makeEditor([
+ makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, markerText: '1.', path: [1], numberingType: 'decimal' }),
+ makeListParagraph({
+ id: 'li-2',
+ numId: 1,
+ ilvl: 1,
+ markerText: 'a.',
+ path: [1, 1],
+ numberingType: 'lowerLetter',
+ }),
+ ]);
+ const restartNumbering = editor.commands!.restartNumbering as ReturnType;
+
+ const result = listsRestartAdapter(editor, {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-2' },
+ });
+
+ expect(result.success).toBe(false);
+ if (result.success) return;
+ expect(result.failure.code).toBe('NO_OP');
+ expect(restartNumbering).not.toHaveBeenCalled();
+ });
+
+ it('throws TARGET_NOT_FOUND for stale list targets', () => {
+ const editor = makeEditor([
+ makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, markerText: '1.', path: [1], numberingType: 'decimal' }),
+ ]);
+
+ expect(() =>
+ listsExitAdapter(editor, {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'missing' },
+ }),
+ ).toThrow('List item target was not found');
+ });
+
+ it('maps explicit non-applied exit command to INVALID_TARGET', () => {
+ const editor = makeEditor(
+ [makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, markerText: '1.', path: [1], numberingType: 'decimal' })],
+ { exitListItemAt: vi.fn(() => false) },
+ );
+
+ const result = listsExitAdapter(editor, {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ });
+
+ expect(result.success).toBe(false);
+ if (result.success) return;
+ expect(result.failure.code).toBe('INVALID_TARGET');
+ });
+
+ describe('dryRun', () => {
+ function makeListEditor() {
+ return makeEditor([
+ makeListParagraph({
+ id: 'li-1',
+ text: 'One',
+ numId: 1,
+ ilvl: 1,
+ markerText: '1.',
+ path: [1],
+ numberingType: 'decimal',
+ }),
+ makeListParagraph({
+ id: 'li-2',
+ text: 'Two',
+ numId: 1,
+ ilvl: 1,
+ markerText: '2.',
+ path: [2],
+ numberingType: 'decimal',
+ }),
+ ]);
+ }
+
+ it('insert: returns placeholder success without mutating the document', () => {
+ const editor = makeListEditor();
+ const insertListItemAt = editor.commands!.insertListItemAt as ReturnType;
+
+ const result = listsInsertAdapter(
+ editor,
+ {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ position: 'after',
+ },
+ { dryRun: true },
+ );
+
+ expect(result.success).toBe(true);
+ expect(insertListItemAt).not.toHaveBeenCalled();
+ });
+
+ it('setType: returns success without dispatching command', () => {
+ const editor = makeListEditor();
+ const setListTypeAt = editor.commands!.setListTypeAt as ReturnType;
+
+ const result = listsSetTypeAdapter(
+ editor,
+ {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ kind: 'bullet',
+ },
+ { dryRun: true },
+ );
+
+ expect(result.success).toBe(true);
+ expect(setListTypeAt).not.toHaveBeenCalled();
+ });
+
+ it('indent: returns success without dispatching command', () => {
+ vi.spyOn(ListHelpers, 'hasListDefinition').mockReturnValue(true);
+ const editor = makeListEditor();
+ const increaseListIndent = editor.commands!.increaseListIndent as ReturnType;
+
+ const result = listsIndentAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } },
+ { dryRun: true },
+ );
+
+ expect(result.success).toBe(true);
+ expect(increaseListIndent).not.toHaveBeenCalled();
+ });
+
+ it('outdent: returns success without dispatching command', () => {
+ const editor = makeListEditor();
+ const decreaseListIndent = editor.commands!.decreaseListIndent as ReturnType;
+
+ const result = listsOutdentAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } },
+ { dryRun: true },
+ );
+
+ expect(result.success).toBe(true);
+ expect(decreaseListIndent).not.toHaveBeenCalled();
+ });
+
+ it('restart: returns success without dispatching command', () => {
+ const editor = makeListEditor();
+ const restartNumbering = editor.commands!.restartNumbering as ReturnType;
+
+ const result = listsRestartAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-2' } },
+ { dryRun: true },
+ );
+
+ expect(result.success).toBe(true);
+ expect(restartNumbering).not.toHaveBeenCalled();
+ });
+
+ it('exit: returns placeholder success without dispatching command', () => {
+ const editor = makeListEditor();
+ const exitListItemAt = editor.commands!.exitListItemAt as ReturnType;
+
+ const result = listsExitAdapter(
+ editor,
+ { target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' } },
+ { dryRun: true },
+ );
+
+ expect(result.success).toBe(true);
+ if (!result.success) return;
+ expect(result.paragraph.nodeId).toBe('(dry-run)');
+ expect(exitListItemAt).not.toHaveBeenCalled();
+ });
+ });
+
+ it('throws CAPABILITY_UNAVAILABLE for tracked insert dry-run without a configured user', () => {
+ const editor = makeEditor([
+ makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, markerText: '1.', path: [1], numberingType: 'decimal' }),
+ ]);
+
+ expect(() =>
+ listsInsertAdapter(
+ editor,
+ {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ position: 'after',
+ },
+ { changeMode: 'tracked', dryRun: true },
+ ),
+ ).toThrow('requires a user to be configured');
+ });
+
+ it('returns TARGET_NOT_FOUND failure when post-apply list item resolution fails', () => {
+ const children = [
+ makeListParagraph({
+ id: 'li-1',
+ text: 'One',
+ numId: 1,
+ ilvl: 0,
+ markerText: '1.',
+ path: [1],
+ numberingType: 'decimal',
+ }),
+ ];
+
+ // Custom insertListItemAt that returns true but inserts a node with a
+ // different sdBlockId/paraId than what was requested, making it
+ // unresolvable by resolveInsertedListItem.
+ const insertListItemAt = vi.fn((options: { pos: number; position: 'before' | 'after'; sdBlockId?: string }) => {
+ const inserted = makeListParagraph({
+ id: 'unrelated-id',
+ sdBlockId: 'unrelated-sdBlockId',
+ numId: 1,
+ ilvl: 0,
+ markerText: '',
+ path: [1],
+ numberingType: 'decimal',
+ });
+ const at = options.position === 'before' ? 0 : 1;
+ children.splice(at, 0, inserted);
+ return true;
+ });
+
+ const editor = makeEditor(children, { insertListItemAt });
+
+ const result = listsInsertAdapter(
+ editor,
+ {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ position: 'after',
+ },
+ { changeMode: 'direct' },
+ );
+
+ // Contract: success:false means no mutation was applied.
+ // The mutation DID apply, so we must return success with the generated ID.
+ expect(result.success).toBe(true);
+ if (!result.success) return;
+ expect(result.item.nodeType).toBe('listItem');
+ expect(typeof result.item.nodeId).toBe('string');
+ expect(result.item.nodeId).not.toBe('(dry-run)');
+ });
+
+ it('throws same error for tracked insert non-dry-run without a configured user', () => {
+ const editor = makeEditor([
+ makeListParagraph({ id: 'li-1', numId: 1, ilvl: 0, markerText: '1.', path: [1], numberingType: 'decimal' }),
+ ]);
+
+ expect(() =>
+ listsInsertAdapter(
+ editor,
+ {
+ target: { kind: 'block', nodeType: 'listItem', nodeId: 'li-1' },
+ position: 'after',
+ },
+ { changeMode: 'tracked' },
+ ),
+ ).toThrow('requires a user to be configured');
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/lists-adapter.ts b/packages/super-editor/src/document-api-adapters/lists-adapter.ts
new file mode 100644
index 0000000000..99b5f0a1cd
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/lists-adapter.ts
@@ -0,0 +1,398 @@
+import { v4 as uuidv4 } from 'uuid';
+import type { Editor } from '../core/Editor.js';
+import type {
+ ListInsertInput,
+ ListItemInfo,
+ ListSetTypeInput,
+ ListsExitResult,
+ ListsGetInput,
+ ListsInsertResult,
+ ListsListQuery,
+ ListsListResult,
+ ListsMutateItemResult,
+ ListTargetInput,
+ MutationOptions,
+} from '@superdoc/document-api';
+import { DocumentApiAdapterError } from './errors.js';
+import { requireEditorCommand, ensureTrackedCapability, rejectTrackedMode } from './helpers/mutation-helpers.js';
+import { clearIndexCache, getBlockIndex } from './helpers/index-cache.js';
+import { collectTrackInsertRefsInRange } from './helpers/tracked-change-refs.js';
+import {
+ listItemProjectionToInfo,
+ listListItems,
+ resolveListItem,
+ type ListItemProjection,
+} from './helpers/list-item-resolver.js';
+import { ListHelpers } from '../core/helpers/list-numbering-helpers.js';
+
+type InsertListItemAtCommand = (options: {
+ pos: number;
+ position: 'before' | 'after';
+ text?: string;
+ sdBlockId?: string;
+ tracked?: boolean;
+}) => boolean;
+
+type SetListTypeAtCommand = (options: { pos: number; kind: 'ordered' | 'bullet' }) => boolean;
+type ExitListItemAtCommand = (options: { pos: number }) => boolean;
+type SetTextSelectionCommand = (options: { from: number; to?: number }) => boolean;
+
+function toListsFailure(code: 'NO_OP' | 'INVALID_TARGET', message: string, details?: unknown) {
+ return { success: false as const, failure: { code, message, details } };
+}
+
+function resolveInsertedListItem(editor: Editor, sdBlockId: string): ListItemProjection {
+ const index = getBlockIndex(editor);
+ const byNodeId = index.candidates.find(
+ (candidate) => candidate.nodeType === 'listItem' && candidate.nodeId === sdBlockId,
+ );
+ if (byNodeId) return resolveListItem(editor, { kind: 'block', nodeType: 'listItem', nodeId: byNodeId.nodeId });
+
+ const bySdBlockId = index.candidates.find((candidate) => {
+ if (candidate.nodeType !== 'listItem') return false;
+ const attrs = (candidate.node as { attrs?: { sdBlockId?: unknown } }).attrs;
+ return typeof attrs?.sdBlockId === 'string' && attrs.sdBlockId === sdBlockId;
+ });
+
+ if (bySdBlockId) {
+ return resolveListItem(editor, { kind: 'block', nodeType: 'listItem', nodeId: bySdBlockId.nodeId });
+ }
+
+ throw new DocumentApiAdapterError(
+ 'TARGET_NOT_FOUND',
+ `Inserted list item with sdBlockId "${sdBlockId}" could not be resolved after insertion.`,
+ );
+}
+
+function selectionAnchorPos(item: ListItemProjection): number {
+ return item.candidate.pos + 1;
+}
+
+function setSelectionToListItem(editor: Editor, item: ListItemProjection): boolean {
+ const setTextSelection = requireEditorCommand(
+ editor.commands?.setTextSelection as SetTextSelectionCommand | undefined,
+ 'lists (setTextSelection)',
+ ) as SetTextSelectionCommand;
+ const anchor = selectionAnchorPos(item);
+ return Boolean(setTextSelection({ from: anchor, to: anchor }));
+}
+
+function isAtMaximumLevel(editor: Editor, item: ListItemProjection): boolean {
+ if (item.numId == null || item.level == null) return false;
+ return !ListHelpers.hasListDefinition(editor, item.numId, item.level + 1);
+}
+
+function isRestartNoOp(editor: Editor, item: ListItemProjection): boolean {
+ if (item.ordinal !== 1) return false;
+ if (item.numId == null) return false;
+
+ const index = getBlockIndex(editor);
+ const currentIndex = index.candidates.findIndex(
+ (candidate) => candidate.nodeType === 'listItem' && candidate.nodeId === item.address.nodeId,
+ );
+ if (currentIndex <= 0) return true;
+
+ for (let cursor = currentIndex - 1; cursor >= 0; cursor -= 1) {
+ const previous = index.candidates[cursor]!;
+ if (previous.node.type.name !== 'paragraph') {
+ return true;
+ }
+ if (previous.nodeType !== 'listItem') {
+ return true;
+ }
+
+ const previousProjection = resolveListItem(editor, {
+ kind: 'block',
+ nodeType: 'listItem',
+ nodeId: previous.nodeId,
+ });
+
+ return previousProjection.numId !== item.numId || previousProjection.level !== item.level;
+ }
+
+ return true;
+}
+
+function withListTarget(editor: Editor, input: ListTargetInput): ListItemProjection {
+ return resolveListItem(editor, input.target);
+}
+
+export function listsListAdapter(editor: Editor, query?: ListsListQuery): ListsListResult {
+ return listListItems(editor, query);
+}
+
+export function listsGetAdapter(editor: Editor, input: ListsGetInput): ListItemInfo {
+ const item = resolveListItem(editor, input.address);
+ return listItemProjectionToInfo(item);
+}
+
+export function listsInsertAdapter(
+ editor: Editor,
+ input: ListInsertInput,
+ options?: MutationOptions,
+): ListsInsertResult {
+ const target = withListTarget(editor, { target: input.target });
+ const changeMode = options?.changeMode ?? 'direct';
+ const mode = changeMode === 'tracked' ? 'tracked' : 'direct';
+ if (mode === 'tracked') ensureTrackedCapability(editor, { operation: 'lists.insert' });
+
+ const insertListItemAt = requireEditorCommand(
+ editor.commands?.insertListItemAt as InsertListItemAtCommand | undefined,
+ 'lists.insert (insertListItemAt)',
+ ) as InsertListItemAtCommand;
+
+ if (options?.dryRun) {
+ return {
+ success: true,
+ item: { kind: 'block', nodeType: 'listItem', nodeId: '(dry-run)' },
+ insertionPoint: {
+ kind: 'text',
+ blockId: '(dry-run)',
+ range: { start: 0, end: 0 },
+ },
+ };
+ }
+
+ const createdId = uuidv4();
+ const didApply = insertListItemAt({
+ pos: target.candidate.pos,
+ position: input.position,
+ text: input.text ?? '',
+ sdBlockId: createdId,
+ tracked: mode === 'tracked',
+ });
+
+ if (!didApply) {
+ return toListsFailure('INVALID_TARGET', 'List item insertion could not be applied at the requested target.', {
+ target: input.target,
+ position: input.position,
+ });
+ }
+
+ clearIndexCache(editor);
+
+ let created: ListItemProjection;
+ try {
+ created = resolveInsertedListItem(editor, createdId);
+ } catch {
+ // Mutation already applied — contract requires success: true.
+ // Fall back to the generated ID we assigned to the command.
+ return {
+ success: true,
+ item: { kind: 'block', nodeType: 'listItem', nodeId: createdId },
+ insertionPoint: {
+ kind: 'text',
+ blockId: createdId,
+ range: { start: 0, end: 0 },
+ },
+ };
+ }
+
+ return {
+ success: true,
+ item: created.address,
+ insertionPoint: {
+ kind: 'text',
+ blockId: created.address.nodeId,
+ range: { start: 0, end: 0 },
+ },
+ trackedChangeRefs:
+ mode === 'tracked'
+ ? collectTrackInsertRefsInRange(editor, created.candidate.pos, created.candidate.end)
+ : undefined,
+ };
+}
+
+export function listsSetTypeAdapter(
+ editor: Editor,
+ input: ListSetTypeInput,
+ options?: MutationOptions,
+): ListsMutateItemResult {
+ rejectTrackedMode('lists.setType', options);
+ const target = withListTarget(editor, { target: input.target });
+ if (target.kind === input.kind) {
+ return toListsFailure('NO_OP', 'List item already has the requested list kind.', {
+ target: input.target,
+ kind: input.kind,
+ });
+ }
+
+ const setListTypeAt = requireEditorCommand(
+ editor.commands?.setListTypeAt as SetListTypeAtCommand | undefined,
+ 'lists.setType (setListTypeAt)',
+ ) as SetListTypeAtCommand;
+
+ if (options?.dryRun) {
+ return { success: true, item: target.address };
+ }
+
+ const didApply = setListTypeAt({
+ pos: target.candidate.pos,
+ kind: input.kind,
+ });
+
+ if (!didApply) {
+ return toListsFailure('INVALID_TARGET', 'List type conversion could not be applied.', {
+ target: input.target,
+ kind: input.kind,
+ });
+ }
+
+ return {
+ success: true,
+ item: target.address,
+ };
+}
+
+export function listsIndentAdapter(
+ editor: Editor,
+ input: ListTargetInput,
+ options?: MutationOptions,
+): ListsMutateItemResult {
+ rejectTrackedMode('lists.indent', options);
+ const target = withListTarget(editor, input);
+ if (isAtMaximumLevel(editor, target)) {
+ return toListsFailure('NO_OP', 'List item is already at the maximum supported level.', { target: input.target });
+ }
+
+ const increaseListIndent = requireEditorCommand(
+ editor.commands?.increaseListIndent as (() => boolean) | undefined,
+ 'lists.indent (increaseListIndent)',
+ ) as () => boolean;
+
+ if (options?.dryRun) {
+ return { success: true, item: target.address };
+ }
+
+ if (!setSelectionToListItem(editor, target)) {
+ return toListsFailure('INVALID_TARGET', 'List item target could not be selected for indentation.', {
+ target: input.target,
+ });
+ }
+
+ const didApply = increaseListIndent();
+ if (!didApply) {
+ return toListsFailure('INVALID_TARGET', 'List indentation could not be applied.', { target: input.target });
+ }
+
+ return {
+ success: true,
+ item: target.address,
+ };
+}
+
+export function listsOutdentAdapter(
+ editor: Editor,
+ input: ListTargetInput,
+ options?: MutationOptions,
+): ListsMutateItemResult {
+ rejectTrackedMode('lists.outdent', options);
+ const target = withListTarget(editor, input);
+ if ((target.level ?? 0) <= 0) {
+ return toListsFailure('NO_OP', 'List item is already at level 0.', { target: input.target });
+ }
+
+ const decreaseListIndent = requireEditorCommand(
+ editor.commands?.decreaseListIndent as (() => boolean) | undefined,
+ 'lists.outdent (decreaseListIndent)',
+ ) as () => boolean;
+
+ if (options?.dryRun) {
+ return { success: true, item: target.address };
+ }
+
+ if (!setSelectionToListItem(editor, target)) {
+ return toListsFailure('INVALID_TARGET', 'List item target could not be selected for outdent.', {
+ target: input.target,
+ });
+ }
+
+ const didApply = decreaseListIndent();
+ if (!didApply) {
+ return toListsFailure('INVALID_TARGET', 'List outdent could not be applied.', { target: input.target });
+ }
+
+ return {
+ success: true,
+ item: target.address,
+ };
+}
+
+export function listsRestartAdapter(
+ editor: Editor,
+ input: ListTargetInput,
+ options?: MutationOptions,
+): ListsMutateItemResult {
+ rejectTrackedMode('lists.restart', options);
+ const target = withListTarget(editor, input);
+ if (target.numId == null) {
+ return toListsFailure('INVALID_TARGET', 'List restart requires numbering metadata on the target item.', {
+ target: input.target,
+ });
+ }
+ if (isRestartNoOp(editor, target)) {
+ return toListsFailure('NO_OP', 'List item is already the start of a sequence that effectively starts at 1.', {
+ target: input.target,
+ });
+ }
+
+ const restartNumbering = requireEditorCommand(
+ editor.commands?.restartNumbering as (() => boolean) | undefined,
+ 'lists.restart (restartNumbering)',
+ ) as () => boolean;
+
+ if (options?.dryRun) {
+ return { success: true, item: target.address };
+ }
+
+ if (!setSelectionToListItem(editor, target)) {
+ return toListsFailure('INVALID_TARGET', 'List item target could not be selected for restart.', {
+ target: input.target,
+ });
+ }
+
+ const didApply = restartNumbering();
+ if (!didApply) {
+ return toListsFailure('INVALID_TARGET', 'List restart could not be applied.', { target: input.target });
+ }
+
+ return {
+ success: true,
+ item: target.address,
+ };
+}
+
+export function listsExitAdapter(editor: Editor, input: ListTargetInput, options?: MutationOptions): ListsExitResult {
+ rejectTrackedMode('lists.exit', options);
+ const target = withListTarget(editor, input);
+
+ const exitListItemAt = requireEditorCommand(
+ editor.commands?.exitListItemAt as ExitListItemAtCommand | undefined,
+ 'lists.exit (exitListItemAt)',
+ ) as ExitListItemAtCommand;
+
+ if (options?.dryRun) {
+ return {
+ success: true,
+ paragraph: {
+ kind: 'block',
+ nodeType: 'paragraph',
+ nodeId: '(dry-run)',
+ },
+ };
+ }
+
+ const didApply = exitListItemAt({ pos: target.candidate.pos });
+ if (!didApply) {
+ return toListsFailure('INVALID_TARGET', 'List exit could not be applied.', { target: input.target });
+ }
+
+ return {
+ success: true,
+ paragraph: {
+ kind: 'block',
+ nodeType: 'paragraph',
+ nodeId: target.address.nodeId,
+ },
+ };
+}
diff --git a/packages/super-editor/src/document-api-adapters/track-changes-adapter.test.ts b/packages/super-editor/src/document-api-adapters/track-changes-adapter.test.ts
new file mode 100644
index 0000000000..22cb40a30e
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/track-changes-adapter.test.ts
@@ -0,0 +1,374 @@
+import { describe, expect, it, vi } from 'vitest';
+import type { Editor } from '../core/Editor.js';
+import {
+ trackChangesAcceptAdapter,
+ trackChangesAcceptAllAdapter,
+ trackChangesGetAdapter,
+ trackChangesListAdapter,
+ trackChangesRejectAdapter,
+ trackChangesRejectAllAdapter,
+} from './track-changes-adapter.js';
+import { TrackDeleteMarkName, TrackInsertMarkName } from '../extensions/track-changes/constants.js';
+import { getTrackChanges } from '../extensions/track-changes/trackChangesHelpers/getTrackChanges.js';
+
+vi.mock('../extensions/track-changes/trackChangesHelpers/getTrackChanges.js', () => ({
+ getTrackChanges: vi.fn(),
+}));
+
+function makeEditor(overrides: Partial = {}): Editor {
+ return {
+ state: {
+ doc: {
+ content: { size: 100 },
+ textBetween(from: number, to: number) {
+ return `excerpt-${from}-${to}`;
+ },
+ },
+ },
+ commands: {
+ acceptTrackedChangeById: vi.fn(() => true),
+ rejectTrackedChangeById: vi.fn(() => true),
+ acceptAllTrackedChanges: vi.fn(() => true),
+ rejectAllTrackedChanges: vi.fn(() => true),
+ },
+ ...overrides,
+ } as unknown as Editor;
+}
+
+describe('track-changes adapters', () => {
+ it('lists tracked changes with stable trackedChange entity addresses', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ {
+ mark: {
+ type: { name: TrackInsertMarkName },
+ attrs: { id: 'tc-1', author: 'Ada', authorEmail: 'ada@example.com' },
+ },
+ from: 2,
+ to: 5,
+ },
+ {
+ mark: {
+ type: { name: TrackDeleteMarkName },
+ attrs: { id: 'tc-1' },
+ },
+ from: 5,
+ to: 8,
+ },
+ ] as never);
+
+ const result = trackChangesListAdapter(makeEditor());
+ expect(result.total).toBe(1);
+ expect(result.matches[0]).toMatchObject({
+ kind: 'entity',
+ entityType: 'trackedChange',
+ });
+ expect(typeof result.matches[0]?.entityId).toBe('string');
+ expect(result.changes?.[0]?.id).toBe(result.matches[0]?.entityId);
+ expect(result.changes?.[0]?.type).toBe('insert');
+ expect(result.changes?.[0]?.excerpt).toContain('excerpt-2-8');
+ });
+
+ it('respects list type filters and pagination', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ {
+ mark: {
+ type: { name: TrackInsertMarkName },
+ attrs: { id: 'tc-1' },
+ },
+ from: 1,
+ to: 2,
+ },
+ {
+ mark: {
+ type: { name: TrackDeleteMarkName },
+ attrs: { id: 'tc-2' },
+ },
+ from: 3,
+ to: 4,
+ },
+ ] as never);
+
+ const result = trackChangesListAdapter(makeEditor(), { type: 'delete', limit: 1, offset: 0 });
+ expect(result.total).toBe(1);
+ expect(result.matches).toHaveLength(1);
+ expect(result.changes?.[0]?.type).toBe('delete');
+ });
+
+ it('gets a tracked change by id', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ {
+ mark: {
+ type: { name: TrackInsertMarkName },
+ attrs: { id: 'tc-1' },
+ },
+ from: 2,
+ to: 5,
+ },
+ ] as never);
+
+ const editor = makeEditor();
+ const listed = trackChangesListAdapter(editor, { limit: 1 });
+ const id = listed.matches[0]?.entityId;
+ expect(typeof id).toBe('string');
+ const change = trackChangesGetAdapter(editor, { id: id as string });
+ expect(change.id).toBe(id);
+ });
+
+ it('throws for unknown tracked change ids', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([] as never);
+ expect(() => trackChangesGetAdapter(makeEditor(), { id: 'missing' })).toThrow('was not found');
+
+ try {
+ trackChangesGetAdapter(makeEditor(), { id: 'missing' });
+ } catch (error) {
+ expect((error as { code?: string }).code).toBe('TARGET_NOT_FOUND');
+ }
+ });
+
+ it('maps accept/reject commands to receipts', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ {
+ mark: {
+ type: { name: TrackInsertMarkName },
+ attrs: { id: 'tc-1' },
+ },
+ from: 1,
+ to: 2,
+ },
+ ] as never);
+
+ const acceptTrackedChangeById = vi.fn(() => true);
+ const rejectTrackedChangeById = vi.fn(() => true);
+ const acceptAllTrackedChanges = vi.fn(() => true);
+ const rejectAllTrackedChanges = vi.fn(() => true);
+ const editor = makeEditor({
+ commands: {
+ acceptTrackedChangeById,
+ rejectTrackedChangeById,
+ acceptAllTrackedChanges,
+ rejectAllTrackedChanges,
+ } as never,
+ });
+
+ const listed = trackChangesListAdapter(editor, { limit: 1 });
+ const id = listed.matches[0]?.entityId as string;
+ expect(typeof id).toBe('string');
+
+ expect(trackChangesAcceptAdapter(editor, { id }).success).toBe(true);
+ expect(trackChangesRejectAdapter(editor, { id }).success).toBe(true);
+ expect(trackChangesAcceptAllAdapter(editor, {}).success).toBe(true);
+ expect(trackChangesRejectAllAdapter(editor, {}).success).toBe(true);
+ expect(acceptTrackedChangeById).toHaveBeenCalledWith('tc-1');
+ expect(rejectTrackedChangeById).toHaveBeenCalledWith('tc-1');
+ });
+
+ it('throws TARGET_NOT_FOUND when accepting/rejecting an unknown id', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([] as never);
+
+ expect(() => trackChangesAcceptAdapter(makeEditor(), { id: 'missing' })).toThrow('was not found');
+ expect(() => trackChangesRejectAdapter(makeEditor(), { id: 'missing' })).toThrow('was not found');
+
+ try {
+ trackChangesAcceptAdapter(makeEditor(), { id: 'missing' });
+ } catch (error) {
+ expect((error as { code?: string }).code).toBe('TARGET_NOT_FOUND');
+ }
+ });
+
+ it('throws CAPABILITY_UNAVAILABLE when accept/reject commands are missing', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ {
+ mark: {
+ type: { name: TrackInsertMarkName },
+ attrs: { id: 'tc-1' },
+ },
+ from: 1,
+ to: 2,
+ },
+ ] as never);
+
+ const editor = makeEditor({
+ commands: {
+ acceptTrackedChangeById: undefined,
+ rejectTrackedChangeById: undefined,
+ acceptAllTrackedChanges: vi.fn(() => true),
+ rejectAllTrackedChanges: vi.fn(() => true),
+ } as never,
+ });
+
+ const listed = trackChangesListAdapter(editor, { limit: 1 });
+ const id = listed.matches[0]?.entityId as string;
+
+ expect(() => trackChangesAcceptAdapter(editor, { id })).toThrow('Accept tracked change command is not available');
+ expect(() => trackChangesRejectAdapter(editor, { id })).toThrow('Reject tracked change command is not available');
+ });
+
+ it('returns NO_OP failure when accept/reject commands do not apply', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([
+ {
+ mark: {
+ type: { name: TrackInsertMarkName },
+ attrs: { id: 'tc-1' },
+ },
+ from: 1,
+ to: 2,
+ },
+ ] as never);
+
+ const editor = makeEditor({
+ commands: {
+ acceptTrackedChangeById: vi.fn(() => false),
+ rejectTrackedChangeById: vi.fn(() => false),
+ acceptAllTrackedChanges: vi.fn(() => true),
+ rejectAllTrackedChanges: vi.fn(() => true),
+ } as never,
+ });
+
+ const listed = trackChangesListAdapter(editor, { limit: 1 });
+ const id = listed.matches[0]?.entityId as string;
+
+ const acceptReceipt = trackChangesAcceptAdapter(editor, { id });
+ const rejectReceipt = trackChangesRejectAdapter(editor, { id });
+ expect(acceptReceipt.success).toBe(false);
+ expect(acceptReceipt.failure?.code).toBe('NO_OP');
+ expect(rejectReceipt.success).toBe(false);
+ expect(rejectReceipt.failure?.code).toBe('NO_OP');
+ });
+
+ it('throws CAPABILITY_UNAVAILABLE for missing accept-all/reject-all commands', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([] as never);
+
+ const editor = makeEditor({
+ commands: {
+ acceptTrackedChangeById: vi.fn(() => true),
+ rejectTrackedChangeById: vi.fn(() => true),
+ acceptAllTrackedChanges: undefined,
+ rejectAllTrackedChanges: undefined,
+ } as never,
+ });
+
+ expect(() => trackChangesAcceptAllAdapter(editor, {})).toThrow(
+ 'Accept all tracked changes command is not available',
+ );
+ expect(() => trackChangesRejectAllAdapter(editor, {})).toThrow(
+ 'Reject all tracked changes command is not available',
+ );
+ });
+
+ it('returns NO_OP failure when accept-all/reject-all do not apply', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([] as never);
+
+ const editor = makeEditor({
+ commands: {
+ acceptTrackedChangeById: vi.fn(() => true),
+ rejectTrackedChangeById: vi.fn(() => true),
+ acceptAllTrackedChanges: vi.fn(() => false),
+ rejectAllTrackedChanges: vi.fn(() => false),
+ } as never,
+ });
+
+ const acceptAllReceipt = trackChangesAcceptAllAdapter(editor, {});
+ const rejectAllReceipt = trackChangesRejectAllAdapter(editor, {});
+ expect(acceptAllReceipt.success).toBe(false);
+ expect(acceptAllReceipt.failure?.code).toBe('NO_OP');
+ expect(rejectAllReceipt.success).toBe(false);
+ expect(rejectAllReceipt.failure?.code).toBe('NO_OP');
+ });
+
+ it('returns NO_OP failure when accept-all/reject-all report true but no tracked changes exist', () => {
+ vi.mocked(getTrackChanges).mockReturnValue([] as never);
+
+ const editor = makeEditor({
+ commands: {
+ acceptTrackedChangeById: vi.fn(() => true),
+ rejectTrackedChangeById: vi.fn(() => true),
+ acceptAllTrackedChanges: vi.fn(() => true),
+ rejectAllTrackedChanges: vi.fn(() => true),
+ } as never,
+ });
+
+ const acceptAllReceipt = trackChangesAcceptAllAdapter(editor, {});
+ const rejectAllReceipt = trackChangesRejectAllAdapter(editor, {});
+ expect(acceptAllReceipt.success).toBe(false);
+ expect(acceptAllReceipt.failure?.code).toBe('NO_OP');
+ expect(rejectAllReceipt.success).toBe(false);
+ expect(rejectAllReceipt.failure?.code).toBe('NO_OP');
+ });
+
+ it('resolves stable ids across calls when raw ids differ', () => {
+ const marks = [
+ {
+ mark: {
+ type: { name: TrackInsertMarkName },
+ attrs: { id: 'raw-1', date: '2026-02-11T00:00:00.000Z' },
+ },
+ from: 2,
+ to: 5,
+ },
+ ];
+
+ vi.mocked(getTrackChanges).mockImplementation(() => marks as never);
+ const editor = makeEditor();
+
+ const listed = trackChangesListAdapter(editor, { limit: 1 });
+ const stableId = listed.matches[0]?.entityId;
+ expect(typeof stableId).toBe('string');
+
+ marks[0] = {
+ ...marks[0],
+ mark: {
+ ...marks[0].mark,
+ attrs: { ...marks[0].mark.attrs, id: 'raw-2' },
+ },
+ };
+
+ const resolved = trackChangesGetAdapter(editor, { id: stableId as string });
+ expect(resolved.id).toBe(stableId);
+ });
+
+ it('throws TARGET_NOT_FOUND when accepting an id that was already processed', () => {
+ const marks = [
+ {
+ mark: {
+ type: { name: TrackInsertMarkName },
+ attrs: { id: 'raw-1' },
+ },
+ from: 2,
+ to: 5,
+ },
+ ];
+
+ vi.mocked(getTrackChanges).mockImplementation(() => marks as never);
+
+ const state = {
+ doc: {
+ content: { size: 100 },
+ textBetween(from: number, to: number) {
+ return `excerpt-${from}-${to}`;
+ },
+ },
+ };
+ const acceptTrackedChangeById = vi.fn(() => {
+ marks.splice(0, marks.length);
+ // Simulate ProseMirror creating a new doc reference after mutation
+ state.doc = { ...state.doc };
+ return true;
+ });
+
+ const editor = makeEditor({
+ state: state as never,
+ commands: {
+ acceptTrackedChangeById,
+ rejectTrackedChangeById: vi.fn(() => true),
+ acceptAllTrackedChanges: vi.fn(() => true),
+ rejectAllTrackedChanges: vi.fn(() => true),
+ } as never,
+ });
+
+ const listed = trackChangesListAdapter(editor, { limit: 1 });
+ const stableId = listed.matches[0]?.entityId as string;
+
+ expect(trackChangesAcceptAdapter(editor, { id: stableId }).success).toBe(true);
+ expect(() => trackChangesAcceptAdapter(editor, { id: stableId })).toThrow('was not found');
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/track-changes-adapter.ts b/packages/super-editor/src/document-api-adapters/track-changes-adapter.ts
new file mode 100644
index 0000000000..3d4d3aa9fc
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/track-changes-adapter.ts
@@ -0,0 +1,135 @@
+import type { Editor } from '../core/Editor.js';
+import type {
+ Receipt,
+ TrackChangeInfo,
+ TrackChangesAcceptAllInput,
+ TrackChangesAcceptInput,
+ TrackChangesGetInput,
+ TrackChangesListInput,
+ TrackChangesRejectAllInput,
+ TrackChangesRejectInput,
+ TrackChangeType,
+ TrackChangesListResult,
+} from '@superdoc/document-api';
+import { DocumentApiAdapterError } from './errors.js';
+import { requireEditorCommand } from './helpers/mutation-helpers.js';
+import { paginate } from './helpers/adapter-utils.js';
+import {
+ groupTrackedChanges,
+ resolveTrackedChange,
+ resolveTrackedChangeType,
+ type GroupedTrackedChange,
+} from './helpers/tracked-change-resolver.js';
+import { normalizeExcerpt, toNonEmptyString } from './helpers/value-utils.js';
+
+function buildTrackChangeInfo(editor: Editor, change: GroupedTrackedChange): TrackChangeInfo {
+ const excerpt = normalizeExcerpt(editor.state.doc.textBetween(change.from, change.to, ' ', '\ufffc'));
+ const type = resolveTrackedChangeType(change);
+
+ return {
+ address: {
+ kind: 'entity',
+ entityType: 'trackedChange',
+ entityId: change.id,
+ },
+ id: change.id,
+ type,
+ author: toNonEmptyString(change.attrs.author),
+ authorEmail: toNonEmptyString(change.attrs.authorEmail),
+ authorImage: toNonEmptyString(change.attrs.authorImage),
+ date: toNonEmptyString(change.attrs.date),
+ excerpt,
+ };
+}
+
+function filterByType(changes: GroupedTrackedChange[], requestedType?: TrackChangeType): GroupedTrackedChange[] {
+ if (!requestedType) return changes;
+ return changes.filter((change) => resolveTrackedChangeType(change) === requestedType);
+}
+
+function requireTrackChangeById(editor: Editor, id: string): GroupedTrackedChange {
+ const change = resolveTrackedChange(editor, id);
+ if (change) return change;
+
+ throw new DocumentApiAdapterError('TARGET_NOT_FOUND', `Tracked change "${id}" was not found.`, {
+ id,
+ });
+}
+
+function toNoOpReceipt(message: string, details?: unknown): Receipt {
+ return {
+ success: false,
+ failure: {
+ code: 'NO_OP',
+ message,
+ details,
+ },
+ };
+}
+
+export function trackChangesListAdapter(editor: Editor, input?: TrackChangesListInput): TrackChangesListResult {
+ const query = input;
+ const grouped = filterByType(groupTrackedChanges(editor), query?.type);
+ const paged = paginate(grouped, query?.offset, query?.limit);
+ const changes = paged.items.map((item) => buildTrackChangeInfo(editor, item));
+ const matches = changes.map((change) => change.address);
+
+ return {
+ matches,
+ total: paged.total,
+ changes: changes.length ? changes : undefined,
+ };
+}
+
+export function trackChangesGetAdapter(editor: Editor, input: TrackChangesGetInput): TrackChangeInfo {
+ const { id } = input;
+ return buildTrackChangeInfo(editor, requireTrackChangeById(editor, id));
+}
+
+export function trackChangesAcceptAdapter(editor: Editor, input: TrackChangesAcceptInput): Receipt {
+ const { id } = input;
+ const change = requireTrackChangeById(editor, id);
+
+ const acceptById = requireEditorCommand(editor.commands?.acceptTrackedChangeById, 'Accept tracked change');
+ const didAccept = Boolean(acceptById(change.rawId));
+ if (didAccept) return { success: true };
+
+ return toNoOpReceipt(`Accept tracked change "${id}" produced no change.`, { id });
+}
+
+export function trackChangesRejectAdapter(editor: Editor, input: TrackChangesRejectInput): Receipt {
+ const { id } = input;
+ const change = requireTrackChangeById(editor, id);
+
+ const rejectById = requireEditorCommand(editor.commands?.rejectTrackedChangeById, 'Reject tracked change');
+ const didReject = Boolean(rejectById(change.rawId));
+ if (didReject) return { success: true };
+
+ return toNoOpReceipt(`Reject tracked change "${id}" produced no change.`, { id });
+}
+
+export function trackChangesAcceptAllAdapter(editor: Editor, _input: TrackChangesAcceptAllInput): Receipt {
+ const acceptAll = requireEditorCommand(editor.commands?.acceptAllTrackedChanges, 'Accept all tracked changes');
+
+ if (groupTrackedChanges(editor).length === 0) {
+ return toNoOpReceipt('Accept all tracked changes produced no change.');
+ }
+
+ const didAcceptAll = Boolean(acceptAll());
+ if (didAcceptAll) return { success: true };
+
+ return toNoOpReceipt('Accept all tracked changes produced no change.');
+}
+
+export function trackChangesRejectAllAdapter(editor: Editor, _input: TrackChangesRejectAllInput): Receipt {
+ const rejectAll = requireEditorCommand(editor.commands?.rejectAllTrackedChanges, 'Reject all tracked changes');
+
+ if (groupTrackedChanges(editor).length === 0) {
+ return toNoOpReceipt('Reject all tracked changes produced no change.');
+ }
+
+ const didRejectAll = Boolean(rejectAll());
+ if (didRejectAll) return { success: true };
+
+ return toNoOpReceipt('Reject all tracked changes produced no change.');
+}
diff --git a/packages/super-editor/src/document-api-adapters/write-adapter.test.ts b/packages/super-editor/src/document-api-adapters/write-adapter.test.ts
new file mode 100644
index 0000000000..1ce963985d
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/write-adapter.test.ts
@@ -0,0 +1,708 @@
+import { describe, expect, it, vi } from 'vitest';
+import type { Node as ProseMirrorNode } from 'prosemirror-model';
+import type { Editor } from '../core/Editor.js';
+import { writeAdapter } from './write-adapter.js';
+import * as trackedChangeResolver from './helpers/tracked-change-resolver.js';
+
+type NodeOptions = {
+ attrs?: Record;
+ text?: string;
+ isInline?: boolean;
+ isBlock?: boolean;
+ isLeaf?: boolean;
+ inlineContent?: boolean;
+ nodeSize?: number;
+};
+
+function createNode(typeName: string, children: ProseMirrorNode[] = [], options: NodeOptions = {}): ProseMirrorNode {
+ const attrs = options.attrs ?? {};
+ const text = options.text ?? '';
+ const isText = typeName === 'text';
+ const isInline = options.isInline ?? isText;
+ const isBlock = options.isBlock ?? (!isInline && typeName !== 'doc');
+ const inlineContent = options.inlineContent ?? isBlock;
+ const isLeaf = options.isLeaf ?? (isInline && !isText && children.length === 0);
+
+ const contentSize = children.reduce((sum, child) => sum + child.nodeSize, 0);
+ const nodeSize = isText ? text.length : options.nodeSize != null ? options.nodeSize : isLeaf ? 1 : contentSize + 2;
+
+ return {
+ type: { name: typeName },
+ attrs,
+ text: isText ? text : undefined,
+ nodeSize,
+ isText,
+ isInline,
+ isBlock,
+ inlineContent,
+ isTextblock: inlineContent,
+ isLeaf,
+ childCount: children.length,
+ child(index: number) {
+ return children[index]!;
+ },
+ descendants(callback: (node: ProseMirrorNode, pos: number) => void) {
+ let offset = 0;
+ for (const child of children) {
+ callback(child, offset);
+ offset += child.nodeSize;
+ }
+ },
+ } as unknown as ProseMirrorNode;
+}
+
+function makeEditor(text = 'Hello'): {
+ editor: Editor;
+ dispatch: ReturnType;
+ insertTrackedChange: ReturnType;
+ textBetween: ReturnType;
+ tr: {
+ insertText: ReturnType;
+ delete: ReturnType;
+ setMeta: ReturnType;
+ addMark: ReturnType;
+ };
+} {
+ const textNode = createNode('text', [], { text });
+ const paragraph = createNode('paragraph', [textNode], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const tr = {
+ insertText: vi.fn(),
+ delete: vi.fn(),
+ setMeta: vi.fn(),
+ addMark: vi.fn(),
+ };
+ tr.insertText.mockReturnValue(tr);
+ tr.delete.mockReturnValue(tr);
+ tr.setMeta.mockReturnValue(tr);
+ tr.addMark.mockReturnValue(tr);
+
+ const dispatch = vi.fn();
+ const insertTrackedChange = vi.fn(() => true);
+ const textBetween = vi.fn((from: number, to: number) => {
+ const start = Math.max(0, from - 1);
+ const end = Math.max(start, to - 1);
+ return text.slice(start, end);
+ });
+
+ const editor = {
+ state: {
+ doc: {
+ ...doc,
+ textBetween,
+ },
+ tr,
+ },
+ commands: {
+ insertTrackedChange,
+ },
+ options: {
+ user: { name: 'Test User' },
+ },
+ dispatch,
+ } as unknown as Editor;
+
+ return { editor, dispatch, insertTrackedChange, textBetween, tr };
+}
+
+function makeEditorWithDuplicateBlockIds(): {
+ editor: Editor;
+ dispatch: ReturnType;
+ tr: {
+ insertText: ReturnType;
+ delete: ReturnType;
+ setMeta: ReturnType;
+ addMark: ReturnType;
+ };
+} {
+ const firstTextNode = createNode('text', [], { text: 'Hello' });
+ const secondTextNode = createNode('text', [], { text: 'World' });
+ const firstParagraph = createNode('paragraph', [firstTextNode], {
+ attrs: { sdBlockId: 'dup' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const secondParagraph = createNode('paragraph', [secondTextNode], {
+ attrs: { sdBlockId: 'dup' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [firstParagraph, secondParagraph], { isBlock: false });
+
+ const tr = {
+ insertText: vi.fn(),
+ delete: vi.fn(),
+ setMeta: vi.fn(),
+ addMark: vi.fn(),
+ };
+ tr.insertText.mockReturnValue(tr);
+ tr.delete.mockReturnValue(tr);
+ tr.setMeta.mockReturnValue(tr);
+ tr.addMark.mockReturnValue(tr);
+
+ const dispatch = vi.fn();
+
+ const editor = {
+ state: {
+ doc: {
+ ...doc,
+ textBetween: vi.fn((from: number, to: number) => {
+ const docText = 'Hello\nWorld';
+ const start = Math.max(0, from - 1);
+ const end = Math.max(start, to - 1);
+ return docText.slice(start, end);
+ }),
+ },
+ tr,
+ },
+ commands: {
+ insertTrackedChange: vi.fn(() => true),
+ },
+ options: {
+ user: { name: 'Test User' },
+ },
+ dispatch,
+ } as unknown as Editor;
+
+ return { editor, dispatch, tr };
+}
+
+function makeEditorWithoutEditableTextBlock(): {
+ editor: Editor;
+} {
+ const table = createNode('table', [], {
+ attrs: { sdBlockId: 't1' },
+ isBlock: true,
+ inlineContent: false,
+ });
+ const doc = createNode('doc', [table], { isBlock: false, inlineContent: false });
+
+ const tr = {
+ insertText: vi.fn(),
+ delete: vi.fn(),
+ setMeta: vi.fn(),
+ addMark: vi.fn(),
+ };
+ tr.insertText.mockReturnValue(tr);
+ tr.delete.mockReturnValue(tr);
+ tr.setMeta.mockReturnValue(tr);
+ tr.addMark.mockReturnValue(tr);
+
+ const editor = {
+ state: {
+ doc: {
+ ...doc,
+ textBetween: vi.fn(() => ''),
+ },
+ tr,
+ },
+ commands: {
+ insertTrackedChange: vi.fn(() => true),
+ },
+ dispatch: vi.fn(),
+ } as unknown as Editor;
+
+ return { editor };
+}
+
+function makeEditorWithBlankParagraph(): {
+ editor: Editor;
+ dispatch: ReturnType;
+ insertTrackedChange: ReturnType;
+ tr: {
+ insertText: ReturnType;
+ delete: ReturnType;
+ setMeta: ReturnType;
+ addMark: ReturnType;
+ };
+} {
+ const paragraph = createNode('paragraph', [], {
+ attrs: { sdBlockId: 'p1' },
+ isBlock: true,
+ inlineContent: true,
+ });
+ const doc = createNode('doc', [paragraph], { isBlock: false });
+
+ const tr = {
+ insertText: vi.fn(),
+ delete: vi.fn(),
+ setMeta: vi.fn(),
+ addMark: vi.fn(),
+ };
+ tr.insertText.mockReturnValue(tr);
+ tr.delete.mockReturnValue(tr);
+ tr.setMeta.mockReturnValue(tr);
+ tr.addMark.mockReturnValue(tr);
+
+ const dispatch = vi.fn();
+ const insertTrackedChange = vi.fn(() => true);
+
+ const editor = {
+ state: {
+ doc: {
+ ...doc,
+ textBetween: vi.fn(() => ''),
+ },
+ tr,
+ },
+ commands: {
+ insertTrackedChange,
+ },
+ dispatch,
+ } as unknown as Editor;
+
+ return { editor, dispatch, insertTrackedChange, tr };
+}
+
+describe('writeAdapter', () => {
+ it('applies direct replace mutations', () => {
+ const { editor, dispatch, tr } = makeEditor('Hello');
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'World',
+ },
+ { changeMode: 'direct' },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(receipt.resolution).toMatchObject({
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ range: { from: 1, to: 6 },
+ text: 'Hello',
+ });
+ expect(tr.insertText).toHaveBeenCalledWith('World', 1, 6);
+ expect(tr.setMeta).toHaveBeenCalledWith('inputType', 'programmatic');
+ expect(dispatch).toHaveBeenCalledTimes(1);
+ });
+
+ it('sets skipTrackChanges metadata for direct writes to preserve direct mutation semantics', () => {
+ const { editor, tr } = makeEditor('Hello');
+
+ writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'World',
+ },
+ { changeMode: 'direct' },
+ );
+
+ expect(tr.setMeta).toHaveBeenCalledWith('skipTrackChanges', true);
+ });
+
+ it('creates tracked changes for tracked writes', () => {
+ const resolverSpy = vi
+ .spyOn(trackedChangeResolver, 'toCanonicalTrackedChangeId')
+ .mockReturnValue('resolved-change-id');
+ const { editor, insertTrackedChange } = makeEditor('Hello');
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'World',
+ },
+ { changeMode: 'tracked' },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(receipt.inserted?.[0]?.entityType).toBe('trackedChange');
+ expect(insertTrackedChange).toHaveBeenCalledTimes(1);
+ expect(insertTrackedChange.mock.calls[0]?.[0]).toMatchObject({
+ from: 1,
+ to: 6,
+ text: 'World',
+ });
+ expect(typeof insertTrackedChange.mock.calls[0]?.[0]?.id).toBe('string');
+ resolverSpy.mockRestore();
+ });
+
+ it('returns canonical tracked-change entity ids when resolver can map raw ids', () => {
+ const resolverSpy = vi
+ .spyOn(trackedChangeResolver, 'toCanonicalTrackedChangeId')
+ .mockReturnValue('stable-change-id');
+ const { editor } = makeEditor('Hello');
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'World',
+ },
+ { changeMode: 'tracked' },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(receipt.inserted?.[0]?.entityId).toBe('stable-change-id');
+ resolverSpy.mockRestore();
+ });
+
+ it('returns degraded success without inserted ref when canonical resolution fails', () => {
+ const resolverSpy = vi.spyOn(trackedChangeResolver, 'toCanonicalTrackedChangeId').mockReturnValue(null);
+ const { editor } = makeEditor('Hello');
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'World',
+ },
+ { changeMode: 'tracked' },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(receipt.inserted).toBeUndefined();
+ resolverSpy.mockRestore();
+ });
+
+ it('returns failure when target cannot be resolved', () => {
+ const { editor } = makeEditor('Hello');
+
+ expect(() =>
+ writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'missing', range: { start: 0, end: 5 } },
+ text: 'World',
+ },
+ { changeMode: 'direct' },
+ ),
+ ).toThrow('Mutation target could not be resolved.');
+ });
+
+ it('throws INVALID_TARGET when target block id is ambiguous across multiple text blocks', () => {
+ const { editor, dispatch, tr } = makeEditorWithDuplicateBlockIds();
+
+ try {
+ writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'dup', range: { start: 0, end: 1 } },
+ text: 'X',
+ },
+ { changeMode: 'direct' },
+ );
+ throw new Error('Expected writeAdapter to throw for ambiguous blockId target.');
+ } catch (error) {
+ expect((error as { code?: string }).code).toBe('INVALID_TARGET');
+ }
+
+ expect(tr.insertText).not.toHaveBeenCalled();
+ expect(dispatch).not.toHaveBeenCalled();
+ });
+
+ it('requires collapsed targets for insert', () => {
+ const { editor } = makeEditor('Hello');
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'insert',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'X',
+ },
+ { changeMode: 'direct' },
+ );
+
+ expect(receipt.success).toBe(false);
+ expect(receipt.failure).toMatchObject({
+ code: 'INVALID_TARGET',
+ });
+ });
+
+ it('defaults insert-without-target to the first paragraph at offset 0', () => {
+ const { editor, dispatch, tr } = makeEditor('Hello');
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'insert',
+ text: 'X',
+ },
+ { changeMode: 'direct' },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(receipt.resolution.target.range).toEqual({ start: 0, end: 0 });
+ expect(receipt.resolution.range).toEqual({ from: 1, to: 1 });
+ expect(tr.insertText).toHaveBeenCalledWith('X', 1, 1);
+ expect(tr.setMeta).toHaveBeenCalledWith('inputType', 'programmatic');
+ expect(dispatch).toHaveBeenCalledTimes(1);
+ });
+
+ it('supports insert-without-target for blank text blocks', () => {
+ const { editor, dispatch, tr } = makeEditorWithBlankParagraph();
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'insert',
+ text: 'X',
+ },
+ { changeMode: 'direct' },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(receipt.resolution.target).toEqual({
+ kind: 'text',
+ blockId: 'p1',
+ range: { start: 0, end: 0 },
+ });
+ expect(receipt.resolution.range).toEqual({ from: 1, to: 1 });
+ expect(receipt.resolution.text).toBe('');
+ expect(tr.insertText).toHaveBeenCalledWith('X', 1, 1);
+ expect(dispatch).toHaveBeenCalledTimes(1);
+ });
+
+ it('supports tracked insert-without-target using the default insertion point', () => {
+ const { editor, insertTrackedChange } = makeEditor('Hello');
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'insert',
+ text: 'X',
+ },
+ { changeMode: 'tracked' },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(insertTrackedChange).toHaveBeenCalledTimes(1);
+ expect(insertTrackedChange.mock.calls[0]?.[0]).toMatchObject({
+ from: 1,
+ to: 1,
+ text: 'X',
+ });
+ expect(typeof insertTrackedChange.mock.calls[0]?.[0]?.id).toBe('string');
+ });
+
+ it('throws TARGET_NOT_FOUND for insert-without-target when no editable text block exists', () => {
+ const { editor } = makeEditorWithoutEditableTextBlock();
+
+ expect(() =>
+ writeAdapter(
+ editor,
+ {
+ kind: 'insert',
+ text: 'X',
+ },
+ { changeMode: 'direct' },
+ ),
+ ).toThrow('Mutation target could not be resolved.');
+ });
+
+ it('throws CAPABILITY_UNAVAILABLE when tracked writes are unavailable', () => {
+ const { editor } = makeEditor('Hello');
+ (editor.commands as { insertTrackedChange?: unknown }).insertTrackedChange = undefined;
+
+ expect(() =>
+ writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'World',
+ },
+ { changeMode: 'tracked' },
+ ),
+ ).toThrow('requires the insertTrackedChange command');
+ });
+
+ it('throws CAPABILITY_UNAVAILABLE when tracked dry-run capability is unavailable', () => {
+ const { editor } = makeEditor('Hello');
+ (editor.commands as { insertTrackedChange?: unknown }).insertTrackedChange = undefined;
+
+ expect(() =>
+ writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'World',
+ },
+ { changeMode: 'tracked', dryRun: true },
+ ),
+ ).toThrow('requires the insertTrackedChange command');
+ });
+
+ it('throws CAPABILITY_UNAVAILABLE for tracked write without a configured user', () => {
+ const { editor } = makeEditor('Hello');
+ (editor as { options: { user?: unknown } }).options.user = undefined;
+
+ expect(() =>
+ writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'World',
+ },
+ { changeMode: 'tracked' },
+ ),
+ ).toThrow('requires a user to be configured');
+ });
+
+ it('returns explicit NO_OP when replacement text is unchanged', () => {
+ const { editor, textBetween } = makeEditor('Hello');
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'Hello',
+ },
+ { changeMode: 'direct' },
+ );
+
+ expect(receipt.success).toBe(false);
+ expect(receipt.failure).toMatchObject({
+ code: 'NO_OP',
+ });
+ expect(textBetween).toHaveBeenCalledWith(1, 6, '\n', '\ufffc');
+ });
+
+ it('returns INVALID_TARGET for replace with empty text', () => {
+ const { editor } = makeEditor('Hello');
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: '',
+ },
+ { changeMode: 'direct' },
+ );
+
+ expect(receipt.success).toBe(false);
+ expect(receipt.failure).toMatchObject({
+ code: 'INVALID_TARGET',
+ });
+ });
+
+ it('applies the same NO_OP rule for tracked replace as direct replace', () => {
+ const { editor, insertTrackedChange } = makeEditor('Hello');
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'Hello',
+ },
+ { changeMode: 'tracked' },
+ );
+
+ expect(receipt.success).toBe(false);
+ expect(receipt.failure).toMatchObject({
+ code: 'NO_OP',
+ });
+ expect(insertTrackedChange).not.toHaveBeenCalled();
+ });
+
+ it('returns NO_OP when tracked write command does not apply', () => {
+ const { editor } = makeEditor('Hello');
+ (editor.commands as { insertTrackedChange?: ReturnType }).insertTrackedChange = vi.fn(() => false);
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'World',
+ },
+ { changeMode: 'tracked' },
+ );
+
+ expect(receipt.success).toBe(false);
+ expect(receipt.failure).toMatchObject({
+ code: 'NO_OP',
+ });
+ });
+
+ it('supports direct dry-run without mutating editor state', () => {
+ const { editor, dispatch, tr } = makeEditor('Hello');
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'World',
+ },
+ { changeMode: 'direct', dryRun: true },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(receipt.resolution).toMatchObject({
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ range: { from: 1, to: 6 },
+ text: 'Hello',
+ });
+ expect(tr.insertText).not.toHaveBeenCalled();
+ expect(tr.delete).not.toHaveBeenCalled();
+ expect(dispatch).not.toHaveBeenCalled();
+ });
+
+ it('supports tracked dry-run without applying tracked changes', () => {
+ const { editor, insertTrackedChange, dispatch } = makeEditor('Hello');
+
+ const receipt = writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'World',
+ },
+ { changeMode: 'tracked', dryRun: true },
+ );
+
+ expect(receipt.success).toBe(true);
+ expect(receipt.resolution.range).toEqual({ from: 1, to: 6 });
+ expect(insertTrackedChange).not.toHaveBeenCalled();
+ expect(dispatch).not.toHaveBeenCalled();
+ });
+
+ it('keeps direct and tracked writes deterministic on the same target window', () => {
+ const { editor, insertTrackedChange } = makeEditor('Hello');
+
+ const directReceipt = writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'World',
+ },
+ { changeMode: 'direct' },
+ );
+ expect(directReceipt.success).toBe(true);
+
+ const trackedReceipt = writeAdapter(
+ editor,
+ {
+ kind: 'replace',
+ target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 5 } },
+ text: 'Again',
+ },
+ { changeMode: 'tracked' },
+ );
+ expect(trackedReceipt.success).toBe(true);
+ expect(insertTrackedChange).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/write-adapter.ts b/packages/super-editor/src/document-api-adapters/write-adapter.ts
new file mode 100644
index 0000000000..0d55fd8db4
--- /dev/null
+++ b/packages/super-editor/src/document-api-adapters/write-adapter.ts
@@ -0,0 +1,210 @@
+import { v4 as uuidv4 } from 'uuid';
+import type { Editor } from '../core/Editor.js';
+import type {
+ MutationOptions,
+ ReceiptFailure,
+ TextAddress,
+ TextMutationReceipt,
+ WriteRequest,
+} from '@superdoc/document-api';
+import { DocumentApiAdapterError } from './errors.js';
+import { ensureTrackedCapability } from './helpers/mutation-helpers.js';
+import { applyDirectMutationMeta } from './helpers/transaction-meta.js';
+import { resolveDefaultInsertTarget, resolveTextTarget, type ResolvedTextTarget } from './helpers/adapter-utils.js';
+import { buildTextMutationResolution, readTextAtResolvedRange } from './helpers/text-mutation-resolution.js';
+import { toCanonicalTrackedChangeId } from './helpers/tracked-change-resolver.js';
+
+function validateWriteRequest(request: WriteRequest, resolvedTarget: ResolvedWriteTarget): ReceiptFailure | null {
+ if (request.kind === 'insert') {
+ if (!request.text) {
+ return {
+ code: 'INVALID_TARGET',
+ message: 'Insert operations require non-empty text.',
+ };
+ }
+
+ if (resolvedTarget.range.from !== resolvedTarget.range.to) {
+ return {
+ code: 'INVALID_TARGET',
+ message: 'Insert operations require a collapsed target range.',
+ };
+ }
+
+ return null;
+ }
+
+ if (request.kind === 'replace') {
+ if (request.text == null || request.text.length === 0) {
+ return {
+ code: 'INVALID_TARGET',
+ message: 'Replace operations require non-empty text. Use delete for removals.',
+ };
+ }
+
+ if (resolvedTarget.resolution.text === request.text) {
+ return {
+ code: 'NO_OP',
+ message: 'Replace operation produced no change.',
+ };
+ }
+
+ return null;
+ }
+
+ if (resolvedTarget.range.from === resolvedTarget.range.to) {
+ return {
+ code: 'NO_OP',
+ message: 'Delete operation produced no change for a collapsed range.',
+ };
+ }
+
+ return null;
+}
+
+type ResolvedWriteTarget = {
+ requestedTarget?: TextAddress;
+ effectiveTarget: TextAddress;
+ range: ResolvedTextTarget;
+ resolution: ReturnType;
+};
+
+function resolveWriteTarget(editor: Editor, request: WriteRequest): ResolvedWriteTarget | null {
+ const requestedTarget = request.target;
+
+ if (request.kind === 'insert' && !request.target) {
+ const fallback = resolveDefaultInsertTarget(editor);
+ if (!fallback) return null;
+
+ const text = readTextAtResolvedRange(editor, fallback.range);
+ return {
+ requestedTarget,
+ effectiveTarget: fallback.target,
+ range: fallback.range,
+ resolution: buildTextMutationResolution({
+ requestedTarget,
+ target: fallback.target,
+ range: fallback.range,
+ text,
+ }),
+ };
+ }
+
+ const target = request.target;
+ if (!target) return null;
+
+ const range = resolveTextTarget(editor, target);
+ if (!range) return null;
+
+ const text = readTextAtResolvedRange(editor, range);
+ return {
+ requestedTarget,
+ effectiveTarget: target,
+ range,
+ resolution: buildTextMutationResolution({
+ requestedTarget,
+ target,
+ range,
+ text,
+ }),
+ };
+}
+
+function applyDirectWrite(
+ editor: Editor,
+ request: WriteRequest,
+ resolvedTarget: ResolvedWriteTarget,
+): TextMutationReceipt {
+ if (request.kind === 'delete') {
+ const tr = applyDirectMutationMeta(editor.state.tr.delete(resolvedTarget.range.from, resolvedTarget.range.to));
+ editor.dispatch(tr);
+ return { success: true, resolution: resolvedTarget.resolution };
+ }
+
+ // text is guaranteed non-empty for insert/replace after validateWriteRequest
+ const tr = applyDirectMutationMeta(
+ editor.state.tr.insertText(request.text ?? '', resolvedTarget.range.from, resolvedTarget.range.to),
+ );
+ editor.dispatch(tr);
+ return { success: true, resolution: resolvedTarget.resolution };
+}
+
+function applyTrackedWrite(
+ editor: Editor,
+ request: WriteRequest,
+ resolvedTarget: ResolvedWriteTarget,
+): TextMutationReceipt {
+ ensureTrackedCapability(editor, { operation: 'write' });
+ // insertTrackedChange is guaranteed to exist after ensureTrackedCapability.
+ const insertTrackedChange = editor.commands!.insertTrackedChange!;
+ const text = request.kind === 'delete' ? '' : (request.text ?? '');
+
+ const changeId = uuidv4();
+ const didApply = insertTrackedChange({
+ from: resolvedTarget.range.from,
+ to: request.kind === 'insert' ? resolvedTarget.range.from : resolvedTarget.range.to,
+ text,
+ id: changeId,
+ });
+
+ if (!didApply) {
+ return {
+ success: false,
+ resolution: resolvedTarget.resolution,
+ failure: {
+ code: 'NO_OP',
+ message: 'Tracked write command did not apply a change.',
+ },
+ };
+ }
+ const publicChangeId = toCanonicalTrackedChangeId(editor, changeId);
+
+ return {
+ success: true,
+ resolution: resolvedTarget.resolution,
+ ...(publicChangeId
+ ? {
+ inserted: [
+ {
+ kind: 'entity',
+ entityType: 'trackedChange',
+ entityId: publicChangeId,
+ },
+ ],
+ }
+ : {}),
+ };
+}
+
+function toFailureReceipt(failure: ReceiptFailure, resolvedTarget: ResolvedWriteTarget): TextMutationReceipt {
+ return {
+ success: false,
+ resolution: resolvedTarget.resolution,
+ failure,
+ };
+}
+
+export function writeAdapter(editor: Editor, request: WriteRequest, options?: MutationOptions): TextMutationReceipt {
+ const resolvedTarget = resolveWriteTarget(editor, request);
+ if (!resolvedTarget) {
+ throw new DocumentApiAdapterError('TARGET_NOT_FOUND', 'Mutation target could not be resolved.', {
+ target: request.target,
+ });
+ }
+
+ const validationFailure = validateWriteRequest(request, resolvedTarget);
+ if (validationFailure) {
+ return toFailureReceipt(validationFailure, resolvedTarget);
+ }
+
+ const mode = options?.changeMode ?? 'direct';
+ if (options?.dryRun) {
+ if (mode === 'tracked') ensureTrackedCapability(editor, { operation: 'write' });
+ return { success: true, resolution: resolvedTarget.resolution };
+ }
+
+ if (mode === 'tracked') {
+ return applyTrackedWrite(editor, request, resolvedTarget);
+ }
+
+ return applyDirectWrite(editor, request, resolvedTarget);
+}
diff --git a/packages/super-editor/src/extensions/comment/comments-helpers.js b/packages/super-editor/src/extensions/comment/comments-helpers.js
index 33d12a748f..4bd1e14523 100644
--- a/packages/super-editor/src/extensions/comment/comments-helpers.js
+++ b/packages/super-editor/src/extensions/comment/comments-helpers.js
@@ -10,41 +10,66 @@ const TRACK_CHANGE_MARKS = [TrackInsertMarkName, TrackDeleteMarkName, TrackForma
*
* @param {Object} param0
* @param {string} param0.commentId The comment ID
+ * @param {string} [param0.importedId] The imported comment ID
* @param {import('prosemirror-state').EditorState} state The current editor state
* @param {import('prosemirror-state').Transaction} tr The current transaction
* @param {Function} param0.dispatch The dispatch function
- * @returns {void}
+ * @returns {boolean} True if any comment marks were removed
*/
-export const removeCommentsById = ({ commentId, state, tr, dispatch }) => {
- const positions = getCommentPositionsById(commentId, state.doc);
+export const removeCommentsById = ({ commentId, importedId, state, tr, dispatch }) => {
+ const positions = getCommentPositionsById(commentId, state.doc, importedId);
+ const anchorNodePositions = [];
+
+ state.doc.descendants((node, pos) => {
+ const nodeTypeName = node.type?.name;
+ if (nodeTypeName !== 'commentRangeStart' && nodeTypeName !== 'commentRangeEnd') return;
+ const wid = node.attrs?.['w:id'];
+ if (wid === commentId || (importedId && wid === importedId)) {
+ anchorNodePositions.push(pos);
+ }
+ });
+
+ if (!positions.length && !anchorNodePositions.length) return false;
// Remove the mark
- positions.forEach(({ from, to }) => {
- tr.removeMark(from, to, state.schema.marks[CommentMarkName]);
+ positions.forEach(({ from, to, mark }) => {
+ tr.removeMark(from, to, mark);
});
+
+ // Remove resolved-comment anchors (commentRangeStart/commentRangeEnd) when present.
+ anchorNodePositions
+ .slice()
+ .sort((a, b) => b - a)
+ .forEach((pos) => {
+ tr.delete(pos, pos + 1);
+ });
+
dispatch(tr);
+ return true;
};
/**
* Get the positions of a comment by ID
*
* @param {String} commentId The comment ID
+ * @param {String} [importedId] The imported comment ID
* @param {import('prosemirror-model').Node} doc The prosemirror document
- * @returns {Array} The positions of the comment
+ * @returns {Array<{from:number,to:number,mark:Object}>} The positions and exact mark instances for the comment
*/
-export const getCommentPositionsById = (commentId, doc) => {
+export const getCommentPositionsById = (commentId, doc, importedId) => {
const positions = [];
doc.descendants((node, pos) => {
const { marks } = node;
- const commentMark = marks.find((mark) => mark.type.name === CommentMarkName);
-
- if (commentMark) {
- const { attrs } = commentMark;
- const { commentId: currentCommentId } = attrs;
- if (commentId === currentCommentId) {
- positions.push({ from: pos, to: pos + node.nodeSize });
- }
- }
+ marks
+ .filter((mark) => mark.type.name === CommentMarkName)
+ .forEach((commentMark) => {
+ const { attrs } = commentMark;
+ const currentCommentId = attrs?.commentId;
+ const currentImportedId = attrs?.importedId;
+ if (commentId === currentCommentId || (importedId && importedId === currentImportedId)) {
+ positions.push({ from: pos, to: pos + node.nodeSize, mark: commentMark });
+ }
+ });
});
return positions;
};
@@ -54,16 +79,19 @@ export const getCommentPositionsById = (commentId, doc) => {
* This returns the raw segments (per inline node) rather than merged contiguous ranges.
*
* @param {string} commentId The comment ID to match
+ * @param {string} [importedId] The imported comment ID to match
* @param {import('prosemirror-model').Node} doc The ProseMirror document
* @returns {Array<{from:number,to:number,attrs:Object}>} Segments containing mark attrs
*/
-const getCommentMarkSegmentsById = (commentId, doc) => {
+const getCommentMarkSegmentsById = (commentId, doc, importedId) => {
const segments = [];
doc.descendants((node, pos) => {
if (!node.isInline) return;
const commentMark = node.marks?.find(
- (mark) => mark.type.name === CommentMarkName && mark.attrs?.commentId === commentId,
+ (mark) =>
+ mark.type.name === CommentMarkName &&
+ (mark.attrs?.commentId === commentId || (importedId && mark.attrs?.importedId === importedId)),
);
if (!commentMark) return;
@@ -83,11 +111,12 @@ const getCommentMarkSegmentsById = (commentId, doc) => {
* so this returns both the raw segments and the merged ranges.
*
* @param {string} commentId The comment ID to match
+ * @param {string} [importedId] The imported comment ID to match
* @param {import('prosemirror-model').Node} doc The ProseMirror document
* @returns {{segments:Array<{from:number,to:number,attrs:Object}>,ranges:Array<{from:number,to:number,internal:boolean}>}}
*/
-const getCommentMarkRangesById = (commentId, doc) => {
- const segments = getCommentMarkSegmentsById(commentId, doc);
+const getCommentMarkRangesById = (commentId, doc, importedId) => {
+ const segments = getCommentMarkSegmentsById(commentId, doc, importedId);
if (!segments.length) return { segments, ranges: [] };
const ranges = [];
@@ -127,17 +156,18 @@ const getCommentMarkRangesById = (commentId, doc) => {
*
* @param {Object} param0
* @param {string} param0.commentId The comment ID
+ * @param {string} [param0.importedId] The imported comment ID
* @param {import('prosemirror-state').EditorState} param0.state The current editor state
* @param {import('prosemirror-state').Transaction} param0.tr The current transaction
* @param {Function} param0.dispatch The dispatch function
* @returns {boolean} True if the comment mark existed and was processed
*/
-export const resolveCommentById = ({ commentId, state, tr, dispatch }) => {
+export const resolveCommentById = ({ commentId, importedId, state, tr, dispatch }) => {
const { schema } = state;
const markType = schema.marks?.[CommentMarkName];
if (!markType) return false;
- const { segments, ranges } = getCommentMarkRangesById(commentId, state.doc);
+ const { segments, ranges } = getCommentMarkRangesById(commentId, state.doc, importedId);
if (!segments.length) return false;
segments.forEach(({ from, to, attrs }) => {
diff --git a/packages/super-editor/src/extensions/comment/comments-plugin.js b/packages/super-editor/src/extensions/comment/comments-plugin.js
index 32eb99bca4..3f0577140b 100644
--- a/packages/super-editor/src/extensions/comment/comments-plugin.js
+++ b/packages/super-editor/src/extensions/comment/comments-plugin.js
@@ -31,6 +31,7 @@ export const CommentsPlugin = Extension.create({
* @category Command
* @param {string|Object} contentOrOptions - Comment content as a string, or an options object
* @param {string} [contentOrOptions.content] - The comment content (text or HTML)
+ * @param {string} [contentOrOptions.commentId] - Explicit comment ID (defaults to a new UUID)
* @param {string} [contentOrOptions.author] - Author name (defaults to user from editor config)
* @param {string} [contentOrOptions.authorEmail] - Author email (defaults to user from editor config)
* @param {string} [contentOrOptions.authorImage] - Author image URL (defaults to user from editor config)
@@ -67,12 +68,13 @@ export const CommentsPlugin = Extension.create({
}
// Handle string or options object
- let content, author, authorEmail, authorImage, isInternal;
+ let content, explicitCommentId, author, authorEmail, authorImage, isInternal;
if (typeof contentOrOptions === 'string') {
content = contentOrOptions;
} else if (contentOrOptions && typeof contentOrOptions === 'object') {
content = contentOrOptions.content;
+ explicitCommentId = contentOrOptions.commentId;
author = contentOrOptions.author;
authorEmail = contentOrOptions.authorEmail;
authorImage = contentOrOptions.authorImage;
@@ -80,7 +82,7 @@ export const CommentsPlugin = Extension.create({
}
// Generate a unique comment ID
- const commentId = uuidv4();
+ const commentId = explicitCommentId ?? uuidv4();
const resolvedInternal = isInternal ?? false;
// Get user defaults from editor config
@@ -143,14 +145,14 @@ export const CommentsPlugin = Extension.create({
addCommentReply:
(options = {}) =>
({ editor }) => {
- const { parentId, content, author, authorEmail, authorImage } = options;
+ const { parentId, content, author, authorEmail, authorImage, commentId: explicitCommentId } = options;
if (!parentId) {
console.warn('addCommentReply requires a parentId');
return false;
}
- const commentId = uuidv4();
+ const commentId = explicitCommentId ?? uuidv4();
const configUser = editor.options?.user || {};
const commentPayload = normalizeCommentEventPayload({
@@ -230,7 +232,7 @@ export const CommentsPlugin = Extension.create({
({ commentId, importedId }) =>
({ tr, dispatch, state }) => {
tr.setMeta(CommentsPluginKey, { event: 'deleted' });
- removeCommentsById({ commentId, importedId, state, tr, dispatch });
+ return removeCommentsById({ commentId, importedId, state, tr, dispatch });
},
setActiveComment:
@@ -241,42 +243,49 @@ export const CommentsPlugin = Extension.create({
},
setCommentInternal:
- ({ commentId, isInternal }) =>
+ ({ commentId, importedId, isInternal }) =>
({ tr, dispatch, state }) => {
const { doc } = state;
- let foundStartNode;
- let foundPos;
+ const commentMarkType = this.editor.schema.marks[CommentMarkName];
+ if (!commentMarkType) return false;
+ const matchedSegments = [];
- // Find the commentRangeStart node that matches the comment ID
tr.setMeta(CommentsPluginKey, { event: 'update' });
doc.descendants((node, pos) => {
- if (foundStartNode) return;
-
+ if (!node.isInline) return;
const { marks = [] } = node;
- const commentMark = marks.find((mark) => mark.type.name === CommentMarkName);
-
- if (commentMark) {
- const { attrs } = commentMark;
- const wid = attrs.commentId;
- if (wid === commentId) {
- foundStartNode = node;
- foundPos = pos;
- }
- }
+ marks
+ .filter((mark) => mark.type.name === CommentMarkName)
+ .forEach((commentMark) => {
+ const { attrs } = commentMark;
+ const wid = attrs.commentId;
+ const importedWid = attrs.importedId;
+ if (wid === commentId || (importedId && importedWid === importedId)) {
+ matchedSegments.push({
+ from: pos,
+ to: pos + node.nodeSize,
+ attrs,
+ mark: commentMark,
+ });
+ }
+ });
});
- // If no matching node, return false
- if (!foundStartNode) return false;
-
- // Update the mark itself
- tr.addMark(
- foundPos,
- foundPos + foundStartNode.nodeSize,
- this.editor.schema.marks[CommentMarkName].create({
- commentId,
- internal: isInternal,
- }),
- );
+ if (!matchedSegments.length) return false;
+
+ matchedSegments.forEach(({ from, to, attrs, mark }) => {
+ tr.removeMark(from, to, mark);
+ tr.addMark(
+ from,
+ to,
+ commentMarkType.create({
+ ...attrs,
+ commentId: attrs?.commentId ?? commentId,
+ importedId: attrs?.importedId ?? importedId,
+ internal: isInternal,
+ }),
+ );
+ });
tr.setMeta(CommentsPluginKey, { type: 'setCommentInternal' });
dispatch(tr);
@@ -284,10 +293,114 @@ export const CommentsPlugin = Extension.create({
},
resolveComment:
- ({ commentId }) =>
+ ({ commentId, importedId }) =>
({ tr, dispatch, state }) => {
tr.setMeta(CommentsPluginKey, { event: 'update' });
- return resolveCommentById({ commentId, state, tr, dispatch });
+ return resolveCommentById({ commentId, importedId, state, tr, dispatch });
+ },
+ editComment:
+ ({ commentId, importedId, content, text }) =>
+ ({ editor }) => {
+ const nextCommentId = commentId ?? importedId;
+ if (!nextCommentId) return false;
+
+ const normalizedText = content ?? text ?? '';
+ const payload = normalizeCommentEventPayload({
+ conversation: {
+ commentId: nextCommentId,
+ importedId,
+ commentText: normalizedText,
+ updatedTime: Date.now(),
+ },
+ editorOptions: editor.options,
+ fallbackCommentId: nextCommentId,
+ fallbackInternal: false,
+ });
+
+ editor.emit('commentsUpdate', {
+ type: comments_module_events.UPDATE,
+ comment: payload,
+ activeCommentId: nextCommentId,
+ });
+
+ return true;
+ },
+ moveComment:
+ ({ commentId, from, to }) =>
+ ({ tr, dispatch, state, editor }) => {
+ if (!commentId) return false;
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return false;
+ if (from >= to) return false;
+
+ const { doc } = state;
+ const resolved = findRangeById(doc, commentId);
+ if (!resolved) return false;
+
+ const markType = editor.schema?.marks?.[CommentMarkName];
+ if (!markType) return false;
+
+ tr.setMeta(CommentsPluginKey, { event: 'update' });
+
+ const segments = [];
+ doc.descendants((node, pos) => {
+ if (!node.isInline) return;
+ const commentMark = node.marks?.find(
+ (mark) =>
+ mark.type.name === CommentMarkName &&
+ (mark.attrs?.commentId === commentId || mark.attrs?.importedId === commentId),
+ );
+ if (!commentMark) return;
+ segments.push({
+ from: pos,
+ to: pos + node.nodeSize,
+ attrs: commentMark.attrs,
+ mark: commentMark,
+ });
+ });
+
+ if (segments.length > 0) {
+ segments.forEach((segment) => {
+ tr.removeMark(segment.from, segment.to, segment.mark);
+ });
+
+ const attrs = segments[0]?.attrs ?? { commentId };
+ const mappedFrom = tr.mapping.map(from);
+ const mappedTo = tr.mapping.map(to);
+ tr.addMark(mappedFrom, mappedTo, markType.create(attrs));
+ if (dispatch) dispatch(tr);
+ return true;
+ }
+
+ const startType = editor.schema?.nodes?.commentRangeStart;
+ const endType = editor.schema?.nodes?.commentRangeEnd;
+ if (!startType || !endType) return false;
+
+ let startPos = null;
+ let endPos = null;
+ let startAttrs = { 'w:id': commentId };
+ doc.descendants((node, pos) => {
+ if (node.type.name === 'commentRangeStart' && node.attrs?.['w:id'] === commentId) {
+ startPos = pos;
+ startAttrs = { ...node.attrs };
+ }
+ if (node.type.name === 'commentRangeEnd' && node.attrs?.['w:id'] === commentId) {
+ endPos = pos;
+ }
+ });
+
+ if (startPos == null || endPos == null) return false;
+
+ const toDelete = [startPos, endPos].sort((a, b) => b - a);
+ toDelete.forEach((pos) => {
+ tr.delete(pos, pos + 1);
+ });
+
+ const mappedFrom = tr.mapping.map(from);
+ const mappedTo = tr.mapping.map(to);
+ tr.insert(mappedTo, endType.create({ 'w:id': commentId }));
+ tr.insert(mappedFrom, startType.create({ ...startAttrs, 'w:id': commentId }));
+ if (dispatch) dispatch(tr);
+ return true;
},
setCursorById:
(id) =>
@@ -295,7 +408,9 @@ export const CommentsPlugin = Extension.create({
const { from } = findRangeById(state.doc, id) || {};
if (from != null) {
state.tr.setSelection(TextSelection.create(state.doc, from));
- editor.view.focus();
+ if (editor.view && typeof editor.view.focus === 'function') {
+ editor.view.focus();
+ }
return true;
}
return false;
@@ -782,7 +897,7 @@ const handleTrackedChangeTransaction = (trackedChangeMeta, trackedChanges, newEd
return newTrackedChanges;
};
-const getTrackedChangeText = ({ nodes, mark, trackedChangeType, isDeletionInsertion, marks }) => {
+const getTrackedChangeText = ({ nodes, mark, trackedChangeType, isDeletionInsertion }) => {
let trackedChangeText = '';
let deletionText = '';
@@ -847,8 +962,8 @@ const createOrUpdateTrackedChangeComment = ({ event, marks, deletionNodes, nodes
// Collect nodes from the tracked changes found
// We need to get the actual nodes at those positions
let nodesWithMark = [];
- trackedChangesWithId.forEach(({ from, to, mark }) => {
- newEditorState.doc.nodesBetween(from, to, (node, pos) => {
+ trackedChangesWithId.forEach(({ from, to }) => {
+ newEditorState.doc.nodesBetween(from, to, (node) => {
// Only collect inline text nodes
if (node.isText) {
// Check if this node has the mark (it should, since getTrackChanges found it)
@@ -897,7 +1012,6 @@ const createOrUpdateTrackedChangeComment = ({ event, marks, deletionNodes, nodes
state: newEditorState,
nodes: nodesToUse,
mark: trackedMark,
- marks,
trackedChangeType,
isDeletionInsertion,
deletionNodes,
diff --git a/packages/super-editor/src/extensions/comment/comments-plugin.test.js b/packages/super-editor/src/extensions/comment/comments-plugin.test.js
index 76a185d79f..a01dd9c8c1 100644
--- a/packages/super-editor/src/extensions/comment/comments-plugin.test.js
+++ b/packages/super-editor/src/extensions/comment/comments-plugin.test.js
@@ -315,6 +315,22 @@ describe('CommentsPlugin commands', () => {
expect(updatedMark?.attrs.internal).toBe(false);
});
+ it('supports moveComment capability checks when dispatch is undefined', () => {
+ const schema = createCommentSchema();
+ const mark = schema.marks[CommentMarkName].create({ commentId: 'c-move', internal: true });
+ const paragraph = schema.node('paragraph', null, [schema.text('Hello', [mark])]);
+ const doc = schema.node('doc', null, [paragraph]);
+ const { editor, commands } = createEditorEnvironment(schema, doc);
+
+ const command = commands.moveComment({ commentId: 'c-move', from: 2, to: 4 });
+
+ let result;
+ expect(() => {
+ result = command({ tr: editor.state.tr, dispatch: undefined, state: editor.state, editor });
+ }).not.toThrow();
+ expect(result).toBe(true);
+ });
+
it('focuses editor when moving the cursor to a comment by id', () => {
const schema = createCommentSchema();
const mark = schema.marks[CommentMarkName].create({ commentId: 'c-10', internal: true });
diff --git a/packages/super-editor/src/extensions/comment/comments.test.js b/packages/super-editor/src/extensions/comment/comments.test.js
index 9282fde88e..03a45e4c1c 100644
--- a/packages/super-editor/src/extensions/comment/comments.test.js
+++ b/packages/super-editor/src/extensions/comment/comments.test.js
@@ -115,7 +115,9 @@ describe('comment helpers', () => {
const positions = getCommentPositionsById('comment-123', state.doc);
- expect(positions).toEqual([{ from: 1, to: 6 }]);
+ expect(positions).toEqual([expect.objectContaining({ from: 1, to: 6 })]);
+ expect(positions[0].mark).toBeDefined();
+ expect(positions[0].mark.type.name).toBe(CommentMarkName);
});
it('removes comments by id and dispatches transaction', () => {
@@ -127,7 +129,11 @@ describe('comment helpers', () => {
removeCommentsById({ commentId: 'comment-123', state, tr, dispatch });
- expect(removeSpy).toHaveBeenCalledWith(1, 6, schema.marks[CommentMarkName]);
+ expect(removeSpy).toHaveBeenCalledWith(
+ 1,
+ 6,
+ expect.objectContaining({ type: expect.objectContaining({ name: CommentMarkName }) }),
+ );
expect(dispatch).toHaveBeenCalledWith(tr);
});
diff --git a/packages/super-editor/src/extensions/comment/helpers/comment-target-resolver.js b/packages/super-editor/src/extensions/comment/helpers/comment-target-resolver.js
new file mode 100644
index 0000000000..79f99bb9c4
--- /dev/null
+++ b/packages/super-editor/src/extensions/comment/helpers/comment-target-resolver.js
@@ -0,0 +1,185 @@
+import { CommentMarkName } from '../comments-constants.js';
+
+const COMMENT_RANGE_NODE_TYPES = new Set(['commentRangeStart', 'commentRangeEnd']);
+
+const toNonEmptyString = (value) => {
+ if (typeof value !== 'string') return null;
+ return value.length > 0 ? value : null;
+};
+
+const resolveMoveIds = ({ commentId, importedId }) => {
+ const canonicalId = toNonEmptyString(commentId);
+ const candidateImportedId = toNonEmptyString(importedId);
+ const fallbackImportedId = candidateImportedId && candidateImportedId !== canonicalId ? candidateImportedId : null;
+ return { canonicalId, fallbackImportedId };
+};
+
+const collectCanonicalMarkSegments = (doc, canonicalId) => {
+ const segments = [];
+ doc.descendants((node, pos) => {
+ if (!node.isInline) return;
+ const commentMark = node.marks?.find(
+ (mark) => mark.type.name === CommentMarkName && mark.attrs?.commentId === canonicalId,
+ );
+ if (!commentMark) return;
+ segments.push({
+ from: pos,
+ to: pos + node.nodeSize,
+ attrs: commentMark.attrs ?? {},
+ mark: commentMark,
+ });
+ });
+ return segments;
+};
+
+const collectImportedMarkSegments = (doc, importedId) => {
+ const segments = [];
+ doc.descendants((node, pos) => {
+ if (!node.isInline) return;
+ const commentMark = node.marks?.find(
+ (mark) => mark.type.name === CommentMarkName && mark.attrs?.importedId === importedId,
+ );
+ if (!commentMark) return;
+ segments.push({
+ from: pos,
+ to: pos + node.nodeSize,
+ attrs: commentMark.attrs ?? {},
+ mark: commentMark,
+ });
+ });
+ return segments;
+};
+
+const collectAnchorsById = (doc, id) => {
+ const anchors = [];
+ doc.descendants((node, pos) => {
+ if (!COMMENT_RANGE_NODE_TYPES.has(node.type?.name)) return;
+ if (node.attrs?.['w:id'] !== id) return;
+ anchors.push({
+ pos,
+ typeName: node.type.name,
+ attrs: { ...node.attrs },
+ });
+ });
+ return anchors;
+};
+
+/**
+ * Resolve which identity should be used when mutating comment marks/anchors.
+ *
+ * Resolution order:
+ * 1) canonical ID (commentId)
+ * 2) imported ID fallback (only if canonical has no targets)
+ *
+ * @param {import('prosemirror-model').Node} doc - The ProseMirror document to search
+ * @param {Object} ids - Comment identifiers to resolve
+ * @param {string} [ids.commentId] - Canonical comment ID
+ * @param {string} [ids.importedId] - Imported comment ID (used as fallback)
+ * @returns {{ status: 'resolved', strategy: 'canonical' | 'imported-fallback', matchId: string, canonicalId: string | null, fallbackImportedId: string | null } | { status: 'unresolved', reason: 'missing-identifiers' | 'no-targets' } | { status: 'ambiguous', reason: 'multiple-comment-ids' | 'canonical-mismatch', matchId: string }}
+ */
+export const resolveCommentIdentity = (doc, { commentId, importedId }) => {
+ const { canonicalId, fallbackImportedId } = resolveMoveIds({ commentId, importedId });
+
+ if (!canonicalId && !fallbackImportedId) {
+ return { status: 'unresolved', reason: 'missing-identifiers' };
+ }
+
+ if (canonicalId) {
+ const canonicalMarks = collectCanonicalMarkSegments(doc, canonicalId);
+ const canonicalAnchors = collectAnchorsById(doc, canonicalId);
+ if (canonicalMarks.length > 0 || canonicalAnchors.length > 0) {
+ return {
+ status: 'resolved',
+ strategy: 'canonical',
+ matchId: canonicalId,
+ canonicalId,
+ fallbackImportedId,
+ };
+ }
+ }
+
+ if (!fallbackImportedId) {
+ return { status: 'unresolved', reason: 'no-targets' };
+ }
+
+ const fallbackMarks = collectImportedMarkSegments(doc, fallbackImportedId);
+ const fallbackAnchors = collectAnchorsById(doc, fallbackImportedId);
+ if (fallbackMarks.length === 0 && fallbackAnchors.length === 0) {
+ return { status: 'unresolved', reason: 'no-targets' };
+ }
+
+ const distinctCommentIds = new Set(
+ fallbackMarks.map((segment) => toNonEmptyString(segment.attrs?.commentId)).filter((id) => !!id),
+ );
+ if (distinctCommentIds.size > 1) {
+ return { status: 'ambiguous', reason: 'multiple-comment-ids', matchId: fallbackImportedId };
+ }
+
+ if (canonicalId && distinctCommentIds.size === 1 && !distinctCommentIds.has(canonicalId)) {
+ return { status: 'ambiguous', reason: 'canonical-mismatch', matchId: fallbackImportedId };
+ }
+
+ return {
+ status: 'resolved',
+ strategy: 'imported-fallback',
+ matchId: fallbackImportedId,
+ canonicalId,
+ fallbackImportedId,
+ };
+};
+
+/**
+ * Collect all inline-node segments that carry a comment mark matching the resolved identity.
+ *
+ * @param {import('prosemirror-model').Node} doc - The ProseMirror document
+ * @param {ReturnType} identity - A resolved identity from {@link resolveCommentIdentity}
+ * @returns {Array<{ from: number, to: number, attrs: Object, mark: Object }>} Mark segments, empty when identity is not resolved
+ */
+export const collectCommentMarkSegments = (doc, identity) => {
+ if (!identity || identity.status !== 'resolved') return [];
+ return identity.strategy === 'canonical'
+ ? collectCanonicalMarkSegments(doc, identity.matchId)
+ : collectImportedMarkSegments(doc, identity.matchId);
+};
+
+/**
+ * Collect commentRangeStart/commentRangeEnd anchor nodes matching the resolved identity.
+ *
+ * @param {import('prosemirror-model').Node} doc - The ProseMirror document
+ * @param {ReturnType} identity - A resolved identity from {@link resolveCommentIdentity}
+ * @returns {Array<{ pos: number, typeName: string, attrs: Object }>} Anchor nodes, empty when identity is not resolved
+ */
+export const collectCommentAnchorNodes = (doc, identity) => {
+ if (!identity || identity.status !== 'resolved') return [];
+ return collectAnchorsById(doc, identity.matchId);
+};
+
+/**
+ * Find the paired commentRangeStart/commentRangeEnd positions for a resolved identity.
+ *
+ * @param {import('prosemirror-model').Node} doc - The ProseMirror document
+ * @param {ReturnType} identity - A resolved identity from {@link resolveCommentIdentity}
+ * @returns {{ startPos: number, endPos: number, startAttrs: Object } | null} Range positions, or null when anchors are missing/incomplete
+ */
+export const collectCommentRangeAnchors = (doc, identity) => {
+ if (!identity || identity.status !== 'resolved') return null;
+ let startPos = null;
+ let endPos = null;
+ let startAttrs = { 'w:id': identity.matchId };
+
+ doc.descendants((node, pos) => {
+ if (!COMMENT_RANGE_NODE_TYPES.has(node.type?.name)) return;
+ if (node.attrs?.['w:id'] !== identity.matchId) return;
+ if (node.type.name === 'commentRangeStart') {
+ startPos = pos;
+ startAttrs = { ...node.attrs };
+ return;
+ }
+ if (node.type.name === 'commentRangeEnd') {
+ endPos = pos;
+ }
+ });
+
+ if (startPos == null || endPos == null) return null;
+ return { startPos, endPos, startAttrs };
+};
diff --git a/packages/super-editor/src/extensions/comment/helpers/comment-target-resolver.test.js b/packages/super-editor/src/extensions/comment/helpers/comment-target-resolver.test.js
new file mode 100644
index 0000000000..632eb70760
--- /dev/null
+++ b/packages/super-editor/src/extensions/comment/helpers/comment-target-resolver.test.js
@@ -0,0 +1,225 @@
+import { describe, expect, it } from 'vitest';
+import { Schema } from 'prosemirror-model';
+import {
+ resolveCommentIdentity,
+ collectCommentMarkSegments,
+ collectCommentAnchorNodes,
+ collectCommentRangeAnchors,
+} from './comment-target-resolver.js';
+
+const schema = new Schema({
+ nodes: {
+ doc: { content: 'block+' },
+ paragraph: { group: 'block', content: 'inline*' },
+ commentRangeStart: {
+ group: 'inline',
+ inline: true,
+ atom: true,
+ attrs: { 'w:id': { default: '' } },
+ },
+ commentRangeEnd: {
+ group: 'inline',
+ inline: true,
+ atom: true,
+ attrs: { 'w:id': { default: '' } },
+ },
+ text: { group: 'inline' },
+ },
+ marks: {
+ commentMark: {
+ attrs: {
+ commentId: { default: '' },
+ importedId: { default: '' },
+ },
+ },
+ },
+});
+
+function docWithCommentMark(commentId, importedId, text = 'hello') {
+ const mark = schema.marks.commentMark.create({ commentId, importedId });
+ const textNode = schema.text(text, [mark]);
+ return schema.nodes.doc.create(null, [schema.nodes.paragraph.create(null, [textNode])]);
+}
+
+function docWithAnchors(id) {
+ const start = schema.nodes.commentRangeStart.create({ 'w:id': id });
+ const end = schema.nodes.commentRangeEnd.create({ 'w:id': id });
+ return schema.nodes.doc.create(null, [schema.nodes.paragraph.create(null, [start, schema.text('body'), end])]);
+}
+
+function emptyDoc() {
+ return schema.nodes.doc.create(null, [schema.nodes.paragraph.create()]);
+}
+
+// --- resolveCommentIdentity ---
+
+describe('resolveCommentIdentity', () => {
+ it('returns unresolved with missing-identifiers when both ids are empty', () => {
+ const result = resolveCommentIdentity(emptyDoc(), { commentId: '', importedId: '' });
+ expect(result).toEqual({ status: 'unresolved', reason: 'missing-identifiers' });
+ });
+
+ it('returns unresolved with missing-identifiers when both ids are undefined', () => {
+ const result = resolveCommentIdentity(emptyDoc(), {});
+ expect(result).toEqual({ status: 'unresolved', reason: 'missing-identifiers' });
+ });
+
+ it('resolves via canonical strategy when commentId matches marks', () => {
+ const doc = docWithCommentMark('c1', 'i1');
+ const result = resolveCommentIdentity(doc, { commentId: 'c1', importedId: 'i1' });
+ expect(result).toEqual({
+ status: 'resolved',
+ strategy: 'canonical',
+ matchId: 'c1',
+ canonicalId: 'c1',
+ fallbackImportedId: 'i1',
+ });
+ });
+
+ it('resolves via canonical strategy when commentId matches anchors', () => {
+ const doc = docWithAnchors('c1');
+ const result = resolveCommentIdentity(doc, { commentId: 'c1' });
+ expect(result).toEqual({
+ status: 'resolved',
+ strategy: 'canonical',
+ matchId: 'c1',
+ canonicalId: 'c1',
+ fallbackImportedId: null,
+ });
+ });
+
+ it('falls back to imported-fallback when canonical has no targets', () => {
+ const doc = docWithCommentMark('', 'i1');
+ const result = resolveCommentIdentity(doc, { commentId: 'c-missing', importedId: 'i1' });
+ expect(result).toEqual({
+ status: 'resolved',
+ strategy: 'imported-fallback',
+ matchId: 'i1',
+ canonicalId: 'c-missing',
+ fallbackImportedId: 'i1',
+ });
+ });
+
+ it('returns unresolved no-targets when canonical has no targets and no importedId', () => {
+ const doc = emptyDoc();
+ const result = resolveCommentIdentity(doc, { commentId: 'c1' });
+ expect(result).toEqual({ status: 'unresolved', reason: 'no-targets' });
+ });
+
+ it('returns unresolved no-targets when neither canonical nor imported have targets', () => {
+ const doc = emptyDoc();
+ const result = resolveCommentIdentity(doc, { commentId: 'c1', importedId: 'i1' });
+ expect(result).toEqual({ status: 'unresolved', reason: 'no-targets' });
+ });
+
+ it('does not use importedId as fallback when it equals commentId', () => {
+ const doc = emptyDoc();
+ const result = resolveCommentIdentity(doc, { commentId: 'same', importedId: 'same' });
+ expect(result).toEqual({ status: 'unresolved', reason: 'no-targets' });
+ });
+
+ it('returns ambiguous multiple-comment-ids when fallback marks have different commentIds', () => {
+ const mark1 = schema.marks.commentMark.create({ commentId: 'a', importedId: 'shared' });
+ const mark2 = schema.marks.commentMark.create({ commentId: 'b', importedId: 'shared' });
+ const doc = schema.nodes.doc.create(null, [
+ schema.nodes.paragraph.create(null, [schema.text('one', [mark1]), schema.text('two', [mark2])]),
+ ]);
+ const result = resolveCommentIdentity(doc, { commentId: 'missing', importedId: 'shared' });
+ expect(result).toEqual({ status: 'ambiguous', reason: 'multiple-comment-ids', matchId: 'shared' });
+ });
+
+ it('returns ambiguous canonical-mismatch when fallback mark has a different canonical id', () => {
+ const doc = docWithCommentMark('other-canonical', 'i1');
+ const result = resolveCommentIdentity(doc, { commentId: 'c1', importedId: 'i1' });
+ expect(result).toEqual({ status: 'ambiguous', reason: 'canonical-mismatch', matchId: 'i1' });
+ });
+});
+
+// --- collectCommentMarkSegments ---
+
+describe('collectCommentMarkSegments', () => {
+ it('returns empty array for non-resolved identity', () => {
+ const doc = docWithCommentMark('c1', 'i1');
+ expect(collectCommentMarkSegments(doc, null)).toEqual([]);
+ expect(collectCommentMarkSegments(doc, { status: 'unresolved', reason: 'no-targets' })).toEqual([]);
+ });
+
+ it('collects mark segments for canonical strategy', () => {
+ const doc = docWithCommentMark('c1', 'i1');
+ const identity = resolveCommentIdentity(doc, { commentId: 'c1', importedId: 'i1' });
+ const segments = collectCommentMarkSegments(doc, identity);
+ expect(segments.length).toBe(1);
+ expect(segments[0].attrs.commentId).toBe('c1');
+ });
+
+ it('collects mark segments for imported-fallback strategy', () => {
+ const doc = docWithCommentMark('', 'i1');
+ const identity = resolveCommentIdentity(doc, { commentId: 'missing', importedId: 'i1' });
+ expect(identity.status).toBe('resolved');
+ const segments = collectCommentMarkSegments(doc, identity);
+ expect(segments.length).toBe(1);
+ expect(segments[0].attrs.importedId).toBe('i1');
+ });
+});
+
+// --- collectCommentAnchorNodes ---
+
+describe('collectCommentAnchorNodes', () => {
+ it('returns empty array for non-resolved identity', () => {
+ const doc = docWithAnchors('c1');
+ expect(collectCommentAnchorNodes(doc, null)).toEqual([]);
+ });
+
+ it('collects anchor nodes for a resolved identity', () => {
+ const doc = docWithAnchors('c1');
+ const identity = resolveCommentIdentity(doc, { commentId: 'c1' });
+ const anchors = collectCommentAnchorNodes(doc, identity);
+ expect(anchors.length).toBe(2);
+ const typeNames = anchors.map((a) => a.typeName).sort();
+ expect(typeNames).toEqual(['commentRangeEnd', 'commentRangeStart']);
+ });
+});
+
+// --- collectCommentRangeAnchors ---
+
+describe('collectCommentRangeAnchors', () => {
+ it('returns null for non-resolved identity', () => {
+ const doc = docWithAnchors('c1');
+ expect(collectCommentRangeAnchors(doc, null)).toBeNull();
+ });
+
+ it('returns start and end positions for a resolved identity', () => {
+ const doc = docWithAnchors('c1');
+ const identity = resolveCommentIdentity(doc, { commentId: 'c1' });
+ const range = collectCommentRangeAnchors(doc, identity);
+ expect(range).not.toBeNull();
+ expect(range.startPos).toBeLessThan(range.endPos);
+ expect(range.startAttrs['w:id']).toBe('c1');
+ });
+
+ it('returns null when only start anchor exists', () => {
+ const start = schema.nodes.commentRangeStart.create({ 'w:id': 'c1' });
+ const doc = schema.nodes.doc.create(null, [schema.nodes.paragraph.create(null, [start])]);
+ const identity = {
+ status: 'resolved',
+ strategy: 'canonical',
+ matchId: 'c1',
+ canonicalId: 'c1',
+ fallbackImportedId: null,
+ };
+ expect(collectCommentRangeAnchors(doc, identity)).toBeNull();
+ });
+
+ it('returns null when only end anchor exists', () => {
+ const end = schema.nodes.commentRangeEnd.create({ 'w:id': 'c1' });
+ const doc = schema.nodes.doc.create(null, [schema.nodes.paragraph.create(null, [end])]);
+ const identity = {
+ status: 'resolved',
+ strategy: 'canonical',
+ matchId: 'c1',
+ canonicalId: 'c1',
+ fallbackImportedId: null,
+ };
+ expect(collectCommentRangeAnchors(doc, identity)).toBeNull();
+ });
+});
diff --git a/packages/super-editor/src/extensions/search/search.js b/packages/super-editor/src/extensions/search/search.js
index 869b9235b5..571c46e554 100644
--- a/packages/super-editor/src/extensions/search/search.js
+++ b/packages/super-editor/src/extensions/search/search.js
@@ -136,6 +136,8 @@ const getPositionTracker = (editor) => {
* @property {boolean} [highlight=true] - Whether to apply CSS classes for visual highlighting of search matches.
* When true, matches are styled with 'ProseMirror-search-match' or 'ProseMirror-active-search-match' classes.
* When false, matches are tracked without visual styling, useful for programmatic search without UI changes.
+ * @property {number} [maxMatches=1000] - Maximum number of matches to return.
+ * @property {boolean} [caseSensitive=false] - Whether the search should be case-sensitive.
*/
/**
@@ -348,6 +350,7 @@ export const Search = Extension.create({
searchPattern = new RegExp(body, flags.includes('g') ? flags : flags + 'g');
} else {
searchPattern = String(patternInput);
+ caseSensitive = typeof options?.caseSensitive === 'boolean' ? options.caseSensitive : false;
}
// Ensure search index is valid
diff --git a/packages/super-editor/src/extensions/track-changes/track-changes.js b/packages/super-editor/src/extensions/track-changes/track-changes.js
index c5729e74a1..c09c1beccd 100644
--- a/packages/super-editor/src/extensions/track-changes/track-changes.js
+++ b/packages/super-editor/src/extensions/track-changes/track-changes.js
@@ -258,6 +258,7 @@ export const TrackChanges = Extension.create({
from = state.selection.from,
to = state.selection.to,
text = '',
+ id,
user,
comment,
addToHistory = true,
@@ -296,7 +297,7 @@ export const TrackChanges = Extension.create({
// For replacements (both deletion and insertion), generate a shared ID upfront
// so the deletion and insertion marks are linked together
const isReplacement = from !== to && text;
- const sharedId = isReplacement ? uuidv4() : null;
+ const sharedId = id ?? (isReplacement ? uuidv4() : null);
let changeId = sharedId;
let insertPos = to; // Default insert position is after the selection
diff --git a/packages/super-editor/src/extensions/types/comment-commands.ts b/packages/super-editor/src/extensions/types/comment-commands.ts
index 411825cab1..e24f516771 100644
--- a/packages/super-editor/src/extensions/types/comment-commands.ts
+++ b/packages/super-editor/src/extensions/types/comment-commands.ts
@@ -8,6 +8,8 @@
export type AddCommentOptions = {
/** The comment content (text or HTML) */
content?: string;
+ /** Explicit comment ID (defaults to a new UUID) */
+ commentId?: string;
/** Author name (defaults to user from editor config) */
author?: string;
/** Author email (defaults to user from editor config) */
@@ -59,13 +61,17 @@ export type RemoveCommentOptions = {
/** Options for setActiveComment command */
export type SetActiveCommentOptions = {
/** The comment ID to set as active */
- commentId: string;
+ commentId: string | null;
+ /** The imported comment ID */
+ importedId?: string;
};
/** Options for setCommentInternal command */
export type SetCommentInternalOptions = {
/** The comment ID to update */
commentId: string;
+ /** The imported comment ID */
+ importedId?: string;
/** Whether the comment should be internal */
isInternal: boolean;
};
@@ -74,12 +80,38 @@ export type SetCommentInternalOptions = {
export type ResolveCommentOptions = {
/** The comment ID to resolve */
commentId: string;
+ /** The imported comment ID */
+ importedId?: string;
+};
+
+/** Options for editComment command */
+export type EditCommentOptions = {
+ /** The comment ID to edit */
+ commentId: string;
+ /** The imported comment ID */
+ importedId?: string;
+ /** Updated content (text or HTML) */
+ content?: string;
+ /** Updated content alias */
+ text?: string;
+};
+
+/** Options for moveComment command */
+export type MoveCommentOptions = {
+ /** The comment ID to move */
+ commentId: string;
+ /** Absolute ProseMirror start position */
+ from: number;
+ /** Absolute ProseMirror end position */
+ to: number;
};
/** Options for addCommentReply command */
export type AddCommentReplyOptions = {
/** The ID of the parent comment or tracked change to reply to */
parentId: string;
+ /** Optional explicit comment ID for deterministic callers */
+ commentId?: string;
/** The reply content (text or HTML) */
content?: string;
/** Author name (defaults to user from editor config) */
@@ -156,6 +188,18 @@ export interface CommentCommands {
*/
resolveComment: (options: ResolveCommentOptions) => boolean;
+ /**
+ * Edit an existing comment payload.
+ * @param options - Object containing comment id and updated content
+ */
+ editComment: (options: EditCommentOptions) => boolean;
+
+ /**
+ * Move a comment anchor to a new document range.
+ * @param options - Object containing comment id and absolute target positions
+ */
+ moveComment: (options: MoveCommentOptions) => boolean;
+
/**
* Set cursor position to a comment by ID
* @param id - The comment ID to navigate to
diff --git a/packages/super-editor/src/extensions/types/specialized-commands.ts b/packages/super-editor/src/extensions/types/specialized-commands.ts
index ed4b5afa95..568946250d 100644
--- a/packages/super-editor/src/extensions/types/specialized-commands.ts
+++ b/packages/super-editor/src/extensions/types/specialized-commands.ts
@@ -15,6 +15,8 @@ type SearchMatch = {
export type SearchCommandOptions = {
highlight?: boolean;
+ maxMatches?: number;
+ caseSensitive?: boolean;
};
type DocumentSectionCreateOptions = {
diff --git a/packages/super-editor/src/extensions/types/track-changes-commands.ts b/packages/super-editor/src/extensions/types/track-changes-commands.ts
index 2e3c03f431..d28bb2be21 100644
--- a/packages/super-editor/src/extensions/types/track-changes-commands.ts
+++ b/packages/super-editor/src/extensions/types/track-changes-commands.ts
@@ -32,6 +32,8 @@ export type InsertTrackedChangeOptions = {
to?: number;
/** Replacement text */
text?: string;
+ /** Explicit change ID for deterministic callers (defaults to a new UUID) */
+ id?: string;
/** Author override for the tracked change (defaults to editor user if not provided) */
user?: Partial;
/** Optional comment reply to attach to the tracked change */
diff --git a/packages/super-editor/src/index.public-api.test.js b/packages/super-editor/src/index.public-api.test.js
new file mode 100644
index 0000000000..b99c921cab
--- /dev/null
+++ b/packages/super-editor/src/index.public-api.test.js
@@ -0,0 +1,12 @@
+import { readFileSync } from 'node:fs';
+import { resolve } from 'node:path';
+import { describe, expect, it } from 'vitest';
+
+describe('public root exports', () => {
+ it('does not expose document-api adapter assembly from the package root', () => {
+ const indexPath = resolve(import.meta.dirname, 'index.js');
+ const source = readFileSync(indexPath, 'utf8');
+
+ expect(source).not.toMatch(/export\s*\{\s*assembleDocumentApiAdapters\s*\}/);
+ });
+});
diff --git a/packages/super-editor/src/tests/extensions/search/search-find-replace.test.js b/packages/super-editor/src/tests/extensions/search/search-find-replace.test.js
index 4f5bc883cc..d4ca3efe8b 100644
--- a/packages/super-editor/src/tests/extensions/search/search-find-replace.test.js
+++ b/packages/super-editor/src/tests/extensions/search/search-find-replace.test.js
@@ -887,6 +887,31 @@ describe('Search find/replace commands', () => {
editor.destroy();
}
});
+
+ it('should respect caseSensitive option for string patterns', () => {
+ const editor = createDocxTestEditor();
+
+ try {
+ const { doc, paragraph, run } = editor.schema.nodes;
+ const testDoc = doc.create(null, [
+ paragraph.create(null, [run.create(null, [editor.schema.text('Test TEST test TeSt')])]),
+ ]);
+
+ const baseState = EditorState.create({
+ schema: editor.schema,
+ doc: testDoc,
+ plugins: editor.state.plugins,
+ });
+ editor.setState(baseState);
+
+ const matches = editor.commands.search('test', { caseSensitive: true });
+
+ expect(matches).toHaveLength(1);
+ expect(matches[0].text).toBe('test');
+ } finally {
+ editor.destroy();
+ }
+ });
});
describe('Whole word matching', () => {
diff --git a/packages/super-editor/tsconfig.types.json b/packages/super-editor/tsconfig.types.json
index e1215386a9..f33a527683 100644
--- a/packages/super-editor/tsconfig.types.json
+++ b/packages/super-editor/tsconfig.types.json
@@ -12,6 +12,7 @@
{ "path": "../layout-engine/painters/dom/tsconfig.json" },
{ "path": "../layout-engine/style-engine/tsconfig.json" },
{ "path": "../word-layout/tsconfig.json" },
- { "path": "../../shared/common/tsconfig.json" }
+ { "path": "../../shared/common/tsconfig.json" },
+ { "path": "../document-api/tsconfig.json" }
]
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7a350685c0..b7e7adca40 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -387,6 +387,9 @@ importers:
semantic-release-pnpm:
specifier: ^1.0.2
version: 1.0.2(semantic-release@24.2.9(typescript@5.9.3))
+ tsx:
+ specifier: 'catalog:'
+ version: 4.21.0
typescript:
specifier: 'catalog:'
version: 5.9.3
@@ -630,6 +633,8 @@ importers:
specifier: 'catalog:'
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.2)(esbuild@0.27.2)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@27.3.0(canvas@3.2.0))(tsx@4.21.0)(yaml@2.8.2)
+ packages/document-api: {}
+
packages/esign:
devDependencies:
'@testing-library/jest-dom':
@@ -1072,6 +1077,9 @@ importers:
'@superdoc/contracts':
specifier: workspace:*
version: link:../layout-engine/contracts
+ '@superdoc/document-api':
+ specifier: workspace:*
+ version: link:../document-api
'@superdoc/layout-bridge':
specifier: workspace:*
version: link:../layout-engine/layout-bridge
diff --git a/tsconfig.references.json b/tsconfig.references.json
index 474f318020..83d8bd29f4 100644
--- a/tsconfig.references.json
+++ b/tsconfig.references.json
@@ -9,6 +9,7 @@
{ "path": "packages/layout-engine/painters/dom/tsconfig.json" },
{ "path": "packages/layout-engine/style-engine/tsconfig.json" },
{ "path": "packages/layout-engine/layout-bridge/tsconfig.types.json" },
+ { "path": "packages/document-api/tsconfig.json" },
{ "path": "packages/super-editor/tsconfig.types.json" },
{ "path": "packages/superdoc/tsconfig.types.json" }
]
diff --git a/vitest.config.mjs b/vitest.config.mjs
index 495d349ca2..27dd4dd6d6 100644
--- a/vitest.config.mjs
+++ b/vitest.config.mjs
@@ -12,6 +12,7 @@ export default defineConfig({
// Use package directories; Vitest will pick up each package's vite.config.js
projects: [
'./packages/super-editor',
+ './packages/document-api',
'./packages/superdoc',
'./packages/ai',
'./packages/collaboration-yjs',