Skip to content

Commit 39fbea0

Browse files
committed
docs(document-api): soften metadata.remove atomicity wording + add story (SD-3104)
The metadata.remove contract claimed 'both succeed or neither does,' but the adapter writes to two separate state systems (ProseMirror doc + OOXML package) with no shared commit primitive. The adapter resolves the target up-front so the common failure mode lands before any state change, but a crash strictly between the two writes can leave a dangling payload. Wording now reflects that. Also adds tests/doc-api-stories/tests/metadata/all-commands.ts: a real Editor + DOCX round-trip story covering attach / list / get / resolve / update / remove. Goes through the SDK + CLI like the rest of the doc-api stories. Catches the gap that smoke unit tests cannot: that the adapter actually wraps a text range, writes the payload, and resolves the anchor back to a SelectionTarget on a live editor.
1 parent 4f81522 commit 39fbea0

7 files changed

Lines changed: 323 additions & 12 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1077,5 +1077,5 @@
10771077
}
10781078
],
10791079
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
1080-
"sourceHash": "c071807d15c2622572bb4828f566d9480d3a62698ad30f08341aeb9b4f15df1e"
1080+
"sourceHash": "97cf1fa7f05b40382d9839c0b60c7b97ccb7fddec642bc62383946ca76a75ee8"
10811081
}

apps/docs/document-api/reference/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,5 +641,5 @@ The tables below are grouped by namespace.
641641
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/metadata/list"><code>metadata.list</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.metadata.list(...)</code></span> | List anchored-metadata entries in the document, optionally filtered by consumer namespace and/or a `within` selection (returns only entries whose anchor overlaps `within`). |
642642
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/metadata/get"><code>metadata.get</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.metadata.get(...)</code></span> | Get a single anchored-metadata entry by id, including its JSON payload. |
643643
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/metadata/update"><code>metadata.update</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.metadata.update(...)</code></span> | Replace the JSON payload of an existing anchored-metadata entry. Replace semantics; no merge. The anchor is left untouched. |
644-
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/metadata/remove"><code>metadata.remove</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.metadata.remove(...)</code></span> | Atomically remove an anchored-metadata entry. Strips the anchor SDT wrapper (its content stays in the document) and deletes the payload entry from the Storage Part; both succeed or neither does. When the backing part has no remaining entries, the part itself is removed. |
644+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/metadata/remove"><code>metadata.remove</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.metadata.remove(...)</code></span> | Remove an anchored-metadata entry. Strips the anchor content-control wrapper (its content stays in the document) and deletes the payload entry from the Storage Part. In v1 these writes are sequenced, not transactional: the adapter resolves the target up-front so missing-target failures land before any state change, but a crash strictly between the two writes can leave a dangling payload. When the backing part has no remaining entries, the part itself is removed. |
645645
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><a href="/document-api/reference/metadata/resolve"><code>metadata.resolve</code></a></span> | <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.metadata.resolve(...)</code></span> | Find where an anchored-metadata entry is anchored in the document. Returns the SelectionTarget covering the anchor content. |

apps/docs/document-api/reference/metadata/remove.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
---
22
title: metadata.remove
33
sidebarTitle: metadata.remove
4-
description: Atomically remove an anchored-metadata entry. Strips the anchor SDT wrapper (its content stays in the document) and deletes the payload entry from the Storage Part; both succeed or neither does. When the backing part has no remaining entries, the part itself is removed.
4+
description: "Remove an anchored-metadata entry. Strips the anchor content-control wrapper (its content stays in the document) and deletes the payload entry from the Storage Part. In v1 these writes are sequenced, not transactional: the adapter resolves the target up-front so missing-target failures land before any state change, but a crash strictly between the two writes can leave a dangling payload. When the backing part has no remaining entries, the part itself is removed."
55
---
66

