Skip to content

S10-e: browser deps off the core default path, and G-DIET tightens - #298

Merged
KnockOutEZ merged 8 commits into
studio-handofffrom
slice-s10e-diet-tighten
Aug 11, 2026
Merged

S10-e: browser deps off the core default path, and G-DIET tightens#298
KnockOutEZ merged 8 commits into
studio-handofffrom
slice-s10e-diet-tighten

Conversation

@KnockOutEZ

@KnockOutEZ KnockOutEZ commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Slice S10-e per internal-docs/superpowers/specs/2026-08-10-s10-diet-tiering-spec.md §5. Base bc0ccf4b.

What comes off the default path

playwright moves from dependencies to an optional peer dependency. npm installs optionalDependencies but not optional peers, so this is the one manifest lever that actually removes bytes from npm i -g wigolo — verified against a control fixture before relying on it.

Measured on the shipped G-DIET protocol (npm install --omit=dev --ignore-scripts --no-workspaces into an empty dir), darwin-arm64, minutes apart:

node_modules packages
base bc0ccf4b 698 MiB 388
this branch 681 MiB 386

Per-package diff: exactly playwright + playwright-core removed, nothing added.

How a browser user still gets one

  • The five value imports (browser-pool, playwright-tier, browser-probe, browser-acquire, studio/session-browser) plus the TUI probe's second require now route through one lazy seam, src/fetch/browser-driver.ts, resolving from wigolo's own tree or the data directory. Type imports stay literal — they are erased, and they keep the callers fully typed against a package that may not be on disk.
  • wigolo warmup --browser — and the existing lazy BrowserAcquirer on first escalation, which drives exactly that — acquires the driver into ~/.wigolo/browser-driver, then installs the binary from that same copy.
  • The driver stays a devDependency, so this repo's suites and the studio e2e specs are unaffected.

Proven end to end on a real clean global install (clean-machine-smoke, macOS): Installing browser engine (chromium)... playwright installedBrowser: ok, with the package absent from the dependency tree. Also verified locally against the live registry: an empty prefix goes from unresolvable to resolvable in one call.

The new G-DIET threshold, and why it is not a round number

≤ 693 MiB, down from 720.

The gate runs on exactly one machine class (clean-machine-smoke, macos-latest, node 22), and that runner measures 685 where this laptop measures 681. The threshold is anchored to the runner:

  • clean build 685 passes with 8 MiB (1.2%) of headroom;
  • the regression it exists to catch — the driver returning to dependencies, worth exactly 17 MiB — lands at 702 and reds with 9 MiB of margin.

