Skip to content

fix: redact dead rich-text style values in shallow snapshot exports - #1072

Merged
lodystage[bot] merged 2 commits into
mainfrom
fix/1057-user-reported-issue
Aug 11, 2026
Merged

fix: redact dead rich-text style values in shallow snapshot exports#1072
lodystage[bot] merged 2 commits into
mainfrom
fix/1057-user-reported-issue

Conversation

@lodystage

@lodystage lodystage Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Shallow snapshot export retained the value of a rich-text mark whose entire range had been deleted, even though every read API reported the data as gone. Deleted character content was already dropped correctly, so the leaked mark value defeated the documented content-redaction use of shallow snapshots. Reported with a failing test in #1057.

Deleting styled text removes the text chunks but style anchors stay in the state sequence forever, and the richtext state codec serialized each start anchor's value verbatim into the shallow-root state bytes.

Fix

Before the state KV is exported, shallow export now nulls the values of style pairs that are dead at the shallow root (no text between the anchors), operating directly on the live KvWrapper with no serialization round-trip:

  • redact_dead_style_values (richtext snapshot codec): rewrites only the columnar tail of a Text container payload; single pass, pairs matched by id, dead iff no text span lies between the anchors.
  • redact_dead_text_styles (shallow_snapshot.rs): scans only the two Text key ranges of the state KV; applied on all export paths — both reuse branches (so blobs produced by older versions are cleaned on re-export), the checkout branch, and StateOnly.

Key invariants (full rationale in context/shallow-snapshot-style-redaction.md):

  • Only values are nulled; anchors stay. Op positions are entity indexes that count each anchor, so removing anchors would corrupt replay. Null-valued anchors are a pre-existing state shape (unmark produces them).
  • expand: both pairs are never redacted. An empty both-expand pair still captures future inserts, so its value is live data; nulling it would break convergence with full-history replicas (verified experimentally). For after/none/before, insertion can never re-enter an empty pair, and inserts concurrent with the deletion cannot be imported into a shallow doc (ImportUpdatesThatDependsOnOutdatedVersion), so the value is truly unobservable.
  • The exported latest state only redacts pairs already dead at the root. Styles that die after the root keep their values so historical checkouts in the retained range still render correctly.

Validation

Closes #1057.

🤖 Generated with Claude Code

…1057)

Rationale and invariants: context/shallow-snapshot-style-redaction.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

WASM Size Report

  • Original size: 3112.21 KB
  • Gzipped size: 1033.23 KB
  • Brotli size: 724.70 KB

- Structured leak assertions: internal tests decode the exported state KV
  and assert StyleOp values directly (root + nested text key ranges,
  overlay whitelist keep/redact, legacy unredacted blob cleaned on
  re-export), since LZ4 can hide secrets from raw byte scans.
- Collapse the root/overlay redaction protocol into redact_export_states
  so the historical-checkout invariant lives in one place.
- Merge the two shallow-root reuse branches; overlay decided by ops_num.
- Single ContainerWrapper::decode_header shared by all header readers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lodystage
lodystage Bot merged commit 4d3d3f1 into main Aug 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant