Skip to content

Commit bf0d4b8

Browse files
authored
fix(docs): coherence pass on doc api, clean up dead code, update CLI SKILL.md (#2424)
* refactor(document-api): coherence pass — dead code, validation consistency, type safety * fix(cli): correct SKILL.md and README payload flags, workflows, and command references * fix(cli): align runtime document api with current contract * chore: fix cli
1 parent e06e98b commit bf0d4b8

56 files changed

Lines changed: 810 additions & 838 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/cli/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,10 +311,11 @@ superdoc info ./contract.docx --pretty
311311

312312
## Input payload flags
313313

314-
- `--query-json`, `--query-file`
315-
- `--address-json`, `--address-file`
316-
- `--target-json`, `--target-file`
317-
- `--at-json`, `--at-file` (for `create paragraph`)
314+
- `--query-json`, `--query-file` (`find`, `lists list`)
315+
- `--address-json`, `--address-file` (`get-node`, `lists get`)
316+
- `--target-json` (mutation commands — no `--target-file` counterpart; use flat flags `--block-id`/`--start`/`--end` as alternative)
317+
- `--input-json`, `--input-file` (`call`, `create paragraph`)
318+
- `--at-json`, `--at-file` (`create paragraph`)
318319

319320
## Stdin support
320321

apps/cli/skill/SKILL.md

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Edit, query, and transform Word documents with the SuperDoc CLI v1
55

66
# SuperDoc CLI (v1)
77

8-
Use SuperDoc CLI for DOCX work. Prefer canonical v1 commands.
8+
Use SuperDoc CLI for DOCX work. Use v1 commands (canonical operations and their helper wrappers).
99
Do not default to legacy commands unless explicitly needed for v0-style bulk workflows.
1010

1111
Use `superdoc` if installed, or `npx @superdoc-dev/cli@latest` as a fallback.
@@ -28,12 +28,13 @@ Use `describe command` for per-command args and constraints.
2828

2929
```bash
3030
superdoc open ./contract.docx
31-
superdoc find --type text --pattern "termination"
31+
superdoc query match --select-json '{"type":"text","pattern":"termination"}' --require exactlyOne
3232
superdoc replace --target-json '{"kind":"text","blockId":"p1","range":{"start":0,"end":11}}' --text "expiration"
3333
superdoc save --in-place
3434
superdoc close
3535
```
3636

37+
- Always use `query match` (not `find`) to discover mutation targets — it returns exact addresses with cardinality guarantees.
3738
- After `open`, commands run against the active/default session when `<doc>` is omitted.
3839
- Use `superdoc session list|set-default|save|close` for explicit session control.
3940
- `close` on dirty state requires `--discard` or a prior `save`.
@@ -57,28 +58,80 @@ superdoc replace ./proposal.docx \
5758

5859
- In stateless mode (`<doc>` provided), mutating commands require `--out` unless using `--dry-run`.
5960

61+
### Safety: preview before apply
62+
63+
- Use `--dry-run` to preview any mutation without applying it.
64+
- Use `--expected-revision <n>` with stateful mutations for optimistic concurrency checks.
65+
6066
## Common v1 Commands
6167

62-
- Search text/nodes: `find --type text --pattern "..."` or `find --query-json '{...}'`
63-
- Replace text: `replace --target-json '{...}' --text "..."`
64-
- Add/edit comments: `comments add|reply|edit|resolve|remove`
65-
- Review tracked changes: `track-changes list|accept|reject|accept-all|reject-all`
68+
### Query & inspect
69+
70+
- Search/browse content: `find --type text --pattern "..."` or `find --query-json '{...}'`
71+
- Find mutation target: `query match --select-json '{...}' --require exactlyOne`
72+
- Inspect blocks: `blocks list`, `get-node`, `get-node-by-id`
6673
- Extract content: `get-text`, `get-markdown`, `get-html`
67-
- Low-level direct invoke: `call <operationId> --input-json '{...}'`
74+
75+
### Mutate
76+
77+
- Replace text: `replace --target-json '{...}' --text "..."`
78+
- Insert inline text: `insert --block-id <id> --offset <n> --value "..."`
79+
- Delete text/node: `delete --target-json '{...}'`
80+
- Delete blocks: `blocks delete`, `blocks delete-range`
81+
- Batch mutations: `mutations apply --steps-json '[...]' --atomic true --change-mode direct`
82+
- Create paragraph: `create paragraph --text "..."` (with optional `--at-json`)
83+
- Create heading: `create heading --input-json '{"level":<n>,"text":"..."}'`
84+
85+
### Format
86+
87+
- Apply formatting: `format apply --block-id <id> --start <n> --end <n> --inline-json '{"bold":true}'`
88+
- Shortcuts: `format bold`, `format italic`, `format underline`, `format strikethrough`
89+
90+
### Lists
91+
92+
- List items: `lists list`, `lists get`
93+
- Insert list item: `lists insert --node-id <id> --position after --text "..."`
94+
- Modify: `lists indent`, `lists outdent`, `lists set-level`, `lists set-type`, `lists convert-to-text`
95+
96+
### Comments
97+
98+
- Add/reply: `comments add`, `comments reply`
99+
- Read: `comments get`, `comments list`
100+
- Edit/resolve/move: `comments edit`, `comments resolve`, `comments move`, `comments set-internal`
101+
- Delete: `comments delete` (canonical) or `comments remove` (alias)
102+
103+
### Track changes
104+
105+
- List: `track-changes list`, `track-changes get`
106+
- Decide: `track-changes accept`, `track-changes reject`, `track-changes accept-all`, `track-changes reject-all`
107+
108+
### History
109+
110+
- `history get`, `history undo`, `history redo`
111+
112+
### Low-level
113+
114+
- Direct invoke: `call <operationId> --input-json '{...}'` (JSON output only — `--pretty` is not supported)
68115

69116
## JSON/File Payload Flags
70117

71-
Use one of each pair (not both):
118+
Not all `--*-file` variants are available on every command. Use `describe command <name>` to check.
119+
120+
Always supported alongside their `-json` counterpart (use one, not both):
121+
122+
| Flag pair | Available on |
123+
|-----------|-------------|
124+
| `--query-json` / `--query-file` | `find`, `lists list` |
125+
| `--address-json` / `--address-file` | `get-node`, `lists get` |
126+
| `--input-json` / `--input-file` | `call`, `create paragraph` |
127+
| `--at-json` / `--at-file` | `create paragraph` |
72128

73-
- `--query-json` or `--query-file`
74-
- `--target-json` or `--target-file`
75-
- `--address-json` or `--address-file`
76-
- `--input-json` or `--input-file` (for `call`)
129+
`--target-json` is widely available on mutation commands but has **no** `--target-file` counterpart. Use flat flags (`--block-id`, `--start`, `--end`) as an alternative to `--target-json`.
77130

78131
## Output and Global Flags
79132

80133
- Default output is JSON envelope.
81-
- Use `--pretty` for human-readable output.
134+
- Use `--pretty` for human-readable output (not supported by `call`).
82135
- Global flags: `--output <json|pretty>`, `--session <id>`, `--timeout-ms <n>`.
83136
- `<doc>` can be `-` to read DOCX bytes from stdin.
84137

apps/cli/src/__tests__/host.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ describe('CLI host mode', () => {
328328
await invokeAndValidate('doc.comments.list', ['comments', 'list', docPath, '--include-resolved', 'false']);
329329

330330
await host.shutdown();
331-
});
331+
}, 15_000);
332332

