feat(npm): compose the npm virtual read path at the root, part 2/3 (S31 plan: 14/19)

📦 Stacked MRs

This is part 2 of 3 of Step 14 of the S31 (npm-virtual) plan, the composition root. It targets main. Review and merge bottom-up.

🎯 What this part does

  • Adds wire_npm_virtual.go: one virtual.Resolver per process, built over the shared remote tier, with the Step 11 assembly and the Step 12 facade composed on top. The prober holds the composition root's shared upstreamhttp Doer, and the resolver reads health through the memoizing reader.
  • Installs the negative cache on the resolver when the boot has a cache client. Without one, no cover is installed.
  • Asserts that remoteAuditLogSink implements remote.UpstreamExclusionAuditSink, and refuses a sink that cannot record an exclusion.
  • Adds the npmRemoteMints test seam and the CacheRedis field on npmRemoteWiring, and the three slot builders for the packument, dist-tags and download routes. The builders return no option, so all three routes stay on the interim 501.
  • Composition failures panic at boot; an unwired remote tier leaves the slots on 501.

📝 Plan and spec amendments

Each correction updates docs/plans/2026-08-21-s31-npm-virtual.md to match the code as measured:

  1. virtual.NewResolver rejects eight configurations at construction, not four.
  2. The ResolveTimeout dependency is met, so the future tense is gone.
  3. The production exclusion sink already exists; this step adds the assertion and the concrete-type pin.
  4. Registration happens in wire_npm.go, not main.go.
  5. The prober pin is asserted on the assembly's Doer identity, since the prober is built per position inside the assembly.
  6. An unset npm.public_registry_url is accepted at construction (the change in part 1).
  7. The virtual repository is created through the management API; the associations and the rule row are seeded by SQL because no route writes rules.
  8. This step installs the negative cache.
  9. The Files bullet names the real signatures and the composition file set.

No Status-table row is edited here.

✅ Tests

The composition pins are unit tests over recording mints: the shared flight registry and cache stores, the Doer identity, the memoizing health reader, the production audit sink type, the boot refusals for each unusable knob, and the negative cache. The negative-cache pin drives one collection through the wired resolver and asserts it read through the boot's cache client, so wiring WithNegativeCache(nil) fails it.

docs/testing/ and docs/user/ are unchanged: every npm virtual route still answers 501. The configuration reference and the observability catalog say the resolver now carries the negative cache.

📏 Size

About 1,120 changed lines, above the 500-line guide. By file group: composition builder and its wiring about 250, unit tests about 555, remote-tier changes about 110, plan and docs about 145, the format-package error arm about 60.

The builder, buildNpmVirtualComposition, is one function whose negative-cache, health, audit and assembly steps depend on each other, so a cut inside it would not build. The two other groups that are not the builder ship with it because it reads them:

  • The npmRemoteMints resolver constructor and the CacheRedis field on npmRemoteWiring are what the builder calls to mint the resolver and to install the negative cache, so the builder and its tests do not compile without them.
  • The errVirtualPublicRegistryURLUnset arm in virtual_read_errors.go, its page and its test classify the refusal the assembly raises for an unset npm.public_registry_url, so it lands with the composition that builds that assembly.

Stacked MRs (review/merge bottom-up)

Related to #895 (closed)

Edited by David Fernandez

Merge request reports

Loading
Loading