Skip to content

refactor(graph): split spec-hash traversal from encoding behind a SpecEncoder trait - #1107

Merged
bryan-minimal merged 1 commit into
mainfrom
refactor/spec-hasher-extraction
Jul 30, 2026
Merged

refactor(graph): split spec-hash traversal from encoding behind a SpecEncoder trait#1107
bryan-minimal merged 1 commit into
mainfrom
refactor/spec-hasher-extraction

Conversation

@bryan-minimal

@bryan-minimal bryan-minimal commented Jul 30, 2026

Copy link
Copy Markdown
Member

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.rs computes SpecHash, minimal's content-address cache key. Today it interleaves two concerns:

  1. Traversal — walk the build graph, discover every referenced spec, assign each a stable discovery index (handles cycles via a seen-map).
  2. Encoding — turn each spec's fields and edges into the exact bytes fed to Blake3.

The encoding was scattered as direct hasher.write_all(...) calls across five functions. This PR routes all byte emission through a small SpecEncoder trait, with one implementation — LegacyEncoder — that reproduces the current scheme exactly (raw concatenation, in-band markers, little-endian widths). Traversal (process) is untouched; the new encode() method walks the already-discovered specs and emits through the trait.

The trait's methods carry semantic intent the legacy encoder deliberately collapses:

trait SpecEncoder {
    fn tag(&mut self, tag: &[u8]);      // fixed structural marker  (b"src", b"i", ...)
    fn bytes(&mut self, bytes: &[u8]);  // variable content        (a name, glob, url)
    fn index(&mut self, idx: usize);    // a discovery index
    fn number(&mut self, n: f64);       // AttrValue::Number
    fn target(&mut self, target: &Target);
    fn finish(self) -> SpecHash;
}

For the legacy scheme tag and bytes are both a raw write_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

  • The two hand-constructed golden tests (attrs_hash, subset_hash) pin fixed expected digests with no environmental input. They pass unchanged — direct proof the encoder emits identical bytes.
  • The other three hasher tests (spec_tree, cycle, abstract_deps_hashed) ingest specs through Nickel and assert per-arch golden values via the arch_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_info helper.

🤖 Generated with Claude Code

Note

Split spec-hash traversal from encoding behind a SpecEncoder trait in graph

  • Introduces a SpecEncoder trait in spec_hasher.rs that abstracts byte-encoding operations (tag, bytes, index, number, target, finish), decoupling graph traversal from the hashing implementation.
  • Adds LegacyEncoder as the epoch-0 implementation of SpecEncoder, preserving the existing concatenation-based hashing scheme (little-endian usize, f64 LE bytes, Target::hash_to).
  • Refactors SpecHasher::hash to delegate encoding to self.encode(LegacyEncoder::new()), and rewrites build_attrs_hash, build_input_hash, build_output_hash, and build_attrvalue_hash to accept a generic SpecEncoder instead of a direct Hasher.
  • Adds encode_subset_info as a shared helper replacing previously duplicated inline subset-output encoding logic.
  • Risk: no change to hash output is intended, but any divergence in traversal or emit order between old inlined code and the new encoder path would silently produce different spec hashes.

Macroscope summarized 59e2d4f.

Summary by CodeRabbit

  • Refactor
    • Improved internal specification hash generation while preserving existing hash results.
    • Unified hashing behavior across specification and subset calculations.
    • No changes to public interfaces or end-user functionality.

…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>
@bryan-minimal
bryan-minimal requested a review from a team as a code owner July 30, 2026 20:26
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Refactors spec hashing to separate traversal from byte serialization through a private SpecEncoder abstraction. LegacyEncoder preserves the existing Blake3 byte stream, while SpecHasher and SubsetHasher use shared generic encoding helpers.

Changes

Spec hashing encoder refactor

Layer / File(s) Summary
Encoder abstraction and legacy serialization
crates/graph/src/spec_hasher.rs
Introduces the private SpecEncoder trait and LegacyEncoder implementation for tags, bytes, indices, numbers, targets, and finalization using the historical hash encoding.
Generic spec and subset encoding
crates/graph/src/spec_hasher.rs
Makes spec traversal and build/edge attribute helpers generic over SpecEncoder, and updates subset hashing to use the legacy encoder path.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: 0chroma

Poem

I’m a rabbit with hashes tucked under my ear,
An encoder refactor makes the byte paths clear.
Tags hop in order, indices march neat,
Legacy bytes keep their familiar beat.
Blake3 hums softly—no old trail is gone,
While subset specs bounce happily on.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is clear, concise, and accurately summarizes the main refactor in the changeset.
Description check ✅ Passed The description covers the summary and testing evidence well, though it doesn't follow the template headings and checklist format exactly.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/spec-hasher-extraction

Comment @coderabbitai help to get the list of available commands.

@bryan-minimal
bryan-minimal merged commit 3e69ddc into main Jul 30, 2026
30 checks passed
@bryan-minimal
bryan-minimal deleted the refactor/spec-hasher-extraction branch July 30, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants