Independent save-bundle collision acceptance — 2026-10-26
ACCEPT exact repair ef3fdad258660c3b838e0836aff32b3ef4a4f8b3 for source-level normalized-destination collision rejection. Parent 9148ceb18768955f647130c2e936228429892477 is the accepted ancestor, not current main. This review changes only independent tests and documentation/evidence. No implementation repair or main merge.
Candidate lib/save-bundle.js: Git blob and complete closure pins are in the evidence pins.json; SHA-256 4ac22b490b0c201d5c4d7ee0047882522eeb4690d93b095e4eb755d84590f3ee. Ancestor source: blob 3e557cfb3429e80ca41426cf79797b2326dca3bc, SHA-256 3683e564797c1df2c4e9e335b7eb2fca5edcf7e5f440f125bdadbcc846f5f848. The actual commit parent and ancestor source were checked, rather than treating current main as the baseline.
The supplied unchanged baseline receipt remains 2 pass / 3 fail; supplied repaired receipt remains 5 pass / 0 fail. Their bytes, the original hashed collision ZIP, original checkpoint/result and baseline source are preserved verbatim under test/evidence/save-bundle-independent-oct26/. An independent rerun of the supplied five repaired groups passes. Packaging and unrelated suites were not rerun.
The new test/save-bundle-independent-acceptance.test.cjs passes 9 groups, with 47 independently constructed genuine ZIP fixtures. It builds local/central ZIP headers and bitwise CRCs without either the production ZIP writer or the supplied fixture builder, and hashes payloads with Node crypto. Actual ZIP catalog/extraction, readBundle, inspectBundle and importBundle run locally; there are no network calls. ZIP bytes, SHA-256s and manifest hashes are retained in independent/. Successful group checkpoints are written by atomic rename, bound to candidate/source/test hashes.
Coverage includes distinct raw case collisions in both orders, directory case and UTF-8 case folding; slash, empty, dot and parent aliases refused by sanitization; raw duplicate members separately from duplicate manifest specs; metadata/path agreement across case and separators; different-drive and different-destination claims; unsafe absolute, drive-marker, device and control-character paths; late size/hash and state hash errors; distinct drives, directories, punctuation, zero-byte files, all FILETIMEs/attributes, deterministic export/import/export; legacy case-insensitive slash globs including ? and the existing behavior that * crosses directories. Rejected decode/import cases check both merge and replace with real small VirtualFS instances: file Map mutators, directory Set mutators, parent-directory creation and storage clear/import have sentinels, and the original file/directory snapshots remain unchanged.
Public API case-preservation challenge
There is an observable API change for hand-authored uppercase ZIP members: readBundle().files[].path and inspectBundle().files[].path now return lowercase guest paths. The unchanged ancestor returned c:\Save\Slot.sav; the repair returns c:\save\slot.sav. The test executes both sources on the same independently built ZIP and records that difference explicitly. The original member spelling remains in .member, and memberToGuestPath() still preserves that spelling. Callers displaying .path may therefore display lowercase after this repair.
This is acceptable for this fix: VirtualFS.files is explicitly keyed by normalized paths; VirtualFS._normPath folds case/slashes, and the persistence normalizer does the same. Existing export already normalizes paths and ZIP members to lowercase. The ancestor could decode a mixed-case member with an absent manifest path, but its import allow-list then refused that same member with normal case-insensitive persistence globs. The repair makes the returned destination agree with the path actually validated and written. The independent test proves normal writer-produced ancestor bundles remain byte-identical and decode identically, and raw member spelling remains available for display. No promise of preserving original display-case filenames across save export/import is established here: the existing export already discarded that information. Integrators must still review consumers that rely on the old uppercase .path presentation.
Limits and incidents
Acceptance covers the pinned source and bounded local Node behavior, not current-main integration. The coordinator must reconcile the repair and tests with current main before merge, review consumers of the public path value, and run any required full-sync/server/browser integration. No full-sync runtime, guest, gameplay, compilation, service restart, deploy, Boat or SSH guest access occurred. No test box was assigned or touched; lifecycle ownership remains with the coordinator.
The donor disclosed an initial read-excerpt limit overrun; its original result/recovery receipts are retained without alteration. This review also had an output-limit incident: the first local sparse index was loaded without skip-worktree bits, so an unrestricted local git status/diff --stat unexpectedly reported absent excluded paths and exceeded the 128 KiB stdout policy (tool rendering truncated it). The isolated index was corrected using an internal bounded NUL stream, without checking out excluded paths or modifying root/donor repositories. Subsequent status output is bounded. A first independent test run had 8 passes and one test-oracle failure caused by strict Buffer-versus-Uint8Array prototype comparison; the original test/TAP are retained, and the assertion was corrected to compare bytes. The final unchanged candidate passes 9/9. These incidents do not alter the supplied baseline receipts and are not represented as compliant initial preparation.
Recovery
Recover this branch from the verified remote, preserving its parent candidate. No full checkout is needed. Minimal source/test paths are enumerated in pins.json. Evidence is under test/evidence/save-bundle-independent-oct26/, including a complete hash manifest and the 47 ZIP identities. Independent test command from a focused checkout:
SAVE_ACCEPT_BASELINE="$PWD/test/evidence/save-bundle-independent-oct26/supplied-save-bundle.baseline.js" \
SAVE_ACCEPT_EVIDENCE=/tmp/save-bundle-independent-rerun \
timeout 35s node --max-old-space-size=192 --test --test-reporter=tap test/save-bundle-independent-acceptance.test.cjs
The explicit ancestor comparison is marked skipped when its baseline environment input is absent; it was enabled in this acceptance. The supplied group command is timeout 35s node --max-old-space-size=192 --test --test-reporter=tap test/save-bundle-normalized-collision.test.cjs.
All donor Git object symlinks were skipped. Missing focused blobs were recovered from regular working leaves only after recomputing the exact Git blob ID against the pinned tree. Only candidate/base commits, needed tree metadata and focused blobs were recovered locally; no donor history was materialized, no fetch was used, and no donor/root Git refs or index were written. Admission receipts account for checkout/Git, test artifacts, publication copies, launcher/session/future positive database/WAL growth and an 8 MiB reserve; installed Node and read-only standing donors are separate. Trusted dedicated GitHub SSH identity/known-hosts configuration is retained for branch publication only.