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 betaline 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
contractPendingOperationsentries, 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, becauseopenapi3filter.ValidateResponsealready runs against the served manifests list and arequiredentry 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
requiredwidening 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, andopenapi3filter.ValidateResponsechecks 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
EXPLAINpin, 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
requiredin the serializer step. Step 3's shared-base pin requires every declared property to appear in its schema'srequiredlist, 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
Depsfield, so steps 16 and 18 would write the same one-line bump to the count literal atinternal/managementapi/handler_test.go:443with no dependency edge between them. Two identical edits to one line merge clean and leave a count one short, andrequireDepspanics 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
redoclylints 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
EXPLAINpin and ceiling measurement into a step that serves nothing. - Import the OCI header and blob-reader mechanisms for the raw route.
internal/managementapiimports 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
PreviewLimitmean 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 aCompleteflag instead and rejects the zero value on an argument-guard sentinel. - A deadline on the page-facts fetch. Declined: no database read in
internal/managementapiorinternal/datastorearms one and there is nostatement_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_countas 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
Depsseam 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:
recordRemoteProxyFaultedsets a field on the OCI remote proxy's wide-event record,internal/managementapihas no such record and its two counter series are not fault families, so theErrorline is the only observable and D5 names that residual. - Ordering Step 16 behind Step 12 so the configuration-reference row could say
referrers_countis 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
Completeread. Rejected:container_remote_tagshas no write-side cap, so that read would be the unboundedSELECTdocs/dev/database-query-patterns.mdforbids. The remoteCompleteshape keeps its row-number filter at a constant, 1,000, andtags_countbeside 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_tagsinto the remoteCompleteread as its window. Rejected: lowering the knob would shrink every remote detail'stagswhile 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/servermiddleware chain in the raw route's overlong test to reproduce production's writer. The property that matters is a writer with noReadFrom, which every wrapper in that chain is, so the test uses a wrapper of that shape and a source with noWriteTo.
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
sourcefield 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