Skip to content

Commit 33e907b

Browse files
committed
fix: normalize review namespace into trackChanges, harden input validation
1 parent 18c9b7f commit 33e907b

19 files changed

Lines changed: 281 additions & 241 deletions

File tree

apps/docs/document-api/reference/_generated-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,5 +119,5 @@
119119
}
120120
],
121121
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
122-
"sourceHash": "a8eef0a5f30ba4a353cc0124d1274eaa79550ad9ea63c8815fb1eb915356f085"
122+
"sourceHash": "71719d809aa97aaaf7d9200dedd1edf47dc16486b208b473cc009257667bed77"
123123
}

apps/docs/document-api/reference/comments/patch.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ _No fields._
7777
- `COMMAND_UNAVAILABLE`
7878
- `CAPABILITY_UNAVAILABLE`
7979
- `INVALID_TARGET`
80+
- `INVALID_INPUT`
8081

8182
## Non-applied failure codes
8283

apps/docs/document-api/reference/track-changes/decide.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ _No fields._
6767
- `TARGET_NOT_FOUND`
6868
- `COMMAND_UNAVAILABLE`
6969
- `CAPABILITY_UNAVAILABLE`
70+
- `INVALID_INPUT`
71+
- `INVALID_TARGET`
7072

7173
## Non-applied failure codes
7274

apps/docs/document-engine/cli.mdx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,32 @@ superdoc save
4242
superdoc close
4343
```
4444

45+
## Tracked mode for mutations
46+
47+
Use `--tracked` on mutating commands to apply edits as tracked changes instead of direct edits.
48+
49+
```bash
50+
# Replace text as a tracked change
51+
superdoc replace \
52+
--target-json '{"kind":"text","blockId":"...","range":{"start":0,"end":9}}' \
53+
--text "NewCo Inc." \
54+
--tracked
55+
56+
# Insert text as a tracked change
57+
superdoc insert --text "Added clause" --tracked
58+
```
59+
60+
`--tracked` is shorthand for `--change-mode tracked`:
61+
62+
```bash
63+
superdoc replace \
64+
--target-json '{"kind":"text","blockId":"...","range":{"start":0,"end":9}}' \
65+
--text "NewCo Inc." \
66+
--change-mode tracked
67+
```
68+
69+
For commands that do not support tracked mode, the CLI returns `TRACK_CHANGE_COMMAND_UNAVAILABLE`.
70+
4571
## Commands
4672

4773
### Lifecycle
@@ -70,11 +96,10 @@ The CLI exposes all [Document API operations](/document-api/overview) as command
7096
| `superdoc format underline` | `format.underline` | Toggle underline on a range |
7197
| `superdoc format strikethrough` | `format.strikethrough` | Toggle strikethrough on a range |
7298
| `superdoc create paragraph` | `create.paragraph` | Insert a new paragraph |
73-
| `superdoc comments add` | `comments.add` | Add a comment thread |
99+
| `superdoc comments create` | `comments.create` | Create a comment thread |
74100
| `superdoc comments list` | `comments.list` | List all comments |
75101
| `superdoc track-changes list` | `trackChanges.list` | List tracked changes |
76-
| `superdoc track-changes accept` | `trackChanges.accept` | Accept a tracked change |
77-
| `superdoc track-changes reject` | `trackChanges.reject` | Reject a tracked change |
102+
| `superdoc track-changes decide` | `trackChanges.decide` | Accept or reject a tracked change |
78103

79104
Run `superdoc --help` for the full list, or `superdoc describe` for machine-readable metadata.
80105

apps/docs/document-engine/sdks.mdx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ The CLI is bundled with the SDK — no separate install needed.
3535
```typescript
3636
import { SuperDocClient } from '@superdoc-dev/sdk';
3737

38-
const client = new SuperDocClient();
38+
const client = new SuperDocClient({
39+
defaultChangeMode: 'tracked',
40+
});
3941

