feat(maven): the virtual composition root (S30 plan: 15/25)

Summary

The process-lifetime wiring for a virtual Maven read: maven.NewHandler composes the Step 11b facade over the assembly's seams and the flight holder it already constructs, buildMavenDeps supplies those seams from production stores, and the composition root carries the production remote.UpstreamExclusionAuditSink — the LabKit-logged UpstreamExclusionEvent writer virtual.NewResolver requires before any virtual resolver can boot.

Three things this MR establishes:

  • The composition stays inside maven.NewHandler. The virtual seams arrive through maven.Deps — VirtualResolutionStore, the two batched lookups, the memoizing health reader, the exclusion sink — and NewHandler builds the assembly and facade over its own newMavenRemoteFlights holder through the mavenVirtualFlights adapter, so the coalescing criterion's shared-instance property holds without exporting any handler-internal type. A partially wired boot composes nothing. A supplied resolution block the resolver would refuse is refused at config load, naming both keys and both values, so this root builds no throwaway resolver to panic over one; and where every seam is present and a constructor still refuses, the handler build panics rather than dropping the stack to a silent interim 501.
  • The production exclusion audit sink is consumed, not landed. !2940 (merged) (S32 container virtual step 14) merged first and landed the sink, its position_index/resolution_phase keys, and its catalog row, so this MR drops its copies of both. What it adds is the Maven half of that sink's boot contract: remoteAuditSink keeps its three seams and buildMavenDeps asserts the exclusion one, so a root sink that records no exclusion fails the boot rather than handing the resolver nothing to record a denial through.
  • The slot keeps its interim answer. buildMavenDispatcher passes no virtual read handler, so every virtual read class still answers 501 not_implemented until Step 17 swaps it.

Scope

One step of 25 in the S30 plan (Step 15). It wires and boots: the facade is composed but no route serves a virtual read as of this commit — the dispatch slot keeps the interim 501 — so no client-visible behavior changes.

Depends on

  • Merged main at a2242c7f6. main carries the Step 11b squash (!2914 (merged)) and the S32 container virtual composition (!2940 (merged)); this branch merged that tip, so the diff is this step's own change set and no stacked parent remains.

Plan and spec amendments

