Skip to content

Commit e4fdf12

Browse files
feat(track-changes): structural tracked changes for whole-table insert/delete (SD-3360)
Support importing, listing, deciding, and exporting whole-table insert/delete tracked changes in v1. A whole inserted/deleted table is encoded in OOXML as <w:ins>/<w:del> inside each row's <w:trPr> (rows may carry distinct w:ids); v1 previously dropped these on import, never listed them, could not accept/reject them, and lost them on export. - Schema: add a `trackChange` revision slot to the tableRow node. - Import: tr-translator reads <w:ins>/<w:del> in <w:trPr> into the row attr. - Enumerate: group a table's rows into ONE structural change (kind.type='structural', subtype table-insert/table-delete) when every row is tracked and shares one side; ids may differ. Partial/mixed rows are surfaced but undecidable. - Decide: accept-insert/reject-delete clear the rows; reject-insert/ accept-delete remove the table; partial-range fails closed with INVALID_INPUT; non-whole-table shapes fail closed with CAPABILITY_UNAVAILABLE; inline changes inside a removed table are retired. Graph identity is table-scoped to avoid id collisions. - Export: tr-translator emits <w:ins>/<w:del> in <w:trPr> for roundtrip. - Contract: add 'structural' type + table-insert/table-delete subtype to the document-api surface; regenerate artifacts. Validated against the Labs oracle (sdk-v1): the eight failing-known STRUCT-TABLE conformance/roundtrip/safety cases now pass, zero regression. Deferred: visual paint, and row/column/cell + property structural changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 03ab3f3 commit e4fdf12

31 files changed

Lines changed: 1608 additions & 34 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
@@ -1078,5 +1078,5 @@
10781078
}
10791079
],
10801080
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
1081-
"sourceHash": "5f439c4117bbb2e55f227e7711df415c1173f8c476954c6e412a4b8b45edd1a3"
1081+
"sourceHash": "608536b99749f3a5a1988fa9d39bfafb41aa3814485b64df6812050db98e9d0f"
10821082
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Returns a CommentInfo object with the comment text, author, date, and thread met
6565
| `trackedChangeLink` | CommentTrackedChangeLink \| null | no | One of: CommentTrackedChangeLink, null |
6666
| `trackedChangeStory` | StoryLocator \| null | no | One of: StoryLocator, null |
6767
| `trackedChangeText` | string \| null | no | |
68-
| `trackedChangeType` | enum | no | `"insert"`, `"delete"`, `"replacement"`, `"format"` |
68+
| `trackedChangeType` | enum | no | `"insert"`, `"delete"`, `"replacement"`, `"format"`, `"structural"` |
6969

7070
### Example response
7171

@@ -206,7 +206,8 @@ Returns a CommentInfo object with the comment text, author, date, and thread met
206206
"insert",
207207
"delete",
208208
"replacement",
209-
"format"
209+
"format",
210+
"structural"
210211
]
211212
}
212213
},

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,8 @@ Returns a CommentsListResult with an array of comment threads and total count.
222222
"insert",
223223
"delete",
224224
"replacement",
225-
"format"
225+
"format",
226+
"structural"
226227
]
227228
}
228229
},

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,8 @@ _No fields._
309309
"insert",
310310
"delete",
311311
"replacement",
312-
"format"
312+
"format",
313+
"structural"
313314
],
314315
"type": "string"
315316
},

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ Returns a Receipt confirming the decision was applied; reports NO_OP if the chan
125125
"id": {
126126
"type": "string"
127127
},
128+
"range": {
129+
"description": "Partial-range qualifier on an id target. Rejected with INVALID_INPUT for indivisible (e.g. structural) revisions.",
130+
"type": "object"
131+
},
128132
"story": {
129133
"$ref": "#/$defs/StoryLocator"
130134
}

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Retrieve a single tracked change by ID.
2020

2121
## Expected result
2222

23-
Returns a TrackChangeInfo object with the change type (`insert`, `delete`, `replacement`, `format`), author, date, affected content, and raw imported Word OOXML revision IDs (`w:id`) when available.
23+
Returns a TrackChangeInfo object with the change type (`insert`, `delete`, `replacement`, `format`, `structural`), author, date, affected content, and raw imported Word OOXML revision IDs (`w:id`) when available. Structural changes (whole-table insert/delete) carry a `subtype` of `table-insert` or `table-delete`.
2424