4042
// Open a document
4143
await client.doc.open({ doc: './contract.docx' });
@@ -51,7 +53,6 @@ The CLI is bundled with the SDK — no separate install needed.
5153
await client.doc.mutations.apply({
5254
expectedRevision: match.evaluatedRevision,
5355
atomic: true,
54-
changeMode: 'direct',
5556
steps: [
5657
{
5758
id: 'replace-acme',
@@ -76,7 +77,7 @@ The CLI is bundled with the SDK — no separate install needed.
7677

7778

7879
async def main():
79-
client = AsyncSuperDocClient()
80+
client = AsyncSuperDocClient(default_change_mode="tracked")
8081

8182
# Open a document
8283
await client.doc.open({"doc": "./contract.docx"})
@@ -95,7 +96,6 @@ The CLI is bundled with the SDK — no separate install needed.
9596
{
9697
"expectedRevision": match["evaluatedRevision"],
9798
"atomic": True,
98-
"changeMode": "direct",
9999
"steps": [
100100
{
101101
"id": "replace-acme",
@@ -117,6 +117,8 @@ The CLI is bundled with the SDK — no separate install needed.
117117
</Tab>
118118
</Tabs>
119119

120+
Set `defaultChangeMode: 'tracked'` (Node) or `default_change_mode='tracked'` (Python) to make mutations use tracked changes by default. If you pass `changeMode` on a specific call, that explicit value overrides the default.
121+
120122
{/* SDK_OPERATIONS_START */}
121123
## Available operations
122124

apps/mcp/src/tools/comments.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function registerCommentTools(server: McpServer, sessions: SessionManager
2525
const { api } = sessions.get(session_id);
2626
const parsed = JSON.parse(target);
2727
const result = api.invoke({
28-
operationId: 'comments.add',
28+
operationId: 'comments.create',
2929
input: { text, target: parsed },
3030
});
3131
return {
@@ -87,7 +87,7 @@ export function registerCommentTools(server: McpServer, sessions: SessionManager
8787
try {
8888
const { api } = sessions.get(session_id);
8989
const result = api.invoke({
90-
operationId: 'comments.reply',
90+
operationId: 'comments.create',
9191
input: { parentCommentId: comment_id, text },
9292
});
9393
return {
@@ -117,8 +117,8 @@ export function registerCommentTools(server: McpServer, sessions: SessionManager
117117
try {
118118
const { api } = sessions.get(session_id);
119119
const result = api.invoke({
120-
operationId: 'comments.resolve',
121-
input: { commentId: comment_id },
120+
operationId: 'comments.patch',
121+
input: { commentId: comment_id, status: 'resolved' },
122122
});
123123
return {
124124
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],

apps/mcp/src/tools/track-changes.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ export function registerTrackChangesTools(server: McpServer, sessions: SessionMa
5757
async ({ session_id, id }) => {
5858
try {
5959
const { api } = sessions.get(session_id);
60-
const result = api.invoke({ operationId: 'review.decide', input: { decision: 'accept', target: { id } } });
60+
const result = api.invoke({
61+
operationId: 'trackChanges.decide',
62+
input: { decision: 'accept', target: { id } },
63+
});
6164
return {
6265
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
6366
};
@@ -85,7 +88,10 @@ export function registerTrackChangesTools(server: McpServer, sessions: SessionMa
8588
async ({ session_id, id }) => {
8689
try {
8790
const { api } = sessions.get(session_id);
88-
const result = api.invoke({ operationId: 'review.decide', input: { decision: 'reject', target: { id } } });
91+
const result = api.invoke({
92+
operationId: 'trackChanges.decide',
93+
input: { decision: 'reject', target: { id } },
94+
});
8995
return {
9096
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
9197
};
@@ -112,7 +118,7 @@ export function registerTrackChangesTools(server: McpServer, sessions: SessionMa
112118
try {
113119
const { api } = sessions.get(session_id);
114120
const result = api.invoke({
115-
operationId: 'review.decide',
121+
operationId: 'trackChanges.decide',
116122
input: { decision: 'accept', target: { scope: 'all' } },
117123
});
118124
return {
@@ -141,7 +147,7 @@ export function registerTrackChangesTools(server: McpServer, sessions: SessionMa
141147
try {
142148
const { api } = sessions.get(session_id);
143149
const result = api.invoke({
144-
operationId: 'review.decide',
150+
operationId: 'trackChanges.decide',
145151
input: { decision: 'reject', target: { scope: 'all' } },
146152
});
147153
return {

packages/document-api/scripts/lib/contract-output-artifacts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export function buildAgentArtifacts(): GeneratedFile[] {
182182
{
183183
id: 'comment-thread-lifecycle',
184184
title: 'Comment lifecycle workflow',
185-
operations: ['comments.add', 'comments.reply', 'comments.resolve'],
185+
operations: ['comments.create', 'comments.patch', 'comments.delete'],
186186
},
187187
{
188188
id: 'list-manipulation',

packages/document-api/src/README.md

Lines changed: 21 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ Locate text in the document and replace it:
9090

9191
```ts
9292
const result = editor.doc.find({ type: 'text', text: 'foo' });
93-
const target = result.context?.[0]?.textRanges?.[0];
93+
const target = result.items?.[0]?.context?.textRanges?.[0];
9494
if (target) {
9595
editor.doc.replace({ target, text: 'bar' });
9696
}
@@ -126,13 +126,13 @@ const receipt = editor.doc.insert(
126126
Add a comment, reply, then resolve the thread:
127127

128128
```ts
129-
const target = result.context?.[0]?.textRanges?.[0];
130-
const addReceipt = editor.doc.comments.add({ target, text: 'Review this section.' });
129+
const target = result.items?.[0]?.context?.textRanges?.[0];
130+
const createReceipt = editor.doc.comments.create({ target, text: 'Review this section.' });
131131
// Use the comment ID from the receipt to reply
132132
const comments = editor.doc.comments.list();
133-
const thread = comments.matches[0];
134-
editor.doc.comments.reply({ parentCommentId: thread.commentId, text: 'Looks good.' });
135-
editor.doc.comments.resolve({ commentId: thread.commentId });
133+
const thread = comments.items[0];
134+
editor.doc.comments.create({ parentCommentId: thread.id, text: 'Looks good.' });
135+
editor.doc.comments.patch({ commentId: thread.id, status: 'resolved' });
136136
```
137137

138138
### Workflow: List Manipulation
@@ -141,8 +141,8 @@ Insert a list item, change its type, then indent it:
141141

142142
```ts
143143
const lists = editor.doc.lists.list();
144-
const firstItem = lists.matches[0];
145-
const insertResult = editor.doc.lists.insert({ target: firstItem, position: 'after', text: 'New item' });
144+
const firstItem = lists.items[0];
145+
const insertResult = editor.doc.lists.insert({ target: firstItem.address, position: 'after', text: 'New item' });
146146
if (insertResult.success) {
147147
editor.doc.lists.setType({ target: insertResult.item, kind: 'ordered' });
148148
editor.doc.lists.indent({ target: insertResult.item });
@@ -430,95 +430,36 @@ Convert a list item back into a plain paragraph, exiting the list. Supports dry-
430430

431431
### Comments
432432

433-
### `comments.add`
433+
### `comments.create`
434434

435-
Attach a new comment to a text range.
435+
Create a new comment thread or reply. When `parentCommentId` is provided, creates a reply. Otherwise creates a root comment anchored to the given text range.
436436

437-
- **Input**: `AddCommentInput` (`{ target, text }`)
438-
- **Output**: `Receipt`
439-
- **Mutates**: Yes
440-
- **Idempotency**: non-idempotent
441-
- **Failure codes**: `INVALID_TARGET`, `NO_OP`
442-
443-
### `comments.edit`
444-
445-
Update the body text of an existing comment.
446-
447-
- **Input**: `EditCommentInput` (`{ commentId, text }`)
448-
- **Output**: `Receipt`
449-
- **Mutates**: Yes
450-
- **Idempotency**: conditional
451-
- **Failure codes**: `NO_OP`
452-
453-
### `comments.reply`
454-
455-
Add a reply to an existing comment thread.
456-
457-
- **Input**: `ReplyToCommentInput` (`{ parentCommentId, text }`)
437+
- **Input**: `CommentsCreateInput` (`{ text, target?, parentCommentId? }`)
458438
- **Output**: `Receipt`
459439
- **Mutates**: Yes
460440
- **Idempotency**: non-idempotent
461441
- **Failure codes**: `INVALID_TARGET`
462442

463-
### `comments.move`
464-
465-
Move a comment to a different text range.
466-
467-
- **Input**: `MoveCommentInput` (`{ commentId, target }`)
468-
- **Output**: `Receipt`
469-
- **Mutates**: Yes
470-
- **Idempotency**: conditional
471-
- **Failure codes**: `INVALID_TARGET`, `NO_OP`
443+
### `comments.patch`
472444

473-
### `comments.resolve`
445+
Field-level patch on an existing comment. Exactly one mutation field must be provided per call.
474446

475-
Resolve an open comment, marking it as addressed.
476-
477-
- **Input**: `ResolveCommentInput` (`{ commentId }`)
447+
- **Input**: `CommentsPatchInput` (`{ commentId, text?, target?, status?, isInternal? }`)
478448
- **Output**: `Receipt`
479449
- **Mutates**: Yes
480450
- **Idempotency**: conditional
481-
- **Failure codes**: `NO_OP`
451+
- **Failure codes**: `INVALID_INPUT`, `INVALID_TARGET`, `NO_OP`
482452

483-
### `comments.remove`
453+
### `comments.delete`
484454

485455
Remove a comment from the document.
486456

487-
- **Input**: `RemoveCommentInput` (`{ commentId }`)
457+
- **Input**: `CommentsDeleteInput` (`{ commentId }`)
488458
- **Output**: `Receipt`
489459
- **Mutates**: Yes
490460
- **Idempotency**: conditional
491461
- **Failure codes**: `NO_OP`
492462

493-
### `comments.setInternal`
494-
495-
Set or clear the internal/private flag on a comment.
496-
497-
- **Input**: `SetCommentInternalInput` (`{ commentId, isInternal }`)
498-
- **Output**: `Receipt`
499-
- **Mutates**: Yes
500-
- **Idempotency**: conditional
501-
- **Failure codes**: `NO_OP`, `INVALID_TARGET`
502-
503-
### `comments.setActive`
504-
505-
Set which comment is currently active/focused. Pass `null` to clear.
506-
507-
- **Input**: `SetCommentActiveInput` (`{ commentId }`)
508-
- **Output**: `Receipt`
509-
- **Mutates**: Yes
510-
- **Idempotency**: conditional
511-
- **Failure codes**: `INVALID_TARGET`
512-
513-
### `comments.goTo`
514-
515-
Scroll to and focus a comment in the document.
516-
517-
- **Input**: `GoToCommentInput` (`{ commentId }`)
518-
- **Output**: `Receipt`
519-
- **Mutates**: No
520-
- **Idempotency**: conditional
521-
522463
### `comments.get`
523464

524465
Retrieve full information for a single comment by ID. Throws `TARGET_NOT_FOUND` when the comment is not found.
@@ -557,42 +498,12 @@ Retrieve full information for a single tracked change by its canonical ID. Throw
557498
- **Mutates**: No
558499
- **Idempotency**: idempotent
559500

560-
### `trackChanges.accept`
561-
562-
Accept a tracked change, applying it permanently to the document. Returns `NO_OP` when the change has already been accepted.
563-
564-
- **Input**: `TrackChangesAcceptInput` (`{ id }`)
565-
- **Output**: `Receipt`
566-
- **Mutates**: Yes
567-
- **Idempotency**: conditional
568-
- **Failure codes**: `NO_OP`
569-
570-
### `trackChanges.reject`
571-
572-
Reject a tracked change, reverting it from the document. Returns `NO_OP` when the change has already been rejected.
573-
574-
- **Input**: `TrackChangesRejectInput` (`{ id }`)
575-
- **Output**: `Receipt`
576-
- **Mutates**: Yes
577-
- **Idempotency**: conditional
578-
- **Failure codes**: `NO_OP`
579-
580-
### `trackChanges.acceptAll`
501+
### `trackChanges.decide`
581502

582-
Accept all tracked changes in the document. Returns `NO_OP` when there are no pending changes.
503+
Accept or reject a tracked change by ID, or accept/reject all changes with `{ scope: 'all' }`.
583504

584-
- **Input**: `TrackChangesAcceptAllInput` (empty object)
505+
- **Input**: `ReviewDecideInput` (`{ decision: 'accept' | 'reject', target: { id } | { scope: 'all' } }`)
585506
- **Output**: `Receipt`
586507
- **Mutates**: Yes
587508
- **Idempotency**: conditional
588-
- **Failure codes**: `NO_OP`
589-
590-
### `trackChanges.rejectAll`
591-
592-
Reject all tracked changes in the document. Returns `NO_OP` when there are no pending changes.
593-
594-
- **Input**: `TrackChangesRejectAllInput` (empty object)
595-
- **Output**: `Receipt`
596-
- **Mutates**: Yes
597-
- **Idempotency**: conditional
598-
- **Failure codes**: `NO_OP`
509+
- **Failure codes**: `NO_OP`, `TARGET_NOT_FOUND`, `COMMAND_UNAVAILABLE`

0 commit comments

Comments
 (0)