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: onevirtual.Resolverper 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 sharedupstreamhttpDoer, 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
remoteAuditLogSinkimplementsremote.UpstreamExclusionAuditSink, and refuses a sink that cannot record an exclusion. - Adds the
npmRemoteMintstest seam and theCacheRedisfield onnpmRemoteWiring, 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 interim501. - 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:
virtual.NewResolverrejects eight configurations at construction, not four.- The
ResolveTimeoutdependency is met, so the future tense is gone. - The production exclusion sink already exists; this step adds the assertion and the concrete-type pin.
- Registration happens in
wire_npm.go, notmain.go. - The prober pin is asserted on the assembly's Doer identity, since the prober is built per position inside the assembly.
- An unset
npm.public_registry_urlis accepted at construction (the change in part 1). - The virtual repository is created through the management API; the associations and the rule row are seeded by SQL because no route writes rules.
- This step installs the negative cache.
- 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
npmRemoteMintsresolver constructor and theCacheRedisfield onnpmRemoteWiringare 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
errVirtualPublicRegistryURLUnsetarm invirtual_read_errors.go, its page and its test classify the refusal the assembly raises for an unsetnpm.public_registry_url, so it lands with the composition that builds that assembly.
Stacked MRs (review/merge bottom-up)
- feat(npm): share the remote tier and drop the U... (!3074 - merged) • David Fernandez • 19.5
- feat(npm): compose the npm virtual read path at... (!3075 - merged) • David Fernandez • 19.5
👈 - test(npm): boot the npm virtual wiring against ... (!3076 - merged) • David Fernandez • 19.5
Related to #895 (closed)