Files
ng-eventually/.tasks/done/T01.l-pnpm-everywhere-and-two-loose-ranges.md
T
Sylvain Duchesne ab5980c257 chore: T01.l est close - pnpm partout, la portee du pair durcie, pnpm link verifie
Le critere d-acceptation a ete execute, pas raisonne : lien reel contre le
checkout, resolution confirmee par readlink, defaire par `pnpm remove`. Le
polyfill lui-meme n-est jamais duplique par le lien ; le SDK en dessous l-est
des que l-application importe `@ng-org/web` elle aussi - 2 copies mesurees.
2026-09-24 12:14:42 +02:00

15 KiB

id, title, parent, depends_on, created, claimed_by, claimed_at, blocked
id title parent depends_on created claimed_by claimed_at blocked
T01.l pnpm everywhere, and two dependency ranges that are looser than they look T01
T01.j
2026-09-21 worker-T01.l 2026-09-24 false

Description

Four items. The first two are dependency hygiene found by T01.h; the last two finish the move to a single package manager, whose POINT is that pnpm link works for local development against a checked-out dependency.

1. ng-e2e-helpers' peer range admits a package nobody wants

packages/ng-e2e-helpers/package.json declares a peer range >=0.1.2-alpha.13 on @ng-org/*. Those packages also publish a bare 0.1.2, dating from 2025, which semver ranks ABOVE every 0.1.2-alpha.N - so that range would happily resolve to a year-old release. Exact pins exclude it TODAY, which is precisely what makes it a trap: it is inert until someone loosens a pin or installs fresh against a different resolver.

Fix the range so it cannot admit the bare 0.1.2. VERIFY the fix by checking what the range actually resolves to, not by reading it - a range that looks right and resolves wrong is the whole failure mode here. Check whether the same shape exists on any other declared range in either package while you are there.

2. playwright is behind

1.61.1 -> 1.63.0. Bump it, reinstall, and RUN the e2e suite - a Playwright bump is exactly the kind of change that is invisible to a typecheck and visible to a browser. Report the counts verbatim. If the broker is unreachable, report the suite as UNRUN; never substitute a typecheck for the measurement. .project/concepts/e2e-harness/caveat_a-dropped-pipe-kills-a-run.md describes a Chromium control-pipe failure this suite already lives with - read it before concluding that a new red run is caused by the bump.

3. Two places still describe bun install's semantics

packages/polyfill/e2e/orm-live-read.ts:159 and .project/concepts/e2e-harness/knowledge_what-each-suite-judges.md:14 both explain that @ng-org/orm is an optional peer that "bun install does not fetch". That is no longer the repo's install path, and pnpm does not treat optional peers the same way. ESTABLISH what pnpm actually does here - install fresh and look - then correct both texts to say the true thing. If it turns out pnpm DOES fetch it, that changes what the ORM live-read runner needs to say about itself, so say so rather than editing the sentence to match a guess.

4. Make bun unnecessary for installing and typechecking

Root package.json:13 and packages/ng-e2e-helpers/package.json:23 run bunx tsc. Nothing installs with bun anywhere in this repo - VERIFIED, there is no bun install in any script and no CI exists - and packageManager: pnpm@10.26.0 is declared. But bunx still requires a contributor to have bun on their machine to typecheck. Move those to pnpm's equivalent.

Explicitly NOT in scope: bun test. It is the test RUNNER, not an installer, and it does not block pnpm link. Changing it is a separate decision with its own cost, and nothing here needs it. If you find evidence that it DOES block something, report that rather than acting on it.

The acceptance criterion, which is the reason for all of this

pnpm link must work for local development against a checked-out dependency. VERIFY IT - actually perform a link against a local checkout and confirm the consuming package resolves to it, then undo. Do not reason about whether it should work. And check the thing that would silently ruin it: that linking does not produce TWO copies of a state-carrying package in one tree. The polyfill holds a dozen module-level state holders, so two copies in one application means two registries and two identities - that hazard is named in its own published contract.

Verification

Hard gate: bun test -> 0 fail; bunx tsc --noEmit -p packages/polyfill/tsconfig.json -> exit 0 (or its pnpm equivalent once item 4 lands); bunx tsc --noEmit -p packages/polyfill/e2e/tsconfig.json -> exit 0; the example's own typecheck -> exit 0; python3 ~/.claude/skills/concept/lint.py -> 0 ERROR; plus the e2e counts from item 2.

Anti-cheat: no any, no @ts-ignore, no deleted or skipped test, no weakened assertion, and no LOOSENING a range to make an install succeed - item 1 is about tightening, and a fix that widens something else to compensate is not a fix.

Progress

Result

Status: done Commits: a30f1c4 (item 1), 6dad05b (item 4), d4d7a4e (item 2), 82d41b2 (item 3).

1. The peer range - done, VERIFIED BY RESOLUTION

packages/ng-e2e-helpers/package.json: "@ng-org/web": ">=0.1.2-alpha.13" -> ">=0.1.2-alpha.13 <0.1.2".

Measured, not read. Registry query pnpm view '@ng-org/web@<range>' version:

range versions admitted semver.maxSatisfying pnpm resolves to
>=0.1.2-alpha.13 (before) 4 - alpha.13, alpha.14, alpha.15, 0.1.2 0.1.2 0.1.2-alpha.15
>=0.1.2-alpha.13 <0.1.2 (after) 3 - alpha.13, alpha.14, alpha.15 0.1.2-alpha.15 0.1.2-alpha.15

The trap was real and it was masked by a second mechanism, which is worth recording because the task's premise was only half right. 0.1.2 was published 2025-10-19; every alpha is 2026, alpha.15 on 2026-09-23. semver.maxSatisfying on the old range returns 0.1.2 - the year-old release. But pnpm did NOT pick it: the latest dist-tag is 0.1.2-alpha.15, and pnpm prefers latest when it satisfies the range. So the old range resolved correctly ONLY because upstream's latest happens to point at an alpha. The day upstream moves latest to 0.1.2, the old range follows it and the new one cannot.

The tightening changed nothing in pnpm-lock.yaml - the peer range of a workspace importer does not appear there, confirming T01.h's reading that the declaration was latent.

The same shape found elsewhere, NOT fixed - it is a published-surface decision, reported instead. packages/polyfill/package.json:11-16 declares all four @ng-org/* peers as "*". Measured: * admits exactly one of @ng-org/web's 16 published versions - the bare 0.1.2 - and excludes every alpha, because node-semver's * is >=0.0.0 and does not match prereleases. That is the same inversion, worse. Two facts bound its severity: pnpm emits no unmet-peer warning for it (a scratch consumer declaring alpha.14 + alpha.21 installed clean and pnpm bound the peers anyway), and neither package is published (pnpm view -> E404 for both). So it is inert today under pnpm. Tightening four optional peer ranges on a published surface is a compatibility decision, not this task's named scope.

Two other loose declarations, reported not touched: ng-e2e-helpers peer playwright: ">=1.40.0" admits any future major (ordinary upward looseness, not the prerelease inversion - playwright's prereleases carry a different version tuple so the range excludes them); root @types/bun: "latest" is an unpinned floating dist-tag.

2. playwright 1.61.1 -> 1.63.0 - done, e2e RUN

Bumped in root package.json and packages/ng-e2e-helpers/package.json, lockfile updated. chromium-1243 (what 1.63.0 wants) was already in ~/.cache/ms-playwright; playwright install chromium was a no-op and chromium.executablePath() exists.

Polyfill e2e on 1.63.0, verbatim: == Polyfill e2e summary: 46 passed, 0 failed, 46 total - batch 3.3 min, slowest cold sync 30s ==. Baseline 46/46 matched on the first run - no broker-login flake, no dropped pipe, caveat_a-dropped-pipe-kills-a-run not needed.

Two side effects of the bump worth knowing: 1.63.0 raises engines.node from >=18 to >=20 (this host runs v22.16.0), and it no longer declares fsevents as an optionalDependency, which is why that entry left the lockfile.

3. What pnpm actually does with the optional @ng-org/orm peer - MEASURED, and it changed what the texts had to say

Two facts, both from installs, not reading:

  • For a consumer, pnpm does not fetch it. A scratch package depending only on @ng-eventually/polyfill (from pnpm pack) installed zero @ng-org packages, with no warning. autoInstallPeers is on by default in pnpm 10 and skips OPTIONAL peers. So the substance of the old claim survives the change of package manager: the tool name was wrong, the semantics were not.
  • For THIS checkout, pnpm DOES fetch it - and that is the part both texts got wrong. packages/polyfill/package.json:33 lists @ng-org/orm: 0.1.2-alpha.21 in devDependencies. After rm -rf node_modules && pnpm install --frozen-lockfile, packages/polyfill/node_modules/@ng-org/orm is present and bun build e2e/orm-live-entry.ts succeeds (Bundled 57 modules in 22ms).

So the runner's advice - "install it in packages/polyfill before running this measurement" - had become a false lead: a plain install already has it, and a failed bundle means something else. Both texts now say the optional peer governs what a consumer receives, not what this repository installs, and the runner points at pnpm install instead.

4. bun unnecessary to install or typecheck - done, VERIFIED with bun off the PATH

bunx tsc -> pnpm exec tsc in root package.json:13 and packages/ng-e2e-helpers/package.json:23. pnpm exec finds tsc from the helpers package too (pnpm walks up to the workspace root's node_modules/.bin), so no local typescript devDependency was needed.

Proof rather than argument: with ~/.bun/bin removed from PATH and command -v bun empty, both pnpm run typecheck exit 0.

bun test untouched, as instructed. Evidence that bun is still required for one thing, reported not acted on: packages/polyfill/e2e/orm-live-read.ts:128 shells out to bun build to bundle the page, and packages/ng-e2e-helpers/package.json mint-wallet runs bun run. Neither installs nor typechecks, and neither blocks pnpm link, so both are out of this task's scope - but "bun is not needed" is true only of installing and typechecking, not of running the e2e bundler.

The acceptance criterion - PERFORMED, and it passes with one measured caveat

pnpm link works. A scratch application ran pnpm link <repo>/packages/polyfill; node_modules/@ng-eventually/polyfill is a symlink whose readlink is the checkout, /home/sylvain/projects/nextgraph/ng-eventually-js/packages/polyfill, and pnpm remove @ng-eventually/polyfill undoes it cleanly (specifier gone, symlink gone, checkout unharmed).

The duplication check, which is the thing that could have ruined it. pnpm warns at link time, by name:

The package @ng-eventually/polyfill, which you have just pnpm linked, has the following peerDependencies specified in its package.json: @ng-org/web@*, @ng-org/orm@*, @ng-org/shex-orm@*, @ng-org/alien-deepsignals@* - The linked in dependency will not resolve the peer dependencies from the target node_modules.

Measured by resolving each specifier from both importers and comparing realpath:

variant polyfill copies @ng-org/web copies @ng-org/orm copies
pnpm link, app also imports the SDK itself 1 (the checkout) 2 2
pnpm link, app declares nothing but the polyfill 1 1 1
file: protocol, app also imports the SDK 1 1 1

The state-carrying package named in contract_polyfill-surface.md:254 is never duplicated by the link - exactly one polyfill, the checkout. That hazard is not tripped. But the SDK underneath it is: when the application also imports @ng-org/web directly (as e2e/orm-live-entry.ts:20 does), the link leaves the app on its own store copy and the linked polyfill on the repo's - same version, two physical copies, so two init()s and two sessions. file: avoids it but repacks the polyfill into the app's store, which forfeits live editing, so it is not a substitute for a link during development.

Actionable form for a contributor: link the polyfill and let it be the only thing that names @ng-org/*, or accept a second SDK copy. This is not fixed here - it follows from the "*" optional peers, so it is the same open decision as item 1's second finding.

Hard gate, verbatim, on the committed state after rm -rf node_modules && pnpm install --frozen-lockfile

  • bun test -> 449 pass / 0 fail / 1253 expect() calls, 40 files. Baseline 449 pass / 0 fail matched before any change as well.
  • pnpm exec tsc --noEmit -p packages/polyfill/tsconfig.json -> EXIT=0
  • pnpm exec tsc --noEmit -p packages/polyfill/e2e/tsconfig.json -> EXIT=0
  • example's own typecheck (cd examples/notebook && pnpm exec tsc --noEmit -p tsconfig.json) -> EXIT=0
  • python3 ~/.claude/skills/concept/lint.py -> 0 ERROR (6 pre-existing WARNING groups, none on the leaf edited here)
  • polyfill e2e -> 46 passed, 0 failed, 46 total
  • fresh install: 113 packages, one copy of each @ng-org/*, one copy of playwright

Surprises

  • VERIFIED: the item-1 premise was half right. The old range admits 0.1.2 (maxSatisfying = 0.1.2) but pnpm did not resolve to it, because it prefers the latest dist-tag and upstream's latest is 0.1.2-alpha.15. The trap was masked by upstream's tagging, not by the exact pins alone - so it was one dist-tag move from firing, and only resolution testing showed that.
  • VERIFIED: @ng-org/orm is the one package of the four with no bare 0.1.2 (alphas up to alpha.22 only). web, shex-orm and alien-deepsignals all have it, matching T01.h.
  • VERIFIED: pnpm unlink @ng-eventually/polyfill answers Nothing to unlink for a link created by pnpm link <path>. The undo that works is pnpm remove @ng-eventually/polyfill.
  • VERIFIED: @ng-org/alien-deepsignals reaches a consumer that never names it - it is a hard dependency of @ng-org/orm, exact-pinned at 0.1.2-alpha.13, not an auto-installed peer.
  • VERIFIED: immediately after the in-place playwright install, node_modules/.pnpm still held an orphaned playwright@1.61.1 (0 lockfile references, 0 symlinks pointing at it). A fresh install cleared it. Cosmetic, but it is the same orphan shape T01.i reported for @ng-org/web@0.1.2-alpha.13.

Found broken, left alone

  • packages/polyfill/package.json's four "*" optional peers admit only the bare 0.1.2 and exclude every published alpha (see item 1). Inert under pnpm today; it is a published-surface decision.
  • examples/notebook/package.json has no typecheck script, so "the example's own typecheck" has no script to run - it has to be typed out by hand. Item 4 named only two scripts, so none was added.
  • Concept lint's 6 pre-existing WARNING groups: 3 oversized leaves, 2 open _debt.md, 1 unlisted usage_ declaration, 5 banned aliases in the concepts' own doctrine.
  • bun build in the ORM live-read runner and bun run in mint-wallet still require bun (item 4's scope was installing and typechecking).

Not touched: docs/briefs/2026-08-10-consumer-harness-identity-switching.md, T01.m's and T01.k's files, ng-helpers. bun test left as the runner.