Skip to content

dx(check): fail fast when node_modules lags the lockfile - #1967

Merged
thymikee merged 3 commits into
mainfrom
dx/1963-doctor-stale-install
Aug 22, 2026
Merged

dx(check): fail fast when node_modules lags the lockfile#1967
thymikee merged 3 commits into
mainfrom
dx/1963-doctor-stale-install

Conversation

@thymikee

@thymikee thymikee commented Aug 22, 2026

Copy link
Copy Markdown
Member

Summary

Fail pnpm check:affected --run before any gate when the current worktree's node_modules/.pnpm/lock.yaml is missing or differs from pnpm-lock.yaml.

Keep this repository-development diagnostic entirely under scripts/check-affected; agent-device doctor remains scoped to runtime, toolchain, and device diagnostics. The preflight uses no subprocess and adds no code or weight to the published package.

Closes #1963

Validation

  • Planted red: the failure-output test rejected the old agent-device doctor guidance and missing worktree path.
  • pnpm check:affected:test — 62 passed.
  • 1,000 warm checks against the real 237 KB lockfiles: 0.056 ms median, 0.085 ms p95.
  • Published-package verification passed with no new runtime artifact.
  • pnpm check:affected --run passed the affected implementation and 7,735 related tests; the broad run twice stopped later on the same untouched tmpdir process-lifecycle race. Its owning pnpm check:tmpdir-leaks:test suite passed 24/24 in isolation.

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.37 MB 2.37 MB 0 B
JS gzip 794.8 kB 794.8 kB 0 B
npm tarball 916.5 kB 916.5 kB +11 B
npm unpacked 3.18 MB 3.18 MB +53 B

npm unpacked components

Component Base Current Diff
JS / dist source 2.51 MB 2.51 MB 0 B
Apple runner source/project 564.2 kB 564.2 kB 0 B
macOS helper source 54.5 kB 54.5 kB 0 B
Android helper artifacts 0 B 0 B 0 B
Other package files 44.4 kB 44.5 kB +53 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.1 ms 28.0 ms -0.1 ms
CLI --help 83.2 ms 81.8 ms -1.4 ms

Top changed chunks: no changes in the largest emitted chunks.

Top changed packed files

Packed file Base Current Diff
package.json 18.2 kB 18.2 kB +53 B

@thymikee

Copy link
Copy Markdown
Member Author

Not ready at 45c417be despite green CI. The new doctor probe runs unconditionally and defaults to findProjectRoot() from the daemon/package installation. Published packages exclude both pnpm-lock.yaml and the installed snapshot, so normal packaged doctor calls gain a node-modules warning and downgrade overall status to warn. With doctor --remote, it diagnoses the server deployment rather than the caller’s worktree and cannot satisfy #1963.

Keep the source-worktree preflight for check:affected, but omit this doctor probe for remote and packaged/no-source-checkout contexts, with route-level packaged and remote JSON assertions. The separate missing-rg false-green static-check issue should be filed independently, not fixed here.

