feat(maven): walk the resolver virtual tier (S30 plan: 8/25)
Summary
Implements Step 8 of the S30 plan: the third tier of maven.Resolver, so
a kind=1 request arrives at the dispatcher carrying its virtual repository
row. Depends on: Step 1's schema (merged), so it targets main directly.
Resolution gains a Virtual field and the resolver a
VirtualRepositoryFinder seam, mirroring the Remote field and
RemoteRepositoryFinder the kind=2 arm walks, with the same fail-loud nil
discipline: a (nil, nil) finder return fails the resolve instead of building
a nil-Virtual resolution. MavenVirtualRepositoryFinderStore is the
read-only finder — one statement over maven_virtual_repositories joined to
its parent, keyed by (namespace_id, repository_id) — and wire_maven.go
binds it through an adapter with two compile-time assertions. Every
maven.NewResolver call site moves to the three-argument form: 31 sites
across five files, the two //go:build integration suites included.
A half-created kind=1 parent — one without its maven_virtual_repositories
row — answers the same 404 a missing repository gets: never a resolution
with a nil field, never a response that confirms the repository exists and is
virtual. A transient datastore fault on the tier wraps as-is for the
dispatcher's 500, never a 404.
Reviewable LOC: 1,234 insertions — past the 500 ceiling (the guardrail's
own count: it excludes only vendored, generated, and binary files, so tests
and docs count toward it). By file group: the finder store and its query
names 129, the resolver tier walk and the context invariants 81, the
composition-root adapter 41, tests 835, sidecar docs 148 (1,234 insertions,
80 deletions, measured at the branch tip). A split would not help: the store,
the tier walk, and the adapter are one seam's three parts — the adapter binds
the store to the resolver's new interface, and the NewResolver arity change
forces every call-site suite into the same MR — so a source-first first part
would carry its tests in a stacked dependency.
References: S30 spec · S30 plan (see "Step 8: resolver virtual tier")
Scope
The diff carries collateral beyond Step 8's Files entry, accepted by the
operator's scope decision — npm's mirrored Step 9 MR (!2061 (merged)) carried the same
classes:
- npm twins: mutual
//nolint:dupldirectives on the two mirrored suites and a guards-comment refresh (the mirrors are prescribed duplicates). - The shared parent-gate table and doc grow the
mavenVirtualParentrow this step's finder declares. - The
SetResolutionkind invariants and their suite: an out-of-range kind and a kind/Virtualmismatch are rejected where they enter (the boundary-validation rule in internal/AGENTS.md). - The adapter's translation suite: the plan books the adapter in
wire_maven.go; its test lands beside it. - The resolver sidecar (
resolver.md) and the finder's guards suite, per the mirroring duty.
Governing ADRs
- ADR-001 (organizations as anchor) —
namespace_idequality leads on both join sides; the integration suite asserts a repository id in one namespace never resolves under another. - ADR-007 (database schema) — the finder is the access path the declared
unique index on
maven_virtual_repositories (namespace_id, repository_id)serves; the composite join andactiveParentPredicate(Maven format, virtual kind, live) match the ADR's legends and active-lookup semantics; deletion gates on the parent alone because the ADR's ERD gives the virtual child nosoft_deleted_atof its own. The plan-shape test pins single-partition pruning on both tables. - ADR-009 (API design) — no route, endpoint, or URL shape added or
altered. The tier-3 miss folds into the same
ErrRepositoryNotFound404every other miss uses, so the response confirms nothing; the resolver stays method-agnostic, so thekind=2PUT keeps its deterministic405. - ADR-004, ADR-005, ADR-008, ADR-012 — no contact: this step adds no limit logic, delivery-path change, storage interaction, or usage event.
- Mirror freshness:
scripts/adr-freshness.shreports two upstream amendments not yet synced — the ADR-007maven_filesmetadata-tombstone index and the ADR-009 npm multi-segment download route. Neither touches a surface this diff implements; both were checked against the upstream text.
Testing
Mirrors npm's step-9 finder suites file for file — the guards suite, the
integration subtest matrix, and the plan-shape EXPLAIN — diffed against
npm's for dropped subtests: none in the finder suites. Two resolver-suite
drops are declared in the test commit, both the tier-miss log record npm's
resolver carries and this plan does not name: npm's miss-log assertions and
its stand-alone VirtualUnderlyingErrorDoesNotWarn case. The recorded
reading: Maven's resolver emits no tier-miss record — the plan names only the
Virtual field, the interface, and the fail-loud nil discipline, and the
dispatcher's preflight line is a resolver fault's only record, as it already
is for the namespace and name tiers.
- The resolver table test walks all three kinds:
kind=1resolves with the populatedVirtualrow keyed by therepositories.idtier 2 resolved;kind=0andkind=2leave it nil and never call the finder. - The
(nil, nil)finder breach, the half-created miss, and the transient fault each have their own pin; the adapter's translation test keeps a real datastore failure unmasked as not-found. - The finder's integration suite covers the kind gate (each non-virtual kind in its own case, with a virtual binding present), the format gate, the soft-deleted parent, cross-namespace exclusion, and the plan shape.
- The
SetResolutioninvariants reject an out-of-range kind and a kind/Virtualmismatch at the boundary.
golangci-lint run with --build-tags=integration and the documented flag
set, which CI lint cannot see: no findings on this branch's added code. The
tagged tree's pre-existing noise is untouched here — contextcheck over the
shared contextless seed helpers (5,462 across the tree, npm's merged twin
file included) and one wrapcheck on gatingRemoteRepositoryFinder's
pass-through, a line this MR does not change. Re-threading contexts through
the shared fixture helpers is a standalone chore MR.
go test ./internal/datastore/ ./internal/format/maven/ ./cmd/artifact-registry/ green at the branch tip.
A five-category pre-push coherence check (plan adherence, acceptance
criteria, spec adherence, ADR adherence, dev guidelines) ran on this branch:
no blockers. The advisories it raised are dispositioned on this branch — the
resolver's interim tier-miss and fault records dropped, restoring the test
commit's declared reading and the handle-each-error-once rule — and the
out-of-Files collateral is the scope decision above.
e2e scenarios
No scenario added or affected: no route serves virtual reads until the
dispatch steps land — the kind=1 arm still answers the interim 501 — and
no kind=1 Maven repository can be created yet, so this MR alone changes no
client-visible behavior.
User documentation
No user-facing surface changes: every surface here — the resolver's third
tier, the read-only finder, and the composition-root adapter — is internal
composition no route serves until the dispatch steps land, so docs/user/**
is unaffected by this MR.
Related to #289