docs(specs): add the covering index the hosted manifest tags read needs
Why
S17 says the hosted manifest tags read rides an index that exists and that the patch adds none. The database review measured otherwise on chore(datastore): the windowed tag-names read o... (!2516 - merged) • Hayley Swimelar • 19.4. The index stops at (namespace_id, container_manifest_id), so name order comes from an Incremental Sort whose cost is whatever the heap order left it. Worst of three runs each, 100 manifests at 1,000 tags, 100,000 rows, PostgreSQL 17.10:
| Heap order | Names inside a manifest | Preview | Complete |
|---|---|---|---|
| Grouped by manifest | ascending | 55.7 ms | 54.1 ms |
| Grouped by manifest | arbitrary | 109.0 ms | 102.1 ms |
| Interleaved | ascending | 74.4 ms | 69.7 ms |
| Interleaved | arbitrary | 129.7 ms | 124.9 ms |
A manifest collects a thousand tags over time, so arbitrary arrival is the ordinary case, and the budget is 100 ms. Widening to (namespace_id, container_manifest_id, name) was measured too: Index Only Scan, Heap Fetches 0, buffers 100,107 down to 938, sort gone. That before-and-after pair is the builder's own run rather than a fifth row of the table above, which is why its buffer figure does not match either layout's. So the spec gains the covering index, the migration that creates it, and the ADR-007 index-list amendment named for the handbook in Follow-ups.
One more site, on the spec author's 2026-09-11 ruling: the referrers list serves digest ascending as its only order rather than as its default.
Test plan
npx markdownlint-cli2 and vale --minAlertLevel error on the file. Both clean.
Context for LLM agents
Rejected alternative: accept the miss and let the counter ladder catch it. The Fetch mechanism already routes a family that misses the 100 ms budget to a denormalized counter. That ladder does not reach this case. It replaces a count with a stored column and leaves the array read alone, and what misses here is the array read's sort, not tags_count. A counter for the tags family would ship the same 102 to 130 ms read beside a cheaper count.
Rejected alternative: keep both indexes. The three-column index is a strict prefix-superset of the two-column one for every consumer of the narrower form, so keeping both buys write amplification and nothing else. The migration drops the narrower index, the shape 20260602142135_extend_container_tags_lower_name_index_with_name.sql already applied to this table's other composite index.
Non-goal: the fill-side cap. container.manifest_max_tags has no upper bound in its proto validation, so the documented 100,000-row ceiling is default-relative rather than enforced. That is a proto and configuration-reference change, out of scope here, and the index holds the budget at any ceiling the knob admits because the plan is index-only either way.
Non-goal: the remote tag arm. The remote read's window is a constant and its table has no per-manifest cap, so the remote figure is a named shape rather than a ceiling. docs(specs): bound the remote manifest tags arr... (!2504 - merged) • Hayley Swimelar • 19.4 settles that arm. This amendment touches the hosted index only.
Why the spec rather than the implementation. S17's Fetch mechanism asserted "this patch adds no index and owes ADR-007 nothing". Adding the index inside an implementation MR would have contradicted a merged spec and skipped the ADR-007 index-list amendment, which ADR-007 carries as (namespace_id, container_manifest_id) on container_tags. Guardrail 13 keeps docs/adr/ out of this repository, so the amendment is named in Follow-ups for a handbook MR.
Related to #1150 (closed)