Add a doctor probe and a check:affected preflight that compare
node_modules/.pnpm/lock.yaml (the exact lockfile snapshot pnpm installed
from) against pnpm-lock.yaml via a content hash — no subprocess. On
mismatch both surfaces report the same one-liner: "node_modules was
installed from a different lockfile; run pnpm install", so a stale
install names its own cause instead of surfacing as bogus format diffs
on files a change never touched (the #1956 incident).

Closes #1963
The probe ran unconditionally from findProjectRoot(), so it fired in two
contexts it cannot diagnose:

- Packaged installs. Published packages ship neither pnpm-lock.yaml (not
  in the package.json `files` allowlist) nor an installed snapshot, so
  every end user's `doctor` gained a spurious node-modules line and a
  degraded overall status.
- `--remote`, where the daemon's own root describes the server
  deployment rather than the caller's worktree, so the answer could not
  address #1963 at all.

Whether a root is a source checkout is now decided by the presence of
pnpm-lock.yaml itself rather than a heuristic about install location, and
'no-source-checkout' is a distinct result rather than a warning, so the
packaged case cannot be represented as a defect. The probe returns
undefined there and the route appends no check, matching how doctor
already models an out-of-scope question (the device family is likewise
absent under --remote). The fresh-worktree catch is preserved: a lockfile
with no installed snapshot is still a hard failure.

The check:affected preflight is unchanged in behavior.

Route-level assertions cover all three contexts (source, packaged,
remote); each was verified to fail against the pre-fix wiring.
@thymikee
thymikee force-pushed the dx/1963-doctor-stale-install branch from 45c417b to 3f02d56 Compare August 22, 2026 13:53
@thymikee

Copy link
Copy Markdown
Member Author

Code-review clean at 3f02d56a. The packaged-install and remote-doctor false warnings are fixed at the owning seam with meaningful route-level regressions, while local source checkouts and check:affected still fail fast for stale/missing installs. #1976 is separate pre-existing workflow debt, not caused by this PR. Completed exact-head checks are green; Coverage and iOS Smoke are still pending. Please refresh the stale PR-body sentence claiming the probe also runs under --remote (non-blocking).

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 22, 2026
@thymikee

Copy link
Copy Markdown
Member Author

Thanks — PR body refreshed at 3f02d56a; the sentence claiming the probe ran unconditionally "including --remote" is gone, replaced by a Scope section that states the two contexts it deliberately sits out and why.

All 29 checks are now green. Coverage went red once on src/daemon/handlers/__tests__/session-device-claims.test.tsENOTEMPTY from the afterEach fs.rmSync(root, { recursive: true, force: true }), a tmpdir-cleanup race in a file this PR does not touch. It passes locally, and main's own recent Coverage failure (run 32567401456) is the same lane flaking on a different unrelated test (wait-runtime.test.ts), so I re-ran that job rather than chasing it; it passed.

Context gating

The discriminator is the presence of pnpm-lock.yaml under the resolved root, not a heuristic about where the code was installed from — it is committed in every worktree and shipped in no published package, so its presence is the fact rather than a proxy for it. no-source-checkout is now a distinct result in LockfileInstallSyncResult rather than a warning variant, so the packaged case is not representable as a defect at the type level.

  • Packaged: the probe returns undefined and the route appends nothing.
  • --remote: the call now sits below the --remote early return in session-doctor.ts, so the gate is structural rather than a conditional that could drift.

I followed doctor's existing vocabulary for an out-of-scope question — absence, as with the device-inventory family under --remote — rather than adding a not-applicable status. Happy to switch to an explicit info line if you'd rather the omission be visible in the JSON; it was a close call and your "omit" was the tiebreaker.

The fresh-worktree catch is intact: a lockfile with no installed snapshot is still a hard failure, and check:affected is unchanged in behavior.

Route-level assertions

Three, in test/integration/provider-scenarios/doctor.test.ts: source checkout (probe present, passing), packaged (no node-modules check, status: pass, and no check mentioning a lockfile at all), and --remote (no node-modules check).

Two details aimed at keeping them from going vacuous:

  • The packaged case points findProjectRoot() at a directory really shaped like a published install, so the real detection runs against a real filesystem rather than a stubbed verdict.
  • The remote case deliberately leaves the root as this real source checkout, so a missing gate surfaces the check instead of the assertion passing for the trivial reason that the probe never runs anywhere. The source-context assertion guards the same thing from the other side.

I verified each is load-bearing by planting the pre-fix behavior: restoring the unconditional/remote-inclusive wiring fails the --remote assertion, and emitting a check for no-source-checkout fails the packaged assertion (and its unit counterpart).

One honest gap: the CLI --remote path proxies to a real remote daemon, so I could not exercise that end-to-end locally. It is covered at the daemon route, which is where the gate lives, plus the planted red.

rg false-green

Filed as #1976. Both Static Checks steps log rg: command not found and the if rg ...; then exit 1; fi shape converts that into a pass, so neither assertion has ever run — the Swift one since #186, the DI-seams one since it was folded in by #1465. Running them locally with ripgrep present, the Swift assertion is clean but the DI-seams one has 6 live violations (?: typeof fetch in src/remote/daemon-proxy.ts, src/cli/auth-session.ts, src/cli/connection/cloud-profile.ts). The issue flags that whoever picks it up needs to decide separately whether those are legitimate HTTP injection seams or the thing the rule meant to ban. Not touched here.

@thymikee thymikee changed the title dx(doctor): flag a worktree whose node_modules lags the lockfile dx(check): fail fast when node_modules lags the lockfile Aug 22, 2026
@thymikee thymikee removed the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 22, 2026
@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-22 15:01 UTC

@thymikee

Copy link
Copy Markdown
Member Author

Updated in 33030f6: the stale-install check now lives only in the worktree-owned check:affected preflight. The daemon/doctor integration and its packaged/remote special cases are removed, and the internal probe is excluded from the published package. It is subprocess-free and measured 0.056 ms median / 0.085 ms p95 across 1,000 warm runs on the real lockfiles. Focused preflight tests pass 62/62; package verification passes. The broad affected gate passed 7,735 related tests but twice hit the same untouched tmpdir lifecycle race under full-suite load; that owning suite passes 24/24 in isolation. I removed the stale ready-for-human label pending fresh exact-head review/CI.

@thymikee
thymikee merged commit 04e4c23 into main Aug 22, 2026
31 checks passed
@thymikee
thymikee deleted the dx/1963-doctor-stale-install branch August 22, 2026 15:01
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.

dx(doctor): flag a worktree whose node_modules lags the lockfile

1 participant