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 anETagderived from the merge's own inputs, withHEADrunning the full merge and a matchingIf-None-Matchanswering304after the database reads alone. A merge the memory budget truncates answers200with no validator, never a short document under the complete merge'sETag. - 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
.md5sidecar answers422ahead 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.mdgains a## Virtual repositoriessection covering the resolution order, the merge, sidecar synthesis, the FIPS.md5refusal, and the no-publish rule.docs/testing/—docs/testing/e2e/maven.md's scope note claimed only two classes resolved and the other two answered501; it now states all four. Thee2e.maven.virtual.<slug>scenario group stays Step 20's, per the plan.- Run recipe —
.claude/skills/run-artifact-registry/SKILL.mddrops the501block for the mounted behavior and adds a served merge-and-sidecar walkthrough;## Keeping this skill currentgains 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.md5NULL-column404encodes ADR-007's nullablemd5; no new usage event, since ADR-012's virtual events are Step 17's. - Configuration reference — not applicable: no
proto/artifactregistry/config/**,internal/config/**, orconfig.example.yamlchange. - 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), andcmd/artifact-registry'sTestBuildMavenDispatcher_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 fromDeps.Config.MaxMetadataSizeandDeps.Config.MergeMemoryBudget(Step 16a's landed field). The mount must be reachable from that composition rather than only fromcmd/artifact-registry/wire_maven.go; if Step 18 threads a new seam instead, the fixture'sDepsliteral needs it and this suite should be re-dispatched. FIPSPostureForTestjoinsexport_test.go(test-only) so the external suite drives the FIPS-on arms, which production computes fromcrypto/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
sha512column, and a NULLmd5on a remote row - is integration-only, because the unit fixture'sRemoteCacheBackingis 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'ssidecar_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)