feat(maven): the winner-path wide event (S30 plan: 19a/25)
Summary
Step 19a's wide event, layer 2 of 2: the maven_virtual_resolve record with its positions[] array, over the exclusion-sink, health, and collaborator recording wrappers, emitted from a defer at the resolution entry.
- The full S30 field set — identity fields,
upstream_count, winner fields (absent rather than zero-valued when there is no winner),resolution_outcome,positions,status,maven_code— withupstream_countcounting live positions. positions[]attributes a503to an upstream when no label can. The four remote collaborators are wrapped as recording adapters, the hosted upstream'smissoutcome and the health reader are recorded, and a denied or ineligible position reachespositions[]through the exclusion-audit recording wrapper — the only slice-visible source of those outcomes, sinceloadAndFilterexcludes them before any collaborator call.- The deferred emission books every arm an inline emitter would leave silent: the recovered panic, the client cancel, the unclassified disconnect, and the infrastructure arm; the rejections ahead of resolution (the write-verb
405, the malformed-path400s, the FIPS422, the rate-limit429) emit no event. - The drain-failure arm records its own verdict on the read's recorder: it resolves after the defer captured the read's verdict, so the record would otherwise report the pre-drain stream.
- The composition root cannot forget the records: the health reader and the exclusion audit sink each get their recording decorator in
composeMavenVirtualResolution(handler.go).
Scope
Layer 2 of 2 of Step 19a of the S30 plan (### Step 19a: winner-path observability), stacked on the families layer until it merges; this MR targets its branch. The merge path's values (Step 19b) are pinned ahead as vocabulary only and land with their producer.
Depends on
- The families layer (!3014 (merged)) — the recorder's bookings land there; merge that MR first, then this one. Both sit on !3003 (merged).
Plan and spec amendments
Per docs/dev/conventions.md (### Review-driven plan and spec corrections), this MR carries the correction its own review proved:
- S30's Wide events section renames the event's HTTP status field
http_status→status, matching LabKit'sfields.HTTPStatusCodestandard field the emission keys on.
Size justification
~1,116 reviewable source lines (+2,043 test, +33 docs) against the families layer, past the 500 reviewable-LOC guide. The recorder, its recording wrappers, the emission, and the install sites are one fixture-driven unit — the suites drive every arm through the composed handler (composeMavenVirtualResolution), so cutting the recorder from its emission leaves two MRs whose suites cannot run standalone; the npm mirror's own assembly part shipped over the ceiling for the same reason. Split by file group: virtual_observe.go 920, virtual_read.go 124, handler.go 42, virtual_assembly.go 25, virtual_read_errors.go 5, suites ~2,043, docs ~33.
Spec coverage
Spec: docs/specs/S30-maven-virtual.md (### Wide events). Plan: docs/plans/2026-09-10-s30-maven-virtual.md, ### Step 19a.
Acceptance criteria
| Criterion | Test |
|---|---|
| Full spec field set on the event | TestServeVirtualRead_EmitsTheFieldSet, TestMavenVirtualWideEvent_HostedWarmHit |
503 attributed to an upstream via positions[] |
TestMavenVirtualWideEvent_UnhealthyRemoteRecordsTheHealthGate |
Denied and ineligible positions reach positions[] through the audit-sink wrapper |
TestMavenVirtualWideEvent_AllDeniedRecordsTheDeniedPosition, TestMavenVirtualWideEvent_DenialAndDataErrorAreDistinct |
| One read books every family once | TestServeVirtualRead_BooksTheFourFamilies |
| Panic, cancel, disconnect, and abandoned arms all book | TestServeVirtualRead_BooksAPanic, TestServeVirtualRead_BooksAnUnclassifiedDisconnect, TestServeVirtualRead_BooksAnAbandonedRead, TestMavenVirtualWideEvent_AbandonedReadStillBooks |
| The arm's own outcome, not the pre-drain stream | TestServeVirtualRead_BooksTheDrainArm, TestServeVirtualRead_BooksTheOutcomeItsArmReached |
| Rejections ahead of resolution book no event | TestServeVirtualRead_RejectionsAheadOfResolutionBookNoEvent |
| Wrappers forward unchanged; recorder ordering | TestMavenVirtualRecorder_WrapperForwardsUnchanged, TestMavenVirtualRecorder_SnapshotOrderIsFirstObservationOrder, TestMavenVirtualExclusionAuditRecorder_ForwardsEveryEvent, TestMavenVirtualHealthStatusRecorder_ForwardsEveryVerdict |
ADR coverage
ADR-001 (namespace_id is an event field, never a label), ADR-007 (the emission's composite-FK citation for NamespaceID ownership; positions[].position is the resolver's list index, not the stored column), ADR-008 (every wrapper is a pass-through), ADR-009 (the event is a log record, not an API surface), ADR-012 (operational metrics + log event sit outside the usage-data decision) — checked against the plan, spec, and cited ADRs by the /validate-step pass; no conflicts.
Tests
go build ./..., go test -short ./internal/format/maven/... ./internal/metrics/..., golangci-lint run, and the comment-caps gate pass at this layer, at the pre-commit hook's full go test -short ./... scope.
User documentation
No docs/user/ surface changes: the event is operator-facing and catalogued in docs/dev/observability.md; the end-of-plan docs pass owns the customer-facing write-up.
No e2e scenario added: the virtual read's end-to-end scenarios are Step 20's, the step that mounts the harness the catalog's virtual rows need.
Related to #289 (closed)