sdoc-editor-cli 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +625 -0
- package/bin/sdoc.js +2 -0
- package/dist/examples/operations/delete-block.json +17 -0
- package/dist/examples/operations/delete-section.json +12 -0
- package/dist/examples/operations/insert-block.json +19 -0
- package/dist/examples/operations/insert-section.json +21 -0
- package/dist/examples/operations/move-block.json +21 -0
- package/dist/examples/operations/move-section.json +16 -0
- package/dist/examples/operations/rename-block-id.json +13 -0
- package/dist/examples/operations/rename-heading.json +13 -0
- package/dist/examples/operations/replace-block.json +21 -0
- package/dist/examples/operations/set-document-title.json +17 -0
- package/dist/examples/operations/set-heading-level.json +13 -0
- package/dist/examples/operations/update-block-attrs.json +13 -0
- package/dist/examples/operations/update-document-metadata.json +15 -0
- package/dist/examples/operations/update-document-settings.json +16 -0
- package/dist/schemas/sdoc.cli.response.schema.json +436 -0
- package/dist/schemas/sdoc.operations.schema.json +390 -0
- package/dist/schemas/sdoc.read.schema.json +142 -0
- package/dist/schemas/sdoc.schema.json +559 -0
- package/dist/sdoc.js +8637 -0
- package/package.json +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SWBaek and Structured Doc Editor contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
# sdoc-editor-cli
|
|
2
|
+
|
|
3
|
+
`sdoc-editor-cli` is the preview-first command-line interface for inspecting,
|
|
4
|
+
validating, creating, and safely changing Structured Doc Editor `.sdoc` and
|
|
5
|
+
legacy `.tiptap.json` documents. It requires Node.js 22.22.2 or newer.
|
|
6
|
+
|
|
7
|
+
The package is distributed as a GitHub Release tarball, not through the public
|
|
8
|
+
npm registry. The similarly named registry package `sdoc` is unrelated.
|
|
9
|
+
|
|
10
|
+
## Safe installation and verification
|
|
11
|
+
|
|
12
|
+
Discover the current versioned tarball from the latest non-prerelease GitHub
|
|
13
|
+
Release. This PowerShell example uses the anonymous GitHub Releases REST API
|
|
14
|
+
and refuses to continue unless exactly one CLI asset matches:
|
|
15
|
+
|
|
16
|
+
```powershell
|
|
17
|
+
$release = Invoke-RestMethod `
|
|
18
|
+
-Headers @{
|
|
19
|
+
Accept = 'application/vnd.github+json'
|
|
20
|
+
'X-GitHub-Api-Version' = '2022-11-28'
|
|
21
|
+
} `
|
|
22
|
+
-Uri 'https://api.github.com/repos/SWBaek/sdoc-editor/releases/latest'
|
|
23
|
+
$cliAssets = @($release.assets | Where-Object {
|
|
24
|
+
$_.name -match '^sdoc-editor-cli-[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?\.tgz$'
|
|
25
|
+
})
|
|
26
|
+
if ($cliAssets.Count -ne 1) {
|
|
27
|
+
throw "Expected exactly one sdoc-editor-cli-*.tgz asset, found $($cliAssets.Count)."
|
|
28
|
+
}
|
|
29
|
+
$env:SDOC_CLI_TGZ_URL = $cliAssets[0].browser_download_url
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The equivalent POSIX flow requires `curl` and `jq`:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
SDOC_CLI_TGZ_URL="$(
|
|
36
|
+
curl -fsSL \
|
|
37
|
+
-H 'Accept: application/vnd.github+json' \
|
|
38
|
+
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
|
39
|
+
https://api.github.com/repos/SWBaek/sdoc-editor/releases/latest |
|
|
40
|
+
jq -er '
|
|
41
|
+
[.assets[] | select(.name | test("^sdoc-editor-cli-[0-9]+[.][0-9]+[.][0-9]+(-[0-9A-Za-z.-]+)?[.]tgz$"))]
|
|
42
|
+
| if length == 1 then .[0].browser_download_url
|
|
43
|
+
else error("expected exactly one sdoc-editor-cli-*.tgz asset")
|
|
44
|
+
end
|
|
45
|
+
'
|
|
46
|
+
)"
|
|
47
|
+
export SDOC_CLI_TGZ_URL
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Anonymous GitHub REST requests are rate-limited (typically 60 requests per
|
|
51
|
+
hour per source IP). Authenticate the API request when a shared runner may
|
|
52
|
+
exceed that limit. The CLI is not published to npm, and releases do not
|
|
53
|
+
provide an unversioned `latest` download alias; always use the selected
|
|
54
|
+
asset's `browser_download_url`.
|
|
55
|
+
|
|
56
|
+
For a project-local install, run these commands from the project that owns the
|
|
57
|
+
dependency:
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
npm install --save-dev "$env:SDOC_CLI_TGZ_URL"
|
|
61
|
+
npm ls sdoc-editor-cli --depth=0
|
|
62
|
+
node .\node_modules\sdoc-editor-cli\dist\sdoc.js --version
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
> **Package name collision warning:** The npm public registry contains an
|
|
66
|
+
> unrelated package named `sdoc`. Even `npx --no-install sdoc` can resolve a
|
|
67
|
+
> cached copy of that unrelated package when the project-local CLI is absent.
|
|
68
|
+
> For deterministic local execution, invoke the installed package entry point
|
|
69
|
+
> directly with `node ./node_modules/sdoc-editor-cli/dist/sdoc.js ...` and verify
|
|
70
|
+
> the dependency first with `npm ls sdoc-editor-cli --depth=0`. A missing local
|
|
71
|
+
> package then fails with a path/module error instead of running another
|
|
72
|
+
> package. Do not install the registry package `sdoc`; it is not part of
|
|
73
|
+
> Structured Doc Editor. Check the current directory before invoking the CLI
|
|
74
|
+
> when an agent may be operating in more than one project.
|
|
75
|
+
|
|
76
|
+
Use a global install only when that scope was explicitly requested:
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
npm install --global "$env:SDOC_CLI_TGZ_URL"
|
|
80
|
+
npm list --global sdoc-editor-cli --depth=0
|
|
81
|
+
sdoc --version
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The remaining examples use `sdoc` for readability. For a local installation,
|
|
85
|
+
replace it with `node ./node_modules/sdoc-editor-cli/dist/sdoc.js`. Do not use
|
|
86
|
+
`npx sdoc` or `npx --no-install sdoc` as a local-presence check.
|
|
87
|
+
|
|
88
|
+
## Help and output
|
|
89
|
+
|
|
90
|
+
```powershell
|
|
91
|
+
sdoc --help
|
|
92
|
+
sdoc help apply
|
|
93
|
+
sdoc inspect --help
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
JSON is the default and stable machine-readable output. `--json` states that
|
|
97
|
+
choice explicitly. `--human` provides concise interactive output and is not a
|
|
98
|
+
stable machine API; its wording and layout may change between releases. Never
|
|
99
|
+
parse human output in an Agent or script. Success is written to stdout;
|
|
100
|
+
structured errors are written to stderr.
|
|
101
|
+
|
|
102
|
+
Every JSON success and failure is one line with the top-level contract
|
|
103
|
+
`sdoc.cli.response/1`. The packaged schema is
|
|
104
|
+
`dist/schemas/sdoc.cli.response.schema.json`. Consumers must ignore unknown
|
|
105
|
+
fields: compatible updates may add fields, commands, operations, diagnostics,
|
|
106
|
+
and capability values, while existing fields retain their documented meaning.
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{"contract":"sdoc.cli.response/1","ok":true,"command":"validate","revision":"sha256:..."}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Failures always include `category` and a non-empty `diagnostics` array:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{"contract":"sdoc.cli.response/1","ok":false,"category":"argument","diagnostics":[{"code":"CLI_MISSING_DOCUMENT","message":"..."}]}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Commands
|
|
119
|
+
|
|
120
|
+
### `capabilities`
|
|
121
|
+
|
|
122
|
+
Reports the installed CLI version, supported contracts and commands, semantic
|
|
123
|
+
operation names, safety limits, read projections/catalog kinds, and built-in
|
|
124
|
+
template IDs. It does not accept a document path. JSON is the default:
|
|
125
|
+
|
|
126
|
+
```powershell
|
|
127
|
+
sdoc capabilities
|
|
128
|
+
sdoc capabilities --json
|
|
129
|
+
sdoc capabilities --human
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"contract": "sdoc.cli.response/1",
|
|
135
|
+
"ok": true,
|
|
136
|
+
"command": "capabilities",
|
|
137
|
+
"cliVersion": "0.9.2",
|
|
138
|
+
"contracts": {
|
|
139
|
+
"document": "sdoc/1.0",
|
|
140
|
+
"operations": "sdoc.operations/1",
|
|
141
|
+
"read": "sdoc.read/1",
|
|
142
|
+
"response": "sdoc.cli.response/1"
|
|
143
|
+
},
|
|
144
|
+
"commands": ["capabilities", "inspect", "validate", "apply", "rename-heading", "set-document-title", "create"],
|
|
145
|
+
"projections": ["catalog", "target", "section", "document"],
|
|
146
|
+
"catalogKinds": ["blocks", "outline", "references", "referenceables"],
|
|
147
|
+
"builtInTemplateIds": ["builtin:blank", "builtin:technical-report", "builtin:design-specification", "builtin:verification-report"]
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The actual result also includes `semanticOperations` and numeric `limits`;
|
|
152
|
+
query it instead of hard-coding the installed package's capabilities.
|
|
153
|
+
|
|
154
|
+
### `inspect`
|
|
155
|
+
|
|
156
|
+
Without `--projection`, returns the existing inspector shape: the SHA-256
|
|
157
|
+
revision of the exact source bytes, metadata, outline, references,
|
|
158
|
+
referenceable nodes, targetable blocks, and an optional selected target. The
|
|
159
|
+
revision includes a UTF-8 BOM when present and changes after
|
|
160
|
+
representation-only edits. Existing no-projection calls retain this behavior.
|
|
161
|
+
|
|
162
|
+
```powershell
|
|
163
|
+
sdoc inspect document.sdoc --json
|
|
164
|
+
sdoc inspect document.sdoc --target-id intro --json
|
|
165
|
+
sdoc inspect document.sdoc --target-path /1/0 --json
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Use the returned `revision`, IDs, paths, node types, and digests to construct an
|
|
169
|
+
operation request. Top-level `metadata` reports the current title, author,
|
|
170
|
+
version, timestamps, and document setting overrides. `--target-path` uses a
|
|
171
|
+
slash-delimited content path. For example, `/1/0` selects
|
|
172
|
+
`doc.content[1].content[0]`. A selected result has this shape:
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"target": {
|
|
177
|
+
"path": [1, 0],
|
|
178
|
+
"node": { "type": "paragraph" },
|
|
179
|
+
"digest": "sha256:...",
|
|
180
|
+
"operationTarget": {
|
|
181
|
+
"kind": "snapshot",
|
|
182
|
+
"path": [1, 0],
|
|
183
|
+
"nodeType": "paragraph",
|
|
184
|
+
"digest": "sha256:..."
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Copy `target.operationTarget` directly instead of assembling a snapshot
|
|
191
|
+
target. Every `blocks[]` entry also includes its canonical `operationTarget`.
|
|
192
|
+
Referenceable nodes receive an ID target; other blocks receive a snapshot
|
|
193
|
+
target. A provisional ID is valid only for that inspected revision; applying
|
|
194
|
+
it persists the ID.
|
|
195
|
+
|
|
196
|
+
An explicit `--projection` selects the additive bounded `sdoc.read/1`
|
|
197
|
+
contract. Projected JSON still uses `contract: "sdoc.cli.response/1"` as its
|
|
198
|
+
top-level response discriminator and exposes the core discriminator separately
|
|
199
|
+
as `readContract: "sdoc.read/1"`. The core `projection`, `revision`, `data`,
|
|
200
|
+
`page`, and `budget` fields remain top-level. Validate request objects against
|
|
201
|
+
`dist/schemas/sdoc.read.schema.json` when another host calls the shared core
|
|
202
|
+
directly.
|
|
203
|
+
|
|
204
|
+
```powershell
|
|
205
|
+
sdoc inspect document.sdoc --projection catalog --json
|
|
206
|
+
sdoc inspect document.sdoc --projection catalog --catalog outline --limit 100 --json
|
|
207
|
+
sdoc inspect document.sdoc --projection target --target-id intro --max-bytes 262144 --max-nodes 1000 --json
|
|
208
|
+
sdoc inspect document.sdoc --projection section --target-path /0 --max-nodes 500 --human
|
|
209
|
+
sdoc inspect document.sdoc --projection document --max-bytes 262144 --max-nodes 1000 --json
|
|
210
|
+
sdoc inspect document.sdoc --projection document --cursor $nextCursor --json
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Projection option rules are strict and are checked before the document is
|
|
214
|
+
read:
|
|
215
|
+
|
|
216
|
+
| Projection | Target | Allowed read options |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `catalog` | none | `--catalog`, `--limit`, `--cursor`, `--max-bytes`, `--max-summary-length`, `--expected-revision` |
|
|
219
|
+
| `target` | exactly one of ID/path | `--max-bytes`, `--max-nodes`, `--expected-revision` |
|
|
220
|
+
| `section` | exactly one of ID/path | `--cursor`, `--max-bytes`, `--max-nodes`, `--expected-revision` |
|
|
221
|
+
| `document` | none | `--cursor`, `--max-bytes`, `--max-nodes`, `--expected-revision` |
|
|
222
|
+
|
|
223
|
+
The catalog defaults to `blocks`. `--limit`, `--max-bytes`, `--max-nodes`,
|
|
224
|
+
and `--max-summary-length` accept canonical positive base-10 integers. Read
|
|
225
|
+
options other than legacy `--target-id` and `--target-path` require an explicit
|
|
226
|
+
projection. Catalog, section, and document results can return
|
|
227
|
+
`page.nextCursor`; pass it back with the same projection/query until
|
|
228
|
+
`page.complete` is true. Cursors bind the exact source bytes and query scope.
|
|
229
|
+
They are opaque integrity tokens, not authentication credentials.
|
|
230
|
+
|
|
231
|
+
### `validate`
|
|
232
|
+
|
|
233
|
+
Checks the persisted document contract and semantic invariants without writing:
|
|
234
|
+
|
|
235
|
+
```powershell
|
|
236
|
+
sdoc validate document.sdoc --json
|
|
237
|
+
sdoc validate legacy.tiptap.json --human
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### `apply`
|
|
241
|
+
|
|
242
|
+
Reads a complete `sdoc.operations/1` request from a UTF-8 JSON file or stdin.
|
|
243
|
+
Malformed UTF-8 is rejected before JSON parsing, locking, or document writes.
|
|
244
|
+
A UTF-8 BOM and non-ASCII JSON content are accepted. Preview is the default;
|
|
245
|
+
only `--write` can modify the named document.
|
|
246
|
+
|
|
247
|
+
```powershell
|
|
248
|
+
sdoc apply document.sdoc --operations operations.json --json
|
|
249
|
+
sdoc apply document.sdoc --operations operations.json --dry-run --json
|
|
250
|
+
sdoc apply document.sdoc --operations operations.json --write --json
|
|
251
|
+
Get-Content -Raw -Encoding utf8 operations.json |
|
|
252
|
+
sdoc apply document.sdoc --operations - --write --json
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`--write` takes a sibling lock, re-reads the file, verifies its revision, and
|
|
256
|
+
atomically replaces it. A no-op is not written. Do not combine `--write` and
|
|
257
|
+
`--dry-run`.
|
|
258
|
+
|
|
259
|
+
The sibling `<document>.lock` records structured owner metadata with a format
|
|
260
|
+
version, process ID, random ownership token, hostname, and creation time. If a
|
|
261
|
+
write finds an existing lock, the CLI reclaims it automatically only when all
|
|
262
|
+
of these conditions are true:
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
{"version":1,"pid":1234,"token":"0123456789abcdef0123456789abcdef","hostname":"workstation","createdAt":"2026-08-06T12:00:00.000Z"}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
- the metadata is recognized and belongs to the current host;
|
|
269
|
+
- the owner process can be conclusively shown to have exited; and
|
|
270
|
+
- the lock is at least 60 seconds old.
|
|
271
|
+
|
|
272
|
+
Recovery atomically moves the stale lock aside, verifies that its owner did not
|
|
273
|
+
change during the move, and then retries normal exclusive acquisition. The
|
|
274
|
+
document is re-read and its revision is checked only after the new lock is
|
|
275
|
+
owned, and the CLI re-checks its ownership token before atomic publication.
|
|
276
|
+
|
|
277
|
+
Live owners, owners on another host, and legacy, malformed, or unsupported
|
|
278
|
+
metadata are never removed automatically. Wait for a known writer to finish.
|
|
279
|
+
For an abandoned lock that cannot be reclaimed automatically, remove the
|
|
280
|
+
`.lock` file manually only after confirming no writer is active on any reported
|
|
281
|
+
host, then re-inspect the document and rebuild the operation from its current
|
|
282
|
+
revision before retrying `--write`.
|
|
283
|
+
|
|
284
|
+
### `rename-heading`
|
|
285
|
+
|
|
286
|
+
Convenience command for a single `renameHeading` operation:
|
|
287
|
+
|
|
288
|
+
```powershell
|
|
289
|
+
$inspection = sdoc inspect document.sdoc --json | ConvertFrom-Json
|
|
290
|
+
sdoc rename-heading document.sdoc --id intro --title "Updated heading" `
|
|
291
|
+
--expected-revision $inspection.revision --json
|
|
292
|
+
sdoc rename-heading document.sdoc --id intro --title "Updated heading" `
|
|
293
|
+
--expected-revision $inspection.revision --write --json
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The preview and a later independent write can have different
|
|
297
|
+
`outputRevision` values because each semantic change supplies a new
|
|
298
|
+
`meta.modified` time. Always treat the write result as authoritative.
|
|
299
|
+
|
|
300
|
+
### `set-document-title`
|
|
301
|
+
|
|
302
|
+
Convenience command for one `setDocumentTitle` operation. `--title` and
|
|
303
|
+
`--expected-revision` are required. Without `--id`, the command changes only
|
|
304
|
+
`meta.title`. With the persistent or provisional ID of an H1, it changes
|
|
305
|
+
`meta.title` and that explicit title heading atomically. The CLI never guesses
|
|
306
|
+
a title heading and never renames the file.
|
|
307
|
+
|
|
308
|
+
```powershell
|
|
309
|
+
$inspection = sdoc inspect document.sdoc --json | ConvertFrom-Json
|
|
310
|
+
sdoc set-document-title document.sdoc --title "Metadata title" `
|
|
311
|
+
--expected-revision $inspection.revision --write --json
|
|
312
|
+
$inspection = sdoc inspect document.sdoc --json | ConvertFrom-Json
|
|
313
|
+
sdoc set-document-title document.sdoc --title "Updated document" --id title-h1 `
|
|
314
|
+
--expected-revision $inspection.revision --write --json
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Use `--discard-formatting` only when replacing marked or non-text content in
|
|
318
|
+
the selected H1 is intentional. It requires `--id`; without an H1 target there
|
|
319
|
+
is no formatting to discard.
|
|
320
|
+
|
|
321
|
+
### `create`
|
|
322
|
+
|
|
323
|
+
Creates a schema-valid `.sdoc` without overwriting an existing path. The
|
|
324
|
+
default template is `builtin:blank`; the default title is the output filename.
|
|
325
|
+
|
|
326
|
+
```powershell
|
|
327
|
+
sdoc create report.sdoc --title "Quarterly Report" --json
|
|
328
|
+
sdoc create report.sdoc --template builtin:technical-report --dry-run --json
|
|
329
|
+
sdoc create design.sdoc --template builtin:design-specification --json
|
|
330
|
+
sdoc create verification.sdoc --template builtin:verification-report --json
|
|
331
|
+
sdoc create report.sdoc --template .\templates\company-report.sdoc --json
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
An explicit file template must be a valid UTF-8 JSON `.sdoc`; malformed UTF-8
|
|
335
|
+
is rejected before a destination is created. A UTF-8 BOM is accepted. Creation
|
|
336
|
+
removes persisted document identity and template-only metadata while
|
|
337
|
+
preserving supported settings, node IDs, and links.
|
|
338
|
+
|
|
339
|
+
## Public schemas and operation contract
|
|
340
|
+
|
|
341
|
+
The package includes:
|
|
342
|
+
|
|
343
|
+
- `dist/schemas/sdoc.operations.schema.json`: draft-07 request schema
|
|
344
|
+
- `dist/schemas/sdoc.read.schema.json`: draft-07 `sdoc.read/1` request union
|
|
345
|
+
- `dist/schemas/sdoc.cli.response.schema.json`: additive CLI JSON response schema
|
|
346
|
+
- `dist/schemas/sdoc.schema.json`: persisted document schema and reusable
|
|
347
|
+
`anyNode` fragment
|
|
348
|
+
- `dist/examples/operations/*.json`: one request for each semantic operation
|
|
349
|
+
|
|
350
|
+
Repository copies live at `sdoc.operations.schema.json`,
|
|
351
|
+
`sdoc.read.schema.json`, `sdoc.schema.json`,
|
|
352
|
+
`cli/schemas/sdoc.cli.response.schema.json`, and `examples/operations/`.
|
|
353
|
+
|
|
354
|
+
Every request has this envelope:
|
|
355
|
+
|
|
356
|
+
```json
|
|
357
|
+
{
|
|
358
|
+
"contract": "sdoc.operations/1",
|
|
359
|
+
"expected": {
|
|
360
|
+
"revision": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
361
|
+
},
|
|
362
|
+
"operations": []
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
In published examples, the all-zero revision means "replace with the exact
|
|
367
|
+
`revision` returned by `inspect`." The all-one snapshot digest means "replace
|
|
368
|
+
with that block's exact `digest` returned by the same inspection." IDs ending
|
|
369
|
+
in `-from-inspect` are also placeholders. These values are syntactically valid
|
|
370
|
+
so schema tools can validate every example, but they are not usable against a
|
|
371
|
+
real document until replaced.
|
|
372
|
+
|
|
373
|
+
Do not add `expected.documentId` in CLI requests. Although inspection may
|
|
374
|
+
report a document ID, the file-only CLI cannot establish a trusted external
|
|
375
|
+
document identity and rejects that precondition as unverifiable. Revision is
|
|
376
|
+
the CLI concurrency contract.
|
|
377
|
+
|
|
378
|
+
### Targets
|
|
379
|
+
|
|
380
|
+
Referenceable `heading`, `image`, `table`, and `mathBlock` nodes use persistent
|
|
381
|
+
or revision-scoped provisional IDs:
|
|
382
|
+
|
|
383
|
+
```json
|
|
384
|
+
{ "kind": "id", "id": "intro", "expectedType": "heading" }
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Other mutable blocks, including `paragraph`, `codeBlock`, and `diagram`, use a
|
|
388
|
+
protected snapshot locator:
|
|
389
|
+
|
|
390
|
+
```json
|
|
391
|
+
{
|
|
392
|
+
"kind": "snapshot",
|
|
393
|
+
"path": [1],
|
|
394
|
+
"nodeType": "paragraph",
|
|
395
|
+
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111"
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Prefer the ready-to-use `operationTarget` returned by `inspect` over copying
|
|
400
|
+
these fields manually. A `diagram` is snapshot-targeted; it is not a
|
|
401
|
+
persistent-ID node.
|
|
402
|
+
|
|
403
|
+
Snapshot targets and provisional IDs are revision-scoped. Re-inspect after any
|
|
404
|
+
source-byte change. A batch resolves all targets before applying its first
|
|
405
|
+
operation, so earlier operations cannot redirect later targets.
|
|
406
|
+
|
|
407
|
+
Destinations are `{ "position": "before"|"after", "target": ... }` or
|
|
408
|
+
`{ "position": "section-end", "target": ... }`. `section-end` targets a
|
|
409
|
+
heading and appends inside that section.
|
|
410
|
+
|
|
411
|
+
### The 14 operations
|
|
412
|
+
|
|
413
|
+
Each operation below has a complete file in `dist/examples/operations/`:
|
|
414
|
+
|
|
415
|
+
| Operation | Required fields | Purpose |
|
|
416
|
+
|---|---|---|
|
|
417
|
+
| `renameHeading` | `target`, `title` | Rename a heading; optional `discardFormatting` permits replacing rich heading content |
|
|
418
|
+
| `insertBlock` | `destination`, `block` | Insert a non-heading Tiptap block |
|
|
419
|
+
| `insertSection` | `target`, `title` | Insert a child section by default, or a same-level sibling with `position: "before"|"after"`; optional `id` and `blocks` |
|
|
420
|
+
| `replaceBlock` | `target`, `block` | Replace a block with the same node type while preserving identity |
|
|
421
|
+
| `updateBlockAttrs` | `target`, `attrs` | Merge block attributes |
|
|
422
|
+
| `moveBlock` | `target`, `destination` | Move a non-heading block |
|
|
423
|
+
| `deleteBlock` | `target` | Delete a non-heading block |
|
|
424
|
+
| `moveSection` | `target`, `destination` | Move a heading and its complete descendant section |
|
|
425
|
+
| `deleteSection` | `target` | Delete a heading and its complete descendant section |
|
|
426
|
+
| `setHeadingLevel` | `target`, `level` | Set a heading level and shift descendant headings by the same delta while preserving IDs |
|
|
427
|
+
| `renameBlockId` | `target`, `newId` | Rename a heading or table ID and rewrite matching internal links |
|
|
428
|
+
| `setDocumentTitle` | `title` | Set `meta.title`; optional `headingTarget` atomically updates an explicit H1 |
|
|
429
|
+
| `updateDocumentMetadata` | `patch` | Set or remove (`null`) the allowed `author` and `version` fields |
|
|
430
|
+
| `updateDocumentSettings` | `patch` | Set or remove (`null`) portable document setting overrides |
|
|
431
|
+
|
|
432
|
+
Operations are validated and applied atomically as one batch. Headings must be
|
|
433
|
+
moved or deleted with section operations. `created`, `modified`, document
|
|
434
|
+
identity, template metadata, arbitrary metadata, and filenames cannot be
|
|
435
|
+
changed by these operations. Portable settings are:
|
|
436
|
+
`headingNumbering`, `headingDecoration`, `headingH1Color` through
|
|
437
|
+
`headingH6Color`, `captionStyle`, `captionNumbering`, `equationNumbering`,
|
|
438
|
+
`crossRefIncludeCaption`, `pdfScale`, `selfContained`, `slideBreakLevel`,
|
|
439
|
+
`slideTransition`, and `showTitleSlide`. Local path settings
|
|
440
|
+
`slideCssPath`, `htmlCssPath`, and `outputDir` are deliberately excluded.
|
|
441
|
+
|
|
442
|
+
`insertSection` keeps its existing child behavior when `position` is omitted or
|
|
443
|
+
set to `"child"`: the new heading is one level deeper and is appended at the
|
|
444
|
+
target section boundary. Set `position` to `"before"` or `"after"` to insert a
|
|
445
|
+
same-level sibling before the target heading or after its complete descendant
|
|
446
|
+
section. This is the supported CLI route for building several peer H1 sections
|
|
447
|
+
without editing raw JSON; the new heading receives the target heading's level
|
|
448
|
+
and its requested persistent ID.
|
|
449
|
+
|
|
450
|
+
`setHeadingLevel` changes an existing heading to level 1-6 without changing its
|
|
451
|
+
persistent ID. Every descendant heading in that section moves by the same
|
|
452
|
+
level delta, preserving the section's relative hierarchy; the operation is
|
|
453
|
+
rejected with `INVALID_HEADING_LEVEL` or `SECTION_LEVEL_OUT_OF_RANGE` when the
|
|
454
|
+
requested level or any resulting descendant level falls outside 1-6. Inspect
|
|
455
|
+
again after the write to confirm the resulting outline paths and parentage.
|
|
456
|
+
|
|
457
|
+
`renameBlockId` requires an existing ID target and a unique non-empty `newId`.
|
|
458
|
+
It preserves the node and heading level, updates internal `#old-id` links in the
|
|
459
|
+
same atomic batch, and rejects duplicate IDs. A newly assigned ID cannot be used
|
|
460
|
+
as another target in that same request because all operation targets are
|
|
461
|
+
resolved from the inspected input revision before mutation begins.
|
|
462
|
+
|
|
463
|
+
### One inspection, one atomic batch
|
|
464
|
+
|
|
465
|
+
Do not inspect once per operation. One revision can guard a batch of up to 100
|
|
466
|
+
operations. This example inspects once, prepares three document-level changes,
|
|
467
|
+
previews them, and then writes the same batch:
|
|
468
|
+
|
|
469
|
+
```powershell
|
|
470
|
+
$inspection = sdoc inspect document.sdoc --target-id title-h1 --json |
|
|
471
|
+
ConvertFrom-Json
|
|
472
|
+
$request = [ordered]@{
|
|
473
|
+
contract = 'sdoc.operations/1'
|
|
474
|
+
expected = @{ revision = $inspection.revision }
|
|
475
|
+
operations = @(
|
|
476
|
+
@{
|
|
477
|
+
op = 'setDocumentTitle'
|
|
478
|
+
title = 'Release Plan'
|
|
479
|
+
headingTarget = $inspection.target.operationTarget
|
|
480
|
+
}
|
|
481
|
+
@{ op = 'updateDocumentMetadata'; patch = @{ author = 'Documentation Team'; version = '2.0' } }
|
|
482
|
+
@{ op = 'updateDocumentSettings'; patch = @{ headingNumbering = $true; captionStyle = 'modern' } }
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
$request | ConvertTo-Json -Depth 100 |
|
|
486
|
+
Set-Content -Encoding utf8 operations.json
|
|
487
|
+
sdoc apply document.sdoc --operations operations.json --json
|
|
488
|
+
sdoc apply document.sdoc --operations operations.json --write --json
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
All targets in a mixed content/metadata batch must come from that same
|
|
492
|
+
inspection. Re-inspect after a successful write before preparing another
|
|
493
|
+
batch; no re-inspection is needed between operations inside one batch.
|
|
494
|
+
|
|
495
|
+
### Supported node and target catalog
|
|
496
|
+
|
|
497
|
+
The packaged `sdoc.schema.json` is authoritative. This concise catalog covers
|
|
498
|
+
the operation-relevant node types and required attributes:
|
|
499
|
+
|
|
500
|
+
| Nodes | Required attributes | Target kind and notes |
|
|
501
|
+
|---|---|---|
|
|
502
|
+
| `heading` | `attrs.level` (1-6) | ID target; rename with `renameHeading`, and move/delete as a complete section |
|
|
503
|
+
| `paragraph`, `blockquote`, `bulletList`, `orderedList`, `taskList`, `taskItem` | None | Snapshot target |
|
|
504
|
+
| `codeBlock` | None (`attrs.language` optional) | Snapshot target |
|
|
505
|
+
| `table` | None | ID target |
|
|
506
|
+
| `tableCell`, `tableHeader` | None | Snapshot target; span, width, and alignment attrs are optional |
|
|
507
|
+
| `image` | None | ID target; a new `src`, when present, must be portable |
|
|
508
|
+
| `mathBlock` | `attrs.latex` | ID target |
|
|
509
|
+
| `diagram` | `attrs.language`, `attrs.code` | Snapshot target; stores source such as Mermaid, PlantUML, or D2 |
|
|
510
|
+
| `horizontalRule`, `hardBreak`, `callout` | None | Snapshot target; callout `variant` is optional |
|
|
511
|
+
| `listItem`, `tableRow` | None | Structural container, not an operation block target |
|
|
512
|
+
| `text`, `mathInline` | `mathInline.attrs.latex` only | Inline content, not an operation target |
|
|
513
|
+
|
|
514
|
+
`updateBlockAttrs` accepts only the attrs defined for that node type.
|
|
515
|
+
`replaceBlock` must preserve the node type, while headings require the
|
|
516
|
+
heading/section operations.
|
|
517
|
+
|
|
518
|
+
Portable image assets use `./images/...`. Draw.io content is an `image` node
|
|
519
|
+
whose `src` is under `./drawio/` and ends in `.drawio.svg`; it is not a
|
|
520
|
+
`diagram` node. The CLI validates document structure and portable references
|
|
521
|
+
but does not render diagrams, create or copy asset files, or fetch assets from
|
|
522
|
+
the network.
|
|
523
|
+
|
|
524
|
+
### Diagram authoring and host rendering
|
|
525
|
+
|
|
526
|
+
For CLI and AI-operator workflows, the source of truth is the `diagram` node:
|
|
527
|
+
`diagram.attrs.language` plus `diagram.attrs.code`. A schema-valid node with
|
|
528
|
+
those attributes is sufficient to author and preserve a diagram in `.sdoc`;
|
|
529
|
+
the CLI owns structural validation and source preservation, not rendering.
|
|
530
|
+
|
|
531
|
+
Rendering belongs to the Structured Doc Editor host/viewer:
|
|
532
|
+
|
|
533
|
+
- Mermaid renders locally in the host.
|
|
534
|
+
- PlantUML, D2, and Graphviz use the host's online preview path only after the
|
|
535
|
+
user grants the required first-use consent.
|
|
536
|
+
- If consent is declined or rendering is unavailable, the diagram source
|
|
537
|
+
remains valid and preserved in the document.
|
|
538
|
+
|
|
539
|
+
Do not install local D2, Graphviz, PlantUML, or other renderers merely because
|
|
540
|
+
the CLI does not render a diagram. Local renderers are optional tools outside
|
|
541
|
+
the CLI authoring contract; AI operators should create or update the diagram
|
|
542
|
+
source node and leave rendering to the host unless the user explicitly asks
|
|
543
|
+
for a separate local-renderer workflow.
|
|
544
|
+
|
|
545
|
+
## Legacy documents
|
|
546
|
+
|
|
547
|
+
Legacy raw Tiptap JSON can be inspected and validated without an upgrade flag.
|
|
548
|
+
Every mutation, including preview, requires `--upgrade-legacy`. Persisting the
|
|
549
|
+
in-place envelope upgrade additionally requires `--write`:
|
|
550
|
+
|
|
551
|
+
```powershell
|
|
552
|
+
sdoc apply legacy.tiptap.json --operations operations.json --upgrade-legacy --json
|
|
553
|
+
sdoc apply legacy.tiptap.json --operations operations.json --upgrade-legacy --write --json
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
This changes the named file in place to an SDOC envelope but does not rename
|
|
557
|
+
its `.tiptap.json` extension. Back up or copy the file to a `.sdoc` path first
|
|
558
|
+
when preserving the legacy filename matters.
|
|
559
|
+
|
|
560
|
+
## PowerShell automation
|
|
561
|
+
|
|
562
|
+
Write non-ASCII JSON explicitly as UTF-8 and keep stdout separate from stderr:
|
|
563
|
+
|
|
564
|
+
```powershell
|
|
565
|
+
$request | ConvertTo-Json -Depth 100 |
|
|
566
|
+
Set-Content -Encoding utf8 operations.json
|
|
567
|
+
|
|
568
|
+
$resultJson = sdoc apply document.sdoc --operations operations.json --json 2>error.json
|
|
569
|
+
if ($LASTEXITCODE -ne 0) {
|
|
570
|
+
$errorResult = Get-Content -Raw -Encoding utf8 error.json | ConvertFrom-Json
|
|
571
|
+
throw "$($errorResult.diagnostics[0].code): $($errorResult.diagnostics[0].message)"
|
|
572
|
+
}
|
|
573
|
+
$result = $resultJson | ConvertFrom-Json
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
## Exit codes
|
|
577
|
+
|
|
578
|
+
| Code | Meaning | Representative diagnostic |
|
|
579
|
+
|---:|---|---|
|
|
580
|
+
| 0 | Success | No diagnostic |
|
|
581
|
+
| 2 | CLI argument or operation request error | `CLI_CONFLICTING_OPTIONS`, `CLI_MISSING_OPERATIONS` |
|
|
582
|
+
| 3 | Document, template, invariant, or legacy-upgrade error | `LEGACY_UPGRADE_REQUIRED` |
|
|
583
|
+
| 4 | Stale revision or precondition conflict | `STALE_REVISION` |
|
|
584
|
+
| 5 | File I/O error | `CLI_READ_FAILED` |
|
|
585
|
+
|
|
586
|
+
On failure, inspect `diagnostics[].code` rather than matching human-readable
|
|
587
|
+
messages. The machine-readable `category` is one of `argument`, `document`,
|
|
588
|
+
`conflict`, `io`, or `internal`. Categories describe the failure source and do
|
|
589
|
+
not replace exit codes; in particular, an `IoError` is categorized as `io`
|
|
590
|
+
while retaining its existing exit code.
|
|
591
|
+
|
|
592
|
+
## Diagnostic recovery
|
|
593
|
+
|
|
594
|
+
Diagnostic messages are explanatory text, not a parsing contract. Automations
|
|
595
|
+
must branch on `diagnostics[].code` from explicit `--json` output.
|
|
596
|
+
|
|
597
|
+
| Diagnostic code(s) | Likely cause | Recovery |
|
|
598
|
+
|---|---|---|
|
|
599
|
+
| `CLI_UNKNOWN_*`, `CLI_MISSING_*`, `CLI_CONFLICTING_OPTIONS`, `CLI_OPTION_REQUIRES_ID` | Misspelled command/flag, omitted value, or incompatible/dependent flags | Run the command-specific `--help`, correct the invocation, and retry |
|
|
600
|
+
| `CLI_INVALID_TARGET_PATH` | `--target-path` is not a slash-delimited non-negative integer path | Copy the path from `inspect.blocks[].path` and format it like `/1/0` |
|
|
601
|
+
| `CLI_INVALID_POSITIVE_INTEGER`, `CLI_INVALID_PROJECTION`, `CLI_INVALID_CATALOG` | A projected read option has a malformed number or unsupported selector | Use `sdoc inspect --help` and pass a documented projection/catalog and canonical positive integer |
|
|
602
|
+
| `CLI_PROJECTION_REQUIRED`, `CLI_PROJECTION_REQUIRES_TARGET`, `CLI_PROJECTION_FORBIDS_TARGET`, `CLI_PROJECTION_OPTION_NOT_SUPPORTED` | Projected read flags are missing their projection, target, or valid projection-specific combination | Follow the projection option matrix above; legacy targets remain valid only when no read-only flags are supplied |
|
|
603
|
+
| `CLI_INVALID_UTF8` | Operation file or stdin bytes are not valid UTF-8 | Re-encode the complete JSON request as UTF-8 and retry; the document and sibling lock are untouched |
|
|
604
|
+
| `CLI_INVALID_JSON`, `INVALID_OPERATION_REQUEST`, `INVALID_OPERATION` | Malformed operations JSON or a request that does not match `sdoc.operations/1` | Validate against the packaged operation schema; inspect `operationIndex` when present |
|
|
605
|
+
| `MALFORMED_JSON`, `DOCUMENT_SCHEMA_INVALID`, `UNSUPPORTED_VERSION` | The input is not valid UTF-8 JSON, violates the document schema, or uses an unsupported SDOC version | Repair or migrate the source; do not force a write |
|
|
606
|
+
| `LEGACY_UPGRADE_REQUIRED` | A legacy `.tiptap.json` mutation omitted the explicit upgrade flag | Re-run with `--upgrade-legacy`, preview first, then add `--write` if intended |
|
|
607
|
+
| `STALE_REVISION` | The document bytes changed after inspection | Re-inspect the current file and rebuild the whole request from that revision |
|
|
608
|
+
| `INVALID_READ_CURSOR`, `STALE_READ_CURSOR`, `READ_CURSOR_SCOPE_MISMATCH` | A cursor is malformed, the exact source bytes changed, or it belongs to another projection/query | Restart the projection from its first page using the current document; never edit or reuse a cursor across queries |
|
|
609
|
+
| `PROJECTION_ITEM_TOO_LARGE` | The next complete catalog entry or subtree cannot fit the requested byte/node budget | Increase the reported limiting budget; projection pages never split a complete item |
|
|
610
|
+
| `TARGET_NOT_FOUND`, `TARGET_NOT_BLOCK`, `TARGET_TYPE_MISMATCH`, `TARGET_DIGEST_MISMATCH` | A selected path/ID is absent, is not a block, changed type, or no longer matches its snapshot | Re-inspect and use the returned `operationTarget`; do not weaken the precondition |
|
|
611
|
+
| `SECTION_OPERATION_REQUIRED`, `HEADING_TARGET_REQUIRED`, `SECTION_TARGET_REQUIRED`, `TITLE_H1_TARGET_REQUIRED` | A block operation was used for a heading, or a title target was not H1 | Use the matching heading/section operation and an inspected heading target |
|
|
612
|
+
| `INVALID_HEADING_LEVEL`, `SECTION_LEVEL_OUT_OF_RANGE` | A requested heading level is outside 1-6, or shifting the section would push a descendant outside that range | Choose a valid target level that keeps every descendant heading within 1-6 |
|
|
613
|
+
| `FORMATTED_HEADING` | Replacing a rich heading would discard marks or inline nodes | Preserve it, or explicitly use `discardFormatting` / `--discard-formatting` |
|
|
614
|
+
| `ATTRIBUTE_NOT_ALLOWED`, `NODE_TYPE_CHANGE` | An attr is not allowed for the node, or replacement changes its type | Consult the node catalog/schema and keep replacements type-compatible |
|
|
615
|
+
| `ID_RENAME_NOT_SUPPORTED`, `ID_RENAME_REQUIRES_EXISTING_ID`, `INVALID_NEW_ID` | An ID rename targeted an unsupported or provisional node, or supplied an invalid new ID | Target an inspected heading/table with an existing persistent ID and choose a unique non-reserved ID |
|
|
616
|
+
| `NEW_NONPORTABLE_ASSET`, `NEW_DANGLING_REFERENCE`, `NEW_UNSAFE_LINK` | The batch introduces an invalid asset path, missing internal target, or unsafe link | Use `./images/...` or `./drawio/*.drawio.svg`, create referenced IDs, and use a safe URL |
|
|
617
|
+
| `DUPLICATE_ID` | The document contains conflicting persistent IDs | Assign unique IDs before retrying |
|
|
618
|
+
| `CLI_TEMPLATE_INVALID` | An explicit template has malformed UTF-8, invalid JSON, or violates the template contract | Repair or re-encode the template as UTF-8; creation leaves the destination absent |
|
|
619
|
+
| `CLI_TARGET_EXISTS` | `create` would overwrite an existing file | Choose a new path; the CLI never overwrites during creation |
|
|
620
|
+
| `CLI_LOCK_UNAVAILABLE` | Another writer holds the sibling lock, or its owner cannot be reclaimed safely | Wait for a known writer to finish. Remove an abandoned lock manually only after confirming no writer is active, then re-inspect before retrying |
|
|
621
|
+
| `CLI_READ_FAILED`, `CLI_ATOMIC_WRITE_FAILED` | Filesystem access or atomic replacement failed | Check path, permissions, free space, and filesystem support; verify the file before retrying |
|
|
622
|
+
|
|
623
|
+
Warnings such as `NONPORTABLE_ASSET`, `DANGLING_REFERENCE`, `UNSAFE_LINK`, and
|
|
624
|
+
`LEGACY_FILE_EXTENSION_RETAINED` describe pre-existing or retained conditions.
|
|
625
|
+
They do not authorize a later mutation to add or increase the same violation.
|
package/bin/sdoc.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contract": "sdoc.operations/1",
|
|
3
|
+
"expected": {
|
|
4
|
+
"revision": "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
|
5
|
+
},
|
|
6
|
+
"operations": [
|
|
7
|
+
{
|
|
8
|
+
"op": "deleteBlock",
|
|
9
|
+
"target": {
|
|
10
|
+
"kind": "snapshot",
|
|
11
|
+
"path": [1],
|
|
12
|
+
"nodeType": "paragraph",
|
|
13
|
+
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|