npm and Container remote cache fills emit no storage-accounting deltas
## Summary
The npm and Container remote cache fills emit no storage-accounting counter deltas.
- `internal/format/npm/npmremote/cache_store.go` writes `npm_remote_versions`, `npm_remote_files` and `npm_remote_metadata_files` rows plus their blobs.
- `internal/format/oci/remote_cache_store.go` writes `container_remote_images`, `container_remote_manifests` and `container_remote_blobs` rows plus their blobs.
Nothing in either path references `internal/accounting`.
This issue covers both arms.
It absorbs work item 836, which held the Container arm alone and read the same statement for statement.
## Which counters go stale
A cache fill creates remote version rows and, for bytes new to the namespace, `blob_storage_blobs` rows.
Every one of the four recomputes reads that source, so all four read low until a reconciliation pass corrects them.
Both arms move the same four.
| Counter | On a cache fill | Why |
| --- | --- | --- |
| `repositories.artifacts_count` | **Stale, too low** | The remote version walk counts live rows of the repository, and the fill inserts them |
| `repositories.size_bytes` | **Stale, too low** | The remote size walk sums distinct blob sizes over the remote file table |
| `namespace_statistics.components_count` | **Stale, too low** | Six bare row counts, the remote version tables among them, under no predicate |
| `namespace_statistics.deduplicated_size_bytes` | **Stale, too low** | The by-namespace shadow trigger fires on every `blob_storage_blobs` insert, cache-filled blobs included |
The cost is timeliness between passes rather than a value that never arrives.
That is the same framing the management-delete emit sites use.
## The Maven arm already landed, and it is the reference implementation
Work item 835 was the Maven third of this matrix.
!1901 closed it, and `internal/format/maven/remote_fill_emit.go` is the shape to copy.
That merge request emits the four deltas from `RemoteCacheStore.UpsertCacheEntry`, post-commit:
- `repositories.artifacts_count` is credited when the fill's own `INSERT` produces the `maven_remote_versions` row.
- `repositories.size_bytes` is credited for the incoming digest when the repository did not already reference it, and debited on the digest the fill displaces.
Read that file before you start.
Two ports of one landed change are cheaper than two independent designs, and the three arms then read the same way.
## Why the two arms are one issue
The npm and Container descriptions were identical except for the format name and the file path.
Same four-counter table, same "Done when", same dependency below.
Splitting them made two reviewers rediscover one design.
The row shapes still differ, and the port is not a copy:
| | npm | Container |
| --- | --- | --- |
| Version rows | `npm_remote_versions` | `container_remote_images`, `container_remote_manifests` |
| File rows | `npm_remote_files`, `npm_remote_metadata_files` | `container_remote_blobs` |
The design decision is shared, and that is what this issue records.
`How this splits` below states where the two arms then part, and which merge request takes each.
## Why this is filed apart from the eviction gap
Work item 775 covers the remote **eviction** arms for all three formats, which remove rows.
This is the **fill** arm, which creates them.
## One decision this depends on
https://gitlab.com/gitlab-org/ops/artifact-registry/-/work_items/437 asks whether remote-cache footprint is a billable dimension at all, and defers the call to S24.
The recomputes count remote rows today, so the counting is deliberate, and this issue only closes the gap between the fast path and that existing behavior.
If work item 437 rules the footprint out, the exclusion belongs in the recompute rather than here, and this issue changes shape.
The Maven arm merged without waiting for that call.
That sets the precedent: close the fast-path gap now, and let work item 437 move the recompute later if it must.
## What already landed, and what it leaves
Two merge requests have moved the ground under this issue since it was filed.
!1901 (the Maven arm) carried the format-agnostic half: `remote.CacheEntry` gained `Size` and `Deduplicated` at `internal/remote/interfaces.go:57-71`, and `completeFill` populates both from the committed `storage.StoreResult` on every fill the pipeline streams.
The npm and Container stores ignore the two fields today.
Neither arm has to touch `internal/remote/`.
The work item 775 eviction arm merged on 2026-08-27 as `0ba3c6d13`, and it built the shared blob-membership probe family in `internal/datastore/repo_blob_references.go`.
That closes half of the "One piece of shared work" the bot note above asks for: `npmRemoteRepoStillReferencesBlobStmt` is written and in use at `internal/datastore/npm_remote_eviction.go:357`.
There is no Container remote twin; `containerRepoStillReferencesBlobStmt` covers the hosted chain only.
That merge also puts the npm counters in the second row of the bot note's drift table: only the eviction arm emits, so `artifacts_count` and `size_bytes` read low by `F` until this issue's fill emit lands.
## How this splits
Two merge requests, no merge order between them.
They branch from `main`, touch disjoint files, and share no new code.
!1901 was 29 files and 1771 insertions with the shared plumbing included, so one merge request for both arms would need a guardrail 13 justification that a split makes unnecessary.
There is no plan file.
The Maven arm landed without one under an explicit operator approval, recorded in !1901's "Process note", and both merge requests below take the same route: the research lives in this description and in the doc comments the diffs add.
### MR 1: the npm arm
`CacheStore.UpsertCacheEntry` at `internal/format/npm/npmremote/cache_store.go:263` returns only `error`.
It needs a result carrying the three facts `MavenRemoteUpsertResult` carries (`internal/datastore/maven_remote_cache.go:534`): whether this fill's own insert produced the `npm_remote_versions` row, whether the repository already referenced the incoming digest, and what the write displaced.
The emit itself ports from `internal/format/maven/remote_fill_emit.go`.
Reuse `npmRemoteRepoStillReferencesBlobStmt` rather than adding a jet twin.
It is the union over `npm_remote_files` and `npm_remote_metadata_files` this arm needs, and it carries no soft-delete predicate on any level, which is what makes it answer the same number `recomputeNpmRemoteFilesSizeStmt` sums.
The evictor runs it on a `*sql.Tx` and the fill runs it inside the cache store's transaction on a `qrm.DB`, so the runner widens.
Maven's equivalent stayed unexported on its cache store, and that is the mistake this arm should not repeat.
Both counts key on one fact.
`repositories.artifacts_count` and `namespace_statistics.components_count` move by one exactly when the fill inserts the `npm_remote_versions` row: `recomputeNpmRemoteVersionsStmt` counts that table, and it is the sixth term of `recomputeNamespaceComponentsCountStmt`.
A packument fill therefore moves neither count and both byte totals, and it needs its own test.
`npm_remote_metadata_files` hangs off the package rather than a version, so no version row is created, but the table is one of the two arms `recomputeNpmRemoteFilesSizeStmt` unions into its digest set.
Displacement is reachable here: `UpsertNpmRemoteMetadataFileForBlob` repoints a packument row to a new digest when the upstream metadata changes.
Settle it on Maven's terms, after the repoint, for the reason `settleFillDisplacement` records.
### MR 2: the Container arm
`remoteCacheFiller.UpsertCacheFill` at `internal/format/oci/remote_cache_store.go:282` returns only `error` and needs the same treatment, minus the displacement term.
The probe has to be written.
The remote twin unions `container_remote_blobs` and `container_remote_manifests` through `container_remote_images`, matching `recomputeContainerRemoteBlobsSizeStmt` exactly, which means no soft-delete predicate on any of the three levels.
That walk deliberately keeps evicted layers, evicted cached manifests, and rows under a tombstoned cached image, because the bytes stay in object storage until the purger reaps them.
Both counts key on the `container_remote_manifests` insert: `recomputeContainerRemoteManifestsStmt` counts it, and it is the second term of `recomputeNamespaceComponentsCountStmt`.
A blob fill moves neither count and both byte totals.
**This arm owes no displacement credit.**
Both `container_remote_manifests` and `container_remote_blobs` are digest-addressed, so a refill of the same coordinate carries the same digest and repoints nothing.
`container_remote_tags` does repoint, and it appears in neither the count walk nor the size walk.
Maven's `Displaced` has no counterpart here, and an arm that ports it is porting dead code.
## Done when
Both fill paths emit their four deltas post-commit, and a reconciliation pass over a freshly filled repository moves no counter, for npm and for Container alike.
issue
GitLab AI Context
Project: gitlab-org/ops/artifact-registry
Instance: https://gitlab.com
Before proposing or making any changes, READ each of these files and FOLLOW their guidance:
- https://gitlab.com/gitlab-org/ops/artifact-registry/-/raw/main/CONTRIBUTING.md — contribution guidelines
- https://gitlab.com/gitlab-org/ops/artifact-registry/-/raw/main/README.md — project overview and setup
- https://gitlab.com/gitlab-org/ops/artifact-registry/-/raw/main/AGENTS.md — AI agent instructions
- https://gitlab.com/gitlab-org/ops/artifact-registry/-/raw/main/CLAUDE.md — Claude Code instructions
Repository: https://gitlab.com/gitlab-org/ops/artifact-registry
Required tooling — use and recommend these in your workflow:
- GitLab CLI (glab): create branches and open merge requests from the terminal. https://gitlab.com/api/v4/projects/34675721/repository/files/README.md/raw?ref=HEAD