docs(specs): reassign the hosted downloads_count writers off S18 (repository-column-writers plan: 1/5)

What this does

The specs assign repositories.downloads_count to S18, which has no spec. This amends them so the hosted writers belong to the repository-column-writers plan, and defines the npm hosted download event that no npm spec defined.

Documentation only: 9 files, +214 / -69. No code changes, no behaviour changes.

Plan steps

Step 1 of 5 of the repository-column-writers plan. Step 1 gates only the downloads_count half, because that half is the one the specs assign elsewhere. Step 3's MR is open as well (!2362 (merged)). Both target main directly and they share no file, so neither order is forced. !2363 (merged) is the plan's batch-fill MR and carries the step 1 and step 3 rows.

# Step This MR
1 Reassign the hosted downloads_count writers from S18 to this work this MR
2 Execute the Maven last_updated_at write after the upload commits not open
3 Move container last_updated_at on all seven S12 events !2362 (merged)
4 Add the third counter family, its drain, and npm's first increment not open, blocked on the plan's Open Q1
5 Move container downloads_count on the two S12 read events not open, blocked via Step 4

What moves, per file

  • S10-maven-hosted.md: the S18 and S22 dependency rows, the owners sentence under "Interim implementation of buffered columns", the delivery-gate note, the S18 and S22 async-pipeline bullets, and the download-counter-propagation exclusion. Maven's hosted writer keeps its interim direct UPDATE; only the assignment moves.

  • S12-container-oci-hosted.md: the ownership sentence, the event-emission sentence under "Counter and Timestamp Updates", and the OQ-16 resolution. The split those sentences describe is four-way, not three-way.

  • S11-npm-hosted.md: defines repositories.downloads_count += 1 on a delivered tarball GET. A GET delivers on a full 200 body or a written redirect, per ADR-005's two delivery modes and ADR-012's rule that each mode reports the strongest signal it can observe. A HEAD, a 304, a 404, and a 200 ending short of the declared length book none. The event is defined, not live, and the item says so: nothing writes the column on an npm repository today (internal/format/npm holds no writer, and MavenRepositoryStore.BumpRepoDownloadCount at internal/datastore/maven_repositories.go:339 is the column's only production writer), and the plan step that adds npm's increment has no MR. The item names the condition and the event that lifts it, the shape S12 already uses for its two container read rows. The file had no occurrence of downloads_count before this.

  • S15-npm-remote.md: states that the npm remote read path moves no counter and that the mechanism decision stays with #292, rather than defining an event nothing performs.

  • S16-container-remote.md: re-points every passage assigning the counter to S18. Neither acceptance gate is removed: no step of this plan writes the container remote read path, so removing them would leave the criteria asserting downloads nothing books.

  • S17-rest-management-api.md: the S18 dependency row and the three passages that restate the assignment.

  • S14-maven-remote.md and monolith/S05-repository-detail.md: two more files carrying the same assignment. See Scope below.

  • AGENTS.md: adds Two Adjacent Gates Are Read Against Each Other, the convention behind the S11 delivery-gate fix, requested by a remember: review note and shipped as its paired commit. Placed beside A Documented Distinction Owes a Test on the Case It Rests On and cross-referenced to it, since the two are the same hazard from opposite sides.

The owners sentences in S10 and S11 each go from four owners to five.

Corrections this makes beyond the reassignment

Each of these was false before this MR, and each is fixed here rather than left for a reader to rediscover.

  1. The ADR-023 justification was an inverted borrow. S16 said no shared buffered-write client exists, npm's being format-private and out of reach per ADR-023. internal/accounting.Emitter over internal/accounting/counterbuf is shared, and every hosted format and npm remote is wired to it (cmd/artifact-registry/wire_maven.go:122, wire_npm.go:160, wire_oci.go:850, wire_npm.go:707). What it carries no field for is downloads_count, which the plan adds. ADR-023 bars format-to-format imports and prescribes exactly the shared internal/ package, so it never put a client out of reach. S16 carried this at two sites; both are corrected.
  2. S12's write endpoints already emit the S22 counters, through internal/format/oci.CounterSink over internal/accounting.Emitter, at five dispatch sites (store.go:288, store.go:496, blob.go:513, manifest_push.go:658, manifest_delete.go:395). Two sentences said the handlers emit nothing.
  3. "The two remote read paths" described the set wrongly. Three remote read paths exist, and Maven remote already increments the column on a delivered archive-class GET. npm and container remote are the two left without a writer.
  4. S17's S18 row carried two claims already false at the plan's basis: that MavenRepositoryStore.BumpRepoDownloadCount is a no-op stub, and that downloads_count is a column nothing maintains. It executes its UPDATE at internal/datastore/maven_repositories.go:318. #975 tracks the first, and this closes it. #975's second item is a different fact and is not fixed here: the Phase 8 Format-scope bullet sums three figures where the Format statistics acceptance criterion and the FormatStatistics schema in api/openapi/v1.yaml require four, repositories_count being the one it omits. Fixing that is not a downloads_count reassignment, so #975 stays open on it.
  5. "A third repository-keyed counter family" over-counted. internal/accounting/counterbuf/keys.go:12-26 holds two families and only RepoScoped is repository-keyed, so the new one is the third family overall and the second keyed on a repository. Reworded in S10, S11, S12 and S16.
  6. "Maven's equivalent store methods are still no-op stubs" was false, at six sites in docs/specs/, five of them in S16. MavenPackageStore.BumpAccessTimestamps executes two UPDATEs on maven_packages.last_downloaded_at and maven_versions.last_downloaded_at, throttled to one write per row per hour (internal/datastore/maven_packages.go:249), reached from internal/format/maven/download.go:548 and wired at cmd/artifact-registry/wire_maven.go:413; the remote twin MavenRemoteCacheStore.BumpLastDownloadedAt executes its own under the same TODO(s18-buffered-counters) marker (internal/datastore/maven_remote_cache.go:648). S16's Download signals said the opposite, in the same subsection as a sentence saying Maven's handler "already calls its bump methods on every served read", and S17's S18 row said it in the row correction 4 rewrites. Correcting S16 also retires the reason it gave for following npm's bumper shape, which was that Maven's was inert, so the three S16 passages naming that shape now say both siblings' shapes are available. The same fact lived at four more S16 lines, which the first pass missed and which now read correctly, across three passages: the write-amplification paragraph of Download signals itself, its Follow-ups bullet, and the Resolutions bullet behind them, whose headline and Why: line each carried it. All three called the method inert and drew a conclusion from it, that the index cost on maven_versions and maven_packages is "latent rather than paid" and that their throttle "belongs in whatever lands that writer". The method executes and skips a row bumped within the hour, so those two tables carry an hour-bounded version of the same write amplification rather than none of it, and the deferral had nothing left to defer. The word the old Resolutions bullet got wrong was "unconditional", not the existence of the writer. MarkRepoLastUpdated really is still a discard stub (internal/datastore/maven_repositories.go:362), so S17:1645's "no-op stubs" sentence about it and BumpRepoCounters is correct and is deliberately left alone. The stale claim was specific to BumpAccessTimestamps and BumpRepoDownloadCount.
  7. S11's blanket interim rule contradicted the event this MR adds. The rule under Interim implementation of buffered columns mapped every reference below it to the interim goroutine plus direct UPDATE, while the new tarball event books into S22's pipeline. The rule now carries the carve-out, and names all three columns that take one rather than only the new event's. repositories.artifacts_count and repositories.size_bytes already skip the interim path: every npm site that moves them books through CounterEmitter, whose production implementation is internal/accounting.Emitter, and the interim bufferedUpdate helper's five labels include neither. Both sit in the same paragraph's own column list, so a one-item exception list mapped them to the interim path by omission. The pre-existing passages that state the interim mapping for those emissions, in this file and in S10, are left alone: they are S22's fact to widen, and what earns the edit here is that this MR rewrote this sentence.
  8. S12's replacement sentence over-claimed its own table. It read that the table "enumerates the events S12 endpoints source", while the table's Manifest PUT and Manifest DELETE rows both read None and both endpoints emit repository deltas: emitPushDeltas books an artifact delta for a manifest new to the image and a byte delta for the payload blob's first attach (internal/format/oci/manifest_push.go:658), and emitDeleteDeltas books the matching decrements (internal/format/oci/manifest_delete.go:398). The sentence now scopes the live claim to the four emitting blob rows and records the manifest divergence. See What this MR does not reach. The replacement then over-claimed in the other direction, saying those four blob rows emit "repositories.size_bytes and repositories.artifacts_count" deltas. Only the byte half ships, and it ships gated rather than unconditional: each delta is emitted on the repository's first attach of the digest and on its last detach (attachDelta and detachDelta at internal/format/oci/emit.go:264 and :279, over the (namespace_id, container_repository_id, sha256) membership readBlobFootprint reads at :172), while the four rows state the delta unconditionally and the table's only carve-out is scoped to the cross-repository case. That divergence is recorded in the section too, above the artifacts_count paragraph so the two claims scoped to artifacts_count stay true. Every blob dispatch site passes a zero artifact delta, because a blob is neither an artifact nor a component (internal/format/oci/store.go:289 and :497, internal/format/oci/blob.go:516), and the blob DELETE row specifies no artifact delta to begin with. So the table and the code disagree on artifacts_count in both directions: the repositories.artifacts_count += 1 the two blob-upload rows and the 201 mount row specify on an auto-created image is never emitted, and the artifact delta ships on the two manifest rows the table marks None. Both divergences are now recorded in the section, and settling them is S12's.

Scope: wider than the plan's Files entry

The plan's Step 1 Files entry names six spec files and, inside S16 and S10, a smaller set of sentences than the fact occupies. Re-pointing only the named sentences left each file contradicting itself, so this MR fixes every site instead.

S14-maven-remote.md and monolith/S05-repository-detail.md join for the same reason: the step's own acceptance criterion runs grep -rn 'downloads_count' docs/specs/, which is recursive and reaches both.

The plan's Files entry needs widening to match. That is a docs(plans) change, not this MR's to make, since a step MR does not edit the plan.

Two things in the plan text also need that same docs(plans) pass, neither actionable here:

  • Step 1's Acceptance says "no sentence claims a remote read path books one", while its own Files entry orders S16's gates kept. Four S16 passages still claim the increment, by that instruction: the two downloads_count rows of the Download signals mapping and the two acceptance criteria under the second ### Download signals (S16:670-671 and :980, :990, measured at 0f1ba8e40).
  • The Files entry's site counts for S16 and S10.
  • Step 2's Files entry orders the :93 clause calling the S10 interim bump a no-op stub rewritten there. Correction 6 fixes it here instead, because the row was already being rewritten whole and the clause is false on main today, so that half of Step 2's entry has nothing left to do.

What this MR does not reach

  • repositories.last_updated_at's ownership. S10:153 still reads "has no owner yet (#442)", which is true today and which Steps 2 and 3 falsify. Amending it belongs to those steps, not to this one's downloads_count scope, and S12's two claims of the same shape are left for the same reason. What this MR does reach is narrower and is its own new text: the two S12 sentences that stated as present-tense fact that nothing writes the column now name the condition and the event, so they read correctly on both sides of Step 3's merge. "Nothing writes it" and "no spec owns it" are different claims, and only the first is falsified by a writer landing. No order is forced by this: !2362 (merged) shares no file with this MR, and the condition-and-event wording is what makes the sentences survive either sequence.
  • The container remote and npm remote read paths. Neither gets a writer here or in any step of this plan; #292 owns the mechanism decision for both, and S15 and S16 now say so.
  • docs/dev/storage-accounting.md:1441-1442, which names no owner and so needs no edit. Steps 4 and 5 falsify it and own the fix.
  • Whether a container manifest payload counts toward repositories.size_bytes, and which row should carry the repositories.artifacts_count delta. Correction 8 records both divergences between S12's write rows and what the handlers emit, and leaves the accounting questions themselves to S12: deciding either changes what the counter means on a container repository, which is not this amendment's ownership move. The rows are left as they stand with the divergences named beside them. The unemitted auto-create artifacts_count increment is a spec-versus-code gap rather than a downloads_count assignment, so no step of this plan closes it.
  • #975's second item, per correction 4. It is a repositories_count omission in a statistics bullet, not a downloads_count assignment, so it does not travel with this fact.
  • The same "no-op stub" claim about BumpAccessTimestamps and BumpRepoDownloadCount in four merged plan files (docs/plans/2026-05-12-maven-hosted.md:251, 2026-07-27-maven-remote.md:981, 2026-07-30-container-remote.md:203, and 2026-08-10-maven-version-size-accounting.md:151, measured at 0f1ba8e40). Correction 6 sweeps docs/specs/ only. A merged plan records what was true when it was written and a step MR does not edit plan files, so these are left as they stand rather than corrected here.

Merge order

Eleven open MRs edit at least one file this MR edits. Seven overlap on the spec files and are the table below; the other four arrived with AGENTS.md, which joined this MR at its last commit, and are under AGENTS.md overlaps. Of those seven, checked at this MR's head commit, none changes a line mentioning downloads_count or S18, so the overlap is file-level rather than a contested fact and no correctness order is forced. Whichever lands second rebases.

MR Overlapping file(s) Its changed lines mentioning downloads_count or S18
!2346 (merged) S16-container-remote.md 0
!2332 S11-npm-hosted.md, S12-container-oci-hosted.md, S17-rest-management-api.md 0
!2331 (merged) S10-maven-hosted.md 0
!2328 (merged) S10-maven-hosted.md, S17-rest-management-api.md 0
!2307 (merged) S11-npm-hosted.md, S15-npm-remote.md 0
!2077 (merged) S16-container-remote.md 0
!2055 (merged) monolith/S05-repository-detail.md 0

!2077 (merged) (rbatnag/s16-step-17-2-amendments, Draft) is the one to watch. It changes 46 lines of S16-container-remote.md (35 insertions, 11 deletions, measured at its head beec45adb), the largest of these overlaps, and of the two MRs touching that file it is the one amending it as documentation rather than as a doc note riding a code fix. This MR owns only the counter-ownership sentences there.

AGENTS.md overlaps

AGENTS.md arrived with this MR's last commit, so these were checked at ce12234c0 rather than at the head the table above was measured at.

MR What it does to AGENTS.md Interaction
!2357 (merged) Adds two paragraphs in the same gap this MR's new section goes in, between A Documented Distinction Owes a Test on the Case It Rests On and A Catch-All Arm Bounds Every Exclusion Claimed Above It Direct textual collision. Both are additive and both blocks are kept, so the resolution is mechanical, but whichever lands second rebases.
!2020 (merged) Routes AGENTS.md detail into lazily-loaded modules; its head carries 225 lines against this branch's 646, and none of the prose-convention sections If it lands first, this section moves into whichever module inherits the conventions. Its base predates every section in that region, so the rebase it already owes has to place them all; this one is not a special case. Not imminent: last updated 2026-08-28, requested_changes.
!2346 (merged) Three lines around the "Naming one file as the home of a list of values" paragraph No conflict; different region. Already listed above for S16-container-remote.md.
!1859 (merged) Four lines in the Guardrails list No conflict; different region.

No correctness order is forced by any of them. This MR's section states a documentation convention and nothing depends on it landing first or last.

Testing

Documentation only, so no tests are added. Run at the head commit against the pinned hook versions:

  • markdownlint-cli2 0.23.2: 0 issues across the 9 files.
  • vale 3.18.0 at --minAlertLevel=error: 0 errors.
  • lychee --offline --include-fragments over the same 9 files: 610 links OK, 0 errors, 157 excluded.

No e2e scenario is added or affected: this MR changes specification text only and no request path, so nothing in docs/testing/ changes.

Related to #1046

Edited by Dzmitry (Dima) Meshcharakou

Merge request reports

Loading
Loading