docs(specs): correct S13 and S16 against the shipped Fetch seam

🎯 What

Corrects four claims in S13 and S16 that went stale as S13's Fetch work landed. All four are wrong against main today, and none of them depends on the two remaining #433 gaps.

This is the spec-only part of a three-way split of !1325, which had grown to 16 files across four packages and is now kept as the combined reference rather than merged. No code changes. Every claim below can be checked against main without reading any Go.

🔍 The four corrections

1. S16's S13 dependency row lists five steps as unbuilt

It says steps 8 (single-flight), 9 (Probe), 11 (standalone request flow), 14 and 15 (health) "have no code on main", and that three S16 integration points depend on them.

All five have landed. internal/remote/ carries singleflight.go, probe.go, standalone.go, serve.go, health.go, and healthjob.go, and Standalone.Serve is at internal/remote/standalone.go, with internal/format/npm/remote_packument.go already calling it.

The row names Standalone.Serve as a seam that now exists, held apart from the seams the Integration point cell enumerates, since that cell does not name it. The merged container-remote plan has the blob-proxy fill composing Fetcher directly and never constructing remote.SingleFlight, and remote.NewStandalone requires a non-nil flight, so the row must not read as settling which seam the fill takes.

2. S16 says the upstream Content-Length cannot cross the Fetch seam

Two places say so: the Content-Length bullet under API Contracts, and the cold-fill criterion under Manifest and blob proxy. The criterion goes further and tells implementers to "assert the no-Content-Length chunked response in both cases" — which is now an instruction to assert the wrong thing.

It can cross. FetchResult.ContentLength carries it as a typed field, which is the separate channel alternative the gap-3 entry itself offered rather than the allowlist exemption it asked for. internal/format/maven/remote_artifact.go:466 and remote_metadata.go:355 already forward it on both cold-fill paths, each guarded on >= 0, and S14's own criterion no longer names a gap.

The follow-up entry is rewritten rather than removed, because what is still missing is narrower than what shipped: ServeResult has no equivalent field, so the length does not reach a slice composing Standalone.Serve. The dependency row says so too, rather than leaving "the body length has landed" to read as the whole gap closing.

The API Contracts bullet names all three cases that read -1 by pointing at S13's Fetch section, the upstream client's gunzip pass among them. Against a gzipping upstream the upstream did send a length and the response still goes out chunked, so the bullet no longer claims the cold fill behaves identically in every case.

The Docker-Content-Digest follow-up above it opened "Unlike Content-Length, the exemption alone is not enough", which points at a remedy the rewritten bullet argues against. That comparison is dropped.

3. S13 does not describe content_length at all

The Fetch operation table still reads StreamHit{body, status, etag}. The field ships on main with no spec text behind it, so the table gains it, along with the three cases that read -1 and why it is a result field rather than a forwardable header — the upstream's own header survives the cap untouched, so a slice reading the header would forward a length the truncated body cannot deliver.

The forward rule is scoped to a raw fetch. Fetch sets ContentLength from the upstream response whether or not FetchOptions.Transform wrapped the body, so a transformed read that took an unscoped rule at its word would forward a length describing bytes the caller never sees.

4. S16 says nothing composes the single-flight layer

The credential-read follow-up rests on it: "nothing composes the single-flight layer today — so per-fetch construction still holds the rule up on its own."

Both siblings compose it now. Maven builds one long-lived flight per repository in internal/format/maven/handler.go, npm builds them through npmremote.NewFlightRegistry at cmd/artifact-registry/wire_npm_remote.go:183, and both already answer the hazard, by different mechanisms — Maven with a delegating builder that reads the pair inside BuildRequest, npm with LiveUpstreamConfigSource reading it from the datastore on every BuildRequest. The entry's conclusion survives; its stated reason does not, and two slices reaching two local answers is the case for one seam rule rather than against it.

Verification

Documentation only, so the Go toolchain has nothing to say about it. markdownlint-cli2, lychee, and vale pass through the pre-commit hooks.

Each claim above is checkable with git grep against main; the paths are named inline rather than left as an exercise.

🔄 Merge order

First of three. Nothing depends on it, and the two that follow both edit the same S13 and S16 sections, so landing this first keeps them from re-resolving the same text.

MR Scope
this one spec corrections against what main already ships
!1808 (merged) FetchOptions.ExpectedDigest reaching the cache-fill commit
!1807 the upstream Docker-Content-Digest cross-check
!1325 the combined reference these three were extracted from — never merged, closed once they land

The S13 dependency row is also rewritten by !1789 (merged)

!1789 (merged) (#397 (closed), content codings) rewrites the same cell at docs/specs/S16-container-remote.md:38. The cell lists every S13-side gap, so each merge request strikes a different item off it: !1789 (merged) drops the per-request no-transparent-decode switch and keeps the Content-Length allowlist exemption, and this one does the reverse. Both rewrite the five-steps claim, in different words.

A resolution that takes either side whole reinstates the other's stale claim. The two need a union, not a choice, and after both land two S13-side changes remain rather than three.

Measured against !1789 (merged)'s head: exactly one conflict region, at line 38. Everything else merges clean, S13 included. This branch has no conflict with main, so landing it first leaves !1789 (merged) a single line to resolve. The resolved cell text is on !1789 (merged) as an inline note, kept there rather than restated here so the two cannot drift.

🧪 e2e scenarios

No scenario is added or affected — documentation only.

Related to #433

Edited by Radamanthus Batnag

Merge request reports

Loading
Loading