docs(specs): scope S13's resolve_timeout to the decision's stages

What this does

Amends seven S13 passages, across the five sections below, that describe a resolve_timeout mechanism the implementation deliberately does not have, and adds the one thing no passage stated: what an expiry answers from.

Review added one correction the same ruling forces at a site outside S13: the Budget expiry row in docs/specs/S31-npm-virtual.md, described under "One site outside S13". A second correction, the ResolveTimeout doc comment in internal/config/virtualrepositories.go, was carried here in 5f163ac5 and is now left to !2622 (merged); see "The ResolveTimeout comment goes to !2622 (merged)".

This is an amendment, not a revision. S13 stays Approved and no ruling is reversed: every change either replaces wording the merged S32 plan already overruled, or replaces a stated reason that does not operate with the one that does.

Why now

!2622 (merged) gives resolution.resolve_timeout its first reader, and reviewing it surfaced the gap. That MR's description declares the deviation and names this amendment as owed; this is that amendment.

The passages

  1. #### Resolution error surface. The ResolveBudgetExpired entry in the verdict taxonomy read every expiry as that error, and closed on the same false premise the Error Cases fix drops, in its own words: "Nothing about the upstreams was established, so it is never a not-found." The taxonomy is what an implementer reads before the acceptance criteria, so left alone it is the passage that gets implemented. It now names the condition the verdict holds under, says an expiry holding a winner or a cached copy serves it and reaches no error, points at the two Error Cases rows, and carries the generalized not-found reason.

  2. ## Configuration. "Applied as a context deadline over the decision and released at winner selection" names a release call that does not exist. The budget is a scope over the decision's stages, so no terminal path runs on it and every serve arm takes the caller's context. Same ruling as docs/plans/2026-08-27-s32-container-virtual.md under ### The resolution deadline belongs in the resolver, not in either slice.

    The same paragraph now says what the budget cannot cover. The upstream list reaches the resolver as an argument, so the reads that load it are outside the bound and no resolver change can bring them inside. A reader sizing the knob against those reads was sizing it against work it never sees.

    Further down the same section, the first of the two consumer rules carried the wording again: "The bound is the resolver's, and it ends at winner selection", closing on "release it when the winner is selected". Left alone, the section asserted both models in one breath. The bullet's argument is untouched, because it still operates: a budget the caller builds around the resolve call cannot express this bound, and the single-flight follower's wait is what settles it. Only the two claims naming a release are replaced.

    The second consumer rule stated the verdict unconditionally: "Expiry is ResolveBudgetExpired". Under the new criterion that is the answer only where nothing settled is left, so the rule now names that condition and gives the other arm its own line. Its heading, "Expiry answers, and it is not ResolverCancelled", is untouched, because an expiry answers either way.

  3. ## Acceptance Criteria. The existing criterion is narrowed to the case it actually describes (nothing else to answer from), and a new criterion states what an expiry answers from: a winner the phases determined, else a cached copy on a consulted position, and the verdict only when neither exists. It names both implementations that fail it, so the criterion is falsifiable.

  4. ## Error Cases. The expiry row rested on a false premise: "Never 404, because no position established an absence." A position whose probe answered 404 has established one for itself, and the expiry arm is reachable in that state. The verdict was right and the reason was not, so the reason is now the one that operates: the expiry leaves positions a complete sweep would have decided, so one position's absence does not generalize to the list. A second row covers the expiry that answers from a winner or a cached copy.

  5. ## Resolutions. The same "released at winner selection" wording, in the recorded resolution. Within S13 the phrase now survives only in that negation, so a reader who greps it lands on the correction rather than on nothing.

One site outside S13

That site had already written the claim down wrong, so this is the same ruling applied where it landed rather than new ground.

  1. docs/specs/S31-npm-virtual.md. The Budget expiry row carried the same false premise the Error Cases fix replaces in S13, "Never 404, because no position established an absence", word for word. S13's replacement reason is ported verbatim, so the two specs state one rule. Only the reason changes: the row keeps its 503, its upstream_unavailable code, and its Retry-After: 1.

    Those were the only two sites carrying it as the expiry's reason. Counted at 8228ffa15, the phrase survives on six other lines, each describing a genuine no-absence state: docs/specs/S30-maven-virtual.md:525 and :526, docs/specs/S31-npm-virtual.md:487, :1304 and :1308, and one assertion message in internal/virtual/collect_verdict_test.go:302. S30 states no budget-expiry row and never mentions resolve_timeout.

The ResolveTimeout comment goes to !2622 (merged)