77
{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
88

99
## Summary
1010

11-
Atomically remove an anchored-metadata entry. Strips the anchor SDT wrapper (its content stays in the document) and deletes the payload entry from the Storage Part; both succeed or neither does. When the backing part has no remaining entries, the part itself is removed.
11+
Remove an anchored-metadata entry. Strips the anchor content-control wrapper (its content stays in the document) and deletes the payload entry from the Storage Part. In v1 these writes are sequenced, not transactional: the adapter resolves the target up-front so missing-target failures land before any state change, but a crash strictly between the two writes can leave a dangling payload. When the backing part has no remaining entries, the part itself is removed.
1212

1313
- Operation ID: `metadata.remove`
1414
- API member path: `editor.doc.metadata.remove(...)`

apps/docs/document-engine/sdks.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
737737
| `doc.metadata.list` | `metadata list` | List anchored-metadata entries in the document, optionally filtered by consumer namespace and/or a `within` selection (returns only entries whose anchor overlaps `within`). |
738738
| `doc.metadata.get` | `metadata get` | Get a single anchored-metadata entry by id, including its JSON payload. |
739739
| `doc.metadata.update` | `metadata update` | Replace the JSON payload of an existing anchored-metadata entry. Replace semantics; no merge. The anchor is left untouched. |
740-
| `doc.metadata.remove` | `metadata remove` | Atomically remove an anchored-metadata entry. Strips the anchor SDT wrapper (its content stays in the document) and deletes the payload entry from the Storage Part; both succeed or neither does. When the backing part has no remaining entries, the part itself is removed. |
740+
| `doc.metadata.remove` | `metadata remove` | Remove an anchored-metadata entry. Strips the anchor content-control wrapper (its content stays in the document) and deletes the payload entry from the Storage Part. In v1 these writes are sequenced, not transactional: the adapter resolves the target up-front so missing-target failures land before any state change, but a crash strictly between the two writes can leave a dangling payload. When the backing part has no remaining entries, the part itself is removed. |
741741
| `doc.metadata.resolve` | `metadata resolve` | Find where an anchored-metadata entry is anchored in the document. Returns the SelectionTarget covering the anchor content. |
742742
| `doc.insertTab` | `insert tab` | Insert a real Word tab node at a collapsed text insertion point. Accepts the same target/ref shortcuts as insert, but only for point inserts. |
743743
| `doc.insertLineBreak` | `insert line-break` | Insert a real Word line-break node at a collapsed text insertion point. Accepts the same target/ref shortcuts as insert, but only for point inserts. |
@@ -1215,7 +1215,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
12151215
| `doc.metadata.list` | `metadata list` | List anchored-metadata entries in the document, optionally filtered by consumer namespace and/or a `within` selection (returns only entries whose anchor overlaps `within`). |
12161216
| `doc.metadata.get` | `metadata get` | Get a single anchored-metadata entry by id, including its JSON payload. |
12171217
| `doc.metadata.update` | `metadata update` | Replace the JSON payload of an existing anchored-metadata entry. Replace semantics; no merge. The anchor is left untouched. |
1218-
| `doc.metadata.remove` | `metadata remove` | Atomically remove an anchored-metadata entry. Strips the anchor SDT wrapper (its content stays in the document) and deletes the payload entry from the Storage Part; both succeed or neither does. When the backing part has no remaining entries, the part itself is removed. |
1218+
| `doc.metadata.remove` | `metadata remove` | Remove an anchored-metadata entry. Strips the anchor content-control wrapper (its content stays in the document) and deletes the payload entry from the Storage Part. In v1 these writes are sequenced, not transactional: the adapter resolves the target up-front so missing-target failures land before any state change, but a crash strictly between the two writes can leave a dangling payload. When the backing part has no remaining entries, the part itself is removed. |
12191219
| `doc.metadata.resolve` | `metadata resolve` | Find where an anchored-metadata entry is anchored in the document. Returns the SelectionTarget covering the anchor content. |
12201220
| `doc.insert_tab` | `insert tab` | Insert a real Word tab node at a collapsed text insertion point. Accepts the same target/ref shortcuts as insert, but only for point inserts. |
12211221
| `doc.insert_line_break` | `insert line-break` | Insert a real Word line-break node at a collapsed text insertion point. Accepts the same target/ref shortcuts as insert, but only for point inserts. |

packages/document-api/src/contract/operation-definitions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6411,7 +6411,7 @@ export const OPERATION_DEFINITIONS = {
64116411
'metadata.remove': {
64126412
memberPath: 'metadata.remove',
64136413
description:
6414-
'Atomically remove an anchored-metadata entry. Strips the anchor SDT wrapper (its content stays in the document) and deletes the payload entry from the Storage Part; both succeed or neither does. When the backing part has no remaining entries, the part itself is removed.',
6414+
'Remove an anchored-metadata entry. Strips the anchor content-control wrapper (its content stays in the document) and deletes the payload entry from the Storage Part. In v1 these writes are sequenced, not transactional: the adapter resolves the target up-front so missing-target failures land before any state change, but a crash strictly between the two writes can leave a dangling payload. When the backing part has no remaining entries, the part itself is removed.',
64156415
expectedResult: 'Returns an AnchoredMetadataMutationResult with the removed entry id on success or a failure.',
64166416
requiresDocumentContext: true,
64176417
metadata: mutationOperation({

packages/document-api/src/metadata/anchored-metadata.types.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,21 @@ export interface AnchoredMetadataAttachSuccess {
203203
export type AnchoredMetadataAttachResult = AnchoredMetadataAttachSuccess | AdapterMutationFailure;
204204

205205
/**
206-
* Successful mutation outcome for update / remove. `remove` is atomic:
207-
* both the anchor SDT (wrapper only — content stays in the document) and
208-
* the payload entry are removed in a single mutation; no dangling state.
209-
* When the backing Storage Part has no remaining entries, the part itself
210-
* is removed.
206+
* Successful mutation outcome for update / remove.
207+
*
208+
* `remove` strips both the anchor content control (wrapper only, content
209+
* stays in the document) and the payload entry, in that order. When the
210+
* backing Storage Part has no remaining entries, the part itself is
211+
* removed.
212+
*
213+
* In v1 these writes are sequenced, not transactional. The anchor lives
214+
* in ProseMirror doc state and the payload lives in the OOXML package:
215+
* two different state systems with no shared commit primitive. The
216+
* adapter resolves the target up-front so the common failure mode
217+
* (`TARGET_NOT_FOUND`) fails before any state change, but a crash
218+
* strictly between the PM dispatch and the customXml write can leave a
219+
* dangling payload entry. A future revision may add cross-state
220+
* compensation.
211221
*/
212222
export interface AnchoredMetadataMutationSuccess {
213223
success: true;

0 commit comments

Comments
 (0)