diff --git a/apps/cli/src/cli/operation-params.ts b/apps/cli/src/cli/operation-params.ts index 05e4f0c515..2dd2f7d103 100644 --- a/apps/cli/src/cli/operation-params.ts +++ b/apps/cli/src/cli/operation-params.ts @@ -609,16 +609,17 @@ const EXTRA_CLI_PARAMS: Partial> = { ], // Text-range operations: flat flags (--block-id, --start, --end) as shortcuts for --target-json 'doc.insert': [ - ...TEXT_TARGET_FLAT_PARAMS, + ...TEXT_TARGET_FLAT_PARAMS_AGENT_HIDDEN, { name: 'offset', kind: 'flag', type: 'number', description: 'Character offset for insertion (alias for --start/--end with same value).', + agentVisible: false as const, }, ], - 'doc.replace': [...TEXT_TARGET_FLAT_PARAMS], - 'doc.delete': [...TEXT_TARGET_FLAT_PARAMS], + 'doc.replace': [...TEXT_TARGET_FLAT_PARAMS_AGENT_HIDDEN], + 'doc.delete': [...TEXT_TARGET_FLAT_PARAMS_AGENT_HIDDEN], 'doc.styles.apply': [ { name: 'target', diff --git a/apps/docs/document-api/available-operations.mdx b/apps/docs/document-api/available-operations.mdx index 1bdade99c0..a19aa9b2fa 100644 --- a/apps/docs/document-api/available-operations.mdx +++ b/apps/docs/document-api/available-operations.mdx @@ -36,6 +36,8 @@ Use the tables below to see what operations are available and where each one is | Mutations | 2 | 0 | 2 | [Reference](/document-api/reference/mutations/index) | | Paragraph Formatting | 19 | 0 | 19 | [Reference](/document-api/reference/format/paragraph/index) | | Paragraph Styles | 2 | 0 | 2 | [Reference](/document-api/reference/styles/paragraph/index) | +| Permission Ranges | 5 | 0 | 5 | [Reference](/document-api/reference/permission-ranges/index) | +| Protection | 3 | 0 | 3 | [Reference](/document-api/reference/protection/index) | | Query | 1 | 0 | 1 | [Reference](/document-api/reference/query/index) | | Ranges | 1 | 0 | 1 | [Reference](/document-api/reference/ranges/index) | | Sections | 18 | 0 | 18 | [Reference](/document-api/reference/sections/index) | @@ -335,6 +337,14 @@ Use the tables below to see what operations are available and where each one is | editor.doc.format.paragraph.clearDirection(...) | [`format.paragraph.clearDirection`](/document-api/reference/format/paragraph/clear-direction) | | editor.doc.styles.paragraph.setStyle(...) | [`styles.paragraph.setStyle`](/document-api/reference/styles/paragraph/set-style) | | editor.doc.styles.paragraph.clearStyle(...) | [`styles.paragraph.clearStyle`](/document-api/reference/styles/paragraph/clear-style) | +| editor.doc.permissionRanges.list(...) | [`permissionRanges.list`](/document-api/reference/permission-ranges/list) | +| editor.doc.permissionRanges.get(...) | [`permissionRanges.get`](/document-api/reference/permission-ranges/get) | +| editor.doc.permissionRanges.create(...) | [`permissionRanges.create`](/document-api/reference/permission-ranges/create) | +| editor.doc.permissionRanges.remove(...) | [`permissionRanges.remove`](/document-api/reference/permission-ranges/remove) | +| editor.doc.permissionRanges.updatePrincipal(...) | [`permissionRanges.updatePrincipal`](/document-api/reference/permission-ranges/update-principal) | +| editor.doc.protection.get(...) | [`protection.get`](/document-api/reference/protection/get) | +| editor.doc.protection.setEditingRestriction(...) | [`protection.setEditingRestriction`](/document-api/reference/protection/set-editing-restriction) | +| editor.doc.protection.clearEditingRestriction(...) | [`protection.clearEditingRestriction`](/document-api/reference/protection/clear-editing-restriction) | | editor.doc.query.match(...) | [`query.match`](/document-api/reference/query/match) | | editor.doc.ranges.resolve(...) | [`ranges.resolve`](/document-api/reference/ranges/resolve) | | editor.doc.sections.list(...) | [`sections.list`](/document-api/reference/sections/list) | diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index 18b133472c..fe07ca9e55 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -320,6 +320,16 @@ "apps/docs/document-api/reference/mutations/apply.mdx", "apps/docs/document-api/reference/mutations/index.mdx", "apps/docs/document-api/reference/mutations/preview.mdx", + "apps/docs/document-api/reference/permission-ranges/create.mdx", + "apps/docs/document-api/reference/permission-ranges/get.mdx", + "apps/docs/document-api/reference/permission-ranges/index.mdx", + "apps/docs/document-api/reference/permission-ranges/list.mdx", + "apps/docs/document-api/reference/permission-ranges/remove.mdx", + "apps/docs/document-api/reference/permission-ranges/update-principal.mdx", + "apps/docs/document-api/reference/protection/clear-editing-restriction.mdx", + "apps/docs/document-api/reference/protection/get.mdx", + "apps/docs/document-api/reference/protection/index.mdx", + "apps/docs/document-api/reference/protection/set-editing-restriction.mdx", "apps/docs/document-api/reference/query/index.mdx", "apps/docs/document-api/reference/query/match.mdx", "apps/docs/document-api/reference/ranges/index.mdx", @@ -983,8 +993,28 @@ "operationIds": ["diff.capture", "diff.compare", "diff.apply"], "pagePath": "apps/docs/document-api/reference/diff/index.mdx", "title": "Diff" + }, + { + "aliasMemberPaths": [], + "key": "protection", + "operationIds": ["protection.get", "protection.setEditingRestriction", "protection.clearEditingRestriction"], + "pagePath": "apps/docs/document-api/reference/protection/index.mdx", + "title": "Protection" + }, + { + "aliasMemberPaths": [], + "key": "permissionRanges", + "operationIds": [ + "permissionRanges.list", + "permissionRanges.get", + "permissionRanges.create", + "permissionRanges.remove", + "permissionRanges.updatePrincipal" + ], + "pagePath": "apps/docs/document-api/reference/permission-ranges/index.mdx", + "title": "Permission Ranges" } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "d4231e3456244bc979f6e72db4da7ca682ec851ea2fefebb48689c299fbad039" + "sourceHash": "4a3601ee0f28a73c712fbe06e8b4913a9ae882a71152f9f6e892ea51137fc5e8" } diff --git a/apps/docs/document-api/reference/blocks/list.mdx b/apps/docs/document-api/reference/blocks/list.mdx index 37189468ea..27c6346695 100644 --- a/apps/docs/document-api/reference/blocks/list.mdx +++ b/apps/docs/document-api/reference/blocks/list.mdx @@ -130,6 +130,10 @@ Returns a BlocksListResult with total block count, an ordered array of block ent "description": "True if text is bold.", "type": "boolean" }, + "color": { + "description": "Text color when explicitly set (e.g. '#000000').", + "type": "string" + }, "fontFamily": { "description": "Font family from first text run.", "type": "string" @@ -165,6 +169,10 @@ Returns a BlocksListResult with total block count, an ordered array of block ent "ordinal": { "type": "number" }, + "ref": { + "description": "Ref handle for this block. Pass directly to superdoc_format or superdoc_edit ref param. Only present for non-empty blocks.", + "type": "string" + }, "styleId": { "description": "Named paragraph style.", "oneOf": [ diff --git a/apps/docs/document-api/reference/capabilities/get.mdx b/apps/docs/document-api/reference/capabilities/get.mdx index df23ceaa4c..f64034b1be 100644 --- a/apps/docs/document-api/reference/capabilities/get.mdx +++ b/apps/docs/document-api/reference/capabilities/get.mdx @@ -1745,6 +1745,46 @@ _No fields._ | `operations.mutations.preview.dryRun` | boolean | yes | | | `operations.mutations.preview.reasons` | enum[] | no | | | `operations.mutations.preview.tracked` | boolean | yes | | +| `operations.permissionRanges.create` | object | yes | | +| `operations.permissionRanges.create.available` | boolean | yes | | +| `operations.permissionRanges.create.dryRun` | boolean | yes | | +| `operations.permissionRanges.create.reasons` | enum[] | no | | +| `operations.permissionRanges.create.tracked` | boolean | yes | | +| `operations.permissionRanges.get` | object | yes | | +| `operations.permissionRanges.get.available` | boolean | yes | | +| `operations.permissionRanges.get.dryRun` | boolean | yes | | +| `operations.permissionRanges.get.reasons` | enum[] | no | | +| `operations.permissionRanges.get.tracked` | boolean | yes | | +| `operations.permissionRanges.list` | object | yes | | +| `operations.permissionRanges.list.available` | boolean | yes | | +| `operations.permissionRanges.list.dryRun` | boolean | yes | | +| `operations.permissionRanges.list.reasons` | enum[] | no | | +| `operations.permissionRanges.list.tracked` | boolean | yes | | +| `operations.permissionRanges.remove` | object | yes | | +| `operations.permissionRanges.remove.available` | boolean | yes | | +| `operations.permissionRanges.remove.dryRun` | boolean | yes | | +| `operations.permissionRanges.remove.reasons` | enum[] | no | | +| `operations.permissionRanges.remove.tracked` | boolean | yes | | +| `operations.permissionRanges.updatePrincipal` | object | yes | | +| `operations.permissionRanges.updatePrincipal.available` | boolean | yes | | +| `operations.permissionRanges.updatePrincipal.dryRun` | boolean | yes | | +| `operations.permissionRanges.updatePrincipal.reasons` | enum[] | no | | +| `operations.permissionRanges.updatePrincipal.tracked` | boolean | yes | | +| `operations.protection.clearEditingRestriction` | object | yes | | +| `operations.protection.clearEditingRestriction.available` | boolean | yes | | +| `operations.protection.clearEditingRestriction.dryRun` | boolean | yes | | +| `operations.protection.clearEditingRestriction.reasons` | enum[] | no | | +| `operations.protection.clearEditingRestriction.tracked` | boolean | yes | | +| `operations.protection.get` | object | yes | | +| `operations.protection.get.available` | boolean | yes | | +| `operations.protection.get.dryRun` | boolean | yes | | +| `operations.protection.get.reasons` | enum[] | no | | +| `operations.protection.get.tracked` | boolean | yes | | +| `operations.protection.setEditingRestriction` | object | yes | | +| `operations.protection.setEditingRestriction.available` | boolean | yes | | +| `operations.protection.setEditingRestriction.dryRun` | boolean | yes | | +| `operations.protection.setEditingRestriction.reasons` | enum[] | no | | +| `operations.protection.setEditingRestriction.tracked` | boolean | yes | | | `operations.query.match` | object | yes | | | `operations.query.match.available` | boolean | yes | | | `operations.query.match.dryRun` | boolean | yes | | @@ -3921,6 +3961,46 @@ _No fields._ "dryRun": false, "tracked": false }, + "permissionRanges.create": { + "available": true, + "dryRun": true, + "tracked": false + }, + "permissionRanges.get": { + "available": true, + "dryRun": false, + "tracked": false + }, + "permissionRanges.list": { + "available": true, + "dryRun": false, + "tracked": false + }, + "permissionRanges.remove": { + "available": true, + "dryRun": true, + "tracked": false + }, + "permissionRanges.updatePrincipal": { + "available": true, + "dryRun": true, + "tracked": false + }, + "protection.clearEditingRestriction": { + "available": true, + "dryRun": true, + "tracked": false + }, + "protection.get": { + "available": true, + "dryRun": false, + "tracked": false + }, + "protection.setEditingRestriction": { + "available": true, + "dryRun": true, + "tracked": false + }, "query.match": { "available": true, "dryRun": false, @@ -16329,6 +16409,286 @@ _No fields._ ], "type": "object" }, + "permissionRanges.create": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "permissionRanges.get": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "permissionRanges.list": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "permissionRanges.remove": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "permissionRanges.updatePrincipal": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "protection.clearEditingRestriction": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "protection.get": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, + "protection.setEditingRestriction": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, "query.match": { "additionalProperties": false, "properties": { @@ -19577,7 +19937,15 @@ _No fields._ "authorities.entries.remove", "diff.capture", "diff.compare", - "diff.apply" + "diff.apply", + "protection.get", + "protection.setEditingRestriction", + "protection.clearEditingRestriction", + "permissionRanges.list", + "permissionRanges.get", + "permissionRanges.create", + "permissionRanges.remove", + "permissionRanges.updatePrincipal" ], "type": "object" }, diff --git a/apps/docs/document-api/reference/create/heading.mdx b/apps/docs/document-api/reference/create/heading.mdx index ec2050b751..1d7f43d48c 100644 --- a/apps/docs/document-api/reference/create/heading.mdx +++ b/apps/docs/document-api/reference/create/heading.mdx @@ -62,6 +62,7 @@ Returns a CreateHeadingResult with the new heading block ID and address. | `insertionPoint.range` | Range | yes | Range | | `insertionPoint.range.end` | integer | yes | | | `insertionPoint.range.start` | integer | yes | | +| `ref` | string | no | | | `success` | `true` | yes | Constant: `true` | | `trackedChangeRefs` | TrackedChangeAddress[] | no | | @@ -92,6 +93,7 @@ Returns a CreateHeadingResult with the new heading block ID and address. "start": 0 } }, + "ref": "handle:abc123", "success": true, "trackedChangeRefs": [ { @@ -222,6 +224,10 @@ Returns a CreateHeadingResult with the new heading block ID and address. "insertionPoint": { "$ref": "#/$defs/TextAddress" }, + "ref": { + "description": "Ref handle for the created block. Pass directly to superdoc_format or superdoc_edit ref param without searching.", + "type": "string" + }, "success": { "const": true }, @@ -287,6 +293,10 @@ Returns a CreateHeadingResult with the new heading block ID and address. "insertionPoint": { "$ref": "#/$defs/TextAddress" }, + "ref": { + "description": "Ref handle for the created block. Pass directly to superdoc_format or superdoc_edit ref param without searching.", + "type": "string" + }, "success": { "const": true }, diff --git a/apps/docs/document-api/reference/create/paragraph.mdx b/apps/docs/document-api/reference/create/paragraph.mdx index 142e1562aa..e2d1c4a43a 100644 --- a/apps/docs/document-api/reference/create/paragraph.mdx +++ b/apps/docs/document-api/reference/create/paragraph.mdx @@ -60,6 +60,7 @@ Returns a CreateParagraphResult with the new paragraph block ID and address. | `paragraph.kind` | `"block"` | yes | Constant: `"block"` | | `paragraph.nodeId` | string | yes | | | `paragraph.nodeType` | `"paragraph"` | yes | Constant: `"paragraph"` | +| `ref` | string | no | | | `success` | `true` | yes | Constant: `true` | | `trackedChangeRefs` | TrackedChangeAddress[] | no | | @@ -90,6 +91,7 @@ Returns a CreateParagraphResult with the new paragraph block ID and address. "nodeId": "node-def456", "nodeType": "paragraph" }, + "ref": "handle:abc123", "success": true, "trackedChangeRefs": [ { @@ -211,6 +213,10 @@ Returns a CreateParagraphResult with the new paragraph block ID and address. "paragraph": { "$ref": "#/$defs/ParagraphAddress" }, + "ref": { + "description": "Ref handle for the created block. Pass directly to superdoc_format or superdoc_edit ref param without searching.", + "type": "string" + }, "success": { "const": true }, @@ -276,6 +282,10 @@ Returns a CreateParagraphResult with the new paragraph block ID and address. "paragraph": { "$ref": "#/$defs/ParagraphAddress" }, + "ref": { + "description": "Ref handle for the created block. Pass directly to superdoc_format or superdoc_edit ref param without searching.", + "type": "string" + }, "success": { "const": true }, diff --git a/apps/docs/document-api/reference/diff/apply.mdx b/apps/docs/document-api/reference/diff/apply.mdx index 85c9c42378..8b6fae1def 100644 --- a/apps/docs/document-api/reference/diff/apply.mdx +++ b/apps/docs/document-api/reference/diff/apply.mdx @@ -26,12 +26,12 @@ Returns a DiffApplyResult with applied operation count and diagnostics. | Field | Type | Required | Description | | --- | --- | --- | --- | -| `diff` | object(version="sd-diff-payload/v1") | yes | | +| `diff` | object | yes | | | `diff.baseFingerprint` | string | yes | | | `diff.coverage` | object | yes | | | `diff.coverage.body` | `true` | yes | Constant: `true` | | `diff.coverage.comments` | boolean | yes | | -| `diff.coverage.headerFooters` | `false` | yes | Constant: `false` | +| `diff.coverage.headerFooters` | boolean | yes | | | `diff.coverage.numbering` | boolean | yes | | | `diff.coverage.styles` | boolean | yes | | | `diff.engine` | enum | yes | `"super-editor"` | @@ -43,12 +43,16 @@ Returns a DiffApplyResult with applied operation count and diagnostics. | `diff.summary.comments` | object | yes | | | `diff.summary.comments.hasChanges` | boolean | yes | | | `diff.summary.hasChanges` | boolean | yes | | +| `diff.summary.headerFooters` | object | yes | | +| `diff.summary.headerFooters.hasChanges` | boolean | yes | | | `diff.summary.numbering` | object | yes | | | `diff.summary.numbering.hasChanges` | boolean | yes | | +| `diff.summary.parts` | object | yes | | +| `diff.summary.parts.hasChanges` | boolean | yes | | | `diff.summary.styles` | object | yes | | | `diff.summary.styles.hasChanges` | boolean | yes | | | `diff.targetFingerprint` | string | yes | | -| `diff.version` | `"sd-diff-payload/v1"` | yes | Constant: `"sd-diff-payload/v1"` | +| `diff.version` | enum | yes | `"sd-diff-payload/v1"`, `"sd-diff-payload/v2"` | ### Example request @@ -59,7 +63,7 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "coverage": { "body": true, "comments": true, - "headerFooters": false, + "headerFooters": true, "numbering": true, "styles": true }, @@ -76,9 +80,15 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "hasChanges": true }, "hasChanges": true, + "headerFooters": { + "hasChanges": true + }, "numbering": { "hasChanges": true }, + "parts": { + "hasChanges": true + }, "styles": { "hasChanges": true } @@ -98,7 +108,7 @@ Returns a DiffApplyResult with applied operation count and diagnostics. | `coverage` | object | yes | | | `coverage.body` | `true` | yes | Constant: `true` | | `coverage.comments` | boolean | yes | | -| `coverage.headerFooters` | `false` | yes | Constant: `false` | +| `coverage.headerFooters` | boolean | yes | | | `coverage.numbering` | boolean | yes | | | `coverage.styles` | boolean | yes | | | `diagnostics` | string[] | yes | | @@ -109,8 +119,12 @@ Returns a DiffApplyResult with applied operation count and diagnostics. | `summary.comments` | object | yes | | | `summary.comments.hasChanges` | boolean | yes | | | `summary.hasChanges` | boolean | yes | | +| `summary.headerFooters` | object | yes | | +| `summary.headerFooters.hasChanges` | boolean | yes | | | `summary.numbering` | object | yes | | | `summary.numbering.hasChanges` | boolean | yes | | +| `summary.parts` | object | yes | | +| `summary.parts.hasChanges` | boolean | yes | | | `summary.styles` | object | yes | | | `summary.styles.hasChanges` | boolean | yes | | | `targetFingerprint` | string | yes | | @@ -124,7 +138,7 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "coverage": { "body": true, "comments": true, - "headerFooters": false, + "headerFooters": true, "numbering": true, "styles": true }, @@ -142,9 +156,15 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "hasChanges": true }, "hasChanges": true, + "headerFooters": { + "hasChanges": true + }, "numbering": { "hasChanges": true }, + "parts": { + "hasChanges": true + }, "styles": { "hasChanges": true } @@ -188,7 +208,6 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "type": "boolean" }, "headerFooters": { - "const": false, "type": "boolean" }, "numbering": { @@ -238,7 +257,9 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "body", "comments", "styles", - "numbering" + "numbering", + "headerFooters", + "parts" ], "type": "string" }, @@ -259,6 +280,18 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "hasChanges": { "type": "boolean" }, + "headerFooters": { + "additionalProperties": false, + "properties": { + "hasChanges": { + "type": "boolean" + } + }, + "required": [ + "hasChanges" + ], + "type": "object" + }, "numbering": { "additionalProperties": false, "properties": { @@ -271,6 +304,18 @@ Returns a DiffApplyResult with applied operation count and diagnostics. ], "type": "object" }, + "parts": { + "additionalProperties": false, + "properties": { + "hasChanges": { + "type": "boolean" + } + }, + "required": [ + "hasChanges" + ], + "type": "object" + }, "styles": { "additionalProperties": false, "properties": { @@ -290,7 +335,9 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "body", "comments", "styles", - "numbering" + "numbering", + "headerFooters", + "parts" ], "type": "object" }, @@ -298,7 +345,10 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "type": "string" }, "version": { - "const": "sd-diff-payload/v1", + "enum": [ + "sd-diff-payload/v1", + "sd-diff-payload/v2" + ], "type": "string" } }, @@ -344,7 +394,6 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "type": "boolean" }, "headerFooters": { - "const": false, "type": "boolean" }, "numbering": { @@ -390,7 +439,9 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "body", "comments", "styles", - "numbering" + "numbering", + "headerFooters", + "parts" ], "type": "string" }, @@ -411,6 +462,18 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "hasChanges": { "type": "boolean" }, + "headerFooters": { + "additionalProperties": false, + "properties": { + "hasChanges": { + "type": "boolean" + } + }, + "required": [ + "hasChanges" + ], + "type": "object" + }, "numbering": { "additionalProperties": false, "properties": { @@ -423,6 +486,18 @@ Returns a DiffApplyResult with applied operation count and diagnostics. ], "type": "object" }, + "parts": { + "additionalProperties": false, + "properties": { + "hasChanges": { + "type": "boolean" + } + }, + "required": [ + "hasChanges" + ], + "type": "object" + }, "styles": { "additionalProperties": false, "properties": { @@ -442,7 +517,9 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "body", "comments", "styles", - "numbering" + "numbering", + "headerFooters", + "parts" ], "type": "object" }, @@ -485,7 +562,6 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "type": "boolean" }, "headerFooters": { - "const": false, "type": "boolean" }, "numbering": { @@ -531,7 +607,9 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "body", "comments", "styles", - "numbering" + "numbering", + "headerFooters", + "parts" ], "type": "string" }, @@ -552,6 +630,18 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "hasChanges": { "type": "boolean" }, + "headerFooters": { + "additionalProperties": false, + "properties": { + "hasChanges": { + "type": "boolean" + } + }, + "required": [ + "hasChanges" + ], + "type": "object" + }, "numbering": { "additionalProperties": false, "properties": { @@ -564,6 +654,18 @@ Returns a DiffApplyResult with applied operation count and diagnostics. ], "type": "object" }, + "parts": { + "additionalProperties": false, + "properties": { + "hasChanges": { + "type": "boolean" + } + }, + "required": [ + "hasChanges" + ], + "type": "object" + }, "styles": { "additionalProperties": false, "properties": { @@ -583,7 +685,9 @@ Returns a DiffApplyResult with applied operation count and diagnostics. "body", "comments", "styles", - "numbering" + "numbering", + "headerFooters", + "parts" ], "type": "object" }, diff --git a/apps/docs/document-api/reference/diff/capture.mdx b/apps/docs/document-api/reference/diff/capture.mdx index 9ee48c734d..ea21abdbe8 100644 --- a/apps/docs/document-api/reference/diff/capture.mdx +++ b/apps/docs/document-api/reference/diff/capture.mdx @@ -39,13 +39,13 @@ _No fields._ | `coverage` | object | yes | | | `coverage.body` | `true` | yes | Constant: `true` | | `coverage.comments` | boolean | yes | | -| `coverage.headerFooters` | `false` | yes | Constant: `false` | +| `coverage.headerFooters` | boolean | yes | | | `coverage.numbering` | boolean | yes | | | `coverage.styles` | boolean | yes | | | `engine` | enum | yes | `"super-editor"` | | `fingerprint` | string | yes | | | `payload` | object | yes | | -| `version` | `"sd-diff-snapshot/v1"` | yes | Constant: `"sd-diff-snapshot/v1"` | +| `version` | enum | yes | `"sd-diff-snapshot/v1"`, `"sd-diff-snapshot/v2"` | ### Example response @@ -54,7 +54,7 @@ _No fields._ "coverage": { "body": true, "comments": true, - "headerFooters": false, + "headerFooters": true, "numbering": true, "styles": true }, @@ -101,7 +101,6 @@ _No fields._ "type": "boolean" }, "headerFooters": { - "const": false, "type": "boolean" }, "numbering": { @@ -134,7 +133,10 @@ _No fields._ "type": "object" }, "version": { - "const": "sd-diff-snapshot/v1", + "enum": [ + "sd-diff-snapshot/v1", + "sd-diff-snapshot/v2" + ], "type": "string" } }, diff --git a/apps/docs/document-api/reference/diff/compare.mdx b/apps/docs/document-api/reference/diff/compare.mdx index 8a97ae79fd..450d8c01fc 100644 --- a/apps/docs/document-api/reference/diff/compare.mdx +++ b/apps/docs/document-api/reference/diff/compare.mdx @@ -26,17 +26,17 @@ Returns a DiffPayload with a summary and opaque payload. | Field | Type | Required | Description | | --- | --- | --- | --- | -| `targetSnapshot` | object(version="sd-diff-snapshot/v1") | yes | | +| `targetSnapshot` | object | yes | | | `targetSnapshot.coverage` | object | yes | | | `targetSnapshot.coverage.body` | `true` | yes | Constant: `true` | | `targetSnapshot.coverage.comments` | boolean | yes | | -| `targetSnapshot.coverage.headerFooters` | `false` | yes | Constant: `false` | +| `targetSnapshot.coverage.headerFooters` | boolean | yes | | | `targetSnapshot.coverage.numbering` | boolean | yes | | | `targetSnapshot.coverage.styles` | boolean | yes | | | `targetSnapshot.engine` | enum | yes | `"super-editor"` | | `targetSnapshot.fingerprint` | string | yes | | | `targetSnapshot.payload` | object | yes | | -| `targetSnapshot.version` | `"sd-diff-snapshot/v1"` | yes | Constant: `"sd-diff-snapshot/v1"` | +| `targetSnapshot.version` | enum | yes | `"sd-diff-snapshot/v1"`, `"sd-diff-snapshot/v2"` | ### Example request @@ -46,7 +46,7 @@ Returns a DiffPayload with a summary and opaque payload. "coverage": { "body": true, "comments": true, - "headerFooters": false, + "headerFooters": true, "numbering": true, "styles": true }, @@ -66,7 +66,7 @@ Returns a DiffPayload with a summary and opaque payload. | `coverage` | object | yes | | | `coverage.body` | `true` | yes | Constant: `true` | | `coverage.comments` | boolean | yes | | -| `coverage.headerFooters` | `false` | yes | Constant: `false` | +| `coverage.headerFooters` | boolean | yes | | | `coverage.numbering` | boolean | yes | | | `coverage.styles` | boolean | yes | | | `engine` | enum | yes | `"super-editor"` | @@ -78,12 +78,16 @@ Returns a DiffPayload with a summary and opaque payload. | `summary.comments` | object | yes | | | `summary.comments.hasChanges` | boolean | yes | | | `summary.hasChanges` | boolean | yes | | +| `summary.headerFooters` | object | yes | | +| `summary.headerFooters.hasChanges` | boolean | yes | | | `summary.numbering` | object | yes | | | `summary.numbering.hasChanges` | boolean | yes | | +| `summary.parts` | object | yes | | +| `summary.parts.hasChanges` | boolean | yes | | | `summary.styles` | object | yes | | | `summary.styles.hasChanges` | boolean | yes | | | `targetFingerprint` | string | yes | | -| `version` | `"sd-diff-payload/v1"` | yes | Constant: `"sd-diff-payload/v1"` | +| `version` | enum | yes | `"sd-diff-payload/v1"`, `"sd-diff-payload/v2"` | ### Example response @@ -93,7 +97,7 @@ Returns a DiffPayload with a summary and opaque payload. "coverage": { "body": true, "comments": true, - "headerFooters": false, + "headerFooters": true, "numbering": true, "styles": true }, @@ -110,9 +114,15 @@ Returns a DiffPayload with a summary and opaque payload. "hasChanges": true }, "hasChanges": true, + "headerFooters": { + "hasChanges": true + }, "numbering": { "hasChanges": true }, + "parts": { + "hasChanges": true + }, "styles": { "hasChanges": true } @@ -152,7 +162,6 @@ Returns a DiffPayload with a summary and opaque payload. "type": "boolean" }, "headerFooters": { - "const": false, "type": "boolean" }, "numbering": { @@ -185,7 +194,10 @@ Returns a DiffPayload with a summary and opaque payload. "type": "object" }, "version": { - "const": "sd-diff-snapshot/v1", + "enum": [ + "sd-diff-snapshot/v1", + "sd-diff-snapshot/v2" + ], "type": "string" } }, @@ -226,7 +238,6 @@ Returns a DiffPayload with a summary and opaque payload. "type": "boolean" }, "headerFooters": { - "const": false, "type": "boolean" }, "numbering": { @@ -276,7 +287,9 @@ Returns a DiffPayload with a summary and opaque payload. "body", "comments", "styles", - "numbering" + "numbering", + "headerFooters", + "parts" ], "type": "string" }, @@ -297,6 +310,18 @@ Returns a DiffPayload with a summary and opaque payload. "hasChanges": { "type": "boolean" }, + "headerFooters": { + "additionalProperties": false, + "properties": { + "hasChanges": { + "type": "boolean" + } + }, + "required": [ + "hasChanges" + ], + "type": "object" + }, "numbering": { "additionalProperties": false, "properties": { @@ -309,6 +334,18 @@ Returns a DiffPayload with a summary and opaque payload. ], "type": "object" }, + "parts": { + "additionalProperties": false, + "properties": { + "hasChanges": { + "type": "boolean" + } + }, + "required": [ + "hasChanges" + ], + "type": "object" + }, "styles": { "additionalProperties": false, "properties": { @@ -328,7 +365,9 @@ Returns a DiffPayload with a summary and opaque payload. "body", "comments", "styles", - "numbering" + "numbering", + "headerFooters", + "parts" ], "type": "object" }, @@ -336,7 +375,10 @@ Returns a DiffPayload with a summary and opaque payload. "type": "string" }, "version": { - "const": "sd-diff-payload/v1", + "enum": [ + "sd-diff-payload/v1", + "sd-diff-payload/v2" + ], "type": "string" } }, diff --git a/apps/docs/document-api/reference/index.mdx b/apps/docs/document-api/reference/index.mdx index f8d6fee0bb..1c3e2574c8 100644 --- a/apps/docs/document-api/reference/index.mdx +++ b/apps/docs/document-api/reference/index.mdx @@ -50,6 +50,8 @@ This reference is sourced from `packages/document-api/src/contract/*`. | Table of Authorities | 11 | 0 | 11 | [Open](/document-api/reference/authorities/index) | | Ranges | 1 | 0 | 1 | [Open](/document-api/reference/ranges/index) | | Diff | 3 | 0 | 3 | [Open](/document-api/reference/diff/index) | +| Protection | 3 | 0 | 3 | [Open](/document-api/reference/protection/index) | +| Permission Ranges | 5 | 0 | 5 | [Open](/document-api/reference/permission-ranges/index) | ## Available operations @@ -587,3 +589,21 @@ The tables below are grouped by namespace. | diff.capture | editor.doc.diff.capture(...) | Capture the current document's diffable state as a versioned snapshot. v1 covers body, comments, styles, and numbering. Header/footer content is not included. | | diff.compare | editor.doc.diff.compare(...) | Compare the current document (base) against a previously captured target snapshot. Returns a versioned diff payload describing the changes from base to target. | | diff.apply | editor.doc.diff.apply(...) | Apply a previously computed diff payload to the current document. The document fingerprint must match the diff base fingerprint. Tracked mode governs body content only; styles, numbering, and comments are always applied directly. | + +#### Protection + +| Operation | API member path | Description | +| --- | --- | --- | +| protection.get | editor.doc.protection.get(...) | Read the current document protection state including editing restrictions, write protection, and read-only recommendation. | +| protection.setEditingRestriction | editor.doc.protection.setEditingRestriction(...) | Enable Word-style editing restriction on the document. Only readOnly mode is supported in v1. | +| protection.clearEditingRestriction | editor.doc.protection.clearEditingRestriction(...) | Disable document-level editing restriction by setting enforcement to off. Preserves the protection element and its metadata for round-trip fidelity. | + +#### Permission Ranges + +| Operation | API member path | Description | +| --- | --- | --- | +| permissionRanges.list | editor.doc.permissionRanges.list(...) | List all permission ranges in the document. Returns only complete paired ranges (both start and end markers present). | +| permissionRanges.get | editor.doc.permissionRanges.get(...) | Get detailed information about a specific permission range by ID. | +| permissionRanges.create | editor.doc.permissionRanges.create(...) | Create a permission range exception region in the document. Inserts matched permStart/permEnd markers at the target. | +| permissionRanges.remove | editor.doc.permissionRanges.remove(...) | Remove a permission range by ID. Removes whichever markers exist for the given ID (start, end, or both). | +| permissionRanges.updatePrincipal | editor.doc.permissionRanges.updatePrincipal(...) | Change which principal is allowed to edit a permission range. Updates the principal fields on the start marker. | diff --git a/apps/docs/document-api/reference/insert.mdx b/apps/docs/document-api/reference/insert.mdx index 30e608e9f4..3f0d0fc77e 100644 --- a/apps/docs/document-api/reference/insert.mdx +++ b/apps/docs/document-api/reference/insert.mdx @@ -233,7 +233,7 @@ Returns an SDMutationReceipt with applied status; resolution reports the inserte "$ref": "#/$defs/StoryLocator" }, "ref": { - "description": "Handle ref string returned by a prior search/query result.", + "description": "Handle ref from superdoc_search result (pass handle.ref value directly). Preferred over building a target object.", "type": "string" }, "type": { diff --git a/apps/docs/document-api/reference/permission-ranges/create.mdx b/apps/docs/document-api/reference/permission-ranges/create.mdx new file mode 100644 index 0000000000..2d8fd42646 --- /dev/null +++ b/apps/docs/document-api/reference/permission-ranges/create.mdx @@ -0,0 +1,148 @@ +--- +title: permissionRanges.create +sidebarTitle: permissionRanges.create +description: Create a permission range exception region in the document. Inserts matched permStart/permEnd markers at the target. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +## Summary + +Create a permission range exception region in the document. Inserts matched permStart/permEnd markers at the target. + +- Operation ID: `permissionRanges.create` +- API member path: `editor.doc.permissionRanges.create(...)` +- Mutates document: `yes` +- Idempotency: `non-idempotent` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a PermissionRangeMutationResult with the created range info on success. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `id` | string | no | | +| `principal` | object | yes | | +| `principal.id` | string | no | | +| `principal.kind` | enum | yes | `"everyone"`, `"editor"` | +| `target` | SelectionTarget | yes | SelectionTarget | +| `target.end` | SelectionPoint | yes | SelectionPoint | +| `target.kind` | `"selection"` | yes | Constant: `"selection"` | +| `target.start` | SelectionPoint | yes | SelectionPoint | + +### Example request + +```json +{ + "id": "id-001", + "principal": { + "id": "id-001", + "kind": "everyone" + }, + "target": { + "end": { + "blockId": "block-abc123", + "kind": "text", + "offset": 0 + }, + "kind": "selection", + "start": { + "blockId": "block-abc123", + "kind": "text", + "offset": 0 + } + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- None + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "principal": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "kind": { + "enum": [ + "everyone", + "editor" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, + "target": { + "$ref": "#/$defs/SelectionTarget" + } + }, + "required": [ + "target", + "principal" + ], + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/permission-ranges/get.mdx b/apps/docs/document-api/reference/permission-ranges/get.mdx new file mode 100644 index 0000000000..65cccbdc92 --- /dev/null +++ b/apps/docs/document-api/reference/permission-ranges/get.mdx @@ -0,0 +1,83 @@ +--- +title: permissionRanges.get +sidebarTitle: permissionRanges.get +description: Get detailed information about a specific permission range by ID. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +## Summary + +Get detailed information about a specific permission range by ID. + +- Operation ID: `permissionRanges.get` +- API member path: `editor.doc.permissionRanges.get(...)` +- Mutates document: `no` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `no` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a PermissionRangeInfo object with the range principal, kind, and positions. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `id` | string | yes | | + +### Example request + +```json +{ + "id": "id-001" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- None + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/permission-ranges/index.mdx b/apps/docs/document-api/reference/permission-ranges/index.mdx new file mode 100644 index 0000000000..fbbc501b2c --- /dev/null +++ b/apps/docs/document-api/reference/permission-ranges/index.mdx @@ -0,0 +1,20 @@ +--- +title: Permission Ranges operations +sidebarTitle: Permission Ranges +description: Permission Ranges operation reference from the canonical Document API contract. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +[Back to full reference](../index) + +Permission range exception operations for protected documents. + +| Operation | Member path | Mutates | Idempotency | Tracked | Dry run | +| --- | --- | --- | --- | --- | --- | +| permissionRanges.list | `permissionRanges.list` | No | `idempotent` | No | No | +| permissionRanges.get | `permissionRanges.get` | No | `idempotent` | No | No | +| permissionRanges.create | `permissionRanges.create` | Yes | `non-idempotent` | No | Yes | +| permissionRanges.remove | `permissionRanges.remove` | Yes | `idempotent` | No | Yes | +| permissionRanges.updatePrincipal | `permissionRanges.updatePrincipal` | Yes | `idempotent` | No | Yes | + diff --git a/apps/docs/document-api/reference/permission-ranges/list.mdx b/apps/docs/document-api/reference/permission-ranges/list.mdx new file mode 100644 index 0000000000..9910f014ef --- /dev/null +++ b/apps/docs/document-api/reference/permission-ranges/list.mdx @@ -0,0 +1,87 @@ +--- +title: permissionRanges.list +sidebarTitle: permissionRanges.list +description: List all permission ranges in the document. Returns only complete paired ranges (both start and end markers present). +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +## Summary + +List all permission ranges in the document. Returns only complete paired ranges (both start and end markers present). + +- Operation ID: `permissionRanges.list` +- API member path: `editor.doc.permissionRanges.list(...)` +- Mutates document: `no` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `no` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a PermissionRangesListResult containing discovered permission ranges with principal and position data. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `limit` | integer | no | | +| `offset` | integer | no | | + +### Example request + +```json +{ + "limit": 50, + "offset": 0 +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- None + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "limit": { + "minimum": 1, + "type": "integer" + }, + "offset": { + "minimum": 0, + "type": "integer" + } + }, + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/permission-ranges/remove.mdx b/apps/docs/document-api/reference/permission-ranges/remove.mdx new file mode 100644 index 0000000000..453ce8437b --- /dev/null +++ b/apps/docs/document-api/reference/permission-ranges/remove.mdx @@ -0,0 +1,101 @@ +--- +title: permissionRanges.remove +sidebarTitle: permissionRanges.remove +description: Remove a permission range by ID. Removes whichever markers exist for the given ID (start, end, or both). +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +## Summary + +Remove a permission range by ID. Removes whichever markers exist for the given ID (start, end, or both). + +- Operation ID: `permissionRanges.remove` +- API member path: `editor.doc.permissionRanges.remove(...)` +- Mutates document: `yes` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a PermissionRangeRemoveResult indicating success or a failure. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `id` | string | yes | | + +### Example request + +```json +{ + "id": "id-001" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- None + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/permission-ranges/update-principal.mdx b/apps/docs/document-api/reference/permission-ranges/update-principal.mdx new file mode 100644 index 0000000000..f16b90b5d4 --- /dev/null +++ b/apps/docs/document-api/reference/permission-ranges/update-principal.mdx @@ -0,0 +1,128 @@ +--- +title: permissionRanges.updatePrincipal +sidebarTitle: permissionRanges.updatePrincipal +description: Change which principal is allowed to edit a permission range. Updates the principal fields on the start marker. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +## Summary + +Change which principal is allowed to edit a permission range. Updates the principal fields on the start marker. + +- Operation ID: `permissionRanges.updatePrincipal` +- API member path: `editor.doc.permissionRanges.updatePrincipal(...)` +- Mutates document: `yes` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a PermissionRangeMutationResult with the updated range info on success. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `id` | string | yes | | +| `principal` | object | yes | | +| `principal.id` | string | no | | +| `principal.kind` | enum | yes | `"everyone"`, `"editor"` | + +### Example request + +```json +{ + "id": "id-001", + "principal": { + "id": "id-001", + "kind": "everyone" + } +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- None + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "principal": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "kind": { + "enum": [ + "everyone", + "editor" + ], + "type": "string" + } + }, + "required": [ + "kind" + ], + "type": "object" + } + }, + "required": [ + "id", + "principal" + ], + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/protection/clear-editing-restriction.mdx b/apps/docs/document-api/reference/protection/clear-editing-restriction.mdx new file mode 100644 index 0000000000..473e7f9ce9 --- /dev/null +++ b/apps/docs/document-api/reference/protection/clear-editing-restriction.mdx @@ -0,0 +1,88 @@ +--- +title: protection.clearEditingRestriction +sidebarTitle: protection.clearEditingRestriction +description: Disable document-level editing restriction by setting enforcement to off. Preserves the protection element and its metadata for round-trip fidelity. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +## Summary + +Disable document-level editing restriction by setting enforcement to off. Preserves the protection element and its metadata for round-trip fidelity. + +- Operation ID: `protection.clearEditingRestriction` +- API member path: `editor.doc.protection.clearEditingRestriction(...)` +- Mutates document: `yes` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ProtectionMutationResult with the updated protection state on success. + +## Input fields + +_No fields._ + +### Example request + +```json +{} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{} +``` + +## Pre-apply throws + +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": {}, + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/protection/get.mdx b/apps/docs/document-api/reference/protection/get.mdx new file mode 100644 index 0000000000..eadd8061ae --- /dev/null +++ b/apps/docs/document-api/reference/protection/get.mdx @@ -0,0 +1,157 @@ +--- +title: protection.get +sidebarTitle: protection.get +description: Read the current document protection state including editing restrictions, write protection, and read-only recommendation. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +## Summary + +Read the current document protection state including editing restrictions, write protection, and read-only recommendation. + +- Operation ID: `protection.get` +- API member path: `editor.doc.protection.get(...)` +- Mutates document: `no` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `no` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a DocumentProtectionState with editingRestriction, writeProtection, and readOnlyRecommended fields. + +## Input fields + +_No fields._ + +### Example request + +```json +{} +``` + +## Output fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `editingRestriction` | object | yes | | +| `editingRestriction.enforced` | boolean | yes | | +| `editingRestriction.formattingRestricted` | boolean | yes | | +| `editingRestriction.mode` | enum | yes | `"none"`, `"readOnly"`, `"comments"`, `"trackedChanges"`, `"forms"` | +| `editingRestriction.passwordProtected` | boolean | yes | | +| `editingRestriction.runtimeEnforced` | boolean | yes | | +| `readOnlyRecommended` | boolean | yes | | +| `writeProtection` | object | yes | | +| `writeProtection.enabled` | boolean | yes | | +| `writeProtection.passwordProtected` | boolean | yes | | + +### Example response + +```json +{ + "editingRestriction": { + "enforced": true, + "formattingRestricted": true, + "mode": "none", + "passwordProtected": true, + "runtimeEnforced": true + }, + "readOnlyRecommended": true, + "writeProtection": { + "enabled": true, + "passwordProtected": true + } +} +``` + +## Pre-apply throws + +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- None + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": {}, + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "editingRestriction": { + "additionalProperties": false, + "properties": { + "enforced": { + "type": "boolean" + }, + "formattingRestricted": { + "type": "boolean" + }, + "mode": { + "enum": [ + "none", + "readOnly", + "comments", + "trackedChanges", + "forms" + ], + "type": "string" + }, + "passwordProtected": { + "type": "boolean" + }, + "runtimeEnforced": { + "type": "boolean" + } + }, + "required": [ + "mode", + "enforced", + "runtimeEnforced", + "passwordProtected", + "formattingRestricted" + ], + "type": "object" + }, + "readOnlyRecommended": { + "type": "boolean" + }, + "writeProtection": { + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "passwordProtected": { + "type": "boolean" + } + }, + "required": [ + "enabled", + "passwordProtected" + ], + "type": "object" + } + }, + "required": [ + "editingRestriction", + "writeProtection", + "readOnlyRecommended" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/protection/index.mdx b/apps/docs/document-api/reference/protection/index.mdx new file mode 100644 index 0000000000..2b6ec59731 --- /dev/null +++ b/apps/docs/document-api/reference/protection/index.mdx @@ -0,0 +1,18 @@ +--- +title: Protection operations +sidebarTitle: Protection +description: Protection operation reference from the canonical Document API contract. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +[Back to full reference](../index) + +Document-level protection state and editing restriction operations. + +| Operation | Member path | Mutates | Idempotency | Tracked | Dry run | +| --- | --- | --- | --- | --- | --- | +| protection.get | `protection.get` | No | `idempotent` | No | No | +| protection.setEditingRestriction | `protection.setEditingRestriction` | Yes | `idempotent` | No | Yes | +| protection.clearEditingRestriction | `protection.clearEditingRestriction` | Yes | `idempotent` | No | Yes | + diff --git a/apps/docs/document-api/reference/protection/set-editing-restriction.mdx b/apps/docs/document-api/reference/protection/set-editing-restriction.mdx new file mode 100644 index 0000000000..8821945427 --- /dev/null +++ b/apps/docs/document-api/reference/protection/set-editing-restriction.mdx @@ -0,0 +1,107 @@ +--- +title: protection.setEditingRestriction +sidebarTitle: protection.setEditingRestriction +description: Enable Word-style editing restriction on the document. Only readOnly mode is supported in v1. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +## Summary + +Enable Word-style editing restriction on the document. Only readOnly mode is supported in v1. + +- Operation ID: `protection.setEditingRestriction` +- API member path: `editor.doc.protection.setEditingRestriction(...)` +- Mutates document: `yes` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a ProtectionMutationResult with the updated protection state on success. + +## Input fields + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `formattingRestricted` | boolean | no | | +| `mode` | enum | yes | `"readOnly"` | + +### Example request + +```json +{ + "formattingRestricted": true, + "mode": "readOnly" +} +``` + +## Output fields + +_No fields._ + +### Example response + +```json +{} +``` + +## Pre-apply throws + +- `INVALID_INPUT` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "properties": { + "formattingRestricted": { + "type": "boolean" + }, + "mode": { + "enum": [ + "readOnly" + ], + "type": "string" + } + }, + "required": [ + "mode" + ], + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + + + +```json +{ + "type": "object" +} +``` + diff --git a/apps/docs/document-engine/sdks.mdx b/apps/docs/document-engine/sdks.mdx index 63b6e668d5..7f661dd5ea 100644 --- a/apps/docs/document-engine/sdks.mdx +++ b/apps/docs/document-engine/sdks.mdx @@ -675,6 +675,16 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.diff.capture` | `diff capture` | Capture the current document's diffable state as a versioned snapshot. v1 covers body, comments, styles, and numbering. Header/footer content is not included. | | `doc.diff.compare` | `diff compare` | Compare the current document (base) against a previously captured target snapshot. Returns a versioned diff payload describing the changes from base to target. | | `doc.diff.apply` | `diff apply` | Apply a previously computed diff payload to the current document. The document fingerprint must match the diff base fingerprint. Tracked mode governs body content only; styles, numbering, and comments are always applied directly. | +| `doc.protection.get` | `protection get` | Read the current document protection state including editing restrictions, write protection, and read-only recommendation. | +| `doc.protection.setEditingRestriction` | `protection set-editing-restriction` | Enable Word-style editing restriction on the document. Only readOnly mode is supported in v1. | +| `doc.protection.clearEditingRestriction` | `protection clear-editing-restriction` | Disable document-level editing restriction by setting enforcement to off. Preserves the protection element and its metadata for round-trip fidelity. | +| `doc.permissionRanges.list` | `permission-ranges list` | List all permission ranges in the document. Returns only complete paired ranges (both start and end markers present). | +| `doc.permissionRanges.get` | `permission-ranges get` | Get detailed information about a specific permission range by ID. | +| `doc.permissionRanges.create` | `permission-ranges create` | Create a permission range exception region in the document. Inserts matched permStart/permEnd markers at the target. | +| `doc.permissionRanges.remove` | `permission-ranges remove` | Remove a permission range by ID. Removes whichever markers exist for the given ID (start, end, or both). | +| `doc.permissionRanges.updatePrincipal` | `permission-ranges update-principal` | Change which principal is allowed to edit a permission range. Updates the principal fields on the start marker. | +| `doc.insertTab` | `insert tab` | Insert a real Word tab node at a collapsed text insertion point. Accepts the same target/ref shortcuts as insert, but only for point inserts. | +| `doc.insertLineBreak` | `insert line-break` | Insert a real Word line-break node at a collapsed text insertion point. Accepts the same target/ref shortcuts as insert, but only for point inserts. | #### Format @@ -1125,6 +1135,16 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.diff.capture` | `diff capture` | Capture the current document's diffable state as a versioned snapshot. v1 covers body, comments, styles, and numbering. Header/footer content is not included. | | `doc.diff.compare` | `diff compare` | Compare the current document (base) against a previously captured target snapshot. Returns a versioned diff payload describing the changes from base to target. | | `doc.diff.apply` | `diff apply` | Apply a previously computed diff payload to the current document. The document fingerprint must match the diff base fingerprint. Tracked mode governs body content only; styles, numbering, and comments are always applied directly. | +| `doc.protection.get` | `protection get` | Read the current document protection state including editing restrictions, write protection, and read-only recommendation. | +| `doc.protection.set_editing_restriction` | `protection set-editing-restriction` | Enable Word-style editing restriction on the document. Only readOnly mode is supported in v1. | +| `doc.protection.clear_editing_restriction` | `protection clear-editing-restriction` | Disable document-level editing restriction by setting enforcement to off. Preserves the protection element and its metadata for round-trip fidelity. | +| `doc.permission_ranges.list` | `permission-ranges list` | List all permission ranges in the document. Returns only complete paired ranges (both start and end markers present). | +| `doc.permission_ranges.get` | `permission-ranges get` | Get detailed information about a specific permission range by ID. | +| `doc.permission_ranges.create` | `permission-ranges create` | Create a permission range exception region in the document. Inserts matched permStart/permEnd markers at the target. | +| `doc.permission_ranges.remove` | `permission-ranges remove` | Remove a permission range by ID. Removes whichever markers exist for the given ID (start, end, or both). | +| `doc.permission_ranges.update_principal` | `permission-ranges update-principal` | Change which principal is allowed to edit a permission range. Updates the principal fields on the start marker. | +| `doc.insert_tab` | `insert tab` | Insert a real Word tab node at a collapsed text insertion point. Accepts the same target/ref shortcuts as insert, but only for point inserts. | +| `doc.insert_line_break` | `insert line-break` | Insert a real Word line-break node at a collapsed text insertion point. Accepts the same target/ref shortcuts as insert, but only for point inserts. | #### Format diff --git a/evals/providers/superdoc-agent-gateway.mjs b/evals/providers/superdoc-agent-gateway.mjs index 25a7026138..737370d623 100644 --- a/evals/providers/superdoc-agent-gateway.mjs +++ b/evals/providers/superdoc-agent-gateway.mjs @@ -20,6 +20,7 @@ import { cleanArgs, cleanupTemp, createTempCopy, + dispatchWithRetry, loadSdk, readCache, resolveOutputPath, @@ -64,7 +65,7 @@ function convertTool(fn, sdk, doc, toolLog) { execute: async (args) => { const cleaned = cleanArgs(args); try { - const result = await sdk.dispatchSuperDocTool(doc, fn.name, cleaned); + const result = await dispatchWithRetry(sdk, doc, fn.name, cleaned); toolLog.push({ tool: fn.name, args: cleaned, ok: true }); return result; } catch (err) { diff --git a/evals/providers/superdoc-agent.mjs b/evals/providers/superdoc-agent.mjs index 8c22ec75ef..c9cf0a52a4 100644 --- a/evals/providers/superdoc-agent.mjs +++ b/evals/providers/superdoc-agent.mjs @@ -21,6 +21,7 @@ import { resolveOutputPath, cleanArgs, writeCache, + dispatchWithRetry, } from './utils.mjs'; const SYSTEM_PROMPT = readFileSync(PATHS.prompt, 'utf8'); @@ -73,12 +74,24 @@ async function runAgentLoop(sdk, doc, activeToolMap, task, model) { const toolLog = []; for (let turn = 0; turn < MAX_TURNS; turn++) { - const response = await openai.chat.completions.create({ - model, - messages, - tools: [...activeToolMap.values()], - temperature: 0, - }); + let response; + for (let attempt = 0; attempt < 3; attempt++) { + try { + response = await openai.chat.completions.create({ + model, + messages, + tools: [...activeToolMap.values()], + temperature: 0, + }); + break; + } catch (err) { + if (attempt < 2 && (err.status === 429 || err.status >= 500 || err.message?.includes('Gateway'))) { + await new Promise((r) => setTimeout(r, 2000 * (attempt + 1))); + continue; + } + throw err; + } + } const message = response.choices[0].message; messages.push(message); @@ -92,7 +105,7 @@ async function runAgentLoop(sdk, doc, activeToolMap, task, model) { let result; try { - result = await sdk.dispatchSuperDocTool(doc, toolName, cleanArgs(toolArgs)); + result = await dispatchWithRetry(sdk, doc, toolName, cleanArgs(toolArgs)); } catch (err) { result = { ok: false, error: err.message }; } diff --git a/evals/providers/utils.mjs b/evals/providers/utils.mjs index 480f25d5cd..56de5b223a 100644 --- a/evals/providers/utils.mjs +++ b/evals/providers/utils.mjs @@ -74,6 +74,52 @@ export function cleanArgs(args) { return rest; } +// --- Ref revision bump (handles REVISION_MISMATCH after create→format) --- + +// V4 ref format prefix — matches the encoding in +// super-editor/src/document-api-adapters/story-runtime/story-ref-codec.ts +// Update this if the ref codec version changes (e.g., v5). +const REF_PREFIX = 'text:v4:'; +const MAX_REF_RETRIES = 3; + +function bumpRefRevision(ref, targetRev) { + if (!ref.startsWith(REF_PREFIX)) return null; + try { + const payload = JSON.parse(Buffer.from(ref.slice(REF_PREFIX.length), 'base64').toString()); + payload.rev = targetRev; + return REF_PREFIX + Buffer.from(JSON.stringify(payload)).toString('base64'); + } catch { return null; } +} + +function bumpAllRefs(args, targetRev) { + const patched = { ...args }; + for (const [key, value] of Object.entries(patched)) { + if (typeof value === 'string' && value.startsWith(REF_PREFIX)) { + const bumped = bumpRefRevision(value, targetRev); + if (bumped) patched[key] = bumped; + } + } + return patched; +} + +/** Dispatch with automatic ref revision bump on REVISION_MISMATCH. */ +export async function dispatchWithRetry(sdk, doc, toolName, args) { + let currentArgs = args; + for (let attempt = 0; attempt <= MAX_REF_RETRIES; attempt++) { + try { + return await sdk.dispatchSuperDocTool(doc, toolName, currentArgs); + } catch (err) { + const msg = err.message ?? ''; + if (attempt < MAX_REF_RETRIES && msg.includes('REVISION_MISMATCH')) { + const match = msg.match(/for revision (\d+)/); + if (match) { currentArgs = bumpAllRefs(currentArgs, match[1]); continue; } + } + throw err; + } + } + throw new Error('Max retries exceeded'); +} + // --- SDK fingerprint (for cache invalidation) --- const SDK_TOOLS_DIR = resolve(EVALS_ROOT, '..', 'packages/sdk/tools'); diff --git a/evals/tests/customer-workflows.yaml b/evals/tests/customer-workflows.yaml index 1cfd69bd7f..be47ac9fc4 100644 --- a/evals/tests/customer-workflows.yaml +++ b/evals/tests/customer-workflows.yaml @@ -16,7 +16,7 @@ vars: fixture: nda.docx keepFile: true - task: 'In the Indemnification section, change "Each Party shall mutually indemnify" to "The Receiving Party shall indemnify the Disclosing Party". Remove the word "mutually" from that section.' + task: 'In the Indemnification section, change "Each Party shall mutually indemnify" to "The Receiving Party shall indemnify the Disclosing Party".' assert: - type: javascript value: | @@ -668,7 +668,45 @@ metric: tool_usage # ============================================================================= -# I. CROSS-DOMAIN COMPOUND (Real customer multi-feature workflows) +# I. PAGE CREATION & STYLE REPLICATION +# Workflow: create new exhibit pages matching existing formatting +# ============================================================================= + +- description: 'Employment: create Exhibit X page matching Exhibit A style with summary' + vars: + fixture: doc-template.docx + keepFile: true + task: 'The document has an "Exhibit A" page near the end (centered, bold). Create a new page at the end for "Exhibit X" that matches the same style as Exhibit A (centered, bold paragraph — NOT a heading). Below it, add a summary paragraph describing what the document covers.' + assert: + - type: javascript + value: | + const d = JSON.parse(output); + const t = d.documentText || ''; + if (!t.includes('Exhibit X')) + return { pass: false, score: 0, reason: 'Exhibit X not created' }; + if (!t.includes('Exhibit A')) + return { pass: false, score: 0, reason: 'Collateral: Exhibit A removed' }; + return { pass: true, score: 1, reason: 'Exhibit X page created' }; + - type: javascript + value: file://lib/checks.cjs:traceAllOk + - type: javascript + value: file://lib/checks.cjs:traceLog + - type: javascript + value: | + const d = JSON.parse(output); + if (!d.toolCalls) return { pass: true, score: 0.5, reason: 'No tool calls data' }; + const tools = d.toolCalls.map(tc => tc.tool); + const hasCreate = tools.includes('superdoc_create'); + const hasFormat = tools.includes('superdoc_format'); + if (!hasCreate) return { pass: false, score: 0, reason: 'No superdoc_create call' }; + if (!hasFormat) return { pass: false, score: 0, reason: 'No superdoc_format call — formatting not applied' }; + const formatCount = tools.filter(t => t === 'superdoc_format').length; + if (formatCount < 3) return { pass: false, score: 0.5, reason: `Only ${formatCount} format calls — expected at least 3 (inline + alignment + page break)` }; + return { pass: true, score: 1, reason: `Tools: ${tools.join(' → ')}` }; + metric: tool_usage + +# ============================================================================= +# J. CROSS-DOMAIN COMPOUND (Real customer multi-feature workflows) # ============================================================================= - description: 'Offer: counter-offer workflow (salary + equity + deadline)' diff --git a/evals/tests/execution.yaml b/evals/tests/execution.yaml index be9f57ea8f..a977355c41 100644 --- a/evals/tests/execution.yaml +++ b/evals/tests/execution.yaml @@ -350,6 +350,54 @@ - type: javascript value: file://lib/checks.cjs:traceAllOk +# ============================================================================= +# LISTS — creation, conversion, multi-item +# ============================================================================= + +- description: 'List: create 3-item bullet list' + vars: + fixture: memorandum.docx + keepFile: true + task: 'Add a 3-item bullet list at the end of the document with items: "Review timeline", "Approve budget", "Notify stakeholders".' + assert: + - type: javascript + value: | + const d = JSON.parse(output); + const t = d.documentText || ''; + const hasAll = ['Review timeline', 'Approve budget', 'Notify stakeholders'].every(i => t.includes(i)); + if (!hasAll) return { pass: false, score: 0, reason: 'Missing list items' }; + return { pass: true, score: 1, reason: 'All items present' }; + - type: javascript + value: file://lib/checks.cjs:traceAllOk + - type: javascript + value: file://lib/checks.cjs:traceLog + +- description: 'List: convert bullet to numbered' + vars: + fixture: document.docx + keepFile: true + task: 'Convert the existing bullet list in the document to a numbered list.' + assert: + - type: javascript + value: | + const d = JSON.parse(output); + if (!d.documentText) return { pass: false, score: 0, reason: 'No content' }; + const listCalls = (d.toolCalls || []).filter(tc => tc.tool === 'superdoc_list'); + const hasSetType = listCalls.some(tc => tc.args?.action === 'set_type'); + const hasCreate = listCalls.some(tc => tc.args?.action === 'create'); + if (!hasSetType && !hasCreate) return { pass: false, score: 0, reason: 'No list conversion call found' }; + return { pass: true, score: 1, reason: hasSetType ? 'Used set_type' : 'Used create' }; + - type: javascript + value: file://lib/checks.cjs:traceAllOk + - type: javascript + value: | + const d = JSON.parse(output); + const tools = (d.toolCalls || []).map(tc => tc.tool); + const hasListCall = tools.some(t => t.includes('list')); + if (!hasListCall) return { pass: false, score: 0, reason: `No superdoc_list call. Tools: ${tools.join(' → ')}` }; + return { pass: true, score: 1, reason: `Used list tool` }; + metric: tool_selection + # ============================================================================= # ASPIRATIONAL # ============================================================================= diff --git a/examples/collaboration/ai-node-sdk/.env.example b/examples/collaboration/ai-node-sdk/.env.example new file mode 100644 index 0000000000..3103e1b6d9 --- /dev/null +++ b/examples/collaboration/ai-node-sdk/.env.example @@ -0,0 +1 @@ +OPENAI_API_KEY=sk-your-key-here diff --git a/examples/collaboration/ai-node-sdk/Makefile b/examples/collaboration/ai-node-sdk/Makefile new file mode 100644 index 0000000000..1b8bc12a26 --- /dev/null +++ b/examples/collaboration/ai-node-sdk/Makefile @@ -0,0 +1,152 @@ +.PHONY: install dev dev-local dev-server dev-client link-local-sdk rebuild-local-sdk restore-npm-sdk kill clean help + +# ─── Config ─────────────────────────────────────────────────────────────────── + +PNPM := pnpm +ROOT := $(shell cd ../../.. && pwd) +SDK_SRC := $(ROOT)/packages/sdk/langs/node +CLI_SRC := $(ROOT)/apps/cli +CLI_ARTIFACT = $(CLI_SRC)/artifacts/$(CLI_TARGET)/$(CLI_BINARY) + +# Platform detection for CLI binary +UNAME_S := $(shell uname -s) +UNAME_M := $(shell uname -m) +ifeq ($(UNAME_S),Darwin) + ifeq ($(UNAME_M),arm64) + CLI_TARGET := darwin-arm64 + else + CLI_TARGET := darwin-x64 + endif +else ifeq ($(UNAME_S),Linux) + ifeq ($(UNAME_M),aarch64) + CLI_TARGET := linux-arm64 + else + CLI_TARGET := linux-x64 + endif +endif +CLI_BINARY := superdoc +CLI_PKG := @superdoc-dev/sdk-$(CLI_TARGET) + +# ─── Default ────────────────────────────────────────────────────────────────── + +help: ## Show this help + @echo "" + @echo " SuperDoc AI Agent Example" + @echo " ─────────────────────────" + @echo "" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}' + @echo "" + +# ─── Setup ──────────────────────────────────────────────────────────────────── + +install: check-env ## Install all dependencies (SDK from npm) + @echo "Installing server dependencies..." + @cd server && npm install + @echo "Installing client dependencies..." + @cd client && $(PNPM) install + @echo "" + @echo "✓ Dependencies installed. Run 'make dev' to start." + +check-env: + @if [ ! -f .env ]; then \ + cp .env.example .env; \ + echo ""; \ + echo " ⚠ Created .env from template. Add your OpenAI API key:"; \ + echo " OPENAI_API_KEY=sk-..."; \ + echo ""; \ + exit 1; \ + fi + @grep -q "OPENAI_API_KEY=sk-" .env 2>/dev/null || { \ + echo " ⚠ OPENAI_API_KEY not set in .env"; \ + exit 1; \ + } + +# ─── Development ────────────────────────────────────────────────────────────── + +kill: ## Kill any running instances + @-pkill -f "y-websocket" 2>/dev/null || true + @-pkill -f "tsx watch.*ai-node-sdk" 2>/dev/null || true + @-pkill -f "vite.*ai-node-sdk" 2>/dev/null || true + @-pkill -f "superdoc.*host" 2>/dev/null || true + @sleep 1 + +dev: check-env kill restore-npm-sdk ## Start with npm SDK (default) + @echo "" + @echo " Server → http://localhost:8090 + ws://localhost:8081" + @echo " Client → http://localhost:5173" + @echo "" + @npx concurrently -k \ + -n SERVER,CLIENT \ + -c magenta,green \ + "cd server && npm run dev" \ + "cd client && $(PNPM) run dev" + +dev-local: check-env kill link-local-sdk ## Start with local SDK (built from monorepo) + @echo "" + @echo " Server → http://localhost:8090 + ws://localhost:8081 (local SDK)" + @echo " Client → http://localhost:5173" + @echo "" + @npx concurrently -k \ + -n SERVER,CLIENT \ + -c magenta,green \ + "cd server && npm run dev" \ + "cd client && $(PNPM) run dev" + +dev-server: check-env ## Start server only + cd server && npm run dev + +dev-client: ## Start client only + cd client && $(PNPM) run dev + +restore-npm-sdk: ## Restore SDK from npm if currently symlinked + @if [ -L server/node_modules/@superdoc-dev/sdk ]; then \ + echo " Restoring npm SDK (removing local symlink)..."; \ + rm -f server/node_modules/@superdoc-dev/sdk; \ + cd server && npm install --prefer-offline --no-audit --no-fund 2>/dev/null; \ + echo " ✓ npm SDK restored"; \ + fi + +# ─── Local SDK ──────────────────────────────────────────────────────────────── + +link-local-sdk: ## Build and link local SuperDoc SDK + CLI + @echo "Linking local SuperDoc SDK ($(CLI_TARGET))..." + @# 1. Build CLI native binary if not already built + @if [ ! -f $(CLI_ARTIFACT) ]; then \ + echo " Building CLI binary..."; \ + cd $(ROOT) && $(PNPM) --filter @superdoc-dev/cli run build:native:host; \ + else \ + echo " CLI binary found"; \ + fi + @# 2. Ensure server node_modules exists + @if [ ! -d server/node_modules ]; then \ + echo " Installing server deps first..."; \ + cd server && npm install; \ + fi + @# 3. Replace npm SDK with symlink to workspace source + @rm -rf server/node_modules/@superdoc-dev/sdk + @ln -sf $(SDK_SRC) server/node_modules/@superdoc-dev/sdk + @echo " ✓ Linked SDK → $(SDK_SRC)" + @# 4. Place CLI binary where the workspace SDK can find it via require.resolve + @# Node follows the symlink real path, so the binary must be in the + @# workspace SDK's own node_modules, not the server's. + @mkdir -p $(SDK_SRC)/node_modules/$(CLI_PKG)/bin + @cp $(CLI_ARTIFACT) $(SDK_SRC)/node_modules/$(CLI_PKG)/bin/$(CLI_BINARY) + @chmod +x $(SDK_SRC)/node_modules/$(CLI_PKG)/bin/$(CLI_BINARY) + @echo " ✓ Linked CLI binary" + @echo "✓ Local SDK ready" + +rebuild-local-sdk: ## Rebuild CLI binary and re-link + @echo "Rebuilding CLI binary..." + @cd $(ROOT) && $(PNPM) --filter @superdoc-dev/cli run build:native:host + @mkdir -p $(SDK_SRC)/node_modules/$(CLI_PKG)/bin + @cp $(CLI_ARTIFACT) $(SDK_SRC)/node_modules/$(CLI_PKG)/bin/$(CLI_BINARY) + @chmod +x $(SDK_SRC)/node_modules/$(CLI_PKG)/bin/$(CLI_BINARY) + @echo "✓ CLI binary updated" + +# ─── Cleanup ────────────────────────────────────────────────────────────────── + +clean: ## Remove node_modules and build artifacts + rm -rf server/node_modules client/node_modules + rm -rf client/dist + @echo "✓ Cleaned" diff --git a/examples/collaboration/ai-node-sdk/README.md b/examples/collaboration/ai-node-sdk/README.md new file mode 100644 index 0000000000..c9d4e242f0 --- /dev/null +++ b/examples/collaboration/ai-node-sdk/README.md @@ -0,0 +1,196 @@ +# SuperDoc AI Agent Example + +Real-time collaborative document editing with an AI agent. You upload a `.docx` file, the AI edits it through SuperDoc's tool system, and every change appears live in your browser via Y.js CRDT sync. + +## How It Works + +There are two processes. The **client** is a React app that renders the SuperDoc editor and a chat interface. The **server** runs the AI agent (OpenAI function calling loop) and a Y.js WebSocket relay. Both connect to the same Y.js collaboration room, so edits made by the AI agent appear instantly in the browser. + +``` +You (browser) Server +┌──────────────────────┐ ┌────────────────────────────┐ +│ │ │ │ +│ SuperDoc Editor │◄── Y.js CRDT ──► │ y-websocket (port 8081) │ +│ (renders document) │ (WebSocket) │ (syncs Y.js state) │ +│ │ │ │ +│ Chat + Tool Logs │── HTTP / SSE ──► │ Agent API (port 8090) │ +│ (sends prompts, │ │ ├─ OpenAI streaming │ +│ shows tool calls) │ │ ├─ SuperDoc SDK │ +│ │ │ └─ Room manager │ +└──────────────────────┘ └────────────────────────────┘ + port 5173 │ + ▼ + OpenAI API +``` + +**The data flow for a single prompt:** + +1. You type "Make the title bold" in the chat sidebar +2. The client sends the prompt to the agent API via `POST /v1/rooms/:id/messages` +3. The agent server builds a message array (system prompt + conversation history + your prompt) and calls the OpenAI API with SuperDoc tool definitions +4. OpenAI responds with tool calls (e.g., `superdoc_search` to find the title, then `superdoc_format` to bold it) +5. The agent executes each tool call against the SuperDoc SDK, which modifies the document via ProseMirror +6. ProseMirror changes propagate through `y-prosemirror` to the Y.js document +7. The y-websocket server relays the Y.js update to the browser +8. The browser's Y.js provider receives the update, applies it to its local Y.Doc, and SuperDoc re-renders +9. Meanwhile, each tool call and token is streamed back to the chat UI via Server-Sent Events + +## Quick Start + +Prerequisites: Node.js 22+, pnpm (for client), an OpenAI API key. + +```bash +# 1. Install dependencies +make install + +# 2. Add your OpenAI API key +# (make install creates .env from template if missing) +echo "OPENAI_API_KEY=sk-..." > .env + +# 3. Start everything +make dev +``` + +Open [http://localhost:5173](http://localhost:5173). Upload a `.docx` file (or click "Use sample document"), then chat with the AI to edit it. + +## Project Structure + +``` +ai-node-sdk/ +├── .env Your OpenAI API key (git-ignored) +├── .env.example Template +├── Makefile All dev commands +│ +├── client/ React frontend (Vite, port 5173) +│ ├── src/ +│ │ ├── pages/ +│ │ │ ├── landing.tsx Create/join room form +│ │ │ └── room.tsx Three-panel editor view +│ │ ├── components/ +│ │ │ ├── editor/ +│ │ │ │ ├── editor-workspace.tsx SuperDoc + Y.js provider wiring +│ │ │ │ ├── editor-layout.tsx Three-panel layout (tools | editor | chat) +│ │ │ │ └── room-header.tsx Room ID, connection status +│ │ │ ├── chat/ +│ │ │ │ ├── chat-sidebar.tsx Chat container, model/mode controls +│ │ │ │ ├── chat-input.tsx Input with inline suggestions +│ │ │ │ ├── message-bubble.tsx User/assistant message rendering +│ │ │ │ └── suggestion-chips.tsx Prompt suggestions +│ │ │ ├── tool-logs/ +│ │ │ │ ├── tool-logs-sidebar.tsx Trace list container +│ │ │ │ ├── trace-group.tsx One trace per prompt +│ │ │ │ └── tool-call-entry.tsx Single tool call with I/O +│ │ │ └── shared/ +│ │ │ ├── json-viewer.tsx Inline JSON display +│ │ │ ├── json-modal.tsx Draggable/resizable JSON window +│ │ │ └── json-modal-manager.tsx Multi-instance modal system +│ │ ├── hooks/ +│ │ │ ├── use-agent-stream.ts SSE consumer, manages traces +│ │ │ ├── use-start-room.ts TanStack mutation for room creation +│ │ │ ├── use-room-status.ts TanStack query, polls agent readiness +│ │ │ └── use-send-message.ts TanStack mutation for chat messages +│ │ ├── lib/ +│ │ │ ├── agent-api.ts Fetch wrappers for all server endpoints +│ │ │ ├── sse-parser.ts Async generator for SSE streams +│ │ │ └── room-names.ts Random room name generator +│ │ └── types/ +│ │ ├── agent.ts SSE events, Trace, ToolCallEntry, ChatMessage +│ │ └── room.ts RoomStatus, RoomConfig +│ └── package.json +│ +└── server/ Agent + collab server (Fastify, ports 8090 + 8081) + ├── src/ + │ ├── index.ts Fastify app, CORS, route registration + │ ├── routes/ + │ │ └── rooms.ts REST endpoints + SSE streaming + │ ├── agent/ + │ │ ├── runner.ts OpenAI streaming loop (async generator) + │ │ └── tools.ts chooseTools + dispatchSuperDocTool wrappers + │ ├── runtime/ + │ │ └── room-manager.ts Multi-room state, conversation history, SSE dispatch + │ └── superdoc/ + │ └── editor.ts SDK client lifecycle (create/dispose) + └── package.json +``` + +## How the Pieces Connect + +### Room creation + +When you click "Create Room", the client sends a `POST /v1/rooms/:roomId/start` request (with the uploaded file as multipart form data). The server saves the file to a temp directory, then boots a SuperDoc SDK client: + +``` +client.open({ doc: '/tmp/room.docx', collaboration: { url: 'ws://localhost:8081', documentId: roomId } }) +``` + +This spawns a headless SuperDoc CLI process that opens the document and connects to the y-websocket room. The CLI's `y-prosemirror` plugin syncs the ProseMirror document state into the Y.js room. Once this completes, `agentReady` becomes `true`. + +The browser polls `GET /v1/rooms/:roomId/status` every second. When `agentReady` is true, the room page renders the editor. The `EditorWorkspace` component creates a `Y.Doc` + `WebsocketProvider` (cached at module level to survive HMR), waits for the `sync` event, then renders `` with the synced Y.Doc. + +### Chat and streaming + +When you send a message, the client calls `POST /v1/rooms/:roomId/messages` which returns a `messageId`. The client immediately opens an SSE stream at `GET /v1/rooms/:roomId/messages/:messageId/stream`. + +The server fires the OpenAI streaming loop (`runner.ts`) as an async generator. Each event (token, tool_call_start, tool_call_end, done) is yielded, pushed to SSE subscribers, and written to the HTTP response as `data: {...}\n\n` lines. + +The client's `useAgentStream` hook parses these events and updates React state: tokens accumulate into the chat bubble, tool calls populate the trace in the left sidebar. + +### Tool execution + +The runner loads all SuperDoc tool definitions via `chooseTools({ provider: 'openai' })` and sends them with the OpenAI request. When OpenAI responds with tool calls, the runner: + +1. Assembles streaming tool call deltas (OpenAI splits function arguments across multiple chunks) +2. Parses the accumulated JSON arguments +3. Dispatches each call via `dispatchSuperDocTool(documentHandle, toolName, args)` +4. The SDK validates the args against the tool schema, routes to the correct document API operation, and executes it +5. The result is sent back to OpenAI as a tool response for the next turn + +This continues for up to 15 turns until OpenAI responds without tool calls (just text). + +### Y.js collaboration + +Both the browser and the SDK use the same `y-websocket` protocol to connect to port 8081. The y-websocket server is a standard Y.js relay: it maintains Y.Doc state per room in memory, syncs new clients on connect, and broadcasts updates between peers. No persistence. Rooms are ephemeral and lost on server restart. + +The browser creates its Y.Doc and provider at module level (not inside a React effect) so they survive Vite HMR. This means you can edit frontend code without losing the document state. + +## Commands + +| Command | What it does | +|---------|-------------| +| `make install` | Install server deps (npm) and client deps (pnpm) | +| `make dev` | Start server + client using the published npm SDK | +| `make dev-local` | Start using the local monorepo SDK (builds CLI binary if needed) | +| `make dev-server` | Start server only | +| `make dev-client` | Start client only | +| `make rebuild-local-sdk` | Rebuild the CLI binary after changes to `apps/cli/` | +| `make kill` | Kill any running instances | +| `make clean` | Remove all node_modules | +| `make help` | Show all commands | + +### Local SDK development + +`make dev-local` symlinks `server/node_modules/@superdoc-dev/sdk` to the workspace source at `packages/sdk/langs/node/` and copies the locally-built CLI binary. Changes to the SDK source are picked up immediately (symlink). Changes to the CLI require `make rebuild-local-sdk`. + +## Server API + +| Method | Path | Purpose | +|--------|------|---------| +| `POST` | `/v1/rooms/:roomId/start` | Create room, upload file, boot SDK client | +| `GET` | `/v1/rooms/:roomId/status` | Room status (agentReady, model, mode) | +| `POST` | `/v1/rooms/:roomId/messages` | Send prompt, returns messageId | +| `GET` | `/v1/rooms/:roomId/messages/:id/stream` | SSE stream of execution events | +| `POST` | `/v1/rooms/:roomId/messages/:id/cancel` | Abort active execution | +| `POST` | `/v1/rooms/:roomId/settings` | Update model or edit mode | +| `POST` | `/v1/rooms/:roomId/stop` | Dispose SDK client, clean up room | + +## Technologies + +| Layer | Technology | Purpose | +|-------|-----------|---------| +| Editor | `@superdoc-dev/react` | SuperDoc React wrapper | +| Collaboration | `yjs` + `y-websocket` | CRDT sync between browser and agent | +| AI | `openai` (streaming) | Chat completions with function calling | +| SDK | `@superdoc-dev/sdk` | Document operations via tool dispatch | +| Frontend | React 19, Vite, Tailwind v4, shadcn/ui | UI framework | +| API calls | TanStack Query | Mutations and polling | +| Server | Fastify | HTTP server with SSE | diff --git a/examples/collaboration/ai-node-sdk/client/index.html b/examples/collaboration/ai-node-sdk/client/index.html new file mode 100644 index 0000000000..760cf42db0 --- /dev/null +++ b/examples/collaboration/ai-node-sdk/client/index.html @@ -0,0 +1,12 @@ + + + + + + SuperDoc AI Agent Example + + +
+ + + diff --git a/examples/collaboration/ai-node-sdk/client/package.json b/examples/collaboration/ai-node-sdk/client/package.json new file mode 100644 index 0000000000..df3b049d86 --- /dev/null +++ b/examples/collaboration/ai-node-sdk/client/package.json @@ -0,0 +1,41 @@ +{ + "name": "ai-node-sdk-client", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-collapsible": "^1.1.0", + "@radix-ui/react-scroll-area": "^1.2.0", + "@radix-ui/react-select": "^2.1.0", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.0", + "@superdoc-dev/react": "^1.0.0-rc.1", + "@tanstack/react-query": "^5.0.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.0.0", + "lucide-react": "^0.400.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-markdown": "^9.0.0", + "react-router-dom": "^7.0.0", + "tailwind-merge": "^2.0.0", + "y-websocket": "^2.1.0", + "yjs": "13.6.19" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.3.0", + "tailwindcss": "^4.0.0", + "typescript": "~5.5.0", + "vite": "^5.4.0" + } +} diff --git a/examples/collaboration/ai-node-sdk/client/public/blank.docx b/examples/collaboration/ai-node-sdk/client/public/blank.docx new file mode 100644 index 0000000000..7deeb3f0c0 Binary files /dev/null and b/examples/collaboration/ai-node-sdk/client/public/blank.docx differ diff --git a/examples/collaboration/ai-node-sdk/client/public/sample.docx b/examples/collaboration/ai-node-sdk/client/public/sample.docx new file mode 100644 index 0000000000..3dfdba3cb2 Binary files /dev/null and b/examples/collaboration/ai-node-sdk/client/public/sample.docx differ diff --git a/examples/collaboration/ai-node-sdk/client/src/App.tsx b/examples/collaboration/ai-node-sdk/client/src/App.tsx new file mode 100644 index 0000000000..a8bd2dc894 --- /dev/null +++ b/examples/collaboration/ai-node-sdk/client/src/App.tsx @@ -0,0 +1,12 @@ +import { Routes, Route } from 'react-router-dom'; +import { LandingPage } from './pages/landing'; +import { RoomPage } from './pages/room'; + +export function App() { + return ( + + } /> + } /> + + ); +} diff --git a/examples/collaboration/ai-node-sdk/client/src/components/chat/chat-input.tsx b/examples/collaboration/ai-node-sdk/client/src/components/chat/chat-input.tsx new file mode 100644 index 0000000000..9d0297c7a8 --- /dev/null +++ b/examples/collaboration/ai-node-sdk/client/src/components/chat/chat-input.tsx @@ -0,0 +1,120 @@ +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/cn'; +import { Send, Square } from 'lucide-react'; +import { useCallback, useRef, useState, type KeyboardEvent } from 'react'; + +const QUICK_SUGGESTIONS = [ + { label: 'Add heading', prompt: 'Add a heading "Executive Summary" at the top of the document' }, + { label: 'Write paragraphs', prompt: 'Add 3 lorem ipsum paragraphs under the first heading' }, + { label: 'Summarize', prompt: 'Add a 5-item numbered list summarizing the entire document under the first heading' }, + { label: 'Format', prompt: 'Make the first heading bold and increase its font size' }, +]; + + +interface ChatInputProps { + onSend: (text: string) => void; + onCancel: () => void; + isStreaming: boolean; + disabled?: boolean; + suggestions?: boolean; + onSuggestionSelect?: (text: string) => void; +} + +export function ChatInput({ + onSend, + onCancel, + isStreaming, + disabled, + suggestions, + onSuggestionSelect, +}: ChatInputProps) { + const [text, setText] = useState(''); + const textareaRef = useRef(null); + + const adjustHeight = useCallback(() => { + const el = textareaRef.current; + if (!el) return; + el.style.height = 'auto'; + const lineHeight = 20; + const maxHeight = lineHeight * 4; + el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`; + }, []); + + const handleSend = useCallback(() => { + const trimmed = text.trim(); + if (!trimmed) return; + onSend(trimmed); + setText(''); + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + } + }, [text, onSend]); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }, + [handleSend], + ); + + return ( +
+ {suggestions && onSuggestionSelect && ( +
+ {QUICK_SUGGESTIONS.map(({ label, prompt }) => ( + + ))} +
+ )} +
+