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/statisticsrow, 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 themonolith/S10cross-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 statisticssection 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_statisticscontract, withmonolith/S02building it — the divisionmonolith/S10uses for the four methods it specifies. (monolith/S06divides 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 evaluatesread_repositoryagainst 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 headerwith 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
statisticsfield 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
statisticscosts 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/S03per-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_repositorypermission, and that a caller without it sees a null field and omitted figures rather than an error. Conforms, and by inheritingmonolith/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 —scopeListingFilterruns 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
artifactRegistryitself, 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