docs(plans): record the S15 rows 10 to 14 and correct the stale plan claims
🎯 Summary
Three docs(plans) changes to the S15 npm remote plan, all driven by the steps currently in flight.
- Records the Status rows: row 10 → !1685 (merged), row 11 → !1683 (merged), row 12 → !1682 (merged), row 13 → !1674 (merged), row 14 → !1686 (merged).
- Corrects two Step 10 claims the merged code contradicts.
- Records a Step 12 claim the code contradicts, and the spec gap behind it, carried over from !1682 (merged).
This branch is the single writer for that table while those steps are open, per CLAUDE.md's step-MR rule.
Rows 13 and 14 were added in review. !1674 (merged) and !1686 (merged) are open, are not drafts, and change no file under docs/plans/, so neither can record its own row and the fill cannot conflict with either branch. Every step currently in flight now has its cell.
🧩 Why the rows are here and not in the step MRs
Rows 10, 11 and 12 are adjacent lines. Each step MR had filled its own row in its own change set, which is what the rule now forbids: git merge-file over origin/main's copy plus two branches' copies exits 1 with conflict markers on those lines, and neither pipeline reports it, so whichever merged second paid a manual resolution.
All three step branches drop their own hunk in the same sitting:
- !1685 (merged):
fc0b00ac - !1683 (merged):
ceb00501 - !1682 (merged):
9b2eb2a9(its plan hunk lived in its stale copy of the step 10 commit; that commit is rewritten without the plan file, so nothing else about the branch changes)
🔎 The corrections
npm.RemoteFetcher does not exist. Step 10's deliverable list named it as one of the three seams a read resolves through. !1685 (merged)'s review dropped the interface: internal/remote/standalone.go records the same decision for its own flight field, since the one-instance-per-repository contract is remote.SingleFlight's and an interface cannot carry it, and the packument route composes remote.Standalone, whose constructor takes the concrete type. The bullet now names the two seams that shipped and says what happened to the third.
The retention bullet named a blocker that is not one. It read that a bound needs an eviction which cannot drop a live entry, because a naive one hands the next caller a second instance for one repository, "the exact condition remote.SingleFlight's contract forbids". What retires that blocker is the eviction case itself, which is both priced and bounded: dropping a map entry leaves an in-flight caller holding its own reference, so a sweep never invalidates an active flight, and the two live flights cost one uncoalesced fetch per in-flight path and a doubled fill cap only until the old one settles. mavenRemoteFlights.sweepLocked bounds the identical structure on that predicate and #672's Notes price a revived entry the same way.
One instance per remote repository stays a hard usage contract. internal/remote/singleflight.go calls it that and names two instances a regression; the doubled fill cap it quotes is what breaking the contract costs, not a license to break it. The first draft of this correction read that sentence as a general license and this MR's review caught it.
The bullet also over-claimed maven as "the shape it takes". The eviction predicate is all that transfers: #672 also asks for a drop count, a TTL configuration knob with config.example.yaml and the configuration reference updated, and a caller that is an in-process ticker tied to the server lifecycle, none of which maven's in-flightFor sweep has.
Step 12 says the legacy tag route is proxied "if reachable". It is not reachable, and no step adds it. S15 Tag installs makes two claims: tag installs resolve client-side, which AC 10 restates and Step 16 owns, and the legacy server-side GET /{package}/{tag} form is proxied upstream and its single-version response run through the packument rewrite, so it is "never relayed raw". Nothing serves the second. internal/format/npm/handler.go registers every npm pattern explicitly and the npm mux carries no catch-all, so no pattern matches a two-segment {package_name}/{tag} and such a request is answered 404 by the mux. Nothing is relayed raw and nothing is proxied either, which makes the absent route read as conformant.
!1682 (merged)'s own copy of this finding blamed the dist-tags document a later step introduces. That reads the spec backwards: the upstream resolves the tag, not the registry, so the route needs the packument transform and the flight and nothing a later step adds. Whether to serve it at all is the spec author's call (guardrail 1), and Open Questions now carries it.
🔁 Folded in from review
cc962aaa closes the four notes from the second review pass, none of which gated the merge.
- Rows 13 and 14, above.
- The
S15 Tag installslink had no fragment, so it landed the reader at the top of a 935-line spec. Tag installs is a bolded run-in paragraph, not a heading, so it has no anchor of its own; the link now carries#api-contracts, the section it sits in, which the plan already links elsewhere for the same section. It was the only one of the plan's in-body links to this spec without a fragment, andlychee --include-fragmentshas nothing to check on an unanchored link. - The route enumeration missed the patterns that pin the literal
-in the first segment, not the second:GET {prefix}/-/all,GET {prefix}/-/v1/searchand the twoPOST {prefix}/-/npm/v1/security/…audit routes.-/allis itself a two-segmentGET, so the old sentence's own counterexample was a registered route. The section's conclusion is unchanged, because-/allserves the audit and search404stub, but that sentence is the evidence the section rests on. It now names the two single-segment patterns, the ones that open with{package_name}and run to three segments or more, and the--first group with-/allcalled out. - The Open Question named neither a gate nor an owner. It now says Step 12 is not gated on the answer, since the route is additive to what Step 12 mounts. And under the drop-the-sentence answer, Step 14 carries the spec edit if the answer arrives before !1686 (merged) merges, the answering follow-up otherwise: Step 11 and Step 14 are the only steps whose Files list
docs/specs/S15-npm-remote.md, and neither amendment touches the API Contracts paragraph the sentence lives in, so the edit had no owner.
✅ Testing
Documentation only. No code, no schema, no route.