Add SchemaBinary fingerprint positional mode - #7370
Merged
tim-smart merged 2 commits intoAug 20, 2026
Merged
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 5bb27c0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 30 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
The fingerprint hashes the compiled layout graph, not the infinite wire shape it denotes. Those coincide for acyclic layouts but not once a cycle is involved, so re-factoring a recursive schema moves the hash without changing a byte on the wire. Say so instead of promising canonicalisation, and pin the alias-level case with a test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tim-smart
added a commit
that referenced
this pull request
Aug 21, 2026
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tim-smart
added a commit
that referenced
this pull request
Aug 21, 2026
Co-authored-by: Claude Opus 5 <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.
Summary
Adds the opt-in fingerprint / positional wire mode for
SchemaBinary, selected by envelope flag bit 0. The default evolution-friendly mode is unchanged and stays the default.SchemaBinary.toCodec(schema, { fingerprint: true })andSchemaBinary.parser(schema, { fingerprint: true })select the new mode14695981039346656037, prime1099511628211, little-endian); a reader whose layout hashes differently rejects the frame rather than guessingThe hash is a Merkle walk over the compiled layout with relative back edges for cycles. It mixes field ids, optionality, wire kinds, variant tags, and array shape; never names, checks, or annotations. An acyclic sub-layout hashes the same whether it is a shared compiled node or written out twice, and a given cycle hashes the same wherever it is entered.
What the hash is, and is not
It hashes the compiled layout graph, not the infinite wire shape that graph denotes. Those coincide for acyclic layouts. They do not once a cycle is involved, because the same unfolding has many finite cyclic representations and de Bruijn style back references encode each of them differently.
Concretely, a self-recursive
Tree, the same schema behind one extra non-recursive alias, and a two-node mutual recursion of the same shape all produce byte-identical frames, and all three hash differently and reject each other. Closing that gap needs bisimulation minimisation over the layout graph, which is a lot of machinery for a case that already fails closed, so this PR documents the semantics instead: peers must ship the same schema definition, not merely the same wire shape. The limitation is pinned by a test rather than left to be discovered in production.Thanks to the review for catching that the original wording promised canonicalisation the implementation does not deliver.
No stream-level fingerprint negotiation: the fingerprint is per frame, so concatenation still works and every frame stands alone.
Results
Sizes are exact; rates are medians of three runs on Node 26.7.0, Linux x64, measured against the optimized default mode in the same tree.
Raw and compressed sizes for the 200-frame per-frame stream, which is the workload the mode is aimed at:
The size result splits by shape. Struct-heavy payloads drop the 5-byte field id and, for fixed-size leaves, the length byte, which is where the 24% to 40% raw savings come from. Index-signature records carry almost no named fields, so they pay the 8-byte fingerprint and save nothing; those two cases get marginally larger. Compression narrows the gap without erasing it: -7.5% gzip and -13.9% zstd on the per-frame stream against -23.7% raw.
The decode gain is reported as a measurement against the optimized parser. The pre-optimization estimate that field varints were 24% of decode time predates the unrolled reader and is not reused as a forecast.
Coverage
Golden vectors for the fingerprint envelope and every positional shape (struct with and without an optional present, union positions, extra-key map, tuple, array, inline leaves), plus:
isInt,fieldId, leaf type, tuple arity,ArrayversusTuple,DateversusDateTimeUtc, and union membership all change itmaxFrameSize, and truncation atend{}, and{ fingerprint: false }Verification
nix develop -c pnpm vitest run --project effect test/unstable/encoding(116 passed)nix develop -c pnpm vitest run --project effect(8709 passed, 47 skipped)nix develop -c pnpm lintnix develop -c pnpm checknix develop -c pnpm --dir packages/effect exec node benchmark/schema/SchemaBinary.tsStacked on the merged #7369 at
0d9caf9380. Keep this PR open for the EFF-783 simplification pass, which continues on this branch.Closes EFF-780
🤖 Generated with Claude Code