docs(specs): declare the S17 container-redesign fast-follow contract

Why

The Docker/OCI detail page and the container rows of the version list need manifest reads S17 never declared: its manifest resource carried eight fields and no GET by digest. This patch declares the contract so the OpenAPI patch and the implementation plan can follow, as Phase 3 did in docs(specs): spec S17 phase 3 format artifact r... (!956 - merged) • David Fernandez • 19.3 and docs(openapi): add S17 Phase 3 artifact read co... (!1129 - merged) • Hayley Swimelar • 19.3.

It declares both relationship directions, a deliberate widening of the work item's child-only wording: S17 promised parent digests at lines 539 and 1506 and children only at 1547, and the monolith S06 and S14 specs render a Referenced-by parent link.

List rows carry *_preview (first ten) plus *_count, with whole arrays on the detail only: a hundred-row page of whole arrays reaches 13.10 MB (tags), 17.84 MB (children), and 185 MB (parent_digests) at the enforced caps, and the contract refuses to cap a response array (api/openapi/v1.yaml:4938-4941).

The work item's "10 extra requests per page at the 1,000-tag cap" is wrong: manifest_max_tags is per manifest and nothing caps an image's tags (25,000 manifests × 1,000 each), so the spec drops the figure.

Governing ADRs

No deviation. ADR-009: the three routes are additions under the repository anchor and :format segment, and the raw route is a manifest's own representation, not ADR-009's file download. ADR-004: the page-weight arithmetic uses its 200-reference cap and skips its stale 250 KB payload row (S12 OQ-15 owns that amendment). ADR-021: all three routes require read_artifact at repository scope, the manifests list's permission.

Test plan

  • docs:lint on both files: markdownlint-cli2 (through pre-commit), vale --minAlertLevel error, and lychee --offline --include-fragments, all clean.
  • git merge-tree --write-tree origin/main HEAD exits 0.
  • ./scripts/adr-freshness.sh exits 0.
Context for LLM agents

Design rationale and rejected alternatives

  • Capped response arrays (tags cut at ten with tags_count beside it): rejected. The delete 409's error.details.parents refuses a cap for the same reason (api/openapi/v1.yaml:4938-4941), and every maxItems in the OpenAPI document bounds a request body, never a response.
  • children whole on the list: rejected, 17.84 MB per hundred-row page at 200 references and 255-byte platform values.
  • One direction only: rejected. The Platforms block needs children and the Referenced-by link needs parents, the edge is one container_manifest_relationships row either way, and ContainerManifestRelationshipStore.ListParentDigestsByChild already serves the parent direction for the delete 409.
  • annotations on the list: rejected, 59.06 MB per page at 128 keys with 512-byte keys and 4,096-byte values. Detail only, no count.
  • Raw route Content-Length from the resource's size: rejected. On a hosted row size is the push-time tree total (computeTreeSize in internal/format/oci/manifest_push.go), wrong on every manifest that references anything. The payload blob's length from blob_storage_blobs is the contract.
  • 501 for referrers on a remote row: rejected. The route is implemented and the answer is empty, so referrers_count is 0 and the route returns 200 with an empty array.
  • The 3 MB page budget is set by this document because nothing in the tree states one, and an unstated budget makes the next array's verdict unreviewable.
  • Query cost beside page weight (review round 1): the statement-count rule alone let a green MR walk 2.5 million parent edges (100 rows × container.image_max_manifests) on one page, so Fetch mechanism prices rows examined per family (100,000 tag rows, 20,000 child edges, 2.5 million parent edges, 25,000 referrer rows image-wide because a manifest names one subject_digest), the parent and child statements also probe container_manifests per edge because a digest-ordered preview needs every digest, and the 100 ms timing budget measured at the cap shape before shipping is the denormalized-counter trigger. The 3 MB budget binds the page only: the detail's four arrays sum to 2.75 MB at the defaults, 92% of it, crossing at about 28,400 manifests per image.

Non-goals

  • api/openapi/v1.yaml: the "no manifest GET" rationale at 1223-1224 and the withheld-column note at 3699-3702 are owed by the OpenAPI MR, which opens after this lands. Bruno follows it.
  • The Go serializer, and the comments in internal/managementapi/handler.go:62-66, internal/managementapi/container_resources.go, and internal/datastore/container_manifest.go that still call the delete the only per-manifest route: the implementation MRs.
  • The implementation plan MR: opens after this lands.
  • A source field: no column exists, and S17 leaves an image-level source behind a schema decision.
  • The monolith docs/specs/monolith/S06-version-detail.md and S14-version-list.md Phase 2 tables, which still say the fast-follow promises the parent direction only and omit the platform triple and the child edge. Other owners. Their remaining staleness wants a new work item, because the item that tracked their earlier drift is closed, Two container-manifest doc statements describe ... (#594 - closed) • Unassigned.

Related to #1150

Edited by Hayley Swimelar

Merge request reports

Loading
Loading