feat(maven): the sidecar handler by parent shape (S30 plan: 18/25)

Step 18 of the S30 Maven virtual plan. Before this MR, a client GET on a virtual Maven repository's package-level maven-metadata.xml or on any checksum sidecar answered 501 not_implemented. All four read classes the dispatch slot routes are now served.

What this ships

  • Package-level maven-metadata.xml — merged from every contributing upstream and served under an ETag derived from the merge's own inputs, with HEAD running the full merge and a matching If-None-Match answering 304 after the database reads alone. A merge the memory budget truncates answers 200 with no validator, never a short document under the complete merge's ETag.
  • Checksum sidecars — .md5, .sha1, .sha256, .sha512, synthesized from the winning position's row at a version-level parent and from the merged bytes at a package-level one. No upstream request ever targets a sidecar URL.
  • FIPS — a .md5 sidecar answers 422 ahead of any resolution, cache lookup, or upstream request.
  • The mount is composed inside maven.NewHandler, so the production wiring and the test floor share one seam.

Verified against a running service

driver.sh smoke green at 94 passed, 0 failed on a rebuild from this branch. Driving the virtual route by hand: with an upstream holding nothing, all four classes answer the read-path 404; after publishing a parent and a package-level document to a hosted upstream —

Read Result
merge GET 200, application/xml, Content-Length: 336, input-derived ETag
merge HEAD 200, the same Content-Length (the full merge runs)
merge If-None-Match 304
.sha1 sidecar GET 200, 40 bytes, text/plain; charset=us-ascii, body equal to shasum -a 1 of the served parent
sidecar If-None-Match 304

go test ./... green, go test -tags=integration ./internal/format/maven/ green, and the Maven-wiring integration tests in cmd/artifact-registry green.

