Skip to content

v0.11.2 — the pnpm builtin tells the truth on pnpm 12, and bounds that measure themselves

Choose a tag to compare

@phantomghost2023 phantomghost2023 released this 22 Sep 13:50
· 4 commits to main since this release

[0.11.2] - 2026-09-22

Fixed

  • The pnpm builtin reported up to 165 false "missing" packages on a freshly synced pnpm-12 tree. Measured end to end on vuejs/core (660 lockfile entries, pnpm install --frozen-lockfile exit 0 immediately before the check). Four independent causes, each found on the wild repo and each fixed against it:
    1. Peer-suffix directory names. pnpm 11 named store dirs name@version; pnpm 12 names them after the lockfile's snapshot key — the peer-resolved form, peers joined by _ — and past 60 characters stores first 27 chars + '_' + sha256(key)[:32]. The recipe was proven before use (19/19 hash directories matched, zero store directories uncovered) and matching is exact again, derived from the lockfile's own spelling rather than a heuristic.
    2. A nested YAML key hijacked the parser. snapshots: entries indent dependencies: maps; the section-key regex let four-space lines match, so the current entry flipped to a junk key and the optional: true that followed landed nowhere — @emnapi/* counted missing though its snapshot marks it optional. Regexes that count spaces now count them exactly.
    3. The package manager records itself. packageManager: pnpm@12.4.2 produces a lockfile entry for pnpm that never materializes in the project's virtual store. Same rule as platform binaries: a package not supposed to be here is not missing.
    4. pnpm 12 filters the recorded lockfile copy. node_modules/.pnpm/lock.yaml holds 645 of 660 entries — exactly the other-OS binaries dropped — so neither full nor machine-expected set equality holds. The check is now subset in both directions: every copied package must be in the lockfile (a foreign resolve adds packages), and every package this machine must have must be in the copy (a copy from another machine lacks them).
      After the fixes the wild verdict is ok: true — 489 package(s) present, matching pnpm-lock.yaml (152 platform-specific or optional skipped, 1 packageManager self-reference skipped) in 17 ms, with damage detection re-proven both ways (deleted vite@8.3.0 → flagged; pnpm install --frozen-lockfile → restored → ok: true).
  • A check stopped by its time bound stamped broken exit ?. Broken means the check ran and said no; a killed run never ran to a verdict. It is blocked — untested, not false, with a note naming the bound and suggesting max_ms. Found on the same wild repo, whose suite needs ~5 minutes against the 120 s default. (The bare exit null note was made legible as no exit within Ns earlier in the same pass.)
  • init discovered nothing on a pnpm-workspace.yaml monorepo. The earlier workspaces fix read package.json's workspaces key; vuejs/core declares its packages in pnpm's own workspace file, so the same zero-candidates breakdown recurred on the very ecosystem this drill targets. pnpm-workspace.yaml now counts as evidence that packages with code exist.
  • Sandbox teardown could crash a verify that already had its results. On Windows, an EBUSY while removing the temp worktree killed the run after the check but before state.save() — stamp lost, worktree leaked, observed live on the wild repo. Teardown is best-effort: retries briefly, then warns and moves on, so a verify's verdict survives a dirty teardown.
  • Three more package-manager builtins met their wild repos; each had its own false-positive class.
    1. npm on npm/cli: a lockfile lists every platform's variants (@typescript/typescript-aix-* …) but npm ci materializes only this machine's — 20 of 1181 "missing", every one foreign-platform. The platform rule pnpm and bun already had is npm's too now: ok: true — 1161 present, 20 platform skipped.
    2. bun on oven-sh/bun (isolated linker): bun 1.2 suffixes a store directory with +<hex> when the package resolves peers (@types+react-dom@18.3.7+52f32cb6c6aeed77), which exact name@version matching can never hit — a freshly installed healthy store read 3/23 missing. Matching folds the suffix off, raw name first so semver build metadata still matches only itself; damage/repair re-proven live (deleted typescript@6.0.2 → named; fresh install → ok: true).
    3. yarn 1 held up: healthy tree zero false positives, root-package damage reported rather than judged, --check-files repair restored ok: true.
  • init trusted node_modules existing — but npm/cli commits most of its node_modules to git (1367 files, no .bin), so a 122-package partial tree wired no prerequisite and the proposed suite claim ran against an install that was never made. When the tree exists, the ecosystem builtin gets the last word: a definitive ok: false wires requires, ok: null leaves discovery's judgment alone.
  • The git journal could silently lose an entry on a fast machine. Entry ids were second-resolution, so an accept and its immediate revert wrote the same filename — the revert overwrote the accept and the journal showed one entry where there were two. Windows passed by latency luck; the ubuntu CI runner caught it. Ids are collision-proof now.
  • A vendored Go build reported "no module cache". The gate demanded GOMODCACHE even when the tree is vendored — correct on a dev machine, wrong on a fresh ubuntu runner. A vendored build verifies without a cache.

Added

  • The repo's own manual runs in GitHub Actions on every push and PR (.github/workflows/manual.yml), with the badge in the README — the same verify a contributor gets from the pre-commit gate, executed where nobody's laptop is involved.
  • CONTRIBUTING.md documents regenerating the hero and social-preview images and keeping the two cards in sync.
  • init calibrates a suite's time bound instead of inventing one. A discovered command check no longer ships a hardcoded max_ms: 120000; the probe measures the first honest run and writes max(120000, measured × 3) rounded to seconds, with observation.measured_ms as provenance — a five-minute suite gets a bound that can hold it (vuejs/core's ~293 s → 879000) while fast suites keep today's default for manual observe to tighten later on accumulated evidence. A check that never finishes gets one 600 s calibration window, then an honest blocked. Along the way the probe adopted verify's own doctrine — a run stopped by a bound is blocked, untested not false (it used to file them broken) — and its note stopped printing max_ms as if it were seconds (120000s).
  • verify waits for a claim's own bound (checkTimeoutMs): the config default_timeout_s was passed as an explicit timeout, which the runner prefers over every claim's max_ms — calibrated bounds would have been decorative, a 15-minute suite still killed at 60 s. The config default now applies only to claims that declare no bound.
  • Regression tests for every fix above: the hash-truncated pnpm-12 directory (fixture uses the real pnpm-computed hash, not a reimplementation), the nested optional: true, the packageManager self-reference, the timeout-blocked stamp, and the EBUSY-tolerant teardown (platform-aware: a live process parked in the worktree on Windows, clean removal on POSIX).

See docs/FIELD-NOTES.md, "Eighth pass" (the pnpm-12 monorepo) and "Ninth pass" (npm, yarn 1, bun, and the bound that measures itself), for the full drill narratives.