sdoc-editor-cli 0.9.2 → 0.10.1

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/README.md CHANGED
@@ -1,98 +1,98 @@
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
-
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 official package is published to the public npm registry as
8
+ [`sdoc-editor-cli`](https://www.npmjs.com/package/sdoc-editor-cli). Its installed
9
+ command is `sdoc`. The separate registry package named `sdoc` is unrelated.
10
+
11
+ ## Install and run
12
+
13
+ Try the current stable release without adding a project dependency:
14
+
15
+ ```powershell
16
+ npx sdoc-editor-cli@latest --help
17
+ ```
18
+
19
+ For a project-pinned version, install the official package as a development
20
+ dependency and invoke it by its exact package name:
21
+
22
+ ```powershell
23
+ npm install --save-dev sdoc-editor-cli
24
+ npx sdoc-editor-cli --version
25
+ npx sdoc-editor-cli capabilities --json
26
+ ```
27
+
28
+ Update that dependency explicitly when the project is ready to adopt the
29
+ current stable release:
30
+
31
+ ```powershell
32
+ npm install --save-dev sdoc-editor-cli@latest
33
+ ```
34
+
35
+ For repeatable CI or Agent automation, commit `package.json` and the lockfile,
36
+ run `npm ci`, and add a project script:
37
+
38
+ ```json
39
+ {
40
+ "scripts": {
41
+ "sdoc": "sdoc"
42
+ }
43
+ }
44
+ ```
45
+
46
+ Pass CLI arguments after npm's `--` separator:
47
+
48
+ ```powershell
49
+ npm run sdoc -- capabilities --json
50
+ ```
51
+
52
+ `npm run` uses the project-local binary and fails when the dependency is
53
+ missing; it does not fetch a command from the registry.
54
+
55
+ > **Package name collision warning:** Use `sdoc-editor-cli` with `npx`. Do not
56
+ > run `npx sdoc` or install the public package named `sdoc`; it is unrelated to
57
+ > Structured Doc Editor.
58
+
59
+ The CLI does not replace itself automatically. Use the explicit npm update
60
+ command above, or let the owning project's dependency automation update its
61
+ manifest and lockfile.
62
+
63
+ ### Version-pinned GitHub Release fallback
64
+
65
+ Every tagged release also attaches a versioned `sdoc-editor-cli-*.tgz` to
66
+ [GitHub Releases](https://github.com/SWBaek/sdoc-editor/releases/latest). Use
67
+ this path when an installation must be pinned to a downloaded release asset.
68
+ After downloading the selected tarball, install it from the project that owns
69
+ the dependency:
70
+
71
+ ```powershell
72
+ npm install --save-dev .\sdoc-editor-cli-X.Y.Z.tgz
73
+ npx sdoc-editor-cli --version
74
+ ```
75
+
76
+ For frequent interactive use across projects, a global install provides the
77
+ short `sdoc` command:
78
+
79
+ ```powershell
80
+ npm install --global sdoc-editor-cli
81
+ sdoc --version
82
+ ```
83
+
84
+ The remaining examples use `sdoc` for readability. After a project-local
85
+ install, replace that prefix with `npx sdoc-editor-cli`, or use the `npm run
86
+ sdoc --` script above. A global installation can run `sdoc` directly.
87
+
88
+ ## Help and output
89
+
90
+ ```powershell
91
+ sdoc --help
92
+ sdoc help apply
93
+ sdoc inspect --help
94
+ ```
95
+
96
96
  JSON is the default and stable machine-readable output. `--json` states that
97
97
  choice explicitly. `--human` provides concise interactive output and is not a
98
98
  stable machine API; its wording and layout may change between releases. Never
@@ -134,7 +134,7 @@ sdoc capabilities --human
134
134
  "contract": "sdoc.cli.response/1",
135
135
  "ok": true,
136
136
  "command": "capabilities",
137
- "cliVersion": "0.9.2",
137
+ "cliVersion": "0.9.3",
138
138
  "contracts": {
139
139
  "document": "sdoc/1.0",
140
140
  "operations": "sdoc.operations/1",
@@ -158,35 +158,35 @@ revision of the exact source bytes, metadata, outline, references,
158
158
  referenceable nodes, targetable blocks, and an optional selected target. The
159
159
  revision includes a UTF-8 BOM when present and changes after
160
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
-
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
190
  Copy `target.operationTarget` directly instead of assembling a snapshot
191
191
  target. Every `blocks[]` entry also includes its canonical `operationTarget`.
192
192
  Referenceable nodes receive an ID target; other blocks receive a snapshot
@@ -227,85 +227,85 @@ projection. Catalog, section, and document results can return
227
227
  `page.nextCursor`; pass it back with the same projection/query until
228
228
  `page.complete` is true. Cursors bind the exact source bytes and query scope.
229
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
-
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
242
  Reads a complete `sdoc.operations/1` request from a UTF-8 JSON file or stdin.
243
243
  Malformed UTF-8 is rejected before JSON parsing, locking, or document writes.
244
244
  A UTF-8 BOM and non-ASCII JSON content are accepted. Preview is the default;
245
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
-
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
302
  Convenience command for one `setDocumentTitle` operation. `--title` and
303
303
  `--expected-revision` are required. Without `--id`, the command changes only
304
304
  `meta.title`. With the persistent or provisional ID of an H1, it changes
305
305
  `meta.title` and that explicit title heading atomically. The CLI never guesses
306
306
  a title heading and never renames the file.
307
-
308
- ```powershell
307
+
308
+ ```powershell
309
309
  $inspection = sdoc inspect document.sdoc --json | ConvertFrom-Json
310
310
  sdoc set-document-title document.sdoc --title "Metadata title" `
311
311
  --expected-revision $inspection.revision --write --json
@@ -317,25 +317,25 @@ sdoc set-document-title document.sdoc --title "Updated document" --id title-h1 `
317
317
  Use `--discard-formatting` only when replacing marked or non-text content in
318
318
  the selected H1 is intentional. It requires `--id`; without an H1 target there
319
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
-
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
334
  An explicit file template must be a valid UTF-8 JSON `.sdoc`; malformed UTF-8
335
335
  is rejected before a destination is created. A UTF-8 BOM is accepted. Creation
336
336
  removes persisted document identity and template-only metadata while
337
337
  preserving supported settings, node IDs, and links.
338
-
338
+
339
339
  ## Public schemas and operation contract
340
340
 
341
341
  The package includes:
@@ -350,276 +350,276 @@ The package includes:
350
350
  Repository copies live at `sdoc.operations.schema.json`,
351
351
  `sdoc.read.schema.json`, `sdoc.schema.json`,
352
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
-
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
586
  On failure, inspect `diagnostics[].code` rather than matching human-readable
587
587
  messages. The machine-readable `category` is one of `argument`, `document`,
588
588
  `conflict`, `io`, or `internal`. Categories describe the failure source and do
589
589
  not replace exit codes; in particular, an `IoError` is categorized as `io`
590
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
- |---|---|---|
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
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
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
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
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
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
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 |
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
607
  | `STALE_REVISION` | The document bytes changed after inspection | Re-inspect the current file and rebuild the whole request from that revision |
608
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
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 |
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
617
  | `DUPLICATE_ID` | The document contains conflicting persistent IDs | Assign unique IDs before retrying |
618
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.
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.