GBC RTC clobber: distinct sidecar cloud slots + cross-ext write-guard + verify harness#11
Open
terafin wants to merge 3 commits into
Open
Conversation
…car saves Cloud-side sibling of the local dedup fix in feat-preserve-rtc. A GBC RTC game stores .srm (32KB SRAM) + .rtc (8B clock sidecar); both uploaded under the same slotName="default" collided as competing versions of one record, so /latest returned the newest (.rtc) and the download wrote the 8B .rtc OVER the 32KB .srm (then removed the old variant). Data loss. Two layers (save-format spec authored by Ullr; RSM model by Idunn): - ROOT (resolve_slot_name_for_sync): sidecar exts (.rtc/.mpk/.cpk) now resolve to a DISTINCT cloud slot, RSM-native form "<primary> (<LABEL>)" -- e.g. "default (RTC)", "default (Controller Pak)", "default (Cartridge Pak)"; parenthetical, never the conflict-key delimiter, mirroring the PSX "Memory Card 1/2" precedent. Primaries (incl. container-equivalent srm/sav, mcr/vmp/gme) keep the base slot. So .srm and .rtc become separate (rom,slot) records and never compete as versions. - GUARD (allow_write): at both download/write sites, never write a canonical whose format crosses the sidecar/primary boundary onto a mismatched target (an 8B .rtc can never land on a .srm). Sidecars must match exactly; primary to primary (PSX mcr to vmp etc.) is always allowed (no regression). Uses RSM's new /latest format field (Option; None on older RSM -> prior behavior). Pure seams exposed for verification: is_sidecar, allow_write, sidecar_label, resolve_slot_name_for_sync. Unit tests cover the logic (guard matrix, slot labels, no-regression primary-to-primary); the DAT-format/real-save gate is Ullr's. 109 lib tests pass. OUT OF SCOPE: N64 battery-format conversion (.srm-combined to .sra/.eep/.fla). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the black-box verification surface Ullr's fixture matrix drives, so the
sidecar slot/guard fix is provable over real save bytes with no network/sync:
- scanner::classify(path, &bytes) -> {system, format, is_sidecar}: split the
existing classify_supported_save into a bytes-feedable core (size + plain-text
passed in) so classification is table-driveable from fixtures, not just live
files. format = bare lowercased ext (matches RSM /latest + the guard).
- `explain-sync --fixtures-dir D --cloud-state S [--manifest M]`: offline (no
config/auth) subcommand. Reads fixtures-manifest.tsv (<relpath>\t<lower
identity>) + a cloud-state JSON keyed "<identity>::<slotName>", then per
fixture prints classify | upload-slot (resolve_slot_name_for_sync) | cloud
format | write-guard verdict (allow_write). Identity is used AS-IS (bare SHA1
for GB, projection-key for N64-pak/PSX).
Verified end-to-end against the Crystal case: legacy collision (.rtc latest
under "default") -> .srm guard=SKIP (clobber prevented); fixed (distinct slots)
-> both ALLOW (each round-trips). classify unit test + 110 lib tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per the save-format spec (Ullr): the gameboy non-.rtc SRAM arm becomes the authoritative enum 512 | 2048 | 8192 | 32768 | 65536 | 131072 across all three helpers. Drops the non-real power-of-two values (1024/4096/16384) and ADDS 131072 — the previous 65536 ceiling silently REJECTED legitimate 128K MBC5 saves (a real classify bug). The .rtc => (1..=64) sidecar arm is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
cat <<'EOF'
The cloud-side sibling of the dedup-key/size-classifier work this branch already carries — folded onto it (Idunn option B) so it is ONE branch: classifier + dedup-key (from feat-scanner) + slot-de-collision + write-guard + verify harness. Base shows just my 2 commits.
Bug
A GBC RTC game stores
.srm(32KB SRAM) +.rtc(8B clock sidecar); both uploaded underslotName="default"collided as competing versions of one record →/latestreturned the newest (.rtc) → the download wrote the 8B.rtcOVER the 32KB.srm(then removed the old variant). Data loss. (Ullr repro, Pokémon Crystal.)Fix (two layers, save-format spec by Ullr / RSM model by Idunn)
resolve_slot_name_for_sync: sidecars (.rtc/.mpk/.cpk) → distinct slot"<primary> (<LABEL>)"(default (RTC)/(Controller Pak)/(Cartridge Pak)) — parenthetical, never the::/:conflict-key delimiter, mirroring the PSX "Memory Card 1/2" precedent. Primaries (incl. container-equivsrm/sav,mcr/vmp/gme) keep the base slot. So.srm/.rtcbecome separate(rom,slot)records, never competing as versions.allow_write: at both download/write sites, never write a canonical whose format crosses the sidecar/primary boundary onto a mismatched target (SIDECAR_FORMATS = {rtc,mpk,cpk}). Sidecars must match exactly; primary↔primary (PSXmcr→vmpetc.) always allowed (no regression). Uses RSM/latests newformatfield (Option; None on older RSM → prior behavior).Verification harness (for Ullrs real-save matrix, no network)
scanner::classify(path,&bytes) -> {system, format, is_sidecar}(bytes-feedable split ofclassify_supported_save).explain-sync --fixtures-dir D --cloud-state S [--manifest M]: offline decision-dump — per fixture prints classify · upload-slot · cloud-format · guard (ALLOW/SKIP). Readsfixtures-manifest.tsv(<relpath>\t<identity>) + cloud-state keyed<identity>::<slotName>; identity used as-is (bare SHA1 GB / projection-key N64-pak/PSX).Gate evidence
"rtc" => (1..=64)✓; dedup-key present ✓; slot+guard+harness intact ✓.explain-syncverified end-to-end: legacy collision →.srmguard=SKIP (clobber prevented); fixed → both ALLOW.api.rsformatfield is mine (no overlap).Base is
feat/scanner-libretro-format-batchto show the slot/guard delta cleanly — redirect tointarweb-devif youd rather gate it as the unified carry.EOF