333333
test('returns parse errors for malformed frames', async () => {
334334
const stateDir = await mkdtemp(path.join(tmpdir(), 'superdoc-host-test-'));

apps/cli/src/__tests__/lib/error-mapping.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,28 @@ describe('mapFailedReceipt: plan-engine code passthrough', () => {
274274
expect(result!.code).toBe('INVALID_ARGUMENT');
275275
});
276276
});
277+
278+
// ---------------------------------------------------------------------------
279+
// textMutation: INVALID_INPUT ordering — plan-engine must win over adapter remap
280+
// ---------------------------------------------------------------------------
281+
282+
describe('mapInvokeError: textMutation INVALID_INPUT ordering', () => {
283+
test('plan-engine INVALID_INPUT passes through verbatim for text mutations', () => {
284+
const error = Object.assign(new Error('step schema invalid'), {
285+
code: 'INVALID_INPUT',
286+
details: {
287+
stepIndex: 0,
288+
operation: 'text.rewrite',
289+
remediation: 'Fix the step payload.',
290+
},
291+
});
292+
293+
const result = mapInvokeError('format.inline.apply' as any, error);
294+
expect(result).toBeInstanceOf(CliError);
295+
// Must preserve INVALID_INPUT — not remap to INVALID_ARGUMENT
296+
expect(result.code).toBe('INVALID_INPUT');
297+
expect(result.details).toMatchObject({
298+
details: { stepIndex: 0, operation: 'text.rewrite' },
299+
});
300+
});
301+
});

