Skip to content

feat(linker,cli,bench): Phase 66 Phase 4f — flip linker default Isolated → Hoisted (dual gate passed)#35

Open
tolgaergin wants to merge 1 commit into
phase66-4e-cache-prunefrom
phase66-4f-linker-default-flip
Open

feat(linker,cli,bench): Phase 66 Phase 4f — flip linker default Isolated → Hoisted (dual gate passed)#35
tolgaergin wants to merge 1 commit into
phase66-4e-cache-prunefrom
phase66-4f-linker-default-flip

Conversation

@tolgaergin
Copy link
Copy Markdown
Contributor

Summary

The Phase 4 capstone. With the v2 virtual store landed in 4d and prune in 4e, hoisted's pre-Phase-66 warm-install regression collapses to identity: both modes now symlink project node_modules/<dep> into the global v2 store, and warm-install becomes pure project-side symlink reconstruction regardless of mode.

Stacks on #34 (Phase 4e — lpm cache prune). Will auto-rebase to main after #30#31#32#33#34 land.

Dual-gate bench result (preplan §6.5)

========================================
 PHASE 66 PHASE 4F DUAL-GATE RESULTS
========================================
 isolated median:  110 ms
 hoisted  median:  110 ms
 hoisted - iso  :   +0 ms

 GATE 1 (hoisted ≤ 200 ms absolute):              ✓
 GATE 2 (hoisted ≤ isolated + 30 ms paired):      ✓

 VERDICT: BOTH GATES PASS — flip the linker default to hoisted.

n=20 paired samples on bench/fixture-large under README footnote 3 methodology. 18 of 20 paired samples landed within ±5 ms of zero; two outliers (+32 ms, +134 ms) look like APFS page-cache warm-up noise — median unaffected.

For context, the pre-Phase-66 numbers (active-roadmap §2.6) were:

Scenario Pre-66 isolated Pre-66 hoisted Δ
warm-install 168 ms 449 ms +281 ms (2.7× regression)

The v2 virtual store closed that gap completely.

What's in this PR

Bench harness — new bench/scripts/run-4f-gate.sh. Round-robin per outer iter, warm-up discarded, samples persisted to CSV for triage. Exits 0 only when both gates pass.

The fliplpm_linker::LinkerMode::default() flipped Isolated → Hoisted with cascading updates:

  • linker_config::resolve_effective_linker returns LinkerMode::default() instead of literal Isolated (future-flip-stable).
  • install_state::compute_install_hash + compute_install_hash_v3 (back-compat shims) follow the new default.
  • lpm install --linker help text rewritten to reflect the new contract.

