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.yamlgo test ./internal/managementapi/golangci-lint runplain and with--build-tags=integrationbash 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