feat(maven): resolve a virtual read over its assembled upstream list (S30 plan: 11b/25)

Summary

The one call a virtual Maven read makes on the winner path: assemble the stored upstream list, run virtual.Resolver over it, and hand back the resolver's verdict with the position list index-parallel with the specs it ran over.

MavenVirtualResolution is a thin composition over Step 10c's MavenVirtualUpstreamAssembly and the shared resolver. It builds the request once, binds the assembled list to the resolution, and relays the resolver's verdict by identity, so the error taxonomy stays Step 11a's. Two things it owns beyond the relay:

  • The dropped soft-deleted associations ride their own field. Step 10c drops those rows before any position is built, so they are not positions; VirtualReadResult.SkippedSoftDeleted carries them for Step 19b's skipped_soft_deleted records, with Positions counting live rows only.
  • An emptied list answers remote.ErrNoUpstreams, and a body handed alongside that verdict is closed rather than leaked.

Scope

One step of 25 in the S30 plan (Step 11b). Library-level only: no route calls the facade yet, so no client-visible behavior changes. Steps 17 and 18 compose it into the winner-path and sidecar handlers.

Depends on

  • Step 11a (!2892 (merged)), merged: the error mapping this facade's verdicts land in. This branch was stacked on it and now diffs against main with only its own five files.
  • Step 10c (!2862 (merged)), merged: the assembly, its dropped-set field, and the shared test fixture this step extends.

Spec coverage

Spec: docs/specs/S30-maven-virtual.md (### Resolution, ### Allow/deny rules). Plan: docs/plans/2026-09-10-s30-maven-virtual.md, ### Step 11b.

Acceptance criteria

Criterion Test
Resolution: single winner on an artifact TestMavenVirtualResolution_SingleWinnerOnAnArtifact
Resolution: hosted-and-remote mix TestMavenVirtualResolution_HostedAndRemoteMix
Resolution: priority winner over a fresh hosted hit TestMavenVirtualResolution_PriorityWinnerOverAFreshHostedHit
Resolution: warm read short-circuit TestMavenVirtualResolution_WarmReadShortCircuits
Resolution: version-level metadata resolves a single winner TestMavenVirtualResolution_VersionLevelMetadataResolvesSingleWinner
Resolution: an emptied list does not hand back a body TestMavenVirtualResolution_AnEmptiedListClosesAForfeitedStream
Resolution: dropped associations reach the wide event's records TestMavenVirtualResolution_NamesTheSkippedSoftDeletedAssociations
Allow/deny: a version rule governs that version's requests TestMavenVirtualResolution_TheVersionRuleGovernsOnlyVersionLevelRequests
Allow/deny: group and artifact rules admit matches TestMavenVirtualResolution_GroupAndArtifactRulesAdmitMatches
Allow/deny: a corrupt rule fails closed on the winner path, for both request levels TestMavenVirtualResolution_ACorruptRuleFailsClosedOnBothRequestLevels
Allow/deny: a corrupted deny rule does not stop denying TestMavenVirtualResolution_ACorruptedDenyRuleDoesNotStopDenying
Allow/deny: an allow and a deny on one pattern, the deny wins TestMavenVirtualResolution_AnAllowAndADenyOnOnePatternIsDenied

The corrupt target_field reaches the resolver's input directly: the column's CHECK rejects every insert of one, which is the point of having it.

Beyond the criteria: the request's ids reach the list read and the position list stays index-parallel (TestMavenVirtualResolution_BindsEveryPositionToTheAssembledPath), a failure still names its positions (TestMavenVirtualResolution_AFailedResolutionStillNamesItsPositions), the winner names its own row and stored position (TestMavenVirtualResolution_NamesTheWinningPositionsOwnRow, TestVirtualReadResult_Winner*), an assembly failure relays without resolving (TestMavenVirtualResolution_RelaysAnAssemblyFailure), the fetch options select the configured body-cap class per target (..._ArtifactReadSelectsTheBlobBodyCapClass, ..._MetadataReadSelectsTheMetadataBodyCapClass), and the constructor refuses an unusable composition (TestNewMavenVirtualResolution_RefusesAnUnusableComposition).

Governing ADRs

  • ADR-001 / ADR-007 (namespace anchor, schema) — every seam is routed by the request's namespace, which the composite foreign key keeps shared with each upstream; the facade contributes no SQL.
  • ADR-004 (limits) — no limit is minted here: body caps defer to the configured class caps, and the 20-upstream cap is write-time.
  • ADR-005 (delivery mode) — no redirect/proxy decision is made; delivery rides the serve steps.
  • ADR-008 (content-addressable storage) — fetches carry no transform and no second key derivation; the shared flight's cache store stays the only writer.
  • ADR-009 (API design) — internal surface only; no route, envelope, or payload shape.
  • ADR-012 (usage data) — no event is emitted here; the facade carries the dimensions Steps 17 and 19b emit with.

Tests

go test ./internal/format/maven/ passes; golangci-lint run reports 0 issues; the comment-cap gate passes. The suites mirror npm's virtual_operations* cases; the npm-only guard shapes that have no Maven counterpart are dropped with their reasons recorded in the sidecar and the plan.

Overlap with open MRs

No open MR touches this diff's five files (checked against all 68 open MRs). !2901 (merged) (Step 13, open) shares the package and edits virtual_assembly.go and virtual_assembly_test.go; its VirtualPosition additions are unexported and change nothing the facade reads. !2900 (merged) (S32 OCI) touches internal/format/oci/ only.

Size

1,691 insertions and 1 deletion across six files: 185 production Go (virtual_operations.go 180 and virtual_assembly.go 5), 113 sidecar documentation (virtual_operations.md), and 1,393 test (virtual_operations_test.go 1,115, the internal seam suite 256, and the shared fixture 22). Production source is far under the 500-line ceiling; the test bulk is npm's facade suite mirrored case-for-case, one case per acceptance bullet and one per rule direction. Splitting the facade would separate the request/result contract from the resolution that consumes it, and the tests cannot move to a later step.

e2e scenarios

No docs/testing/ scenario is added or affected: no route composes the facade yet, so no conformance or e2e path reaches it.

User documentation

No docs/user/ surface changes: nothing a customer can call, see, or configure changes in this step.

Related to #289 (closed)

Edited by Moaz Khalifa

Merge request reports

Loading
Loading