The client companion contract for RomM Device Sync — the layer the server hands to clients, written down as a spec instead of re-guessed per client. It ships three ways: as a normative spec, as language-neutral conformance vectors, and as a freestanding C99 core that already satisfies them.
Vectors are the normative artifact: prose explains, vectors decide.
Covers saves, not save states. The contract's inputs are a server-stamped
content_hash, a per-device sync record and a slot. RomM gives saves all three and save states none of them, so save states become a consumer when the server gives them the same treatment, not before (#20). Releases currently carry x86_64 Linux only.
Note
How this is built. The code is written by an AI coding agent working under my direction. The architecture, the design decisions and the review are mine, and every change is proven against the conformance vectors — reference, native core and binding alike — before it ships.
RomM's Device Sync deliberately splits the work. The server owns hashing (it stamps every save's content_hash and
expects clients to remember it) and detection (negotiate returns upload/download/conflict verdicts). What it hands
back to the client is everything after a verdict: what to record at each sync boundary, what to do with a 409, and
when a conflict must reach the user. There is no resolve endpoint and no server-side resolution policy — RomM's own sync
modes quarantine conflicting files and notify, they never pick a winner.
Today at least three clients (Argosy, Grout, decky-romm-sync) have independently invented that layer — a local baseline hash, a remembered server hash, a "local provably unchanged, so just download" downgrade, force-overwrite only on an explicit user choice — but with different edges. The same slot state can resolve three different ways depending on which client touches it.
gavel writes that delegated layer down. sigil answers "which game is this and what will its save be called", atlas answers where the save lives, and gavel answers what a client does in the space the server hands back.
The rules are not save-specific in principle — they compare content hashes, not file meaning. In practice they cover
saves and only saves, because that is what RomM stamps: a save carries a server content_hash, a slot and a per-device
sync record, and a save state carries none of the three. Without them the identity check has nothing on the server side
to compare against, and every decision falls through to the timestamp comparison — the weakest path in the table, as the
only path.
SPEC.md— the client-side contract: what a client records at each sync boundary (its own baseline hash plus the server-stamped hash), the identity check's two routes, the 409/conflict resolution ladder, overwrite discipline, and the safety invariants — uncertainty never counts as a match; never destroy the only copy; a corrupt or implausibly shrunken local never auto-uploads; force-overwrite only on an explicit user choice.vectors/— language-neutral JSON conformance vectors: the 409/conflict resolution ladder and the full sync decision (input → expected action). These decide. Any implementation in any language can run them and prove it decides the same way; where an implementation and a vector disagree, the vector is right.vectors/README.mdhas the how-to for pointing your own implementation at them.core/— what most clients should take. A native C99 core behind a C ABI: the identity check, the ladder, and the full sync decision, already satisfying every vector. Allocation-free and freestanding — the compiled library imports nothing at all, not even libc, so it loads on any x86_64 Linux whatever the distro ships. Releases carrylibgavel-x86_64-linux.soand its.sha256, and sincev1.0.0the ABI itself is part of the promise.bindings/python/— the official Python binding: a ctypes wrapper over the core, and the worked example of driving the ABI correctly.reference/— a pure-Python second implementation, extracted from decky-romm-sync's production kernel. It exists so the vectors have a first consumer and so a port author has something readable to read; it is not what you ship against, and it is deliberately outside the release promise.
Spec and vectors came first on purpose — they define what the core must do and prove it does it, rather than the core becoming the de-facto contract.
- Bookkeeping (normative) — at each successful sync boundary the client records
last_sync_hash(its own baseline),last_sync_server_hash(the server's value, stored verbatim) andlast_sync_local_size. Recorded only at successful boundaries, never speculatively; a value that could not be obtained is recorded as absent, never as an empty string, because absent means unknown and unknown never proves anything. - The identity check (normative) — "is my local file byte-identical to that server save?", answered by two routes
so a drift between the client's hashing and the server's scheme never silently breaks identity: provenance (local
unchanged since the baseline, and that baseline synced against this exact server content) as the primary, parity
(local hash equals the server's
content_hashdirectly) as the fallback for a file with no sync history on this device. Every compared value must be present and non-empty. - The 409 resolution ladder (normative) — automatic uploads POST with
overwrite=false, and the server's 409 proves the slot's head moved past what this device last synced. The client re-decides from hashes alone: unchanged since baseline → download; byte-identical to the head → download; otherwise → conflict, surfaced to the user. Missing or empty information never yields download. - Overwrite discipline (normative) —
overwrite=trueis sent only to execute an explicit user keep-local decision. No automatic path ever forces, and the client neither retries the POST unchanged nor escalates on its own. - The decision table (informative) — the full per-
(rom, filename, slot)model the reference client uses, for clients that compute detection themselves. Informative means "not required of every client", not "optional to get right": where the family applies, its vectors decide exactly as tightly as the ladder's.
- Detection. negotiate owns "who changed" — this contract starts where the server's verdict ends. gavel is not a competing authority; it documents the client's side of the protocol.
- Hashing schemes, file I/O, transport, UI. The server's hash is authoritative; clients remember what the server said rather than recompute it. Local recomputation is at most a fallback for files a device has never synced.
- Save paths and emulator knowledge. That is sigil's and atlas's territory, and the client's.
Take the compiled core. This is the normal way, and it is what the repo is for: you do not implement the contract,
you link the implementation that already satisfies it. Vendor libgavel-x86_64-linux.so from a release, pin the
.sha256 that ships beside it, and re-verify the checksum in your CI so a swapped binary can never ship silently. You
do not run the vectors — the core satisfies them by construction and upstream CI proves it on every change. What is
worth testing on your side is your own marshalling; bindings/python shows what that layer has to
get right.
Or implement the contract yourself, for anyone the core cannot reach — another architecture, or a language you do not want to write a binding for. Vendor the vector families you conform to at a release tag, not at a raw commit, and record which families you conform to rather than only which release you pinned: the version number is repo-wide, the obligation is per family.
Two surfaces are promised, and either one changing incompatibly is a major release: the contract (every vector's
expected value, plus the normative spec sections) and the C ABI in core/gavel.h — struct layouts,
signatures, enumerator values and the input conventions the header states. Adding vectors, a family, or an exported
function is a minor. Adding a field to an existing struct is not, because it moves the fields after it.
SPEC.md has the full list, including what is explicitly not promised.
If RomM grows a per-device baseline anchor and resolution directives on its verdicts, parts of this contract move server-side and this repo shrinks accordingly. That is the goal, not a failure mode: the contract documents the current client obligation precisely enough that upstreaming it is easy. Until then, three shipped clients resolving the same slot state differently is the problem this solves.
Both vector families exist and the contract is implemented twice over.
The 409 resolution ladder is extracted end-to-end: normative spec sections (bookkeeping, identity check, ladder,
overwrite discipline) and a curated plus exhaustive vector set under vectors/ladder/. The decision-table family
(vectors/decision-table/) covers the full informative decision model the same way — including the provenance identity
route the ladder vectors can't reach.
Every vector runs against the Python reference, the native core, and the Python binding on each PR, alongside differentials between them and sanitizer sweeps over the C. The rules come from a production implementation (decky-romm-sync) that has been through a fair number of real conflict edge cases: baseline drift vs byte-identical restores, corrupt/truncated locals, both-sides-moved divergence, and stale-snapshot upload races — and that now resolves its 409s through the compiled core, with no Python fallback.
Not here yet: bindings beyond Python, and release artifacts beyond x86_64 Linux. Both are tracked as open issues, and both are waiting on a consumer rather than on the work — a binding nobody adopts is speculation.
mise run setup # pinned test dependencies into the local venv
mise run build # build the core as build/libgavel.so
mise run test # every vector against the reference, the C core, and the Python binding
mise run sanitize # the C drivers under ASan + UBSan
mise run validate # vector shape validation
deno fmt --checkCONTRIBUTING.md carries the one rule everything else derives from — never edit a vector to make
an implementation pass — plus what a vector change means for versioning (a changed expected is a major bump, by
design) and how to consume a release as a client author.
MIT. This is an independent project and is not affiliated with, endorsed by, or sponsored by the RomM project.
