Artifact-level accounting — Container/OCI: lock in manifest size and settle the per-image and remote gaps

Context

docs/adr/007_database_schema.md defines artifact-level storage accounting in full — sections #### Artifact-level storage accounting and #### Artifact-level storage accounting reconciliation. S22 excludes this layer and hands it to the format slices; #441 (closed) defers the per-format work and asks for one issue per format. This is the Container/OCI issue.

OCI is the format ADR-007 treats differently, and the only one whose existing column is already correct: container_manifests.size is pre-computed at push and immutable, is the source of truth, and needs no reconciliation. The work here is therefore smaller and mostly about the gaps around it.

What exists today

Column ADR-007 Schema Writer
container_manifests.size pre-computed at push, immutable, source of truth, no reconciliation exists (internal/datastore/migrations/sql/20260526120300_oci_container_manifests.sql:24, with a >= 0 CHECK) computeTreeSize (internal/format/oci/manifest.go:919) → internal/datastore/container_manifest.go:317, in-transaction, INSERT-only under ON CONFLICT DO NOTHING
container_remote_manifests.size mirrors the hosted column with progressive semantics as children are cached table does not exist yet
per-image size / manifests_count not declared none

container_manifests.size is the manifest tree size — this manifest's payload plus every blob and child manifest reachable from it. ADR-007 is explicit that it may double-count blobs shared across manifests, so it is not a substitute for storage usage at any higher scope.

Work

  1. Pin the immutability contract with a test. ON CONFLICT DO NOTHING means an idempotent re-push never rewrites size (persistExisting, internal/datastore/container_manifest_persister.go:329, only moves tags and inserts relationships). ADR-007 rests "no reconciliation is needed" on that property, and nothing currently asserts it.
  2. Settle container_remote_manifests.size semantics before the table lands. ADR-007 says the value "converges to the full tree footprint progressively as children are fetched"; S16 says the MVP leaves an index or manifest list at its own payload size and writes zero relationship rows, and has an acceptance criterion asserting exactly that. One of the two documents is wrong, and the answer decides whether the column needs a maintenance path at all.
  3. Take the per-image schema decision S17 flags: a per-image manifests_count buffered counter (the npm versions_count pattern, counting referrers too) and image-level size have no columns today. ADR-009 promises image-, tag-, and version-level statistics endpoints that need them.
  4. Guard the double-counting rule at the consumers: whatever statistics endpoint eventually reads container_manifests.size must not sum it as a repository or namespace footprint.

Out of scope

  • container_images.last_downloaded_at and container_manifests.last_downloaded_at — S12 assigns both to the counter subsystem and S22 hands them to S18. Neither has a writer today.
  • Repository- and namespace-scoped counters for OCI — S22's, wired at the OCI write and delete sites.

Decisions required

  • container_remote_manifests.size: progressive convergence (ADR-007) or payload-only for the MVP (S16). Whichever wins, the loser's text gets corrected.
  • Per-image aggregates: add manifests_count and an image-level size column, or serve those displays by aggregation at read time.

Done when

  • A test asserts a re-push does not rewrite container_manifests.size.
  • The remote-manifest size semantics are settled in one document and contradicted in none.
  • The per-image schema decision is recorded, with a follow-up issue if columns are added.

Refines #441 (closed).