Per docs/dev/conventions.md (### Review-driven plan and spec corrections), this MR carries the corrections its own review proves, and it supersedes the plan-correction MR !2948 (closed), which is closed unmerged.

  • docs/plans/2026-09-10-s30-maven-virtual.md, Step 15's Files list gains internal/format/maven/handler.go, internal/format/maven/handler_internal_test.go, cmd/artifact-registry/remote_audit_sink_test.go, internal/config/virtualrepositories.go, internal/config/virtualrepositories_resolvetimeout_test.go, config.example.yaml, and docs/dev/configuration-reference.md, and moves the sink and its catalog row out of this step's entry — the S32 container virtual step 14 landed both. Until this lands, the plan on main books four of this MR's twelve paths.
  • The same entry's boot claim is tightened: a supplied resolution block the resolver refuses is refused at config load, not by a panic in buildMavenDeps.

Spec coverage

Spec: docs/specs/S30-maven-virtual.md (### Resolution, ### Health, ### Error Cases, ### Concurrency, ### Configuration). Plan: docs/plans/2026-09-10-s30-maven-virtual.md, ### Step 15.

Acceptance criteria

Criterion Test
A booted instance constructs the full virtual stack TestBuildMavenDispatcher_BootsAndAnswersTheInterim501OnEveryVirtualReadClass, TestBuildMavenDeps_ComposesTheVirtualResolutionOnlyWhenFullyWired
The slot answers 501 on every virtual read class, GET and HEAD TestBuildMavenDispatcher_BootsAndAnswersTheInterim501OnEveryVirtualReadClass (all eight classes)
The virtual stack and the kind=2 fill path share one flight registry TestComposeMavenVirtualResolution_SharesTheHandlersFlightRegistry
The block an operator's own file produces composes TestBuildMavenDeps_ComposesTheLoaderSuppliedResolution
A refused combination with every seam present fails the build, not the slot TestBuildMavenDeps_FailsLoudlyWhenEverySeamIsPresentAndTheResolverRefuses
The wired audit sink is the production type TestRemoteAuditSink_IsTheProductionUpstreamExclusionSink, TestBuildMavenDeps_WiresTheVirtualSeams (the exclusion-sink subtest)
A root sink that records no exclusion fails the build TestBuildMavenDeps_RefusesASinkThatCannotRecordAnExclusion
The sink's records carry the reason token, on the Info floor TestRemoteAuditLogSink_UpstreamExclusionPayload (every enumerated UpstreamExclusionReason column value), TestRemoteAuditLogSink_SurvivesTheConfiguredLevel (the exclusion subtest)
A probe_timeout at or above resolve_timeout is refused at load, naming both keys and both values TestLoad_VirtualRepositories_ResolveTimeoutAtOrBelowProbeTimeoutIsRefused
The seams are production values, no test doubles TestBuildMavenDeps_WiresTheVirtualSeams
The smoke run is green the run recipe's 2026-09-25 re-run on the merged tree: driver.sh down && up && smoke, 80 passed / 0 failed

Governing ADRs

  • ADR-001 / ADR-007 — every seam is namespace-keyed; no schema change, no migration.
  • ADR-004 — no limit is minted: the withdrawn per-repository budget is not wired, and the 20-upstream cap stays write-time.
  • ADR-005 / ADR-009 — no route, envelope, or delivery decision; the interim 501 is the spec's own staged landing of the client surface.
  • ADR-008 — no storage path; the flight holder changes object identity only, not keying.
  • ADR-012 — the exclusion record is a LabKit slog audit record, not a Snowplow usage event; no usage event, dimension, or catalog constant is added (pull-usage emission is Step 17's).

Tests

go test -short -tags= ./... passes, and so do go build ./..., golangci-lint run, and the comment-cap gate. golangci-lint run --build-tags=integration ./cmd/artifact-registry/... reports 11 pre-existing issues in four files this diff does not touch (CI's lint job sets no run.build-tags, so it never compiles them). Each property the review asked for was checked by breaking it: the flight-sharing test against a second newMavenRemoteFlights at the composition, and the refused-combination test against a resolver-refusing block with every seam present. The merge onto main was checked the same way — go test -short -tags= ./... passes at 69 packages, and the run recipe's 2026-09-25 entry records a smoke re-run on the merged tree: down then up then smoke, 80 passed / 0 failed.

Overlap with open MRs

  • !2940 (merged) (S32 container virtual step 14) — merged as a2242c7f6. It landed remoteAuditLogSink.RecordUpstreamExclusion with position_index and resolution_phase (the shape this MR had moved to after the two swapped once) and its own docs/dev/observability.md row, so this MR drops its copies of the method and the row. Its payload test samples reason families; this MR keeps its every-enumerated-value table over the same record, which is the coverage its step's plan text claims.
  • !2916 (merged) (version-cap accounting) — shares docs/dev/observability.md with different rows; whichever lands second rebases one row.
  • !2901 (merged) (Step 13) — shares the package, not this diff's files.
  • !2856 (merged) (hswimelar/s17-p6-step-42-p11) — merges cleanly into main but conflicts with this branch in .claude/skills/run-artifact-registry/SKILL.md, and no pipeline reports it. Merge order: this MR first, because this MR is mergeable into main as it stands while !2856 (merged) is not — so the file that has to move is !2856 (merged)'s, and this MR rebases that file instead if the order is reversed.
  • !2948 (closed) — closed unmerged; its content is in this MR's ## Plan and spec amendments.

Size

870 insertions and 88 deletions across 12 files against the merge base, past the 500-line review threshold, so the split: 186 production Go (internal/format/maven/handler.go 116, cmd/artifact-registry/wire_maven.go 51, internal/config/virtualrepositories.go 19), 576 test (cmd/artifact-registry/wire_maven_test.go 398, cmd/artifact-registry/remote_audit_sink_test.go 94, internal/format/maven/handler_internal_test.go 60, internal/config/virtualrepositories_resolvetimeout_test.go 24; the wire_maven_usagedata_integration_test.go trim is (-10) net), and 106 run-recipe and plan (.claude/skills/run-artifact-registry/SKILL.md 59, docs/plans/2026-09-10-s30-maven-virtual.md 44, the two configuration documents 3).

The change does not split further. The composition root is one seam: the wire, the sink it needs, and the handler seams it composes cannot move to a later step without leaving a resolver that cannot boot. The test bulk is the wiring suite the plan's mirror duty requires plus the four tests review asked for — a shared-instance composition test, a loader-ordering test, and the pair pinning that a block the loader accepts composes while a block it cannot must fail the build — each of which fails when the property it pins is broken. The run-recipe lines are the boot-change guardrail's own requirement, and the plan lines are the correction the same guardrail's review round proved.

e2e scenarios

No docs/testing/ scenario is added or affected: no route serves a virtual read until Step 17 (the slot keeps the interim 501), so no conformance or e2e path reaches the composition. The e2e.maven.virtual.* group is Step 20's. The driver.sh smoke suite is green at 74 passed / 0 failed on this head, recorded in the run recipe.

User documentation

No docs/user/ surface changes. The one customer-facing behavior this MR alters is that a virtual_repositories.resolution block whose probe_timeout is at or above resolve_timeout now fails startup instead of loading: no virtual read is served until Step 17, the ordering was already refused wherever a resolver was built, and docs/user/ documents no virtual_repositories setting — that surface is the operator configuration reference under docs/dev/.

Related to #289 (closed)

Edited by Moaz Khalifa

Merge request reports

Loading
Loading