feat(maven): the merge-path wide event (S30 plan: 19b/25)
Summary
Step 19b's wide event, the merge path's half: the maven_virtual_resolve record's merge-path values — merged vs merged_partial, the merge-side positions[] tokens, the dropped-set records, and the fetch/revalidate legs on the upstream-requests counter — emitted from the mounted classes' own defer, over Step 13's collection.
- The merge path's outcome and tokens.
serveMergedMetadatasettlesmergedvsmerged_partialand every live position's token; a position the collection settled none for keeps what the collaborator wrappers observed, and an entry omitsoutcomealtogether when neither settled one, never rendering an empty token. - The dropped set reaches
positions[]on every read. The assembly registers the soft-deleted associations on the read's recorder — the container sibling's reading — so a dropped association lands as askipped_soft_deletedentry under its stored position column, onno_upstreamsreads included. - A collection position's document read books its legs. The merge path's per-position fetcher wraps with a leg recorder: a conditional GET books
revalidate— the counter's first producer — a full re-fetchfetch, and the merge path issues no probe, which is where a cold-cache merge spike's C×M amplification appears, per the spec's counter row and the operator catalog. - A read that ends parked on the shared budget renders only the tail
omitted_budget. The admitted prefix settles no token — the omitted-outcome arm — and only the positions past the admitted set bookomitted_budget, per the spec's definition. - One event per read across both sidecar parent shapes. The package-level sidecar books the merge's own record; a version-level parent's verdict emits from the winner arm's own instrument pair inside
serveVirtualWinnerSidecar, and the mounted defer does not book it twice.
Scope
Step 19b of the S30 plan (### Step 19b: merge-path observability), on main since Step 18 (!3081 (merged)) merged. That MR's rework restructured the mounted classes' serve — the response recorder, the streaming sidecar digests, the self-emitting version-level sidecar arm — and this step's instrumentation is adapted onto that shape, with the recovery gate keeping a panicking sidecar parent's record single.
Depends on
- Step 18 (!3081 (merged)) — merged; this branch is rebased onto
mainpast it.
Plan and spec amendments
Per docs/dev/conventions.md (### Review-driven plan and spec corrections), this MR carries the corrections its own review proved:
- S30's Wide events section mints the omitted-outcome entry — an entry omits
outcomewhen no verdict resolved for it, a merge position the collection settled none for and no recorded observation filled — the sentence the operator catalog already carried, matching the code'sjson:"outcome,omitempty"rendering. - The plan's Step 10c and Step 11b entries route the dropped soft-deleted set through the read's recorder: Step 10c's drop registers the set there, and Step 19b's emitter reads it from there. Both entries named a
SkippedSoftDeletedtransport field — on the assembled list and on the facade result — that no production reader used once the recorder route landed, so both fields are removed and the drop's record is pinned where the recorder is reachable. - S30's
gitlab_artifact_registry_maven_virtual_upstream_requests_totalrow drops the "revalidateis pinned ahead of the merge path that first books it" clause: this step lands that producer, so the spec now agrees withdocs/dev/observability.mdandinternal/format/maven/virtual_metrics.md, which this MR also corrected. - The plan's Step 19b entry records the wiring the values ride (the assembly's dropped-set registration and per-position leg booking, the typed merge tokens, the mounted defer emission, the recorder-route contract surfaces), the new merge-path suite as a
Create, and the legs acceptance — a collection position's document read books itsfetch/revalidatelegs on the upstream-requests counter, which no later step lands.
Notes and gaps
- The acquire arm's rendering follows the spec's definition. A read that ends parked on the shared budget renders only the budget-omitted tail as
omitted_budget, its admitted prefix's entries omittingoutcome: the acquire can only error once the prefix admitted something, so the all-omitted_budgetrendering the branch first carried described a state the arm cannot be in. An amendment defining the acquire arm's tokens differently would change this rendering, and the code would follow it. - The plan's size forecast stands. The Step 19b entry's
Source: ~100 · Test: ~250estimate sits at ~¼ of the realized size; noSource:line in this plan's history has been edited post-implementation, so the plan-time forecast stands and this description carries the size justification instead.
Size justification
~548 reviewable lines (448 source, 100 docs) plus 1,048 test, measured against main with git diff --numstat origin/main...HEAD, marginally past the 500-reviewable-LOC guide. The emitter, the leg wrapper, and the mounted-class wiring are one fixture-driven unit — the suites drive every arm through the composed handler, so cutting the instrumentation from its wiring leaves two MRs whose suites cannot run standalone. Split by file group: virtual_observe.go 189, virtual_serve.go 99, virtual_read.go 70, virtual_metrics.md 48, the plan 29, virtual_assembly.go 28, virtual_metrics.go 25, virtual_sidecar.go 22, virtual_operations.md 12, the rest ≤9, suites 1,048.
Spec coverage
Spec: docs/specs/S30-maven-virtual.md (### Observability — Metrics, Wide events). Plan: docs/plans/2026-09-10-s30-maven-virtual.md, ### Step 19b.
Acceptance criteria
| Criterion | Test |
|---|---|
| Merge-path values reach the wide event from Step 13's records | TestMavenVirtualWideEvent_MergedDocumentRecordsTheMerge, _PartialMergeRecordsMergedPartial, _MergeAbsencesRecordTheirOwnToken, _MergeRecordsACachedContributionAsContributedStale, _MergeRecordsADeniedPositionWithoutDegrading, _MergeFailureRecordsTheMappedArm |
Dropped-set records reach positions[] |
_AllSoftDeletedListRecordsSkippedPositions, _AllSoftDeletedMergeListRecordsSkippedPositions, _ALiveMergeStillRecordsItsDroppedAssociations |
Budget-stopped merge's omitted_budget entries reach positions[] |
_MergeBudgetOmitsTheTail, TestMavenMergeServe_BudgetOmitsThePositionsPastTheAdmittedPrefix |
| A collection position's document read books its legs | TestMavenVirtualWideEvent_MergeBooksItsFamilies, TestMavenVirtualMergeLegRecorder_BooksTheLegTheReadIssued |
| Omitted-outcome entry | TestMavenVirtualMergePositions_OmitTheOutcomeNeitherSettled |
The parked read renders only the tail omitted_budget |
TestMavenMergeServe_ACapAcquireFailureOmitsOnlyTheTail, TestMavenMergeServe_AZeroAdmissionBudgetIsUnavailable |
| Winner fields always absent on the merge path | requireNoWinnerFields in every merge-path case |
| The version-level sidecar parent books its verdict, once | _VersionLevelSidecarBooksItsParentVerdict, _MountedSidecarPanicBooksItsRecord |
| The package-level sidecar books the merge's own outcome | _PackageLevelSidecarBooksOneSidecarEvent |
ADR coverage
ADR-001 (namespace-scoped ids only), ADR-004 (the 20-upstream cap bounds the count families), ADR-005 (merge outputs stream, never redirect), ADR-007 (no schema change; the dropped set rides existing reads), ADR-008 (the validator keys on input digests; the merged document is deliberately unstored), ADR-009 (no route change; bounded identity-free labels), ADR-012 (the operational log event sits outside the usage-data decision) — checked against the plan, spec, and cited ADRs by three /validate-step passes; no conflicts.
Tests
go build ./..., go test ./internal/format/maven/..., golangci-lint run, and the comment-caps gate pass at this layer; the pre-commit hook's go-test scope ran green on every commit, including the post-rebase adaptation.
User documentation
No docs/user/ surface changes: the event and counter are 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 (!3087 (merged)).
Related to #289 (closed)