feat(remote): step 14 part 1 — stream digests in the fetch pipeline's cache-fill tee
What
Add the format-agnostic streaming-digest tee to internal/remote: the cache-fill tee now computes SHA-1 and SHA-512 always and MD5 unless FIPS mode is on, over exactly the bytes the caller receives, and hands the sums to the widened CacheEntry at upsert — no re-read of the committed blob.
Carries the two fill-consumed fetch-surface additions the Maven fill (next parts) needs:
FetchResult.ContentLength— the upstream's declared length for a verbatim identity-coded stream,-1when unknown so fakes never read as a 0-byte body.- the marked-transport-failure arm in
FallbackEligible, so error mapping classifies a mid-transfer failure as fallback-eligible.
Context
Part 1 of the S14 Maven remote artifact-proxy step. This is the S13-territory seam surface the Maven fill consumes; the fill itself and the stale-revalidation arm land in the stacked parts above.
Spec coverage
Spec: docs/specs/S14-maven-remote.md
Part 1 of Step 14: the plan Amendment's streaming-digest tee in the S13 fetch pipeline, plus the two fill-consumed fetch-surface additions. The Maven wiring — fill handler, digest-column row write, header composition, response mapping — lands in the stacked next parts; this slice ends at the seams the fill consumes. Criteria beyond this slice are not listed.
Acceptance criteria
| # | Criterion (this slice) | Tests |
|---|---|---|
| AC-1 / AC-7 (computation half) | Fill computes SHA-1, SHA-512, and (non-FIPS) MD5 in the same pass over exactly the bytes the caller receives; every fill recomputes from the stream, no re-read of the committed blob | Tee vectors and byte-identity: TestDigestTee_KnownVectors, TestDigestTee_Property_MatchesDirectHashing; fill pipeline: TestCacheFillTee_FillDigestFingerprint, TestCacheFillTee_FillDigestFingerprint_FIPSMode; widened CacheEntry payload end to end: digest asserts in TestFetcher_Fetch_Miss200_TeesIntoCacheAndStreams |
| AC-1 (FIPS half) | FIPS-on fills leave MD5 uncomputed (the NULL column write is the Maven fill's) |
TestDigestTee_FIPSOn_OmitsMD5, TestCacheFillTee_FillDigestFingerprint_FIPSMode (nil MD5 under FIPS) |
| AC-3 (slice) | Cold-fill header subset: upstream Content-Length when present |
Seam only — FetchResult.ContentLength carries resp.ContentLength on a hit, and NewFetchResult plus the error/304 paths declare -1 so fakes never read as a 0-byte body. The header composition and the -1-omits-header rule land with the Maven fill |
Error cases
| # | Condition (this slice) | Tests |
|---|---|---|
| E-1 | A mid-transfer body-read failure is marked and classified fallback-eligible (S13 cache-fallback rule) | TestFallbackEligible_MarkedTransport: marked unexpected EOF, marked plain read failure, marked truncation through coalescing |
| E-2 | Fail closed: an unmarked error, or a marked exclusion-arm sentinel, never becomes eligible | TestFallbackEligible_MarkedTransport: unmarked unexpected EOF; marked cache fill, header injection, caller cancellation, leader canceled, transform rejection (the fallbackEligible-parity exclusion) |
| E-3 | Cap-breach classification stays split: the exported predicate keeps marked cap sentinels eligible (spec-aligned seam), the standalone predicate excludes them (plan research correction) | marked response-size cap stays eligible (TestFallbackEligible_MarkedTransport); TestFetcher_Fetch_BodyCapBreach_IsNotFallbackEligible (unchanged) |
Security considerations
| # | Concern | Tests |
|---|---|---|
| S-1 | FIPS control: MD5 suppression under FIPS; posture captured once at construction (Maven handler parity) so the fill path reads a field, not the live global | TestDigestTee_FIPSOn_OmitsMD5, TestCacheFillTee_FillDigestFingerprint_FIPSMode |
| S-2 | Fallback eligibility fail-closed: only marked transport failures reach the eligible class; the exclusion arm resolves before the marker arm | E-2 rows above |
| S-3 | No new SQL: seam-side change only; persistence statements stay in the slice datastore packages (ADR-023) | No datastore surface touched |
| S-4 | No client-visible surface change: ContentLength never travels through SafeHeaders; fakes declare no body length |
NewFetchResult -1 wiring, field doc in operations.go |
docs/testing/ (Guardrail 13)
No e2e scenario is added or affected: the Maven remote scenario catalog lands with the fill handler in the stacked next parts, and this slice changes only the internal fetch-pipeline seam — it is not reachable from the hosted surface. Unit-level coverage for the new behavior lives in internal/remote (digest tee vectors, fill fingerprint, fallback tables).
Related to #286 (closed)