feat(npm): hosted positions and read vetoes (S31 plan: 13/19)
Split into 4 stacked MRs to keep each within a reviewable size, split by file group (see below). Each part targets the previous (part 1 → main), reviewed/merged bottom-up.
Part 3 of 4 of docs/plans/2026-08-21-s31-npm-virtual.md — Step 13: metadata merge and serve surface (Part 2: collection over positions, and the outcome arms).
Adds hosted positions and their vetoes on top of part 2's remote-only collector: npmMergeContributor.fromHosted, closeHostedMergeBody, npmMergeDeadlineWriter, queuedHostedHitVeto, and Contribute's hosted dispatch arm — removing part 2's temporary hosted-rejection guard. Covers both the refused-rebuild veto and the queued-behind-budget-expiry veto (the package-substitution hazard named in the spec and resolve_budget.md).
Spec: S31 (npm-virtual) Plan: docs/plans/2026-08-21-s31-npm-virtual.md
Related to #894 (closed)
Diff size: ~1183 reviewable LOC across 6 files (AGENTS.md guardrail 15) — 920 in tests (virtual_collect_internal_test.go 910, virtual_read_errors_test.go 10), 178 in production Go, 85 in docs. Split by file group per guardrail 15 rather than shaved to a round number.
No docs/testing/ update: no production route builds an npmMergeCollector yet, and docs/testing/e2e/npm.md:33 already scopes virtual npm reads out (AGENTS.md guardrail 12). No docs/user/ update: this part changes no customer-visible surface (AGENTS.md guardrail 24).
Plan and spec amendments
Review on this MR widened S31-npm-virtual.md's hosted-Hit veto contract: it previously named the refused rebuild as the only veto arm over a hosted Hit (a DB error, a cache row-read fault, or a missing packument blob answered 200 merged_partial from the remotes). It now vetoes the whole read (500, or 503 for a budget expiry) on any hosted-Hit read failure whose own context has not already ended from outside. This is a widened contract, not a transcription fix — it flips the client answer for those failure shapes.
Stacked MRs (review/merge bottom-up)
- feat(npm): collection plumbing seams (S31 Step ... (!3018 - merged) • David Fernandez • 19.5
- feat(npm): collect over remote positions (S31 S... (!3021 - merged) • David Fernandez • 19.5
- feat(npm): hosted positions and read vetoes (S3... (!3022 - merged) • David Fernandez • 19.5
👈 - feat(npm): merge the collected documents into t... (!3023 - merged) • David Fernandez • 19.5