apps/cli/src/lib/document.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { readFile, writeFile } from 'node:fs/promises';
22
import { createHash } from 'node:crypto';
33
import { Editor } from 'superdoc/super-editor';
44
import { BLANK_DOCX_BASE64 } from '@superdoc/super-editor/blank-docx';
5+
import { getDocumentApiAdapters } from '@superdoc/super-editor/document-api-adapters';
56
import { markdownToPmDoc } from '@superdoc/super-editor/markdown';
67

7-
import type { DocumentApi } from '@superdoc/document-api';
8+
import { createDocumentApi, type DocumentApi } from '@superdoc/document-api';
89
import { createCliDomEnvironment } from './dom-environment';
910
import type { CollaborationProfile } from './collaboration';
1011
import { createCollaborationRuntime } from './collaboration';
@@ -65,6 +66,21 @@ export interface FileOutputMeta {
6566
byteLength: number;
6667
}
6768

69+
function bindCurrentDocumentApi(editor: Editor): EditorWithDoc {
70+
const editorWithDoc = editor as EditorWithDoc;
71+
72+
// `superdoc/super-editor` resolves to the published dist bundle, which can
73+
// lag the source-backed document-api contract used by the CLI tests. Shadow
74+
// the bundled getter with a source-backed DocumentApi so runtime behavior and
75+
// response validation stay on the same contract version.
76+
Object.defineProperty(editorWithDoc, 'doc', {
77+
configurable: true,
78+
value: createDocumentApi(getDocumentApiAdapters(editor)),
79+
});
80+
81+
return editorWithDoc;
82+
}
83+
6884
function toUint8Array(data: unknown): Uint8Array {
6985
if (data instanceof Uint8Array) return data;
7086
if (data instanceof ArrayBuffer) return new Uint8Array(data);
@@ -217,7 +233,7 @@ export async function openDocument(
217233
}
218234
}
219235

220-
const editorWithDoc = editor as EditorWithDoc;
236+
const editorWithDoc = bindCurrentDocumentApi(editor);
221237

