Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.

feat(graph-hasher): engine-agnostic GVS hash gating (#459)#469

Merged
zkochan merged 2 commits into
mainfrom
feat/459
May 13, 2026
Merged

feat(graph-hasher): engine-agnostic GVS hash gating (#459)#469
zkochan merged 2 commits into
mainfrom
feat/459

Conversation

@zkochan

@zkochan zkochan commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Closes #459. Ports pnpm's transitivelyRequiresBuild + computeBuiltDepPaths into pacquet-graph-hasher and wires the builtDepPaths-driven engine gating from upstream's calcGraphNodeHash into pacquet's GVS slot-path computation.

When a snapshot — and its entire transitive subtree — has no entry in allowBuilds (and dangerouslyAllowAllBuilds is off), its GVS hash payload now sets engine: null instead of engine: ENGINE_NAME. Pure-JS packages share one slot directory across Node.js versions, OSes, and CPUs, which is what makes the shared global virtual store cross-host portable.

What changed

  • graph-hasher::dep_state — new pub(crate) fn transitively_requires_build, mirroring upstream's transitivelyRequiresBuild verbatim. Notable subtleties preserved: cycle guard uses depPath (not fullPkgId), and a cycle hit returns false without caching to avoid poisoning sibling walks.
  • graph-hasher::calc_graph_node_hash — grows two parameters: built_dep_paths: Option<&HashSet<K>> and build_required_cache: &mut HashMap<K, bool>. None reproduces the always-include-engine shape (matches upstream's builtDepPaths === undefined short-circuit), so callers can opt into the gating only when they have an AllowBuildPolicy available.
  • VirtualStoreLayout::new — grows an Option<&AllowBuildPolicy> parameter. Internally walks snapshots once to derive built_dep_paths, then threads that set through every calc_graph_node_hash call.
  • InstallFrozenLockfile::run — passes the install's existing AllowBuildPolicy to VirtualStoreLayout::new. The policy was already built upstream of this call site so this is a one-line wiring change.

Why pacquet still needed this after #449

#449 introduced the GVS path computation but left every snapshot hashing with engine = ENGINE_NAME. A darwin-arm64-node20 install and a linux-x64-node22 install of the same pure-JS package would land in different GVS slots, defeating the whole point of a shared global store. #459 closes that gap: pure-JS subtrees hash engine-agnostically, native subtrees keep partitioning by engine.

Test plan

  • Six new transitively_requires_build unit tests (self-hit, transitive ancestor, unrelated tree, missing node, cycle termination, cycle-doesn't-mask-sibling-builder)
  • Four new calc_graph_node_hash tests (engine-agnostic when gated, engine-specific for self-builder, engine-specific for ancestor-of-builder, None disables gating)
  • Two new VirtualStoreLayout::new end-to-end tests (slot identical across engines for pure-JS with empty policy; slot differs across engines when the snapshot is in allowBuilds)
  • just ready (926 tests pass)
  • taplo format --check
  • just dylint

Written by an agent (Claude Code, claude-opus-4-7).

Summary by CodeRabbit

  • Improvements
    • More accurate detection of when dependency subgraphs require native builds.
    • Optimized global virtual store slot computation to better handle engine-agnostic (JS-only) packages.
  • Changes
    • Layout initialization now accepts an allow-build policy to control engine inclusion in hashing.
  • Tests
    • Updated and added tests covering build-gating, hashing, and virtual store behavior.

Review Change Stack

Port pnpm's `transitivelyRequiresBuild` + `computeBuiltDepPaths` into
`graph-hasher`, and extend `calc_graph_node_hash` with the
`builtDepPaths`-driven engine-gating that mirrors upstream's
[`calcGraphNodeHash`](https://github.com/pnpm/pnpm/blob/94240bc046/deps/graph-hasher/src/index.ts#L140-L142).

When a snapshot — and its entire transitive subtree — has no entry
in `allowBuilds` (and `dangerouslyAllowAllBuilds` is off), its GVS
hash payload now sets `engine: null` instead of `engine:
ENGINE_NAME`. Pure-JS packages share one slot directory across
Node.js versions, OSes, and CPUs, matching upstream and unlocking
the cross-host store sharing the GVS layout was designed for.

`VirtualStoreLayout::new` grows an `Option<&AllowBuildPolicy>`
parameter. `InstallFrozenLockfile::run` passes the install's
`AllowBuildPolicy` through; callers that pass `None` retain the
pre-#459 always-include-engine shape (matches upstream's
`builtDepPaths === undefined` short-circuit).

Tests:
- six new `transitively_requires_build` unit tests covering self-hit,
  transitive ancestor, unrelated tree, missing node, cycle
  termination, and cycle-doesn't-mask-sibling-builder
- four new `calc_graph_node_hash` tests covering the engine-agnostic
  vs engine-specific paths and the `built_dep_paths = None` shortcut
- two end-to-end `VirtualStoreLayout::new` tests proving a pure-JS
  snapshot's slot directory is identical across two `engine` strings,
  and that a built snapshot's slot differs

---
Written by an agent (Claude Code, claude-opus-4-7).
Copilot AI review requested due to automatic review settings May 13, 2026 16:22
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 73caed2d-83d0-4a89-bcdc-3480a266b61f

📥 Commits

Reviewing files that changed from the base of the PR and between b7e254d and ac776b0.

📒 Files selected for processing (1)
  • crates/graph-hasher/src/global_virtual_store_path.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/graph-hasher/src/global_virtual_store_path.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Dylint

📝 Walkthrough

Walkthrough

Adds transitively_requires_build; uses it in calc_graph_node_hash to optionally null out engine when snapshots (and their transitive deps) don't require builds; threads allow_build_policy through VirtualStoreLayout and InstallFrozenLockfile and updates tests.

Changes

Engine-agnostic GVS hash gating via built_dep_paths

Layer / File(s) Summary
Transitive build requirement detection
crates/graph-hasher/src/dep_state.rs
Adds transitively_requires_build recursive helper with memoization and cycle detection plus tests covering direct hits, transitive discovery, missing nodes, and cycle interactions.
GVS hash engine gating via built_dep_paths
crates/graph-hasher/src/global_virtual_store_path.rs
Changes calc_graph_node_hash signature to accept built_dep_paths: Option<&HashSet<K>> and build_required_cache: &mut HashMap<K, bool>; computes include_engine via transitively_requires_build when gating is provided and sets engine to null when excluded. Tests updated to use per-call caches and gating inputs.
VirtualStoreLayout AllowBuildPolicy integration
crates/package-manager/src/virtual_store_layout.rs
Extends VirtualStoreLayout::new(..., allow_build_policy: Option<&AllowBuildPolicy>), computes a built_dep_paths: Option<HashSet<PackageKey>> from snapshots via AllowBuildPolicy::check, and passes it into calc_graph_node_hash. Adds tests asserting engine-agnostic vs engine-specific slot behavior depending on allowlist.
InstallFrozenLockfile call site
crates/package-manager/src/install_frozen_lockfile.rs
Passes Some(&allow_build_policy) into VirtualStoreLayout::new in the install path to enable production gating behavior.

Sequence Diagram

sequenceDiagram
  participant VSL as VirtualStoreLayout::new
  participant Policy as AllowBuildPolicy
  participant Hasher as calc_graph_node_hash
  participant BuilderCheck as transitively_requires_build

  VSL->>Policy: check snapshots (name, version)
  Policy-->>VSL: per-snapshot build_allowed flags
  VSL->>VSL: build built_dep_paths HashSet
  VSL->>Hasher: pass built_dep_paths & build_required_cache
  Hasher->>BuilderCheck: transitively_requires_build checks dep graph
  BuilderCheck-->>Hasher: boolean: requires build?
  Hasher-->>VSL: compute slot hash (engine or null)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pacquet#444: Related changes to calc_graph_node_hash/VirtualStoreLayout hashing foundation and earlier GVS work.

Suggested reviewers

  • anthonyshew

Poem

🐰 I hop the graph, I sniff each node,

If no builder's found, I'll lighten the load.
Engine goes nil on pure-JS trails,
Store slots survive cross-host tales.
Hooray—one less needless reinstall!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(graph-hasher): engine-agnostic GVS hash gating (#459)' accurately and specifically describes the main change: adding engine-agnostic hashing for the global virtual store via the allow-builds policy.
Description check ✅ Passed The pull request description comprehensively covers the summary, changes, rationale, and test plan. It explains what was changed and why, matching the template's requirements.
Linked Issues check ✅ Passed The PR successfully implements all objectives from issue #459: ports transitively_requires_build and computeBuiltDepPaths, implements engine gating in calc_graph_node_hash, wires built_dep_paths through VirtualStoreLayout, and adds comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing engine-agnostic GVS hash gating: new graph-hasher functions, updated signatures to accept built_dep_paths, VirtualStoreLayout wiring, and InstallFrozenLockfile integration—no extraneous changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/459

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

@github-actions

github-actions Bot commented May 13, 2026

Copy link
Copy Markdown

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.01     15.0±0.43ms   288.9 KB/sec    1.00     14.8±0.46ms   292.4 KB/sec

@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.17%. Comparing base (c614d64) to head (ac776b0).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #469      +/-   ##
==========================================
+ Coverage   88.64%   89.17%   +0.53%     
==========================================
  Files         116      116              
  Lines        9952    10432     +480     
==========================================
+ Hits         8822     9303     +481     
+ Misses       1130     1129       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR ports pnpm’s builtDepPaths-driven “engine in hash payload” gating into pacquet’s global virtual store (GVS) slot hashing, so snapshots that don’t (transitively) require builds hash with engine = null and can share the same slot directory across differing engine strings.

Changes:

  • Add transitively_requires_build and thread built_dep_paths + an install-scoped memoization cache into calc_graph_node_hash.
  • Extend VirtualStoreLayout::new to derive built_dep_paths from AllowBuildPolicy and pass it into every node-hash computation.
  • Wire AllowBuildPolicy from InstallFrozenLockfile::run into the GVS layout construction.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
crates/package-manager/src/virtual_store_layout.rs Derives built_dep_paths from AllowBuildPolicy and threads it into per-snapshot GVS hash computation; adds end-to-end gating tests.
crates/package-manager/src/install_frozen_lockfile.rs Passes the install’s AllowBuildPolicy into VirtualStoreLayout::new to enable gating during frozen installs.
crates/graph-hasher/src/global_virtual_store_path.rs Extends calc_graph_node_hash API to support engine gating based on built_dep_paths, and adds targeted unit tests.
crates/graph-hasher/src/dep_state.rs Adds transitively_requires_build (ported from upstream) plus unit tests for cycle handling and caching behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/graph-hasher/src/global_virtual_store_path.rs Outdated
Comment thread crates/graph-hasher/src/global_virtual_store_path.rs
…docs

- Drop the cross-module intra-doc link `[transitively_requires_build]:
  crate::dep_state::transitively_requires_build` that resolved to a
  pub(crate) item. The link rendered correctly under pacquet's CI
  (which runs `cargo doc --document-private-items`), but downstream
  doc builds without that flag would trip `private_intra_doc_links`.
  Switched to a plain-backticks reference + an inline "private to
  this crate" note so the doc remains informative either way.
- Reword the `build_required_cache` hint to avoid the ambiguous
  `&mut HashMap::new()` phrasing — temporaries-in-call-args is a
  valid pattern but a confusing one to surface at the API doc.
  Pointed callers at the explicit `let mut cache = HashMap::new();`
  + `&mut cache` shape instead.
@github-actions

Copy link
Copy Markdown

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.671 ± 0.155 2.552 3.061 1.04 ± 0.07
pacquet@main 2.577 ± 0.084 2.456 2.755 1.00
pnpm 6.113 ± 0.112 5.952 6.341 2.37 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.67077937988,
      "stddev": 0.15531575858451632,
      "median": 2.62893989068,
      "user": 2.5605913800000004,
      "system": 3.6474907599999993,
      "min": 2.55187367618,
      "max": 3.06128971818,
      "times": [
        2.70228094318,
        2.5672896921799997,
        2.6913888291799997,
        3.06128971818,
        2.7526557211799996,
        2.5623664371799997,
        2.58266738818,
        2.67521239318,
        2.56076900018,
        2.55187367618
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.57679513068,
      "stddev": 0.08396979782990949,
      "median": 2.5608576366799998,
      "user": 2.5504745800000004,
      "system": 3.598221659999999,
      "min": 2.45586457318,
      "max": 2.75463357118,
      "times": [
        2.75463357118,
        2.5859677831799996,
        2.59863927918,
        2.4908421121799997,
        2.66126161918,
        2.55003376718,
        2.5688169141799997,
        2.45586457318,
        2.55289835918,
        2.54899332818
      ]
    },
    {
      "command": "pnpm",
      "mean": 6.11298625168,
      "stddev": 0.11154124427354146,
      "median": 6.104276802679999,
      "user": 8.98668828,
      "system": 4.44794566,
      "min": 5.9524308791800005,
      "max": 6.34086370118,
      "times": [
        5.973705396180001,
        5.9524308791800005,
        6.10880134518,
        6.084184635180001,
        6.06080507318,
        6.20074212818,
        6.34086370118,
        6.15207356218,
        6.15650353618,
        6.09975226018
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 730.2 ± 26.2 693.5 776.4 1.00
pacquet@main 822.5 ± 68.0 734.6 966.1 1.13 ± 0.10
pnpm 2610.5 ± 85.0 2507.5 2733.4 3.58 ± 0.17
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.7301575544000001,
      "stddev": 0.026230773108162665,
      "median": 0.7243671747,
      "user": 0.37047996,
      "system": 1.5814983399999998,
      "min": 0.6935072797,
      "max": 0.7764429567000001,
      "times": [
        0.7764429567000001,
        0.7031568447000001,
        0.7577220787000001,
        0.7463103067,
        0.6935072797,
        0.7225611077,
        0.7161582717,
        0.7261732417000001,
        0.7483093777000001,
        0.7112340787
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.8224829989000002,
      "stddev": 0.0680289468897996,
      "median": 0.8045878572,
      "user": 0.39178456,
      "system": 1.60442494,
      "min": 0.7345946787000001,
      "max": 0.9661496737,
      "times": [
        0.8772675727,
        0.8061633217,
        0.7919156187,
        0.7605835507,
        0.8793753327,
        0.8030123927,
        0.7345946787000001,
        0.7820357727,
        0.9661496737,
        0.8237320747
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.6104956914999997,
      "stddev": 0.08500306212912238,
      "median": 2.5842466507,
      "user": 3.1227089599999998,
      "system": 2.2300388399999997,
      "min": 2.5074749647,
      "max": 2.7334173637,
      "times": [
        2.6776218607,
        2.7314060387,
        2.5894623477,
        2.6646528047,
        2.5074749647,
        2.5649535147,
        2.5165307907,
        2.5790309537000002,
        2.7334173637,
        2.5404062757
      ]
    }
  ]
}

@zkochan zkochan merged commit e3e9f51 into main May 13, 2026
16 checks passed
@zkochan zkochan deleted the feat/459 branch May 13, 2026 16:54
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Engine-agnostic GVS hash gating via allowBuilds-driven built_dep_paths (#432 follow-up)

2 participants