Skip to content

fix(history): own comparison settings before awaiting snapshot storage - #809

Open
JSv4 wants to merge 1 commit into
mainfrom
claude/pr-708-review-cicd-12gwlu
Open

JSv4 wants to merge 1 commit into
mainfrom
claude/pr-708-review-cicd-12gwlu

Conversation

@JSv4

@JSv4 JSv4 commented Sep 23, 2026

Copy link
Copy Markdown
Owner

Follow-up review of #708 (DOCX snapshot store), which is already merged. CI was green on #708 and is green on main, so there was no pipeline failure to fix. This PR fixes the one real defect found in review, plus two small cleanups.

The bug

DocxSnapshotStore.CompareAsync takes a DocxDiffSettings object, then awaits two reads from host blob storage before it passes that object to DocxDiff.CreateComparison. DocxVersionHistory.CompareVersionsAsync adds two more awaits (the version-record reads) in front of that. The returned DocxDiffComparison also keeps the same object by reference and reads it lazily when a product is requested.

So a caller that reuses its settings object and changes it while storage is slow gets a comparison built from the changed values. The tests change AuthorForRevisions, and the redline comes back stamped with the wrong author. #708 already guards against this for the document bytes (CaptureAsync copies them before yielding "to an asynchronous host adapter"). Settings were missed, and the downstream DocxHistoryReader.CompareVersionsToDocxAsync already clones its settings for the same reason.

Fix: clone the settings on entry to both methods, before the first await. DocxDiffSettings.Clone() is the existing shallow copy the DocxCompare front door already uses.

Other changes

  • Doc comment and architecture doc corrected. Both said CompareAsync "retains" DocxDiff's compatibility policy. That reads as if it behaves like the product comparison. In fact it runs the raw engine with its opt-in input-revision defaults and does not apply the DocxCompare pre-accept step. That split is intentional, since the client-facing path goes through DocxCompare. The behavior is unchanged; only the wording now matches it.
  • CompareAsync now null-checks both snapshot references up front instead of failing inside ExportAsync.
  • Removed #nullable enable from the two files feat(history): capture exact DOCX snapshots and reuse lazy comparisons #708 added. The project enables nullable reference types globally, and repo rules forbid per-file directives.

Review notes on the rest of #708

The rest of #708 holds up on review:

  • Capture copies the bytes before the async put.
  • Export validates the reference shape (including null or malformed digests) before any I/O.
  • Export re-verifies length, the raw SHA-256 and the OPC content digest.
  • Size limits are enforced before inspection.

The copyright header #708 originally shipped was already corrected on main.

One pre-existing issue is outside this PR's scope: DocxDiffComparison holds caller settings by reference in every entry point, not just history. The history API no longer exposes it, but a constructor-level clone in the engine would close it everywhere. That's worth a separate change.

Validation (TDD)

  • Wrote DocxSnapshotStoreTests.ComparisonOwnsSettingsBeforeAwaitingHostStorage and DocxVersionHistoryTests.VersionComparisonOwnsSettingsBeforeAwaitingHostStorage first. Each uses a blob store that pauses reads, changes the settings mid-read, then checks the revision author. Both failed before the fix (Actual: "Changed during storage read") and pass after it.
  • 173 history, snapshot, DocxCompare and copyright-guard tests pass locally.
  • Warning counts with --no-incremental are unchanged at the documented baseline (library 175, tests 788).

🤖 Generated with Claude Code

https://claude.ai/code/session_01EuVfv22yFAxchW5H2F7Sk9


Generated by Claude Code

DocxSnapshotStore.CompareAsync and DocxVersionHistory.CompareVersionsAsync
read the caller's DocxDiffSettings only after awaiting host blob reads, and
the returned lazy DocxDiffComparison kept reading that same object. A caller
that reused its settings mid-flight got a comparison stamped with the changed
values (e.g. the wrong revision author). Clone at entry, matching how
CaptureAsync already owns its bytes before yielding.

Also clarifies that CompareAsync is raw-engine semantics (not the DocxCompare
pre-accept front door), null-checks its snapshot arguments up front, and drops
the redundant #nullable enable directives from the files added in #708.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuVfv22yFAxchW5H2F7Sk9

This branch has not been deployed

No deployments
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.

2 participants