Skip to content

fix(content-controls): mutate locked SDTs via AttrSteps and inner-range writes (SD-3123) - #3287

Merged
caio-pizzol merged 2 commits into
mainfrom
caio-pizzol/SD-3123-attr-mutations
May 14, 2026
Merged

fix(content-controls): mutate locked SDTs via AttrSteps and inner-range writes (SD-3123)#3287
caio-pizzol merged 2 commits into
mainfrom
caio-pizzol/SD-3123-attr-mutations

Conversation

@caio-pizzol

@caio-pizzol caio-pizzol commented May 14, 2026

Copy link
Copy Markdown
Contributor

Per ECMA-376, sdtLocked protects the SDT wrapper from removal but the content stays editable. Before this fix, programmatic mutations flowed through editor.commands.updateStructuredContentById, which dispatches tr.replaceWith(pos, pos + node.nodeSize, ...) spanning the entire SDT. The structured-content lock plugin's filterTransaction reads that as wrapper damage and silently filters the transaction on sdtLocked controls — producing false-success no-ops on operations the API allows (text.setValue, replaceContent, appendContent, prependContent, setLockMode).

Two engine changes:

  • applyAttrsUpdate (sdt-properties-write.ts) — switch from editor.commands.updateStructuredContentById to per-key tr.setNodeAttribute. AttrSteps have no from/to range and are explicitly skipped by the lock plugin's filterTransaction. This is the path that patch, setLockMode, patchRawProperties, setType, and the date / setBinding / text.setMultiline family use.
  • replaceSdtTextContent (content-controls-wrappers.ts) — replace the SDT's inner content range (pos+1 to pos+nodeSize-1) instead of rewriting the whole wrapper. The lock plugin classifies inner-range steps as wouldModifyContent: allowed on sdtLocked (per spec, the wrapper is protected but content is editable). contentLocked and sdtContentLocked continue to reject content edits at the wrapper API boundary via the existing assertNotContentLocked guards — adapter-level guard coverage for those is a follow-up.

The search in applyAttrsUpdate uses SDT_NODE_NAMES imported from target-resolution.ts so it resolves the same nodes the upstream resolveSdtByTarget would resolve. documentSection / documentPartObject are intentionally excluded — they have their own write paths and could collide on id if both an SDT and a section share an identifier.

No changes to API-level lock guards (assertNotSdtLocked on patch, setType, setBinding, etc.) — relaxing those is a separate behavior decision and out of scope here.

Tests:

  • 5 new regression tests for sdtLocked (setLockMode, text.setValue, text.clearValue, replaceContent, clearContent) that assert the actual inner-range positions (pos+1, pos+nodeSize-1), not just call counts. A full-wrapper replaceWith would still fail these.
  • 11 existing tests migrated from "expect updateStructuredContentById called" to "expect tr.setNodeAttribute / tr.replaceWith / tr.delete on inner range".
  • 19 metadata-mutation operations added to CC_DIRECT_DISPATCH_OPS in conformance (no synthetic-failure mode after this refactor, matching the existing precedent for wrap/unwrap/etc.).
  • All pass: 47 wrappers + 1170 conformance + 104 SDT-adjacent + 1398 document-api. Zero new TypeScript errors.

Related: SD-3131, SD-3139, IT-1046.

…ge writes (SD-3123)

Per ECMA-376, sdtLocked protects the SDT wrapper from removal but the content
stays editable. Before this fix, programmatic mutations flowed through
editor.commands.updateStructuredContentById, which dispatches
tr.replaceWith(pos, pos + node.nodeSize, ...) spanning the entire SDT. The
structured-content lock plugin's filterTransaction reads that as wrapper
damage and silently filters the transaction on sdtLocked controls -
producing false-success no-ops on operations the API allows (text.setValue,
replaceContent, appendContent, prependContent, setLockMode).

Two engine changes:

- applyAttrsUpdate (sdt-properties-write.ts) - switch from
  editor.commands.updateStructuredContentById to per-key tr.setNodeAttribute.
  AttrSteps have no from/to range and are explicitly skipped by the lock
  plugin's filterTransaction. This is the path that patch, setLockMode,
  patchRawProperties, setType, and the date/binding/multiline family use.
- replaceSdtTextContent (content-controls-wrappers.ts) - replace the SDT's
  inner content range (pos+1 to pos+nodeSize-1) instead of rewriting the
  whole wrapper. The lock plugin classifies inner-range steps as
  wouldModifyContent: allowed on sdtLocked, still blocked on contentLocked
  / sdtContentLocked (both per spec).

The search in applyAttrsUpdate uses SDT_NODE_NAMES (imported from
target-resolution.ts) so it resolves the same nodes the upstream
resolveSdtByTarget would resolve. documentSection / documentPartObject are
intentionally excluded - they have their own write paths and could collide
on id.

No changes to API-level lock guards (assertNotSdtLocked on patch, setType,
setBinding, etc.) - relaxing those is a separate behavior decision and out
of scope here.

Tests:
- 4 new regression tests for sdtLocked (setLockMode, text.setValue,
  text.clearValue, replaceContent) that assert the actual inner-range
  positions (pos+1, pos+nodeSize-1), not just call counts.
- 11 existing tests migrated from "expect updateStructuredContentById
  called" to "expect tr.setNodeAttribute / tr.replaceWith / tr.delete".
- 19 metadata-mutation operations added to CC_DIRECT_DISPATCH_OPS in
  conformance (no synthetic-failure mode after this refactor, matching the
  existing precedent for wrap/unwrap/etc.).

All pass: 46 wrappers + 1170 conformance + 104 SDT-adjacent + 1398
document-api. Zero new TypeScript errors.

Related: SD-3131, SD-3139, IT-1046.
@caio-pizzol
caio-pizzol requested a review from a team as a code owner May 14, 2026 11:05
@linear

linear Bot commented May 14, 2026

Copy link
Copy Markdown

SD-3123

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@caio-pizzol
caio-pizzol merged commit cf75357 into main May 14, 2026
69 checks passed
@caio-pizzol
caio-pizzol deleted the caio-pizzol/SD-3123-attr-mutations branch May 14, 2026 12:14
caio-pizzol added a commit that referenced this pull request May 14, 2026
* fix(content-controls): mutate locked SDTs via AttrSteps and inner-range writes (SD-3123)

Per ECMA-376, sdtLocked protects the SDT wrapper from removal but the content
stays editable. Before this fix, programmatic mutations flowed through
editor.commands.updateStructuredContentById, which dispatches
tr.replaceWith(pos, pos + node.nodeSize, ...) spanning the entire SDT. The
structured-content lock plugin's filterTransaction reads that as wrapper
damage and silently filters the transaction on sdtLocked controls -
producing false-success no-ops on operations the API allows (text.setValue,
replaceContent, appendContent, prependContent, setLockMode).

Two engine changes:

- applyAttrsUpdate (sdt-properties-write.ts) - switch from
  editor.commands.updateStructuredContentById to per-key tr.setNodeAttribute.
  AttrSteps have no from/to range and are explicitly skipped by the lock
  plugin's filterTransaction. This is the path that patch, setLockMode,
  patchRawProperties, setType, and the date/binding/multiline family use.
- replaceSdtTextContent (content-controls-wrappers.ts) - replace the SDT's
  inner content range (pos+1 to pos+nodeSize-1) instead of rewriting the
  whole wrapper. The lock plugin classifies inner-range steps as
  wouldModifyContent: allowed on sdtLocked, still blocked on contentLocked
  / sdtContentLocked (both per spec).

The search in applyAttrsUpdate uses SDT_NODE_NAMES (imported from
target-resolution.ts) so it resolves the same nodes the upstream
resolveSdtByTarget would resolve. documentSection / documentPartObject are
intentionally excluded - they have their own write paths and could collide
on id.

No changes to API-level lock guards (assertNotSdtLocked on patch, setType,
setBinding, etc.) - relaxing those is a separate behavior decision and out
of scope here.

Tests:
- 4 new regression tests for sdtLocked (setLockMode, text.setValue,
  text.clearValue, replaceContent) that assert the actual inner-range
  positions (pos+1, pos+nodeSize-1), not just call counts.
- 11 existing tests migrated from "expect updateStructuredContentById
  called" to "expect tr.setNodeAttribute / tr.replaceWith / tr.delete".
- 19 metadata-mutation operations added to CC_DIRECT_DISPATCH_OPS in
  conformance (no synthetic-failure mode after this refactor, matching the
  existing precedent for wrap/unwrap/etc.).

All pass: 46 wrappers + 1170 conformance + 104 SDT-adjacent + 1398
document-api. Zero new TypeScript errors.

