feat(oci): serve the referrers list endpoint (S12 Step 17)
Why
S12 Step 17: the referrers list endpoint (GET /v2/<name>/referrers/<digest>), the OCI 1.1 discovery mechanism that lets a client find every manifest referencing a subject digest through its subject field. Signatures, SBOMs, and attestations all rely on it. This is the last endpoint before the Step 18 conformance close.
Built on main (Steps 1-12 merged), independent of the in-flight Phase D steps 13-16: referrers is a read-only query over the Step-3 datastore (ListReferrersPage, already merged) wired at the Step-6 dispatcher, with no manifest-push dependency.
What (non-obvious)
- Missing subject returns 200 empty, not 404. A subject digest with no referrers, or absent entirely, returns 200 with an empty image index per OCI 1.1 and S12. Only a malformed subject digest is a 400 (
DIGEST_INVALID). This is the one place the endpoint diverges from the usual "unknown name → 404" shape. - Discrimination is structural (P-6). Image Index children (rows with NULL
subject_digest) never appear in a referrers response, regardless of theartifactTypefilter or page position, because the query filters onsubject_digest =. The exclusion is a predicate, not a post-filter, so no page boundary can leak a child. - Streamed response, bounded memory. The image index streams descriptor-by-descriptor via
json.Encoderagainst theResponseWriter. A worst-case page (1000 referrers × ~520 KB annotations ≈ 520 MB) is never buffered twice. A deterministic unit test asserts the no-second-buffer property through aTotalAllocdelta; a DB-backed integration test confirms correctness at scale. - Pagination ceiling is
pagination_max_size(1000), not the tag-list default (100).nis validated to[0, 1000];lastis a manifest digest parsed throughParseDigest(malformed → 400PAGINATION_NUMBER_INVALID).n=0returns an empty index with noLinkheader.OCI-Filters-Applied: artifactTypeis emitted only when the filter is set.
Commits. The two-agent flow lands a failing test skeleton, then the implementation. Follow-up fix(oci): commits apply review feedback: the S03 wide-event field (operation) and citation corrections, the dispatcher prefix-closure wiring guard with generic panic messages, the probe-row trim before size resolution, and the oci.stream_truncated signal for mid-stream write failures, with matching error-path and wiring-gap tests.
Test plan
go test ./internal/format/oci/... (unit, rapid property, and DB-backed integration), go vet (default and integration tags), and golangci-lint 2.12 all pass. The referrers spec runs under conformance:oci.
Spec coverage (S12 Step 17):
| Criterion | Tests |
|---|---|
| P-6 referrers discrimination | TestPropertyReferrersDiscrimination, TestReferrersExcludeIndexChildren (integration) |
| AC-19 referrers (with filter) | TestReferrersArtifactTypeFilter, TestReferrersDescriptorRendering |
| AC-20 missing subject → 200 empty | TestReferrersMissingSubjectReturns200Empty, TestReferrersMissingSubjectIntegration (integration) |
E-4 DIGEST_INVALID (malformed subject) |
TestReferrersInvalidSubjectDigest |
E-10 NAME_UNKNOWN (repo/image absent) |
TestReferrersRepositoryNotFound, TestReferrersImageNotFound |
E-15 PAGINATION_NUMBER_INVALID |
TestReferrersPaginationValidation |
| S-12 pagination max 1000 | TestReferrersPaginationValidation, TestReferrersResponseStreamingBoundedAllocation |
Related to #19 (closed)
Context for LLM reviewers
Design rationale.
- S-13 → S-12 in the Step 17 coverage slice. The plan slice originally listed S-13 ("digest verification on all writes"). The referrers endpoint is read-only and performs no writes, so S-13 cannot apply; the plan already assigns S-13 to Step 12 (manifest push, the write path). The referrers-relevant security row is S-12 (pagination max 1000), enforced by the
n-ceiling rejection. The slice and the test comments now say S-12. TestReferrersUsesIndexis delegated, not deferred. The plan named an EXPLAIN-asserted index test. The referrers query is built unexported in the datastore (buildReferrersQuery), so the EXPLAIN assertion lives there asTestListReferrersPage_PartialIndexScan(merged Step 3), which proves single-partition pruning and Index Scan. Duplicating it in the handler package is forbidden by the test-author contract.- Wide-event field. This MR emits the S03 discriminator as
operation="referrers.list". The merged manifest-push emitter still usesoci.push_stepand carries nooperationfield; aligning the whole OCI emitter family onoperationis a separate follow-up, not smuggled into this read-only step.
Spec/ADR drift found while fixing (not addressed here). S12's ADR reference list attributes "pagination 100/1,000" to ADR-004, but ADR-004's text enumerates only repos-per-org and tags-per-artifact, no pagination limit. The authoritative value lives in S12's Configuration section, which the code now cites. Correcting the attribution belongs upstream (ADR-004 is handbook-synced) or in a spec edit, not on this branch.
Non-goals (deferred, not omissions).
- Garbage collection, soft-delete, and referrer-orphan cleanup: S20 (this endpoint reads only).
- Size accounting and counters: S22.
operation-field alignment for the merged manifest-push and blob-upload emitters: cross-step observability follow-up.- Phase D manifest steps 13-15: tracked separately; referrers has no dependency on them.