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:dupl directives on the two mirrored suites and a guards-comment refresh (the mirrors are prescribed duplicates).
  • The shared parent-gate table and doc grow the mavenVirtualParent row this step's finder declares.
  • The SetResolution kind invariants and their suite: an out-of-range kind and a kind/Virtual mismatch 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_id equality 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 and activeParentPredicate (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 no soft_deleted_at of 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 ErrRepositoryNotFound 404 every other miss uses, so the response confirms nothing; the resolver stays method-agnostic, so the kind=2 PUT keeps its deterministic 405.
  • 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.sh reports two upstream amendments not yet synced — the ADR-007 maven_files metadata-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=1 resolves with the populated Virtual row keyed by the repositories.id tier 2 resolved; kind=0 and kind=2 leave 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 SetResolution invariants reject an out-of-range kind and a kind/Virtual mismatch 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

Merge request reports

Loading
Loading