The ResolveTimeout doc comment in internal/config/virtualrepositories.go carried the same upstream-list-reads claim, and this branch corrected it in 5f163ac5. !2622 (merged) corrects it as well, on its own branch and in its own words. At 8228ffa15 here and 992263a05 there, git merge-tree reported a conflict on that file, while each branch merged into main cleanly.

ab8347762 drops the correction from this MR, so !2622 (merged) owns it alone. What goes with it is this branch's mechanism clause, "the list reaches the resolver as an argument". !2622 (merged)'s wording states the same ruling (the reads that load the list run before the bound is armed and are not covered) and keeps the "ProbeTimeout is the only field above it inside that bound" sentence, which the two-line struct-field cap in scripts/ci/check-comment-caps.sh left no room for here.

This is the rule this MR already applied to that comment's configuration-reference counterpart, the resolve_timeout row at docs/dev/configuration-reference.md:634: !2622 (merged) edits that row too, so correcting it here would conflict for no gain. Both sites now sit on one branch.

While !2622 (merged) is open, main keeps the old comment and the old row. They are corrected when !2622 (merged) merges. If it closes unmerged, both need a fresh amendment, and this MR no longer supplies one.

Why the cache fall-through is not a new ruling

S13 already rules the analogous shape. The data-error row says that when "some position established an absence, and another position was excluded for a data error", the answer is "serve the cached copy if one exists, otherwise UpstreamUnavailable. Never FileNotFoundOnUpstreams." A budget expiry is the same incomplete list reached by a different route, and the S32 plan rules it in under the section above ("A blown budget still tries the cache before it answers 503"). This MR writes that into S13 rather than establishing it.

What this does not reach

The same wording is still live outside S13, in docs/specs/S31-npm-virtual.md (the ceiling paragraph and the resolve_timeout resolution entry) and in internal/format/npm/virtual_operations.md. Neither is corrected here. S31 states a second release point for the metadata path, "at the last contributor's arrival", which comes from collection mode rather than from this ruling, so it needs its own amendment rather than a wording sweep from this MR. The npm facade doc restates S13's ruling and follows whatever S13 and S31 settle.

The ResolveTimeout doc comment and the resolve_timeout row in docs/dev/configuration-reference.md are not corrected here either. !2622 (merged) owns both, per "The ResolveTimeout comment goes to !2622 (merged)".

S31's Budget expiry row is corrected here, per "One site outside S13", but only its reason. The row keeps its condition, "ran out before resolution reached a verdict", and gains no counterpart to S13's new row for an expiry that answers from a winner or a cached copy. A determined winner is already a verdict, so the only gap is the cached-copy case, and on the metadata routes that runs into collection-mode merge semantics S31 has to settle on its own terms. It travels with the release-point amendment above, not with this MR.

What does not change

The 90s default and its derivation, the three legs outside the budget (the winner's transfer, a follower's commit wait, a warm cache serve), the 503 with Retry-After: 1, and the ResolverCancelled-versus-ResolveBudgetExpired split are all untouched.

File overlap

Re-derived at ab8347762, against the 75 merge requests open at that point (this one included). Dropping the comment correction narrowed the file set back to two: docs/specs/S13-virtual-remote-foundation.md and docs/specs/S31-npm-virtual.md.

Two open MRs touch the S13 spec: !2542 (merged) (dmeshcharakou/s04a-spec-amendment-discard-backfill, head afe2ef236) and !2630 (merged) (dmeshcharakou/s04a-per-format-credential-columns-step-5, head d6955e3db). git merge-tree between ab8347762 and each of those two heads exits 0. No open MR other than this one touches docs/specs/S31-npm-virtual.md.

An earlier version of this section said no open MR touched internal/config/virtualrepositories.go. That was wrong: !2622 (merged) edits it, and while this branch edited it too the pair conflicted there. ab8347762 removes the file from this MR's set, so the claim no longer has to be made.

No merge order is owed against !2542 (merged) or !2630 (merged). The order against !2622 (merged) is below.

Merge order against !2622 (merged)

Either order works. At ab8347762 here and 992263a05 there, the two branches share no file and git merge-tree between the heads exits 0.

An earlier version of this section claimed that with no heads behind it, and at 8228ffa15 here against 992263a05 there it was false: both branches corrected the ResolveTimeout doc comment, in different words, and git merge-tree between those heads conflicted on internal/config/virtualrepositories.go, while each merged into main cleanly. Review caught it before either MR merged, and ab8347762 dropped this MR's copy.

Landing this one first is still tidier, because it closes the deviation !2622 (merged)'s description declares. !2622 (merged)'s code already matches this text.

Related to #1139 (closed)

Edited by Dzmitry (Dima) Meshcharakou

Merge request reports

Loading
Loading