refactor(graph): split spec-hash traversal from encoding behind a SpecEncoder trait - #1107
Merged
Merged
Conversation
…cEncoder trait Byte-for-byte identical output; no cache keys change. Isolates the encoding layer behind one trait so a future hash format is a new impl, not edits threaded through the graph walk. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughRefactors spec hashing to separate traversal from byte serialization through a private ChangesSpec hashing encoder refactor
Estimated code review effort: 4 (Complex) | ~45 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
norrietaylor
approved these changes
Jul 30, 2026
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.
Pure structural refactor of the spec-hash serializer. It changes no bytes and no cache keys — the output is identical to before for every input (see "Byte-identity" below). No
BuildSpec, no wire format, no public API changes.What & why
crates/graph/src/spec_hasher.rscomputesSpecHash, minimal's content-address cache key. Today it interleaves two concerns:The encoding was scattered as direct
hasher.write_all(...)calls across five functions. This PR routes all byte emission through a smallSpecEncodertrait, with one implementation —LegacyEncoder— that reproduces the current scheme exactly (raw concatenation, in-band markers, little-endian widths). Traversal (process) is untouched; the newencode()method walks the already-discovered specs and emits through the trait.The trait's methods carry semantic intent the legacy encoder deliberately collapses:
For the legacy scheme
tagandbytesare both a rawwrite_all— so this is a no-op rename today. The point is what it enables: a length-prefixed / injective successor is a new impl of this trait, selected at the call site, rather than edits threaded through the graph walk.Motivation (context, not required to review this diff)
The spec-hash byte encoding is not prefix-free — distinct specs can serialize to identical bytes (a small set of known collisions exists). A future change will replace the encoding with an injective one, which necessarily changes every cache key (a coordinated "epoch" migration). That change is far safer and smaller to review if the encoding already lives behind a single seam instead of being woven through the traversal. This PR builds that seam with zero behavioral change, so the risky part lands later as an isolated, self-contained diff. It also gives the encoding a clean extraction point for property/formal testing.
Byte-identity — how it's verified
attrs_hash,subset_hash) pin fixed expected digests with no environmental input. They pass unchanged — direct proof the encoder emits identical bytes.spec_tree,cycle,abstract_deps_hashed) ingest specs through Nickel and assert per-arch golden values via thearch_hash!macro; their result is sensitive to the local stdlib version (the macro's own comment notes this). On a machine whose local stdlib differs from the pinned CI one they fail before and after this change, computing the identical hash both times — I diffed the computed value on this branch vs pristine main and they match exactly (41c47d47…in both). So these are a pre-existing local-environment mismatch, not a regression, and they pass in CI's pinned stdlib.Beyond that: 52/55 graph tests pass locally (the 3 above being the known env-sensitive ones), fmt clean, clippy clean on the file.
Incidental tidy
The two byte-identical inline copies of the subset-output edge encoding are factored into one
encode_subset_infohelper.🤖 Generated with Claude Code
Note
Split spec-hash traversal from encoding behind a
SpecEncodertrait in graphSpecEncodertrait in spec_hasher.rs that abstracts byte-encoding operations (tag,bytes,index,number,target,finish), decoupling graph traversal from the hashing implementation.LegacyEncoderas the epoch-0 implementation ofSpecEncoder, preserving the existing concatenation-based hashing scheme (little-endian usize, f64 LE bytes,Target::hash_to).SpecHasher::hashto delegate encoding toself.encode(LegacyEncoder::new()), and rewritesbuild_attrs_hash,build_input_hash,build_output_hash, andbuild_attrvalue_hashto accept a genericSpecEncoderinstead of a directHasher.encode_subset_infoas a shared helper replacing previously duplicated inline subset-output encoding logic.Macroscope summarized 59e2d4f.
Summary by CodeRabbit