chore(api): drop statistics floors the counter pipeline can break

Why

Two specs govern these figures, and the contract agreed with neither.

S22 declines a CHECK (... >= 0) on the four counter columns deliberately: the constraint would abort the whole batched chunk UPDATE and wedge every unrelated scope in it, rather than accept a transient negative that reconciliation corrects from source. Reads expose the stored value, and S22 clamps nowhere. S17 then marks the stored fields as read-as-is, and gives the format scope the sum of the repositories counters.

api/openapi/v1.yaml declared minimum: 0 over those columns anyway. Nothing validates a statistics response against the schema today: every statistics route still answers the placeholder 501, and the per-scope contract sweeps that would drive a real response through validateAgainstContract (internal/managementapi/contract_test.go) are themselves still placeholders there. This package's contract tests do validate other routes' responses against this document today, and will validate these schemas too once those sweeps land, making a floor here a live test-time check at that point. The branch gives up that future check for these four columns: the floor cannot tell a legitimate pipeline negative from a handler bug, so it has to go regardless. The negative is not hypothetical: applyRepoDeltasStmt and applyNamespaceDeltasStmt add a signed delta with no clamp, and npm's unpublish, OCI's manifest delete, and OCI's blob detach each emit negative ones.

What

The split follows the source, not the scope. A figure reading or summing one of the four signed-delta columns loses its floor. Every other figure keeps it, because its source cannot go negative: each repositories_count is a COUNT, the per-repository figures count visible rows, and repositories.downloads_count has a single writer that only adds one.

Two figures are missing from the four-figure count in Statistics contract declares minimum: 0 that no... (#921 - closed) • Unassigned. StoredArtifactsCount and StoredSizeBytes read repositories.artifacts_count and repositories.size_bytes directly, S17 marks the repository scope's three counters stored, and all three RepositoryStatistics branches reach them by $ref. Un-flooring those two as well makes six schema sites and ten figure instances. StoredDownloadsCount keeps its floor, so that trio splits.

The change is strictly more permissive. No response that validated before stops validating, and a strict generated client stops rejecting a served underflow.

Dropping a schema constraint adds, renames, and removes no operation, so api/bruno/** has nothing to mirror and no e2e scenario is added or affected.

Governing ADRs

ADR-009 delegates this class of change. Its Consequences record that "request and response payloads live in the OpenAPI specifications in the Artifact Registry codebase, so payload changes do not require ADR updates", and a response-schema constraint is a payload change. No other ADR reaches it: ADR-011 scopes reconciliation to garbage collection, upload purging, and a deferred validation service, and names none of these counter columns, while ADR-004 sets data and application limits rather than figure domains.

Test plan

TestContract_StatisticsFigures_RefuseNegatives asserted the floor on every figure, which is the claim this MR falsifies. It becomes TestContract_StatisticsFigures_DeclareFloorsBySource and pins the split both ways, so the test fails whether a figure gains a floor or loses one.

Green at 253d542b4:

  • npx @redocly/cli@2.34.0 lint --config api/openapi/redocly.yaml
  • go test ./internal/managementapi/
  • golangci-lint run plain and with --build-tags=integration
  • bash scripts/ci/check-comment-caps.sh --base origin/main

The new assertions were mutation-tested, and every mutation failed the test:

Mutation Caught by
Re-floor NamespaceStatistics.deduplicated_size_bytes unfloored-set match
Un-floor NamespaceStatistics.repositories_count unfloored-set match
Floor StoredDownloadsCount at 1 zero-floor assertion

This branch also merges cleanly under the version statistics contract in feat(api): version statistics contract (S17 Pha... (!1986 - merged) • Hayley Swimelar • 19.4, and the contract suite passes on the merge, so the two do not collide on contract_statistics_test.go.

Context for LLM agents

Rejected alternative: clamp at serialization. S22 argues against it from its own side, because clamping "would hide the only visible symptom of a fast-path defect", and it delegates presentation to the consumer. A clamp here would make this surface the one consumer overriding that delegation.

Non-goals. The Repository resource's own three counters already declare no floor and stay unchanged. StoredDownloadsCount keeps its floor because repositories.downloads_count has one incrementing writer, reconciliation recomputes no downloads from source, and no delta struct carries the column. VersionStatistics lands in feat(api): version statistics contract (S17 Pha... (!1986 - merged) • Hayley Swimelar • 19.4, and its figures are live recomputes rather than counter reads.

Closes #921 (closed)

Related to #316

Edited by Hayley Swimelar

Merge request reports

Loading
Loading