docs(specs): specify namespace statistics for monolith/S04 and S10

Summary

The design's aggregate header on the repositories list (repository count, total storage) and the repository count and stored size in the disable confirmation both want AR's namespace statistics scope. Both specs deferred them to Phase 2 against a Go-service gate, and that gate has closed: S17 Phase 8 has merged, along with the S22 counter pipeline that keeps the two stored fields moving. Neither spec recorded it, so both still tell a reader the work is blocked on AR.

This amendment closes that gap and specifies the surface, so a plan MR can follow. No code ships here.

monolith/S04

  • Dependencies: adds the GET /api/v1/:slug/statistics row, and notes the read is a second request whose figures describe the namespace rather than the loaded page, so they do not move with a filter, a sort, or a page. Adds the monolith/S10 cross-spec row, which carries the merge order: S10's field declaration lands before, or with, the header that selects it.
  • Phase 2: the heading stops naming a single gate. Three rows still wait on an AR list-contract extension; the header does not, so a heading claiming one gate for all four was false. The intro says which row carries which.
  • GraphQL surface: a new ### Namespace statistics section with the selection the header issues, why the field nests, and which phase of S10 declares it.
  • Ruby client: was "None new." Now carries the namespace_statistics contract, with monolith/S02 building it — the division monolith/S10 uses for the four methods it specifies. (monolith/S06 divides it the other way, adding its four methods itself; the section says so rather than claiming both as precedent.) Records that the method presents the per-user token, not the service credential, and that its route evaluates read_repository against the namespace while the list route evaluates it as a listing filter — which is why a viewer can get a populated table and no header.
  • Frontend / UX: a new ### Aggregate header with the two figures, the design frame, and a case table (both present, unmapped namespace, AR refusal, read failure, 404, negative size), plus the counter lag in both directions.
  • Four acceptance criteria, each opening with its phase, four error cases, and Resolutions entries for the size field, the mount point, and the refusal outcome.

monolith/S10

  • GraphQL surface: the statistics field on the registry type, marked Phase 2 in the row and in its paragraph. The field is declared here because the type already stands for the namespace and its parent supplies the slug, so both consumers reach one field instead of declaring one each.
  • GraphQL surface, fan-out: the existing one-AR-call-per-node bound is extended to say that selecting statistics costs two calls per node on two surfaces, neither batchable, and that this is a second deviation from ADR-014's batching rule on the same surface.
  • GraphQL surface, client instance: the resolver acquires the monolith/S03 per-request client for the acting user and the resolved handle, unlike the four lifecycle methods, which memoize their own instance on the service-credential default. The two places that stated the memoized instance as a whole-slice rule now scope it to those four.
  • Activation settings section: the disable-confirmation paragraph loses its reason for omitting the figures — "no contracted endpoint returns them yet" is no longer true — and records that the refusal case S04's header plans for does not reach an owner-only surface.
  • Phase 2 row and the Phase 2 dependency prose: the S17 Phase 8 gate is recorded as closed rather than dropped silently. The Phases table shows the field's declaration as this slice's Phase 2 work.

Decisions worth a reviewer's attention

The statistics field surfaces no GraphQL error on any status: a null field and an omitted header throughout. The two specs disagreed — S04 gave the field a typed error on a denial, S10 a null — and S10's reading wins.

Only one status makes this a decision. monolith/S03's query mapping already answers a 401/403 and a read 404 with a null field, existence-hiding and no error, which is what ADR-021 asks of a read whose resources the principal cannot see, so the field inherits that rather than deciding it. The departure is the outage path, where S03 renders an UnavailableError as a service-unavailable error and this field resolves null instead: the header is supplementary and the table is the page. The accepted cost is that the browser cannot then tell an outage from a refusal; the S02 client's ErrorReporter still reports it, so the signal is in the logs rather than on screen.

The header shows deduplicated_size_bytes, not a sum of the rendered Size column. The repository counter dedups within each repository, so summing that column double-counts a blob two repositories share and reports more than the namespace occupies. A reader comparing header against column will find they disagree; the spec says which is correct.

A negative size renders as zero bytes. The contract gives deduplicated_size_bytes and components_count no minimum: 0 and states a counter underflow can report a negative until reconciliation corrects from source.

The field nests as registry { statistics } rather than becoming a third artifactRegistry-prefixed field on the organization. Both mounts cost the same number of AR calls: AcquiresClient#artifact_registry_slug already resolves the same Rails.cache-backed, strong_memoized mapping to obtain its slug, so the list page pays that resolution either way. Nesting wins on the parent supplying the slug, and on the field being reached only through the artifactRegistry field's own authorization check and null-when-unmapped resolution — the parent field's gate, not the registry type's, which declares no authorization of its own.

Governing ADRs

  • ADR-009: API Design — declares the namespace-level statistics routes this amendment consumes. Conforms: the header reads the namespace scope at the path ADR-009 defines and adds no route.
  • ADR-021: Authorization — the amendment records that the read binds to the namespace-scope read_repository permission, and that a caller without it sees a null field and omitted figures rather than an error. Conforms, and by inheriting monolith/S03's existing query mapping rather than by carving out an exception: ADR-021 gives registry-wide statistics the same namespace-level-role rule it gives repository listing, and requires unreadable resources to be omitted with no error, which is what that mapping already does. AR's own listing arm is not built yet — scopeListingFilter runs an anonymous-only pre-gate and passes an authenticated caller through unfiltered until #861 (closed) lands — so the spec names that condition and its event rather than describing the fallback in the present tense.
  • ADR-014: Frontend to Artifact Registry — two clauses apply. The resolver pattern and server-held credential: conforms, because the figures reach the browser only through the monolith's own GraphQL and no AR credential or AR-domain request originates in the browser. The batching rule for a child field that fans out per parent: deviates, on the same ground and in the same way as artifactRegistry itself, which S10 already records — neither AR surface offers a bulk read to collapse N parent resolutions into. S10's fan-out paragraph now names this second instance and the 200-call shape a 100-node page reaches, so the deviation is recorded where a reader of the bound will find it.
  • The ADR-014 deviation is an existing, recorded one extended to a second field on the same surface, not a new architectural position, so no handbook ADR amendment is needed.

Testing

Documentation only; no code changes and no behavior ships, so no e2e scenario in docs/testing/ is added or affected. The acceptance criteria this amendment adds are the tests the implementing MRs will be held to.

Verified locally: markdownlint-cli2, vale, lychee, editorconfig, gitlint, and the whitespace hooks pass on both files, and every intra-document anchor introduced resolves to a real heading — including the three references updated for the renamed Phase 2 heading.

Overlapping MR

!2005 (closed) also edits docs/specs/monolith/S04-repositories-list.md, a different bullet of the same ## Out of scope list. A trial git merge-tree of the two branches auto-merges S04 with no conflict, so neither blocks the other. (That trial does report conflicts in two docs/plans/ files, which this branch does not touch.)

Related to gitlab-org/gitlab#627138

Edited by Fiona McCawley

Merge request reports

Loading
Loading