Related: SD-3131, SD-3139, IT-1046.

* test(content-controls): add clearContent sdtLocked regression test (SD-3123)

* test(behavior): document-api locked SDT mutations (SD-3144)

Real-editor regression coverage for SD-3123 (#3287). The engine fix was
verified at the unit-test level (mock transaction shapes: AttrSteps for
metadata, inner-range ReplaceSteps for content), but mocks don't run the
real structured-content lock plugin's filterTransaction. This spec closes
that gap by exercising the customer surface (editor.doc.contentControls.*)
in a real browser with the real lock plugin loaded.

Five tests:
- text.setValue updates an sdtLocked inline plain-text control
- replaceContent updates an sdtLocked block rich-text control
- clearContent empties an sdtLocked block control (wrapper preserved)
- round-trip sdtLocked → unlocked → text.setValue (lockMode AttrStep writes)
- contentLocked + text.setValue returns LOCK_VIOLATION (API guard fires)

Lives in tests/behavior/tests/sdt/document-api-locked-mutations.spec.ts.
Does NOT un-fixme the older sdt-lock-modes.spec.ts (different concern —
that one exercises editor.commands.*, SD-3123 is about the Document API).

5/5 pass on chromium. Stacked on SD-3123 (#3287).
@superdoc-bot

superdoc-bot Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in @superdoc-dev/mcp v0.3.0-next.93

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in @superdoc-dev/react v1.2.0-next.137

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in vscode-ext v2.3.0-next.139

@superdoc-bot

superdoc-bot Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in superdoc-cli v0.8.0-next.108

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in superdoc v1.30.0-next.88

The release is available on GitHub release

msviderok pushed a commit to msviderok/superdoc that referenced this pull request May 16, 2026
…ge writes (SD-3123) (superdoc-dev#3287)

* fix(content-controls): mutate locked SDTs via AttrSteps and inner-range writes (SD-3123)

Per ECMA-376, sdtLocked protects the SDT wrapper from removal but the content
stays editable. Before this fix, programmatic mutations flowed through
editor.commands.updateStructuredContentById, which dispatches
tr.replaceWith(pos, pos + node.nodeSize, ...) spanning the entire SDT. The
structured-content lock plugin's filterTransaction reads that as wrapper
damage and silently filters the transaction on sdtLocked controls -
producing false-success no-ops on operations the API allows (text.setValue,
replaceContent, appendContent, prependContent, setLockMode).

Two engine changes:

- applyAttrsUpdate (sdt-properties-write.ts) - switch from
  editor.commands.updateStructuredContentById to per-key tr.setNodeAttribute.
  AttrSteps have no from/to range and are explicitly skipped by the lock
  plugin's filterTransaction. This is the path that patch, setLockMode,
  patchRawProperties, setType, and the date/binding/multiline family use.
- replaceSdtTextContent (content-controls-wrappers.ts) - replace the SDT's
  inner content range (pos+1 to pos+nodeSize-1) instead of rewriting the
  whole wrapper. The lock plugin classifies inner-range steps as
  wouldModifyContent: allowed on sdtLocked, still blocked on contentLocked
  / sdtContentLocked (both per spec).

The search in applyAttrsUpdate uses SDT_NODE_NAMES (imported from
target-resolution.ts) so it resolves the same nodes the upstream
resolveSdtByTarget would resolve. documentSection / documentPartObject are
intentionally excluded - they have their own write paths and could collide
on id.

No changes to API-level lock guards (assertNotSdtLocked on patch, setType,
setBinding, etc.) - relaxing those is a separate behavior decision and out
of scope here.

Tests:
- 4 new regression tests for sdtLocked (setLockMode, text.setValue,
  text.clearValue, replaceContent) that assert the actual inner-range
  positions (pos+1, pos+nodeSize-1), not just call counts.
- 11 existing tests migrated from "expect updateStructuredContentById
  called" to "expect tr.setNodeAttribute / tr.replaceWith / tr.delete".
- 19 metadata-mutation operations added to CC_DIRECT_DISPATCH_OPS in
  conformance (no synthetic-failure mode after this refactor, matching the
  existing precedent for wrap/unwrap/etc.).

All pass: 46 wrappers + 1170 conformance + 104 SDT-adjacent + 1398
document-api. Zero new TypeScript errors.

Related: SD-3131, SD-3139, IT-1046.

* test(content-controls): add clearContent sdtLocked regression test (SD-3123)
msviderok pushed a commit to msviderok/superdoc that referenced this pull request May 16, 2026
…-dev#3291)

* fix(content-controls): mutate locked SDTs via AttrSteps and inner-range writes (SD-3123)

Per ECMA-376, sdtLocked protects the SDT wrapper from removal but the content
stays editable. Before this fix, programmatic mutations flowed through
editor.commands.updateStructuredContentById, which dispatches
tr.replaceWith(pos, pos + node.nodeSize, ...) spanning the entire SDT. The
structured-content lock plugin's filterTransaction reads that as wrapper
damage and silently filters the transaction on sdtLocked controls -
producing false-success no-ops on operations the API allows (text.setValue,
replaceContent, appendContent, prependContent, setLockMode).

Two engine changes:

- applyAttrsUpdate (sdt-properties-write.ts) - switch from
  editor.commands.updateStructuredContentById to per-key tr.setNodeAttribute.
  AttrSteps have no from/to range and are explicitly skipped by the lock
  plugin's filterTransaction. This is the path that patch, setLockMode,
  patchRawProperties, setType, and the date/binding/multiline family use.
- replaceSdtTextContent (content-controls-wrappers.ts) - replace the SDT's
  inner content range (pos+1 to pos+nodeSize-1) instead of rewriting the
  whole wrapper. The lock plugin classifies inner-range steps as
  wouldModifyContent: allowed on sdtLocked, still blocked on contentLocked
  / sdtContentLocked (both per spec).

The search in applyAttrsUpdate uses SDT_NODE_NAMES (imported from
target-resolution.ts) so it resolves the same nodes the upstream
resolveSdtByTarget would resolve. documentSection / documentPartObject are
intentionally excluded - they have their own write paths and could collide
on id.

No changes to API-level lock guards (assertNotSdtLocked on patch, setType,
setBinding, etc.) - relaxing those is a separate behavior decision and out
of scope here.

Tests:
- 4 new regression tests for sdtLocked (setLockMode, text.setValue,
  text.clearValue, replaceContent) that assert the actual inner-range
  positions (pos+1, pos+nodeSize-1), not just call counts.
- 11 existing tests migrated from "expect updateStructuredContentById
  called" to "expect tr.setNodeAttribute / tr.replaceWith / tr.delete".
- 19 metadata-mutation operations added to CC_DIRECT_DISPATCH_OPS in
  conformance (no synthetic-failure mode after this refactor, matching the
  existing precedent for wrap/unwrap/etc.).

All pass: 46 wrappers + 1170 conformance + 104 SDT-adjacent + 1398
document-api. Zero new TypeScript errors.

Related: SD-3131, SD-3139, IT-1046.

* test(content-controls): add clearContent sdtLocked regression test (SD-3123)

* test(behavior): document-api locked SDT mutations (SD-3144)

Real-editor regression coverage for SD-3123 (superdoc-dev#3287). The engine fix was
verified at the unit-test level (mock transaction shapes: AttrSteps for
metadata, inner-range ReplaceSteps for content), but mocks don't run the
real structured-content lock plugin's filterTransaction. This spec closes
that gap by exercising the customer surface (editor.doc.contentControls.*)
in a real browser with the real lock plugin loaded.

Five tests:
- text.setValue updates an sdtLocked inline plain-text control
- replaceContent updates an sdtLocked block rich-text control
- clearContent empties an sdtLocked block control (wrapper preserved)
- round-trip sdtLocked → unlocked → text.setValue (lockMode AttrStep writes)
- contentLocked + text.setValue returns LOCK_VIOLATION (API guard fires)

Lives in tests/behavior/tests/sdt/document-api-locked-mutations.spec.ts.
Does NOT un-fixme the older sdt-lock-modes.spec.ts (different concern —
that one exercises editor.commands.*, SD-3123 is about the Document API).

5/5 pass on chromium. Stacked on SD-3123 (superdoc-dev#3287).
@superdoc-bot

superdoc-bot Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in superdoc-sdk v1.10.0

@superdoc-bot

superdoc-bot Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in @superdoc-dev/mcp v0.6.0

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in superdoc v1.34.0

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in @superdoc-dev/react v1.5.0

The release is available on GitHub release

@superdoc-bot

superdoc-bot Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in vscode-ext v2.6.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants