feat(maven): route the virtual read classes through the dispatch slot (S30 plan: 9/25)
Summary
Implements Step 9 of the S30 plan: the kind=1 arm of serveDownload routes each parsed class — checksum sidecar, package-level maven-metadata.xml, version-level maven-metadata.xml, primary file — through one virtual-read handler dependency on the dispatcher. The slot defaults to the interim 501. The FIPS .md5 short-circuit moves ahead of the dispatch, exactly as the kind=2 arm orders it. The write path is unchanged and pinned against kind=1.
Depends on: Step 8 (!2737 (merged), open), so this MR targets the step-8 branch.
References: S30 spec · S30 plan (see "Step 9: virtual dispatch slot")
Reviewable LOC: 630 insertions — past the 500 ceiling. By file group: routing-table tests 563 (handler_test.go 561, download_test.go threading 2), slot and seams 67 (handler.go 61, export_test.go 5, remote_stub.go 1); 81 deletions, mostly the compressed test-constructor comment and the stub doc. A split would not help: the slot and its routing table are one surface, and the tests are the slot's only coverage. The shape mirrors npm's Step 10 suites minus the handler-label surface Maven does not carry, per the plan.
Scope
The diff carries three paths beyond Step 9's Files entry. All three are required ripples, confirmed by the pre-push coherence check:
export_test.go: the test constructor takesopts ...DispatchOption, because the new suites inject the slot through it.download_test.go: theoptsthreading carries the injected spy into the dispatcher (newVirtualReadDispatchercalls through it).remote_stub.go: thewriteNotImplementeddoc compresses to the writers that remain, as the Scope prose mandates.
Governing ADRs
- ADR-009 (API design) — no new route; client URLs are unchanged.
PUTonkind=1still answers405withAllow: GET, HEADbefore any body byte is read. The FIPS.md5refusal answers422before any resolution query. - ADR-001 (organizations as anchor) — untouched; authorization still answers before the kind gate, and the suspended-namespace
403still precedes both. Both orderings are pinned against an injected denying decision.
No deviation in either ADR.
Testing
go test -count=1 ./internal/format/maven/green, plus./internal/datastore/and./internal/authz/(parent-slice cover).- New suites: every read class reaches the slot (
TestDispatchHandler_VirtualKind_ReadClasses_ReachTheVirtualSlot); the unwired slot answers the interim501(TestDispatchHandler_VirtualKind_UnwiredSlot_ServesInterim501); hosted andkind=2reads never reach the slot (TestDispatchHandler_VirtualSlot_NotReachedByHostedOrRemoteReads); the405/403/ auth-ordering pins; parse rejections precede the kind branch; the FIPS.md5422. - A five-category pre-push coherence check ran on this branch: no blockers. The 13 out-of-
Filesrows from that check belong to the parent slice (!2737 (merged)); the 3 in-slice rows are dispositioned under Scope above.
e2e scenarios
No scenario added or affected: the slot still answers the interim 501 until Step 17 swaps it, so no route serves virtual reads and no client-visible behavior changes.
User documentation
No user-facing surface changes: every status path answers exactly as before, so docs/user/** is unaffected by this MR.
Related to #289 (closed)