S31 Step 6: remote upstream lookup and prober

Part of #290, S31 npm virtual. This slice composes code that is already merged: S11's hosted npm reads, S15's remote legs, and S13's resolution engine in internal/virtual, which has had no caller until now. The three npm_virtual_* tables land in internal/datastore/migrations/, the resolution reads in internal/datastore/, and the loading slice, the serve surface, and the three read handlers in internal/format/npm/.

References: S31 spec · S31 plan (see "Step 6").

Type: feat | Depends on: nothing. This step is a root and can start immediately.

Scope

The batched cache reads for a remote position and the Phase 2 prober the flight registry cannot supply.

Files

  • internal/datastore/npm_virtual_remote_lookup.go (Create): the batched IN-list reads over npm_remote_metadata_files and npm_remote_files with S13's freshness model, one statement whatever the remote position count. The IN list is the remote-position count, so it inherits Step 4's LIMIT on the list read rather than carrying its own; this step names that dependency instead of assuming it, because nothing in the read's own signature bounds the slice it is handed.

    The metadata read takes the document kind and filters on it per position. npm_remote_metadata_files is kind-keyed and S15's remote route already writes abbreviated (kind=2) rows, so a read that ignores the column answers an abbreviated Accept from a fresh full-kind row. The kind comes from the target Step 12 builds.

  • internal/format/npm/virtual_remote_upstream.go (Create): the remote.Upstream adapter, and the prober composed from npmremote.NewLiveUpstreamConfigSource, npmremote.NewUpstreamRequestBuilder, and the upstreamhttp doer. The Doer is a constructor parameter, never built here. Composing separately opens a second path to the upstream client, and the security covers are split across the two collaborators: the base-URL gate and the bearer live in BuildRequest, but the resolved-address deny-list and the own-interface check live in the Doer's dialer. A prober handed a plain http.Client compiles and passes every test in this step, so the assertion that production wires the composition root's shared upstreamhttp Doer is Step 14's, and this step's doc comment says so.

  • internal/datastore/query_names.go (Modify): the remote-lookup query names, in their own group.

  • the paired *_integration_test.go and *_test.go files (Create).

Acceptance

Contributes the remote half of Spec AC, Resolution #15: the batched read issues one statement for 2 and for 20 remote positions. A remote Lookup returns fresh, stale, and miss verdicts off the cache rows, and a prober built here issues a bodyless HEAD against the upstream base the remote repository's row names.

Tests

Integration over seeded cache rows in each freshness state and in both packument kinds, including the cross-variant negative: a fresh full-kind row must not answer an abbreviated read, and the reverse. Unit tests for the prober against an httptest upstream, including a 304 answer mapping to ProbeFresh and a 200 to ProbeHit.

Size

Source ~380 · Test ~840 · Total ~1,220