Guardrails

  • docs/user/ — docs/user/formats/maven.md gains a ## Virtual repositories section covering the resolution order, the merge, sidecar synthesis, the FIPS .md5 refusal, and the no-publish rule.
  • docs/testing/ — docs/testing/e2e/maven.md's scope note claimed only two classes resolved and the other two answered 501; it now states all four. The e2e.maven.virtual.<slug> scenario group stays Step 20's, per the plan.
  • Run recipe — .claude/skills/run-artifact-registry/SKILL.md drops the 501 block for the mounted behavior and adds a served merge-and-sidecar walkthrough; ## Keeping this skill current gains this step's validation entry.
  • ADRs — checked against ADR-004, ADR-005, ADR-007, ADR-009, and ADR-012. The mirror was verified current with scripts/adr-freshness.sh (exit 0). The .md5 NULL-column 404 encodes ADR-007's nullable md5; no new usage event, since ADR-012's virtual events are Step 17's.
  • Configuration reference — not applicable: no proto/artifactregistry/config/**, internal/config/**, or config.example.yaml change.
  • Bruno — not applicable: no api/openapi/** operation added, renamed, or removed.
  • Usage data — not applicable: no event, catalog constant, or dimension change.

Reviewable LOC

1782 insertions / 106 deletions across 18 files, over the 500-line ask. The split:

Group Insertions Deletions
Production (virtual_sidecar.go, virtual_read.go, handler.go, virtual_assembly.go) 600 18
Tests 1118 68
Docs and run recipe 64 20

The step is one seam mounted over two classes and does not split without shipping a half-mounted router: the router change, the sidecar arm, and the mount in NewHandler are mutually dependent. The test volume is the spec's acceptance surface — sidecar synthesis over both parent shapes, the merge's conditional and partial arms, and the counter rules.

Plan and spec amendments

None.

Spec coverage

Spec coverage

Spec: docs/specs/S30-maven-virtual.md Plan: docs/plans/2026-09-10-s30-maven-virtual.md, Step 18

Acceptance criteria

# Criterion Tests
Resolution 6 A sidecar resolves its parent: the parent path fills at the winning remote position and the digest is synthesized from the committed row, one upstream GET for the parent path TestVirtualReadIntegration_SidecarResolvesItsParentWithOneUpstreamGet, TestVirtualSidecar_ServesItsParentByParentShape
Resolution 6 / Metadata merge 21 No sidecar fetch upstream: no request ever targets the sidecar URL TestVirtualReadIntegration_SidecarResolvesItsParentWithOneUpstreamGet, vrRequireNoSidecarFetch (both parent shapes)
Metadata merge 26 A winner change between dependent reads is visible, not masked TestVirtualSidecar_WinnerChangeBetweenDependentReadsIsVisible
Metadata merge 27 FIPS short-circuit on the merged sidecar: .md5 answers 422 before any resolution TestVirtualSidecar_FIPSTurnsAwayMD5BeforeResolution (both parents, GET and HEAD), TestVirtualRead_RejectionsAheadOfResolutionEmitNoUsageEvent
Metadata merge 22 / Step 18 acceptance The merge entry takes Step 16a's slot on every path, the package-level sidecar included TestVirtualRead_MergeBudgetOmitsTheTailOnEveryPackagePath
Error Cases (.md5 NULL) A .md5 under FIPS-off whose winner's row carries a NULL column answers 404, and the merged arm never reaches that row TestVirtualSidecar_MD5NullColumnOnTheWinnerArmAnswers404, TestVirtualReadIntegration_MD5NullColumnOnTheWinnerArmAnswers404, TestVirtualSidecar_MergedParentNeverReachesTheNullColumn404
Conditional requests 6 The sidecar conditional: a matching If-None-Match answers 304 after the parent resolves, on both arms TestVirtualSidecar_ConditionalRequestAnswers304
Conditional requests 2 A partial merge carries no validator TestVirtualRead_PackageLevelMetadataPartialCarriesNoValidator, TestVirtualRead_MergeBudgetOmitsTheTailOnEveryPackagePath
Validation and reads 1 HEAD matches GET on the metadata and sidecar paths TestVirtualRead_PackageLevelMetadataHeadMatchesGet, TestVirtualSidecar_HeadMatchesGet
Sidecar synthesis (Design) The synthesized body is the S10 shape: one lowercase-hex line, text/plain; charset=us-ascii, and the body's own ETag vrRequireSynthesizedSidecar (every sidecar case)
Sidecar synthesis (Design) A synthesized sidecar bumps neither downloads_count nor last_downloaded_at TestVirtualSidecar_SynthesisBumpsNothing
Sidecar synthesis (Design) A version-level parent synthesizes from the winning position's row; a package-level parent from the merged bytes this read generates TestVirtualSidecar_SynthesizesEveryExtensionFromTheWinnersRow, TestVirtualReadIntegration_WarmRemoteWinnerSynthesizesEveryExtension, TestVirtualSidecar_MergedParentSynthesizesEveryExtension
Step 18 acceptance (mount) The package-level and sidecar classes route end to end through the slot, with the merged GET/HEAD live behind Step 14's serve surface TestVirtualRead_TheMountedClassesLeaveTheInterim501, TestVirtualRead_PackageLevelMetadataServesTheMergedDocument, TestBuildMavenDispatcher_RoutesTheMountedClassesThroughTheVirtualSlot
Metadata merge 11 / Error Cases A merge whose every consulted position establishes an absence answers the read-path 404 TestVirtualRead_PackageLevelMetadataAllAbsentAnswers404
Error Cases (404 shape) Every read-path 404 is the S01 JSON envelope assertS01Envelope404 / assertS01Envelope404ForIntegration in the three 404 cases above
Step 18 acceptance (merged arm) The merged GET carries the request's coordinates, the contributors' union, exact framing, and the input-derived validator TestVirtualRead_PackageLevelMetadataServesTheMergedDocument, TestVirtualRead_PackageLevelMetadataConditionalAnswers304

Error cases

# Condition Tests
E-FIPS .md5 sidecar GET/HEAD under FIPS-on: 422 before any resolution, cache lookup, or upstream request TestVirtualSidecar_FIPSTurnsAwayMD5BeforeResolution, TestVirtualRead_RejectionsAheadOfResolutionEmitNoUsageEvent
E-NULL .md5 sidecar under FIPS-off, winner's md5 column NULL: 404 TestVirtualSidecar_MD5NullColumnOnTheWinnerArmAnswers404 (hosted winner), TestVirtualReadIntegration_MD5NullColumnOnTheWinnerArmAnswers404 (remote winner)
E-MERGE-404 Merge produced no document, an absence established at every consulted position, no position omitted: 404 TestVirtualRead_PackageLevelMetadataAllAbsentAnswers404
E-MERGE-PARTIAL Merge read with some positions omitted: 200, merged_partial, no ETag TestVirtualRead_PackageLevelMetadataPartialCarriesNoValidator, TestVirtualRead_MergeBudgetOmitsTheTailOnEveryPackagePath
E-RATE A read past the service's rate limit: 429 before any resolution Step 16b's; the mount is the entry its wiring registers on. Not in this MR.
E-PUT Any PUT on a virtual repository: 405 Step 9's landed arm; unchanged by the mount.
E-405/400 The write-verb 405 and the malformed-path 400s Step 9's landed arm; unchanged by the mount.

Security considerations

# Concern Tests
S-1 No new outbound surface: a sidecar read issues no upstream request for the sidecar path TestVirtualReadIntegration_SidecarResolvesItsParentWithOneUpstreamGet (every recorded upstream URL is the parent's), vrRequireNoSidecarFetch
S-2 The merge parses upstream-controlled XML, and the parser is the hardening surface Step 12/14-owned (virtual_metadata_merge_test.go, virtual_serve_internal_test.go); the mount adds no parser and no new input path.
S-3 Read-triggered event volume is bounded by the shared rate-limit primitive Step 16b-owned; S30 mints no limiter, and this step only mounts the entry its wiring registers on.
S-4 Authorization is on the addressed repository; a composition can widen access S13/S17-owned; unchanged by the mount.
S-5 The recursion guard against a virtual upstream Step 10b-owned (the kind and format gates); unchanged by the mount.
S-6 Credentials never appear on these events S14/S13-owned; unchanged.

Notes and gaps

  • Rewritten tests. Three landed tests pinned the two classes as unarmed, which this step's mount contradicts; each is rewritten with its intent kept. TestVirtualRead_UnarmedClassesKeepTheInterim501 -> TestVirtualRead_TheMountedClassesLeaveTheInterim501, TestVirtualRead_UnarmedClassesEmitNoEvent -> TestVirtualRead_RejectionsAheadOfResolutionEmitNoUsageEvent (the FIPS 422 is a rejection ahead of resolution and still books no usage event), and cmd/artifact-registry's TestBuildMavenDispatcher_AnswersTheInterim501OnTheUnarmedClasses -> TestBuildMavenDispatcher_RoutesTheMountedClassesThroughTheVirtualSlot. Step 17's plan books that flip: "the routing test asserting the package-level metadata and sidecar classes still answer 501 until Step 18".
  • Composition assumption. The route suites compose through maven.NewHandler(maven.Deps{...}), the seam Step 15's composition root and the production root share. The merge arm's per-input cap and in-flight budget are read from Deps.Config.MaxMetadataSize and Deps.Config.MergeMemoryBudget (Step 16a's landed field). The mount must be reachable from that composition rather than only from cmd/artifact-registry/wire_maven.go; if Step 18 threads a new seam instead, the fixture's Deps literal needs it and this suite should be re-dispatched.
  • FIPSPostureForTest joins export_test.go (test-only) so the external suite drives the FIPS-on arms, which production computes from crypto/fips140.Enabled; the in-package S14 sidecar suites write the same field directly for the same reason.
  • Fixture gaps, not spec gaps. A remote winner's own row read - its sha512 column, and a NULL md5 on a remote row - is integration-only, because the unit fixture's RemoteCacheBacking is the zero-value concrete store Step 17's fixture documents. The FIPS-on served shape is pinned at the dispatcher ordering (the 422 precedes the slot, so no resolution ran); the served FIPS matrix for a hosted parent is S10's sidecar_test.go's.
  • MR !3045 (merged) findings F2 and F13 are untouched. No case here asserts the omitted-position prefix verdict F2 covers, and no case depends on the plugins-shaped input charge F13 covers, so no test in this MR fails solely because of either.

Related to #289 (closed)

Merge request reports

Loading
Loading