⚠️ A habitual "clean build + 3%" gives 706, which is above the regression and would pass it. An assertion pins that arithmetic so the next reader does not have to rediscover it. This shipped once at 690 (derived from the laptop's 681) and was re-derived after the runner reported 685 — 690 would have left a clean build only 5 MiB of room on the one machine that measures it.

G_TOTAL_DESKTOP_DROPPED is re-derived because this slice moved one of its inputs; the composed failure window narrows 56 → 44 MiB, so A34's decision to drop that gate is better supported than when it was taken.

Two bugs found by the probes rather than by review

  1. A value memo let concurrent callers resolve different modules. doctor probes three engines through Promise.all, so three callers arrive before any finishes awaiting; with only a value memo each started its own import, and under a mocked graph the first got the mock while its siblings got the real package. Memoizing the promise (as the two existing optional-dep loaders already do) fixed five doctor assertions that were reading the wrong browser.
  2. A probe that passed for having nothing to check. The cache-invalidation test forced absence through the test override, which answers before the memo is consulted — so deleting the invalidation outright left it green (red 0/1). Rebuilt on a module-graph mock that throws once then succeeds, with an attempt counter as the outside signal. Both halves now red under their own mutation.

Windows

The first push was green on ubuntu/macOS and red on both Windows runners: spawn EINVAL, because npm is a batch script there and Node refuses to spawn one without a shell. A shell is not the fix (it joins arguments, and --prefix is a home-directory path). npm now runs as a JS entrypoint under the Node binary, exactly as installBrowser already runs the driver's CLI. Note the degrade behaved correctly throughout — warmup named the cause and carried on to the models.

Verification

  • Core 10228 collected / 10201 passed / 867 files, exit 0 (base 10192/10165/864 → +36 / +3 files; predicted +31).
  • Studio 345 (39) — identical to base. gate:studio green, tests-debt holds at 412 (6 new debt errors fixed, not ratcheted).
  • tsc --noEmit and npm run lint -w apps/studio both clean.
  • 17 mutations, exact red counts: 3/3/1/3/1/2/1/0→1/1/1/1/1/2/1/1/1/8. The 0 is finding (2) above — the hole was closed and re-probed. All reverted by reverse-Edit; git diff -- src/ confirmed empty.

…andatory

The five modules that hold a value import of the browser driver each make the
whole module graph unloadable the moment the package is absent — so an optional
rung becomes a crashed process rather than a missing capability. Route all five,
plus the TUI probe's second require, through one lazy seam that resolves from
either wigolo's own tree or the data directory, and memoizes both answers.

Type imports stay literal: they are erased, and keeping them is what lets the
callers stay fully typed against a package that may not be on disk.
…es it

Taking the driver off the default install path is only half a change; without
the other half the size win is paid for in a capability the product still
advertises. Warmup now installs the driver into the data directory when none
resolves, and drives the browser-binary install from that same copy — resolving
the CLI from a different root than the runtime resolves the module from is how
an install lands beside a revision nothing will load.

The binary stays installBrowser's job: the driver's own postinstall is
suppressed so this cannot fetch a second copy of what the next step fetches.
npm installs optionalDependencies but not OPTIONAL PEER dependencies, so the
peer/optional pair is the one manifest lever that actually removes bytes from
`npm i -g wigolo`. Measured on the shipped G-DIET protocol, darwin-arm64:
698 MiB / 388 packages -> 681 MiB / 386, and the per-package diff moves exactly
playwright and playwright-core with nothing added.

It stays a devDependency so this repo's own suites and the studio e2e specs are
unaffected, and the pinned peer range is the same revision the acquirer installs.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c353eaab-ea3d-45c4-89c6-95a156ce781b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

…dules

The seam memoized the resolved module but not the in-flight resolution, so the
three probes doctor runs through Promise.all each started their own import — and
under a mocked module graph the first got the mock while its siblings got the
real package, which is a difference the caller can see. Memoize the promise, as
the two existing optional-dep loaders already do.

The sync path also gets its own memo rather than sharing one: the two mechanisms
agree by construction in production, so the shared memo bought nothing and cost
an ordering hazard where one early on-disk probe pinned the answer for both.

The dynamic specifier is spelled inline: routed through a const it is not
statically analysable, which resolved the real package straight past a mock and
would be dropped outright by a bundler.
…atch

Both numbers are measured on the gate's own protocol minutes apart: 698 MiB with
the driver on the default install path, 681 without, and the per-package diff
between the two trees is exactly playwright and playwright-core. So 698 is not a
model of the regression, it is the regression.

690 is the midpoint of that window. The habitual 'clean build + 3%' would have
produced 701 — above the very regression the gate exists for — so an assertion
pins that arithmetic rather than leaving the next reader to rediscover it.

The dropped-gate composition is re-derived because this slice moved one of its
inputs; the failure window narrows from 56 MiB to 45, so the decision to drop it
is better supported than when it was taken.
…lidate

Forcing absence through the test override answers before the memo is ever
consulted, so the memo was never populated and deleting the invalidation
outright left the assertion green — it red 0 of 1 under that mutation.

Absence is now produced by the real resolution failing, via a module-graph mock
that throws on first evaluation and succeeds on its second, with the attempt
count as the outside signal: it separates 'served from the memo' from
're-resolved and happened to agree'. Both halves now red under their own
mutation.
Observed in CI, not predicted: green on ubuntu and macOS, red on BOTH Windows
runners with 'could not run the package manager: spawn EINVAL'. On Windows npm
is a batch script, and since the fix for CVE-2024-27980 Node refuses to spawn
one without a shell.

A shell is not the fix — it joins arguments instead of passing a vector, and
--prefix here is a path under the user's home directory, so it would break on
the first space. npm runs as a JS entrypoint under the Node binary instead,
exactly as installBrowser already runs the driver's own CLI.

Also re-derives G-DIET from the RUNNER's 685 rather than a laptop's 681: the
gate runs on one machine class, and the laptop-derived 690 left a clean build
5 MiB of room on the only machine that measures it. 693 restores the balance
(8 MiB headroom, 9 MiB under the regression).
… both readings

Two runs of this branch on the same clean-machine arm measured 685 then 683.
The anchor stays the worse of the two — a threshold set from the better reading
is a threshold that reds the next time the runner has a bad day.
@KnockOutEZ
KnockOutEZ merged commit e7840ca into studio-handoff Aug 11, 2026
18 checks passed
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