docs(plans): add the S17 container manifest reads plan

Why

The S17 manifest-read contract merged as docs(specs): declare the S17 container-redesign... (!2317 - merged) • Hayley Swimelar • 19.4. What it leaves owed is the implementation, tracked in Container manifest reads for the UI: detail by ... (#1150) • Hayley Swimelar, a closed-beta blocker: without a single-manifest read the Docker and OCI version detail page cannot render, and without per-manifest tags and the platform triple the version list's container rows have no Tags or Platforms. AR requires a plan MR to land before any step MR opens, and S17 Phase 3 put its OpenAPI patch at step 1 of its plan, eight days after that plan merged. The OpenAPI patch here is steps 3 and 4, so it cannot open first either.

This plan decomposes ten manifests-list fields and three routes into twenty step MRs, ordered so the closed-beta-blocking work lands first. Steps 1 through 10 are that set: a four-MR prep stack carrying the seams, the schema split, and the declarations, then the eleven-column projection and the two tag reads, then the detail route, the platform triple, and the tags family, which lands the page-facts seam. Steps 11 through 20 are follow-on work under the same work item: referrers_count and the referrers route, the parents and children families, annotations, and the raw payload, with annotations fanning out from the detail route, the three families from the seam, the referrers route from referrers_count and its list-row read, and the raw payload from the declarations alone. The ten-step version drew a review asking for splits, then parallelism, then single parentage, in that order, and the restructure is what those three asks produce. The closed-beta ordering is the round after it.

Five decisions a reviewer would question:

  • The closed-beta-blocking steps come first, and the plan is named for what it builds. D11 names the criterion: the reads the two Phase 2 pages cannot exist without are the detail route, per-manifest tags, and the platform triple, so those and their prep are Steps 1 through 10, marked in each heading, on a Closed beta line per step, and in the Status table. Referrers, parents, children, annotations, and the raw payload render when they arrive and are absent until then, so they follow. S17 calls this surface the container-redesign fast-follow, a fast-follow to its Phase 3; the closed-beta roadmap uses the same word for work outside launch scope, so the plan no longer carries it in its name.
  • Routes are declared ahead of their handlers, and fields are not. Step 4 declares all three operations behind six contractPendingOperations entries, two per operation, and each route step deletes its own pair in the MR that serves the route. A field goes the other way, landing with its serializer, remote arm, and schema pin in one MR, because openapi3filter.ValidateResponse already runs against the served manifests list and a required entry with no emitter fails that suite on merge.
  • The detail route lands before the three array families, and the earlier ordering's cost claim was wrong. The ten-step version put the families first, reading a later required widening as landing on up to three served responses and needing three contract cases updated. It needs none: one base builder per kind, one shared facts fetch, one schema base, and one key-set list make a later field one edit per surface, and openapi3filter.ValidateResponse checks every served response against the schema the same MR changed. So the detail route goes first and depends on the declarations alone, the tags family lands next and carries the page-facts seam, and the parents, children, and referrers families add to a leaf and a seam that already exist. The platform triple depends on no route.
  • Each family's datastore read is its own step that serves nothing. The read, its EXPLAIN pin, and the 100 ms measurement at the family's ceiling land before any handler depends on them, so the parents family's risk surfaces first: 2.5 million parent edges on a hundred-row page, against 100,000 tag rows and 20,000 child edges. A miss splits that one datastore step into a counter migration plus the read, and blocks nothing until its serving step opens. The two closed-beta reads open on day one; the four follow-on reads can open whenever capacity allows.
  • Per-step sizing comes from one field family, not from a Phase 3 MR. Each step's forecast scales from one field family delivered end to end, feat(managementapi): serve dist_tags on the ver... (!2330 - merged) • Hayley Swimelar • 19.4. The split puts five of the twenty steps past 500 total lines and none past 300 on source, against nine of ten over the line before it; the tags family is the fifth, at about 535, because it now carries the seam, and it names the split it rejected. The three Phase 3 MRs that built this surface only cross-check the total of about 7,900 lines, the largest of them the container handlers at 4,066, feat(managementapi): serve the container artifa... (!1133 - merged) • Hayley Swimelar • 19.3.

The diff is 3,113 lines in two files, 3,112 the plan and 1 the index row. A plan is one document by construction, with one Status table denominator and one dependency graph, so a split has nothing to divide (guardrail 18).

Governing ADRs

None. The plan's Dependencies section records the one ADR question this work raises, the raw payload route against ADR-009's JSON-response category, which S17's Follow-ups already tracks.

Testing

The docs lint gates ran on every commit: markdownlint-cli2 through pre-commit, vale --minAlertLevel error, and lychee --offline --include-fragments. git merge-tree --write-tree origin/main HEAD exits 0. Three review passes re-verified about 120 file, line, symbol, and count citations against the tree at the plan's measurement commit 400ef5976. The first two approved the ten-step version after fixes, and the third corrected ten claims the split falsified, the critical path's depth and the leaf-step count among them. The resequencing round renumbered every step cross-reference by exact-match replacement, then re-derived the ownership of the four route-comment sites against handler.go, artifact.go, artifact_internal_test.go, and handler_test.go: the detail route, now the first route to land, falsifies both claims of the imageManifestPattern comment and owns the "eleven wildcard routes" rewords. The round-two pass corrected nine step cross-references the resequencing left stale, ruled the page-facts fetch sequential, specified the raw route's three log arms field by field, split the configuration-reference edit between Steps 12 and 16, and gave the stale-claim sweep two more sites and its derivation command. The round-three pass bounded the remote tag read at a constant window and recorded the S17 contradiction that leaves for the spec author, specified the raw route's two overlong fixture shapes against Go 1.27's net/http, corrected the mirror's dispatch order, re-ruled the shared counts against the route list, and added two more sweep sites.

Context for LLM agents

Design rationale and rejected alternatives

  • Fetch each family's capped array and slice ten in Go. Has an in-tree precedent, and fails on parents: that array is uncapped per manifest and bounded only by container.image_max_manifests (25,000), so a hundred-row page returns up to 2.5 million digest rows to render a thousand.
  • A lateral subquery per row. Rejected on a measured prior in docs/dev/database-query-patterns.md: the lateral rewrite of a neighboring shape cost three times the plan work and forty-four times the buffers. It also folds each family into the list statement, leaving the timing measurement no per-family figure.
  • Declare the new fields optional, then tighten required in the serializer step. Step 3's shared-base pin requires every declared property to appear in its schema's required list, which makes the loose window unshippable rather than discouraged.
  • One flat schema with everything optional. It cannot express the two absences AC #130 (closed), #132 (closed), and #135 (closed) require, and two sibling flat schemas would duplicate 26 lines of field descriptions.
  • Two per-direction relationship reader seams, one for parents and one for children. Rejected on the seam count: each adds a Deps field, so steps 16 and 18 would write the same one-line bump to the count literal at internal/managementapi/handler_test.go:443 with no dependency edge between them. Two identical edits to one line merge clean and leave a count one short, and requireDeps panics at construction rather than failing to compile. One adapter widened per direction never touches that line.
  • One declaration MR per operation. Rejected: three MRs where one does, lengthening the prep stack every serving step waits behind and buying no parallelism, because redocly lints the document whole and its shared components are edited once.
  • A twelve-step variant splitting only by route and family, leaving each datastore read inside its serving step. Rejected in favor of the layer split, which front-loads every EXPLAIN pin and ceiling measurement into a step that serves nothing.
  • Import the OCI header and blob-reader mechanisms for the raw route. internal/managementapi imports no format package by stated convention, so both are mirrored with doc comments naming the originals.
  • One implementation MR. Rejected on size, about 7,900 forecast lines.
  • Let a zero PreviewLimit mean the complete array. Rejected in review: the parameter structs are exported, so a forgotten field on the page path reads the whole family behind the page. Each store takes a Complete flag instead and rejects the zero value on an argument-guard sentinel.
  • A deadline on the page-facts fetch. Declined: no database read in internal/managementapi or internal/datastore arms one and there is no statement_timeout, so it would be the first in those packages, decided inside one family's step. D4 prices the exposure that leaves, the response write deadline included.
  • Keeping referrers_count as the detail route's parent, the shape the twenty-step version shipped first. Rejected once the closed-beta set was named, because it put a follow-on count on the critical path to the page. The tags family carries the page-facts seam instead, and the referrers, parents, and children families stack on it.
  • Moving the raw-payload Deps seam out of Step 2 now that its reader is the last step. Declined: the seam's signature change breaks seven wiring files and was reviewed twice where it sits, so the plan names the eighteen-step gap as an accepted smell rather than re-opening Step 2.
  • A concurrent page-facts fetch over an errgroup. Rejected: it holds one pooled connection per family for every in-flight page to buy back at most 300 ms on a page at every ceiling at once, and pageRemoteSettings, the precedent for the error handling, is sequential.
  • Carrying the mirror's fault booking on the raw route's copy arms. Declined: recordRemoteProxyFaulted sets a field on the OCI remote proxy's wide-event record, internal/managementapi has no such record and its two counter series are not fault families, so the Error line is the only observable and D5 names that residual.
  • Ordering Step 16 behind Step 12 so the configuration-reference row could say referrers_count is served. Rejected: each step writes its own clause into the shared row instead, which keeps the two follow-on families parallel.
  • A measured figure for the remote tag read at a chosen fixture, with no window on its Complete read. Rejected: container_remote_tags has no write-side cap, so that read would be the unbounded SELECT docs/dev/database-query-patterns.md forbids. The remote Complete shape keeps its row-number filter at a constant, 1,000, and tags_count beside the array stays the true total. That deviates from S17's "complete" past 1,000 cached names, so the plan records the contradiction with three resolutions for the spec author and gates Step 9 on the S17 amendment.
  • Passing the configured manifest_max_tags into the remote Complete read as its window. Rejected: lowering the knob would shrink every remote detail's tags while hosted manifests kept the names they were pushed with, and it would give an operator knob a second, read-side meaning.
  • Mounting the whole internal/server middleware chain in the raw route's overlong test to reproduce production's writer. The property that matters is a writer with no ReadFrom, which every wrapper in that chain is, so the test uses a wrapper of that shape and a source with no WriteTo.

Non-goals

  • The OpenAPI, Bruno, and Go changes themselves, which are steps 1 through 20.
  • Amending the monolith S06 and S14 Phase 2 specs against the merged contract, tracked in monolith/S06 and S14 Phase 2 tables are stale a... (#1186) • Unassigned.
  • Any ADR change. Guardrail 13 keeps docs/adr/ out of this repository's MRs.
  • A source field on manifests or images. No column exists, and S17 puts it behind a schema decision.
  • Filling the Status table. A single batch writer records each row as its step MR opens.
  • Renaming S17's own "container-redesign fast-follow" section. The spec keeps its name; the plan explains the difference once.

Related to #1150

Edited by Hayley Swimelar

Merge request reports

Loading
Loading