2525
## Input fields
2626

@@ -60,7 +60,8 @@ Returns a TrackChangeInfo object with the change type (`insert`, `delete`, `repl
6060
| `id` | string | yes | |
6161
| `insertedText` | string | no | |
6262
| `pairedWithChangeId` | string \| null | no | |
63-
| `type` | enum | yes | `"insert"`, `"delete"`, `"replacement"`, `"format"` |
63+
| `subtype` | enum | no | `"table-insert"`, `"table-delete"` |
64+
| `type` | enum | yes | `"insert"`, `"delete"`, `"replacement"`, `"format"`, `"structural"` |
6465
| `wordRevisionIds` | object | no | |
6566
| `wordRevisionIds.delete` | string | no | |
6667
| `wordRevisionIds.format` | string | no | |
@@ -81,7 +82,7 @@ Returns a TrackChangeInfo object with the change type (`insert`, `delete`, `repl
8182
},
8283
"grouping": "standalone",
8384
"id": "id-001",
84-
"pairedWithChangeId": null,
85+
"subtype": "table-insert",
8586
"type": "insert"
8687
}
8788
```
@@ -161,12 +162,20 @@ Returns a TrackChangeInfo object with the change type (`insert`, `delete`, `repl
161162
"null"
162163
]
163164
},
165+
"subtype": {
166+
"description": "Finer classification for structural changes (type === 'structural').",
167+
"enum": [
168+
"table-insert",
169+
"table-delete"
170+
]
171+
},
164172
"type": {
165173
"enum": [
166174
"insert",
167175
"delete",
168176
"replacement",
169-
"format"
177+
"format",
178+
"structural"
170179
]
171180
},
172181
"wordRevisionIds": {

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ List all tracked changes in the document.
2020

2121
## Expected result
2222

23-
Returns a TrackChangesListResult with tracked change entries (`insert`, `delete`, `replacement`, `format`), total count, and raw imported Word OOXML revision IDs (`w:id`) when available.
23+
Returns a TrackChangesListResult with tracked change entries (`insert`, `delete`, `replacement`, `format`, `structural`), total count, and raw imported Word OOXML revision IDs (`w:id`) when available. Structural changes (whole-table insert/delete) carry a `subtype` of `table-insert` or `table-delete`.
2424

2525
## Input fields
2626

@@ -29,7 +29,7 @@ Returns a TrackChangesListResult with tracked change entries (`insert`, `delete`
2929
| `in` | StoryLocator \| `"all"` | no | One of: StoryLocator, `"all"` |
3030
| `limit` | integer | no | |
3131
| `offset` | integer | no | |
32-
| `type` | enum | no | `"insert"`, `"delete"`, `"replacement"`, `"format"` |
32+
| `type` | enum | no | `"insert"`, `"delete"`, `"replacement"`, `"format"`, `"structural"` |
3333

3434
### Example request
3535

@@ -75,7 +75,7 @@ Returns a TrackChangesListResult with tracked change entries (`insert`, `delete`
7575
"targetKind": "text"
7676
},
7777
"id": "id-001",
78-
"pairedWithChangeId": null,
78+
"subtype": "table-insert",
7979
"type": "insert"
8080
}
8181
],
@@ -123,12 +123,13 @@ Returns a TrackChangesListResult with tracked change entries (`insert`, `delete`
123123
"type": "integer"
124124
},
125125
"type": {
126-
"description": "Filter by change type: 'insert', 'delete', 'replacement', or 'format'.",
126+
"description": "Filter by change type: 'insert', 'delete', 'replacement', 'format', or 'structural'.",
127127
"enum": [
128128
"insert",
129129
"delete",
130130
"replacement",
131-
"format"
131+
"format",
132+
"structural"
132133
]
133134
}
134135
},
@@ -192,12 +193,20 @@ Returns a TrackChangesListResult with tracked change entries (`insert`, `delete`
192193
"null"
193194
]
194195
},
196+
"subtype": {
197+
"description": "Finer classification for structural changes (type === 'structural').",
198+
"enum": [
199+
"table-insert",
200+
"table-delete"
201+
]
202+
},
195203
"type": {
196204
"enum": [
197205
"insert",
198206
"delete",
199207
"replacement",
200-
"format"
208+
"format",
209+
"structural"
201210
]
202211
},
203212
"wordRevisionIds": {

apps/mcp/src/generated/catalog.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2442,9 +2442,9 @@ export const MCP_TOOL_CATALOG = {
24422442
"Number of tracked changes to skip for pagination. Only for action 'list'. Omit for other actions.",
24432443
},
24442444
type: {
2445-
enum: ['insert', 'delete', 'replacement', 'format'],
2445+
enum: ['insert', 'delete', 'replacement', 'format', 'structural'],
24462446
description:
2447-
"Filter by change type: 'insert', 'delete', 'replacement', or 'format'. Only for action 'list'. Omit for other actions.",
2447+
"Filter by change type: 'insert', 'delete', 'replacement', 'format', or 'structural'. Only for action 'list'. Omit for other actions.",
24482448
},
24492449
force: {
24502450
type: 'boolean',
@@ -2471,6 +2471,11 @@ export const MCP_TOOL_CATALOG = {
24712471
story: {
24722472
$ref: '#/$defs/StoryLocator',
24732473
},
2474+
range: {
2475+
type: 'object',
2476+
description:
2477+
'Partial-range qualifier on an id target. Rejected with INVALID_INPUT for indivisible (e.g. structural) revisions.',
2478+
},
24742479
},
24752480
additionalProperties: false,
24762481
required: ['id'],

packages/document-api/scripts/lib/reference-docs-artifacts.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,17 @@ function getOperationExamples(
10161016
snapshot: ReturnType<typeof buildContractSnapshot>,
10171017
): { input: unknown; output: unknown } {
10181018
const inputOverrides: Partial<Record<ContractOperationSnapshot['operationId'], unknown>> = {
1019+
// The id-target variant carries an optional `range` qualifier used only to
1020+
// fail closed (INVALID_INPUT) on indivisible revisions. A canonical id
1021+
// decision does NOT pass it, so the auto-generated example's `"range": {}`
1022+
// is misleading — pin an explicit clean id-target example here.
1023+
'trackChanges.decide': {
1024+
decision: 'accept',
1025+
target: {
1026+
id: 'id-001',
1027+
story: { kind: 'story', storyType: 'body' },
1028+
},
1029+
},
10191030
insert: {
10201031
target: {
10211032
kind: 'block',

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2481,7 +2481,7 @@ export const OPERATION_DEFINITIONS = {
24812481
memberPath: 'trackChanges.list',
24822482
description: 'List all tracked changes in the document.',
24832483
expectedResult:
2484-
'Returns a TrackChangesListResult with tracked change entries (`insert`, `delete`, `replacement`, `format`), total count, and raw imported Word OOXML revision IDs (`w:id`) when available.',
2484+
'Returns a TrackChangesListResult with tracked change entries (`insert`, `delete`, `replacement`, `format`, `structural`), total count, and raw imported Word OOXML revision IDs (`w:id`) when available. Structural changes (whole-table insert/delete) carry a `subtype` of `table-insert` or `table-delete`.',
24852485
requiresDocumentContext: true,
24862486
metadata: readOperation({
24872487
idempotency: 'idempotent',
@@ -2496,7 +2496,7 @@ export const OPERATION_DEFINITIONS = {
24962496
memberPath: 'trackChanges.get',
24972497
description: 'Retrieve a single tracked change by ID.',
24982498
expectedResult:
2499-
'Returns a TrackChangeInfo object with the change type (`insert`, `delete`, `replacement`, `format`), author, date, affected content, and raw imported Word OOXML revision IDs (`w:id`) when available.',
2499+
'Returns a TrackChangeInfo object with the change type (`insert`, `delete`, `replacement`, `format`, `structural`), author, date, affected content, and raw imported Word OOXML revision IDs (`w:id`) when available. Structural changes (whole-table insert/delete) carry a `subtype` of `table-insert` or `table-delete`.',
25002500
requiresDocumentContext: true,
25012501
metadata: readOperation({
25022502
idempotency: 'idempotent',

0 commit comments

Comments
 (0)