222238
return {
223239
editor: editorWithDoc,

apps/cli/src/lib/error-mapping.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,14 @@ function mapTextMutationError(operationId: CliExposedOperationId, error: unknown
142142
const message = extractErrorMessage(error);
143143
const details = extractErrorDetails(error);
144144

145-
// Plan-engine errors pass through with original code and structured details
145+
// For direct text-mutation commands, adapter INVALID_INPUT errors reflect CLI
146+
// payload-shape issues (for example flat-flag shortcuts that did not
147+
// normalize into a canonical target), so present them as INVALID_ARGUMENT.
148+
if (code === 'INVALID_INPUT') {
149+
return new CliError('INVALID_ARGUMENT', message, { operationId, details });
150+
}
151+
152+
// Other plan-engine errors pass through with original code and structured details.
146153
const planEngineError = tryMapPlanEngineError(operationId, error, code);
147154
if (planEngineError) return planEngineError;
148155

packages/document-api/scripts/check-contract-parity.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,6 @@ function createNoopAdapters(): DocumentApiAdapters {
112112
},
113113
}),
114114
},
115-
format: {
116-
apply: () => ({
117-
success: true,
118-
resolution: {
119-
target: { kind: 'text', blockId: 'p1', range: { start: 0, end: 1 } },
120-
range: { from: 1, to: 2 },
121-
text: 'x',
122-
},
123-
}),
124-
},
125115
trackChanges: {
126116
list: () => ({ evaluatedRevision: '', total: 0, items: [], page: { limit: 50, offset: 0, returned: 0 } }),
127117
get: ({ id }) => ({

packages/document-api/src/authorities/authorities.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { MutationOptions } from '../write/write.js';
22
import { normalizeMutationOptions } from '../write/write.js';
33
import { DocumentApiValidationError } from '../errors.js';
4+
import { assertTargetPresent } from '../validation-primitives.js';
45
import type {
56
AuthoritiesAddress,
67
AuthorityEntryAddress,
@@ -51,9 +52,7 @@ export type AuthoritiesAdapter = AuthoritiesApi;
5152
// ---------------------------------------------------------------------------
5253

5354
function validateAuthoritiesTarget(target: unknown, operationName: string): asserts target is AuthoritiesAddress {
54-
if (target === undefined || target === null) {
55-
throw new DocumentApiValidationError('INVALID_TARGET', `${operationName} requires a target.`);
56-
}
55+
assertTargetPresent(target, operationName);
5756
const t = target as Record<string, unknown>;
5857
if (t.kind !== 'block' || t.nodeType !== 'tableOfAuthorities' || typeof t.nodeId !== 'string') {
5958
throw new DocumentApiValidationError(
@@ -65,9 +64,7 @@ function validateAuthoritiesTarget(target: unknown, operationName: string): asse
6564
}
6665

6766
function validateAuthorityEntryTarget(target: unknown, operationName: string): asserts target is AuthorityEntryAddress {
68-
if (target === undefined || target === null) {
69-
throw new DocumentApiValidationError('INVALID_TARGET', `${operationName} requires a target.`);
70-
}
67+
assertTargetPresent(target, operationName);
7168
const t = target as Record<string, unknown>;
7269
if (t.kind !== 'inline' || t.nodeType !== 'authorityEntry') {
7370
throw new DocumentApiValidationError(

packages/document-api/src/authorities/authorities.types.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { InlineAnchor } from '../types/base.js';
22
import type { TextTarget } from '../types/address.js';
3-
import type { ReceiptFailure } from '../types/receipt.js';
3+
import type { AdapterMutationFailure } from '../types/adapter-result.js';
44
import type { DiscoveryOutput } from '../types/discovery.js';
55
import type { TocCreateLocation } from '../toc/toc.types.js';
66

@@ -163,21 +163,13 @@ export interface AuthoritiesMutationSuccess {
163163
success: true;
164164
authorities: AuthoritiesAddress;
165165
}
166-
export interface AuthoritiesMutationFailure {
167-
success: false;
168-
failure: ReceiptFailure;
169-
}
170-
export type AuthoritiesMutationResult = AuthoritiesMutationSuccess | AuthoritiesMutationFailure;
166+
export type AuthoritiesMutationResult = AuthoritiesMutationSuccess | AdapterMutationFailure;
171167

172168
export interface AuthorityEntryMutationSuccess {
173169
success: true;
174170
entry: AuthorityEntryAddress;
175171
}
176-
export interface AuthorityEntryMutationFailure {
177-
success: false;
178-
failure: ReceiptFailure;
179-
}
180-
export type AuthorityEntryMutationResult = AuthorityEntryMutationSuccess | AuthorityEntryMutationFailure;
172+
export type AuthorityEntryMutationResult = AuthorityEntryMutationSuccess | AdapterMutationFailure;
181173

182174
// ---------------------------------------------------------------------------
183175
// List results

packages/document-api/src/bookmarks/bookmarks.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { MutationOptions } from '../write/write.js';
22
import { normalizeMutationOptions } from '../write/write.js';
33
import { DocumentApiValidationError } from '../errors.js';
4+
import { assertTargetPresent } from '../validation-primitives.js';
45
import type {
56
BookmarkAddress,
67
BookmarkGetInput,
@@ -32,9 +33,7 @@ export type BookmarksAdapter = BookmarksApi;
3233
// ---------------------------------------------------------------------------
3334

3435
function validateBookmarkTarget(target: unknown, operationName: string): asserts target is BookmarkAddress {
35-
if (target === undefined || target === null) {
36-
throw new DocumentApiValidationError('INVALID_TARGET', `${operationName} requires a target.`);
37-
}
36+
assertTargetPresent(target, operationName);
3837

3938
const t = target as Record<string, unknown>;
4039
if (t.kind !== 'entity' || t.entityType !== 'bookmark' || typeof t.name !== 'string') {

0 commit comments

Comments
 (0)