Test scoping (the long tail) — 7 tests asserted on isolated-shape behavior. Each updated to either:

  • Pin LPM_LINKER=isolated explicitly when testing isolated-shape (install_patches::* tests, install_json_envelope_* snapshot).
  • Use LinkerMode::default() instead of literal Isolated when asserting on default-linker output (hash pin tests, auto_install_if_stale_*, install_invalidates_freshness_cache_on_lpm_linker_flip).
  • Use live_package_dir_with_v2(None) to disable env coupling (live_package_dir_falls_back_to_store_when_unlinked was finding stale entries in the dev's real ~/.lpm/store/v2/links/).

No global LPM_LINKER=isolated pin in the workflow helper — each isolated-shape test pins itself, which is more honest than a global pin (the global would silently mask hoisted-default regressions in tests that don't know they're isolated-shape).

Pre-merge gate

cargo clippy --workspace --all-targets -- -D warnings  ✓
cargo fmt --check                                       ✓
cargo nextest run --workspace --exclude lpm-integration-tests
  → 5742 tests pass, 7 skipped                          ✓
cargo test -p lpm-auth (parallel-deterministic)         ✓
./bench/audit-fixtures/run-all.sh
  → 17 PASS / 1 SKIP / 0 mixed (v2 default)            ✓
LPM_STORE_VERSION=v1 ./bench/audit-fixtures/run-all.sh
  → 17 PASS / 1 SKIP / 0 mixed (v1 downgrade)          ✓
./bench/scripts/run-4f-gate.sh 20 phase4f-decision
  → BOTH GATES PASS                                     ✓

What's next (post-Phase-66)

Phase 66 is complete with this PR. The follow-up work is outside this phase:

  • README perf row refresh with the post-4f warm-install numbers (single-line edit).
  • CHANGELOG + release notes for the 4d default-store flip and the 4f default-linker flip (single rollup).
  • Phase 5 (Phase 63c openat) — independent perf work, queued.

Test plan

  • All updated tests pass (5742 nextest)
  • Audit fixtures green under default (v2) and LPM_STORE_VERSION=v1 downgrade
  • Dual-gate bench passes (n=20)
  • Manual smoke: lpm install on a fresh project produces hoisted-flat node_modules/<dep> symlinks into ~/.lpm/store/v2/links/<key>/...
  • Manual smoke: lpm install --linker isolated still produces isolated layout
  • Manual smoke: LPM_STORE_VERSION=v1 lpm install still works (downgrade rollback)
  • CI matrix green on both audit-fixtures (v1) and audit-fixtures (v2) rows

🤖 Generated with Claude Code

…ted → Hoisted (dual gate passed)

The Phase 4 capstone. With the v2 virtual store landed in 4d and
prune in 4e, hoisted's pre-Phase-66 warm-install regression
(2.7× slower than isolated under v1's clonefile-from-store
materialization) collapses to identity: both modes now symlink
project `node_modules/<dep>` into the global
`~/.lpm/store/v2/links/<graph-key>/` and warm-install becomes pure
project-side symlink reconstruction regardless of mode.

The flip is gated on a paired warm-install bench (preplan §6.5):

```
========================================
 PHASE 66 PHASE 4F DUAL-GATE RESULTS
========================================
 isolated median:  110 ms
 hoisted  median:  110 ms
 hoisted - iso  :   +0 ms

 GATE 1 (hoisted ≤ 200 ms absolute):              ✓
 GATE 2 (hoisted ≤ isolated + 30 ms paired):      ✓

 VERDICT: BOTH GATES PASS — flip the linker default to hoisted.
```

n=20 paired samples on `bench/fixture-large` under README footnote 3
methodology (`rm -rf node_modules` only; lockfile + cache + `.lpm`
all survive). 18 of 20 paired samples landed within ±5 ms of zero;
the two outliers (+32 ms, +134 ms) look like macOS APFS page-cache
warm-up noise, not a systematic bias — median is unaffected.

## What's in this PR

**1. Bench harness** — new `bench/scripts/run-4f-gate.sh`. Round-
robin per outer iter (each pair runs back-to-back, identical state),
warm-up pass discarded, atomic timing via `date +%s%N`. Persists
raw samples to `/tmp/lpm-bench-4f-gate/<tag>-results/samples.csv`
for triage. Exits 0 only when BOTH gates pass.

**2. Default flip** — `lpm_linker::LinkerMode::default()` flipped
Isolated → Hoisted. Module-doc + `Isolated`/`Hoisted` field docs
updated to reflect the new contract. The flip is documented as the
direct consequence of the Phase 4d store-version flip and the
4f bench result.

**3. Cascading updates** to call sites that hard-coded
`LinkerMode::Isolated` as the assumed default:

- `linker_config::resolve_effective_linker` returns
  `LinkerMode::default()` instead of literal `Isolated` so future
  default flips don't require a second edit at this site.
- `linker_config::resolve_effective_linker_from_bytes` same change
  for the malformed-manifest fallback.
- `install_state::compute_install_hash` (back-compat shim used by
  test fixtures) now defaults the linker arg to
  `LinkerMode::default()` — callers expecting "the hash a default
  install would produce" get the post-4f value.
- `install_state::compute_install_hash_v3` same change, same
  rationale.

**4. CLI help text** — `lpm install --linker` description
rewritten. Pre-4f: "isolated (default — pnpm-style isolation) or
hoisted (npm v3+ flat layout). Hoisted is faster on full-wipe / CI
cold-cache workloads, identical on everyday installs, and has
stricter peer-dep semantics — opt-in only." Post-4f: "hoisted
(default since Phase 66 4f — npm v3+ flat layout) or isolated
(pnpm-style strict isolation). Both materialize as project
node_modules/<dep> symlinks into the global virtual store, so
warm-install latency is identical between modes; the distinction
is whether transitive deps are reachable at the project root
(hoisted: yes — npm-compat) or only through each consumer's own
siblings (isolated: no — phantom-dep catcher)."

## Test scoping (the long tail)

Workflow tests + a few unit tests assert on isolated-shape behavior
(`<project>/.lpm/wrappers/...` paths, JSON envelope `symlinked`
counts, hash-pinning against a literal default). The flip would
break their contracts wholesale; pinning each test's intent
explicitly preserves what they test:

- `commands::dev::tests::auto_install_if_stale_writes_v6_install_hash_for_empty_deps` —
  asserted `lines[2] == "l:isolated"`. Updated to assert against
  `LinkerMode::default().as_str()` so the test stays stable across
  any future default flip.
- `install_state::tests::schema_tag_is_baked_into_hash` — updated
  to call `compute_install_hash_v6` with explicit
  `LinkerMode::Isolated`, decoupling the schema-pin from the
  default-linker-pin. The test catches schema tag drift, NOT
  default-linker drift.
- `install_state::tests::populated_v1_wrappers_force_v2_migration_on_default` —
  pre-flip this asserted `compute_install_hash("pkg", "lock")`
  equality which now includes the new default. The shim's behavior
  change handles it.
- `tests/workflows::install::install_invalidates_freshness_cache_on_lpm_linker_flip` —
  the test flips from default to "the other mode". Updated to
  resolve "other" as `match LinkerMode::default() { Hoisted =>
  "isolated", Isolated => "hoisted" }` so the contract holds
  regardless of which is the current default.
- `tests/workflows::install::install_json_envelope_with_one_package_matches_snapshot` —
  envelope-shape snapshot. Pinned to `LPM_LINKER=isolated` so the
  `symlinked` count stays at 2 rather than flipping to 0
  (hoisted's flat real-dir shape).
- `tests/workflows::install_patches::*` — every test in this file
  asserts on `<project>/.lpm/wrappers/<seg>/...` paths (v1 isolated
  layout). Added a file-local `lpm_isolated()` helper that wraps
  `lpm_with_registry` with `.env("LPM_LINKER", "isolated")`;
  bulk-replaced the 14 `lpm_with_registry(` call sites in this
  file with `lpm_isolated(`. Test contract preserved without
  pinning the helper or the env globally.
- `commands::rebuild::tests::live_package_dir_falls_back_to_store_when_unlinked` —
  the env-coupled `live_package_dir` was finding a stale
  `esbuild@0.21.5` entry in the developer's real
  `~/.lpm/store/v2/links/` from an earlier bench run, so the
  fallback assertion failed (left = real-store-path, right =
  fixture). Rewrote the test to call
  `live_package_dir_with_v2(None)` so the v2 store walk is fully
  disabled — pure unit test of the v1 wrapper-probe fallback.

## What did NOT need to change

- v1 isolated fast paths still work via `LPM_LINKER=isolated`
  override.
- v1 store mode still works via `LPM_STORE_VERSION=v1` (Phase 4d
  downgrade-rollback).
- `lpm_workflows::tests::support::lpm()` helper — no global
  `LPM_LINKER=isolated` pin. Each test that needs isolated shape
  pins it locally; tests that exercise the default chain see the
  new default cleanly. This is more honest than a global pin
  (which would silently mask hoisted-default regressions in
  tests that don't even know they're isolated-shape).

## Pre-merge gate

- cargo clippy --workspace --all-targets -- -D warnings ✓
- cargo fmt --check ✓
- cargo nextest run --workspace --exclude lpm-integration-tests
  → 5742 tests pass, 7 skipped ✓
- cargo test -p lpm-auth (parallel-deterministic) ✓
- audit-fixtures default → 17 PASS / 1 SKIP / 0 mixed ✓
- audit-fixtures `LPM_STORE_VERSION=v1` → 17 PASS / 1 SKIP / 0 mixed ✓
- 4f dual-gate bench (n=20) → BOTH GATES PASS ✓

## What's next (post-Phase-66)

Phase 66 is complete. The next blocks on the active-roadmap are
follow-up work outside this phase:

- **README perf row refresh** with the new "hoisted default,
  identical to isolated under v2" warm-install number. Single-
  line edit; saves a bench rerun later.
- **CHANGELOG + release notes** for the 4d default-store flip and
  the 4f default-linker flip. Single rollup entry covering the
  user-visible behavior changes (silent migration, `lpm cache
  prune`, default linker switch).
- **Phase 5** (Phase 63c openat) — independent perf work, queued.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant