fix: resolve tool binaries without npx so pnpm and yarn hosts work - #4
Conversation
Adapters invoked their tools through `npx`. npx falls back to the registry when a binary is not installed locally, so on a host whose dependencies CI never installed, `npx tsc` downloaded `tsc` — an unrelated abandoned package that is not TypeScript — and the collector reported its output as fact. The same path installed a bare `vitest` with no coverage provider and resolved `biome` to a deprecated placeholder that emits no JSON. That state was reachable because the workflow only installed host dependencies when a `package-lock.json` was present, so every pnpm or yarn repo ran the whole gate against an empty node_modules and reported nine "collector failed" blockers that had nothing to do with the code. Adapters now name bare binaries and runShell puts the host's (and the collector cwd's) node_modules/.bin on PATH. All three package managers populate that directory, and a genuinely missing tool now fails with "command not found", which every adapter's TOOL_MISSING check already reports honestly. The workflow installs with whichever package manager the lockfile names. `audit` stays npm-only on purpose: pnpm and yarn emit a different report shape than the npm-audit adapter parses, and wiring it up regardless would report zero advisories on a repo that has them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ Quality gate passedcommit
|
| Check | Baseline | Now | Verdict | |
|---|---|---|---|---|
| TypeScript errors | — | n/a | collector not configured — see docs/loopwright/quality-gate.md |
📊 All metrics
| Check | Baseline | Now | Verdict | |
|---|---|---|---|---|
| ✅ | Failing tests | 0 | 0 | holding |
| ✅ | Failing test suites | 0 | 0 | holding |
| TypeScript errors | — | n/a | collector not configured — see docs/loopwright/quality-gate.md | |
| ✅ | Lint errors | 0 | 0 | holding |
| ✅ | Lint warnings | 0 | 0 | holding |
| ✅ | Critical advisories | 0 | 0 | holding |
| ✅ | High advisories | 0 | 0 | holding |
| ✅ | Suppressed advisories | 0 | 0 | holding |
| 📈 | Line coverage | 84.67% | 85.01% | improved 0.34% |
| 📈 | Branch coverage | 78.93% | 79.44% | improved 0.51% |
| 📈 | Function coverage | 89.65% | 89.71% | improved 0.06% |
| 📈 | Statement coverage | 85.01% | 85.41% | improved 0.40% |
| 📈 | Duplicated code | 0.90% | 0.88% | improved 0.02% |
| ✅ | Highest function complexity | 14 | 14 | holding |
| 📈 | Average function complexity | 1.81 | 1.80 | improved 0.01 |
| ✅ | Oversized files | 0 | 0 | holding |
| ✅ | Skipped tests | 0 | 0 | holding |
| ✅ | Focused tests (.only) | 0 | 0 | holding |
| ✅ | Tests with no assertion | 0 | 0 | holding |
| ✅ | Coverage-ignore hints | 1 | 1 | holding |
| ✅ | Type suppressions (@ts-ignore etc.) | 2 | 2 | holding |
| ✅ | Inline lint suppressions | 2 | 2 | holding |
| ✅ | Empty catch blocks | 0 | 0 | holding |
📈 This PR improves 6 metric(s). Run
node .loopwright/scripts/quality-gate.mjs --update-baselineand commit.loopwright/baseline.jsonto lock the gain in.
Generated by .loopwright/scripts/quality-gate.mjs · reproduce locally with node .loopwright/scripts/run-report.mjs --all && node .loopwright/scripts/quality-gate.mjs · full reports are in the workflow artifacts.
The lint-suppression integrity metric matched `eslint-disable` only. On a biome repo it therefore reported zero however many `biome-ignore` comments the code carried — and a check that cannot see anything reads exactly like nothing to see. The repo this was found on has 12 of them and the gate called it 0, while its own contributor docs list inline lint suppression as a gated shortcut. Match both linters, and name the metric for what it measures rather than for one of the two tools that can produce it: "Lint errors"/"Lint warnings"/ "Inline lint suppressions" instead of the ESLint-specific labels, which were also wrong on every biome host. Labels are cosmetic — the metric keys are unchanged, so existing baselines still match. The fixtures compose their marker strings instead of spelling them out. sources.roots covers the tests directory, so a literal suppression comment in a test would be counted as a real one by the scanner under test, and the metric would climb every time somebody exercised it. The pre-commit hook now skips analyze-source.mjs for the same reason it already skips fixtures: it is where the detectors are defined, so it necessarily spells out every pattern it looks for, and adding one tripped the hook on its own definition. CI still scans the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
Code reviewReviewed the diff (bare-binary resolution via
No CLAUDE.md compliance issues found. |
The previous commit dodged the two self-detection problems rather than naming them. Both are now handled by the exclusion mechanism this repo already had. The suppression fixtures were composed from string fragments so the scanner would not match them. That is the thing the gate exists to discourage, written into the gate's own tests. They are ordinary files under tests/fixtures/ now, spelling out every comment form literally, which is what `sources.ignore: ["**/fixtures/**"]` and the hook's `:(exclude)` were already there for — fixtures exist to give the detectors something to detect. The hook excluded one file by path, `analyze-source.mjs`, because adding a pattern to the detector tripped the detector. Exclude `.loopwright/` wholesale instead: in a host repo it is vendored code nobody hand-edits, so policing it only ever fires on a re-vendor, and in this repo it is the engine, whose definitions necessarily spell out every pattern they look for. The gate still scans it in CI either way, and the gate is the authority — this hook only exists to deliver the same answer sooner. Verified both directions: a suppression staged outside .loopwright/ still fails the hook, and the fixtures no longer trip it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code reviewChecked for bugs and CLAUDE.md compliance. CLAUDE.md compliance looks clean — no rule violations found. Two issues surfaced in the bug pass:
Not flagged: the pre-commit hook's new |
Two npm/eslint assumptions that both fail silently on a repo that uses neither. Found by installing loopwright into a pnpm + biome repo, where the gate reported nine blockers, none of which was a quality problem.
1. Tool binaries resolved through
npxThe workflow installed host dependencies only when a
package-lock.jsonexisted, so on a pnpm or yarn repo that step was a no-op and the whole gate ran against an emptynode_modules. Every adapter then invoked its tool throughnpx, which falls back to the registry when a binary is missing locally:npx tsc --noEmittsc@2.0.4— an unrelated abandoned package that is not TypeScriptnpx vitest run --coveragenpx biome check --reporter=jsonbiomeplaceholder (the real package is@biomejs/biome), which emits no JSONThe
tsccase is the worst: it runs, exits, and reports nonsense. That is precisely the "infrastructure failure must never look like success" outcome the gate exists to prevent.Fix: adapters name bare binaries and
runShellputs the host's — and the collectorcwd's —node_modules/.binonPATH. All three package managers populate that directory, so one mechanism covers npm, pnpm and yarn, and a genuinely missing tool now fails withcommand not found, which every adapter's existingTOOL_MISSINGcheck already reports honestly. The workflow installs with whichever package manager the lockfile names, and enables corepack so the host's pinnedpackageManagerversion is used.2. The lint-suppression metric only knew eslint
integrity.lintSuppressionsmatchedeslint-disableand nothing else, so a biome repo reported 0 however manybiome-ignorecomments it carried. The repo this was found on has 12, and its own contributor docs list inline lint suppression as a gated shortcut. A check that cannot see anything reads exactly like nothing to see.Fix: match both linters. The labels are renamed for what the metric measures rather than for one of the two tools that can produce it —
Lint errors/Lint warnings/Inline lint suppressions. Labels are cosmetic; metric keys are unchanged, so existing baselines still match.3. Self-detection, handled by exclusion rather than by dodging it
Adding a pattern to the detector trips the detector. Two places needed an answer, and both now use the exclusion mechanism this repo already had, rather than a workaround:
tests/fixtures/, spelled out literally in ordinary files, which is exactly whatsources.ignore: ["**/fixtures/**"]and the hook's:(exclude)**/fixtures/**were already there for — fixtures exist to give the detectors something to detect. (An earlier revision of this PR composed the marker strings from fragments so the scanner would not match them; that is the very thing the gate discourages, written into the gate's own tests. Removed.).loopwright/wholesale. In a host repo it is vendored code nobody hand-edits, so policing it only ever fires on a re-vendor. In this repo it is the engine, whose definitions necessarily spell out every pattern they look for. Either way the gate still scans it in CI, and the gate is the authority — the hook exists only to deliver the same answer sooner. Verified both directions: a suppression staged outside.loopwright/still fails the hook; the fixtures no longer trip it.Deliberately not changed
auditstays npm-only.pnpm audit --jsonemits the v1 shape (advisories+metadata) whileauditEntries()reads npm's v2vulnerabilitiesshape — it would clear themetadata.vulnerabilitiesguard and report 0 critical / 0 high on a repo that has them. On the pnpm repo this was found on, that would have hidden 1 critical and 24 high advisories.unconfiguredwarns forever and is honest; a silent false pass is not. Documented in the README.Verification
🤖 Generated with Claude Code