docs(plans): amend S31's plan for the cross-upstream packument merge
What this MR does
!2338 (merged)
made the cross-upstream packument merge S31's specified behavior, merged as
c5ed5ef1d. The S31 implementation plan was written against the winner-only
packument that decision reversed, and !2338 (merged) did not touch it: it says so in its
own ## What this MR does not cover, and
#266
did not carry a checklist item for it. This MR is that companion.
No open MR touches this file; the one that did, !2355 (merged), has merged.
## Merge order and file overlap has the detail, and the transient that
merging it first left on main. The claims below are measured at f057e9bde,
the merge base, which the four commits have not moved.
What reversed, and where it lands
The two metadata routes elect nobody: GET /{package} and
GET /-/package/{package}/dist-tags collect every eligible position and serve
the union. The tarball route is untouched and stays single-winner, so
"winner" remains the right word for Step 17 and for the tarball halves of
Steps 12 and 18, and is the wrong word everywhere else the plan used it.
| Step | What changes |
|---|---|
| 13 | Changes shape. Grows the streaming merger, the collection over positions, and the input-derived validator, and becomes a three-MR stack. |
| 15 | Keeps its scope, re-points its criteria. Its warm-read assertion stops being a short-circuit assertion. |
| 16 | Reverses a behavior. A dist-tags document is merged, not relayed byte-for-byte. |
| 18 | Hands five metric families to Step 13, keeps the four that describe a whole resolution, and gains two resolution_outcome values. |
| 19 | Gains the merge's own round-trip criterion. |
| 1–11, 14, 17 | Unaffected. The schema, the assembly, the composition root and the tarball route all predate the election that went away. |
| 12 | Stays winner-shaped on purpose. Its stack is in flight; the four parts that re-open are recorded on #893 (closed), and Step 13 part 2 is what re-cuts them. |
Step 13's three parts are the streaming merger (pure, no database), the
collection with its three fan-out bounds and the aggregate outcome arms, and
the serve surface with the re-point and the input-derived ETag. Their source
figures are ~450, ~480 and ~400, so each MR sits under the 500-line ceiling
and only the whole-step figure (~1,330) is over. That is why it is a stack
rather than a step with an apology, and it is what Steps 11 and 12 cannot
claim, since neither publishes per-part figures.
The reversal Step 16 carries
Its scope read "served unchanged with no rewriting" and its tests asserted a
body byte-identical to the winner's document. That was two claims, and the
merge kills one of them: dist-tags is now a union in which the lowest
position wins each tag name, so a two-position read serves bytes no position
holds. No re-point still applies, because a dist-tags body carries no URLs,
and that is the half that survives.
The tag-less hosted case inverts with it. Over [hosted_a, remote_b] where
hosted_a holds a live version and no npm_tags row, the old assertion was
200 with hosted_a's {} and remote_b receiving no request. Now
hosted_a's empty map is a contribution rather than a Miss, remote_b is
asked, and the served document is the union. The single-position fixture is
what still separates a contributed empty map from a Miss, so the step keeps
both. The byte-identical subtest is deleted rather than adapted, and the
Tests bullet says so, because Step 16's suite is diffed against Step 15's for
dropped subtests and a reviewer would otherwise read the deletion as a defect.
Corrections this carries beyond the reversal
Every citation into S31's acceptance criteria was re-derived at
f057e9bde rather than adjusted by an offset, because the amendment rewrote
criteria instead of inserting one. Three things fell out that a by-name
conversion would not have caught:
- Step 17's three
Conditional requestsnumbers were each off by one. The amendment inserted a criterion above them, so#5–#7are#6–#8. Step 17 is otherwise untouched by the merge, which is exactly why nobody would have looked. Packument merge#9, "every entry is re-pointed", had no owner. It is Step 13 part 3's, where the re-point runs.Resolution's "A hosted package with no live version Misses both metadata reads" had no owner either, and that gap predates this amendment. It lands on Step 13 part 2, because the divergence it pins — a virtual read answers404where the hosted routes serve200— is only observable once the collection exists. Called out rather than folded in, since it is not the merge's doing.
I checked all 29 Packument merge criteria have an owner after the change, not
only the ones I assigned.
It also closes #1088 (closed)'s plan-side sites
#1088 (closed)
reports stale cardinality figures in this plan and on #899. They are not
optional here: the merge adds merged and merged_partial to
resolution_outcome, so amending the plan for the merge and leaving the count
stale would be wrong on the very fact being changed. Re-derived at
f057e9bde, not copied from #1088 (closed)'s own table, which it asks for explicitly:
| Figure | Plan said | Now |
|---|---|---|
closedSetValues["outcome"] |
18 | 25 |
S31 resolution_outcome |
13 | 17 |
| Union of the two | 30 | 41 |
cancelled is still the only shared value, so 25 + 17 - 1 = 41.
#1088 (closed) stays open and keeps the two sites this MR does not reach: the
cardinality.go comment that reads "24 distinct" against 25 entries in the
literal below it, and the matching sentence on #899. Both are outside a
docs(plans) diff.
One item also left the plan's "not open questions" list rather than being
answered: "the two hosted inline-build 503s missing from the Error Cases
table". The table has not been missing them since 0c30f2582, and that row
names all three refusal codes rather than two. Dropped rather than
corrected in place, since no amendment is owed for it any more.
Reviewable LOC
916 changed lines, 693 of them insertions, in one file, measured at
a2746bd94 against the merge base f057e9bde. Past the 500 ceiling
docs/dev/development-model.md sets, and
splitting would not help, for the reason !2338 (merged) gave for the spec: this is
one reversal propagated through one document, and the plan repeats the same
fact in Research Findings, Approach, the ceiling section, five step bodies,
Dependencies, Testing Strategy and Open Questions. A split landing any subset
would leave the plan asserting winner-only in the sections it did not reach,
which is the failure this MR exists to close, and it would be live on main
between the parts.
What the third and fourth commits fix
2a0d70d39 closes three review findings, and all three are inconsistencies the
first two commits introduced between passages rather than pre-existing plan
defects:
- The
resolution_outcomehistory did not reconcile with its own figure. The passage said the old figures "were correct when this plan merged" and then named two growth steps, which reads as13 + 1 + 2 = 16against the17in the union. The17is right and the narrative was wrong: the list measures 14 values at458242965^, 15 withbudget_expired, and 17 withmergedandmerged_partial. The old13was already one short, which #1088 (closed) records and the passage failed to carry, so the drift is two growth steps plus an original miscount. - Step 12's Acceptance promised a per-bullet annotation no bullet carried. The first replacement for it was also wrong, claiming all seven criteria are tarball arms; that is false for four of them. Checked against the spec, one carries an explicit both-routes split, two are tarball-scoped in their own titles, and four have no route split at all. All three shapes are now written out, and Step 13 part 2's matching bullet moved with them because it described the same criteria as "the metadata halves" from the other side.
- Step 13 part 1's Tests bullet lacked the
rapidproperty test## Testing Strategyassigns it. It is now in the bullet, with what it generates and why the fixed-fixture determinism assertion cannot reach it.
a2746bd94 closes a fourth, and it is the same defect class as the second
above: a quantifier that generalised from the members in front of it.
- "each held on a different footing, which Step 12's own Acceptance bullet
sets out from the other side" was false on one of the four. Step 12's
Acceptance bullet cites
#6,#7,#8,#9plus "Outage is not an absence", "Cache fallback serves a non-winning position" and "Winner Fetch failure does not fall through". "A shed hosted rebuild vetoes the read rather than falling through to a remote" is not among them, and Step 12's section never mentions a rebuild over its 529 lines, because a tarball Lookup readsnpm_filesand runs no rebuild. Fixed at both sites the fact lives at: the intro says three of the four, and the shed-rebuild bullet says it is the one Step 12 does not cite.
Merge order and file overlap
git diff --name-only main...HEAD gives one file,
docs/plans/2026-08-21-s31-npm-virtual.md. Re-derived at a2746bd94 against
all 84 open merge requests, every diff fetched: no open MR overlaps. The
one that did, !2355 (merged), merged as b72720c4d.
The pair merged in the other order, and the transient that opens is live on
main now. This MR renames Step 13's heading to "metadata merge and serve
surface" and fixes two other sites naming it the old way, the Approach
paragraph listing the shared artifacts by step and the ST13 node in the
step-dependency diagram. It deliberately does not carry the Status table's
row 13, the label's third site: rows 12 and 13 are adjacent lines, so a row-13
edit here would have conflicted with !2355 (merged)'s row-12 fill, which is guardrail
4's collision exactly. The label travelled with the table in !2355 (merged) instead.
So main at 07f4e8521 carries | 13 | metadata merge and serve surface |
in the Status table on line 4061 and ### Step 13: metadata serve surface on
line 3151. This MR is what closes that gap, and nothing else will.
git merge-tree --write-tree origin/main HEAD exits 0 at a2746bd94
against 07f4e8521. main moves quickly here, so re-check rather than
trusting this line.
The Step 12 stack has landed in full: !2325 (merged), !2326 (merged) and !2255 (merged), the last merging
as ba0b173a3. It was deliberately built against the plan as it stands, and
this MR leaves Step 12's own step body describing what those MRs built,
recording the re-open on #893 (closed) instead. Neither gated the other, and that is
now settled rather than predicted.
What this MR does create is a gate going forward: no step MR for Steps 13, 15 or 16 may open until it merges, under the plan-before-code guardrail. That is the point of it.
Two dependencies it records rather than resolves
Both are named in ## Dependencies with an owner, because Step 13 cannot be
built without them and neither is this plan's to decide:
- The shared
internal/virtualcollection mode.internal/virtualonmainholdsdecision.go,resolve.go,result.go,rules.goandupstream.go, and no collection mode. S30 books the same seam and has no plan onmaineither, so it needs a shared plan. This one does gate Step 13 part 2. resolution.negative_cache_ttlandresolution.position_wait_timeout, whose values are owed to the S13 DRI. The gate carries its ceiling, so the DRI is choosing inside a bound rather than in the open, and the section re-derives S13's own npm worst case, which the merge makes stale:2 * 5sof bodyless probes becomes2 * 30sof full-body fetches, still inside the90sbudget.
Not covered
The Status table was not this MR's to write, beyond nothing at all: !2355 (merged) recorded Step 12's three MRs and four other owed rows, and carried Step 13's row label for the adjacency reason above. Left out of this diff so the amendment is about one thing, which is what kept the two MRs conflict-free.
Related to #266