docs(plans): batch S32 plan updates (2026-09-24)
What this changes
This wave of S32 plan updates books two gaps from the Step 12 review (!2900 (merged), note) on Step 17. It also corrects Step 15's budget rule, books a traceability correction on Step 16 from its validation run, and carries one S32 spec sentence. Each booking goes on the step that owns it.
- Step 17: S16's fill instruments on a virtual fill. The standalone arm books
bookRemoteUpstreamFetchandobserveRemoteFetchFill. A virtual read books neither, although its fills tee into the same container-remote cache. Step 17 books both in the Fetcher recording wrapper it creates invirtual_wideevent.go, not invirtual_serve.go.fetchWinnersettles every Fetch that does not stream insideResolve(transport failure,304, unusable status), so the serve only ever sees a winner that streams and cannot see when the Fetch started. A Fetch that does not stream books its response in the wrapper. A stream settles once, on its firstReadthat ends or on an earlierClose, and books its response. A complete body also books its fill, with the counted bytes and the time since the Fetch call. A flight follower (FetchResult.Coalesced) books no metric sample, because both helpers Step 14 landed skip it, but it still emits itsfetchrecord. Acceptance and Tests cover the non-streaming answers. - Step 17: the winner fields on a failed drain's records.
drainFilllogs throughlogVirtualMappedFailure, which writes onlyerror_message.writeVirtualReadErrorandwriteVirtualAbandonedReadtakeattrs ...slog.Attr, anddrainFillpasses the failure subject'slogAttrs(), so the record names the virtual repository, the winner, and the remote. - Step 17: ADR-012's virtual pull events.
artifact_pulledcarriesrepository_kind=virtualandcache_hit.virtual_cache_misscarriesupstream_type. The pull event fires on a manifestGETwhose payload the serve delivers in full, the hosted rule. AHEADand a blob read emit none. The S32 spec never mentions ADR-012, so Step 17 follows S30's Step 17 for thecache_hitmeaning and the miss trigger. S30's Step 17 books the sameAnchoredEventfields and event constant, and whichever step lands first adds them. - Step 15: the budget pick is by reference kind. The route arms before
Resolve, so it cannot tell a tag miss from a tag revalidation. A by-digest read arms the miss-path sum, and a tag read arms the revalidation expression, miss or not. The rule now lives in### Every virtual route arms its own write deadline, and the budget is stated against it. Step 15's Scope, Files, Acceptance, and Tests follow it, and its armed-instant table has a by-digest miss, a tag miss, and a stale tag. Step 15 also listsdocs/dev/remote-read-budgets.md, which states only the standalone arms today. - Step 15: the manifest tail is floored at
server.timeouts.write. !3005 (merged) floors the re-arm afterResolveatserver.timeouts.write, as Step 16's blob route does, so a raised write timeout survives on a warm serve's body. The research section said Step 15's route does not floor its tail, and Step 15's Files entry named the re-arm toremote.FillTailBudgetalone. Both now name the floor. - Step 16: the cross-repository blob criterion. The spec's criterion that two virtual repositories and a direct read of their shared remote issue one upstream
GETnames a blob. The traceability table gave it to Step 15's manifest route, and a manifest test cannot see a blob builder that holds its own flight registry. Step 16's Tests entry now carries the blob case, and Step 15 keeps its manifest twin. - S32 spec: the finder read is outside the statement count. The criterion "Statement count follows upstream kinds, not position count" gains one sentence. The read that maps
repositories.idtocontainer_virtual_repositories.idruns once per request before resolution starts, so2 + Kleaves it out. The plan already said so, and the spec was silent. The Step 4a finder (!2976 (merged)) issues that read.
An earlier revision also booked a Step 14 entry for coalesced fill booking. Step 14 merged in !2940 (merged) with its own shape (observeRemoteFetchFill), so that entry is gone.
User documentation
This MR also changes one line in docs/user/formats/oci.md, which publishes to docs.gitlab.com. The line now says you can pull images through a virtual OCI repository, because the virtual manifest handler (!3005 (merged)) and the virtual blob handler (!2984 (merged)) have merged. The two overview pages that name virtual repositories get the same update in !3062 (merged).
Merge timing
Merge this after !3005 (merged) (Step 15), whose tail floor the Step 15 entry above records. !3005 (merged) merged on 2026-09-28, so nothing blocks this MR now.
Related to #291 (closed)