Hand-written. ../harvested/io-and-byte-streams.md is what the documents say; this file is what the
implementation found, and it wins. Each entry names the harvested entry it answers by that entry's
stable key.
IO-1's read primitive andIO-16's host-native bridge cannot be the same Ruby method, and reading the two entries together as one#readcorrupts every streamed upload. Supersedesio-and-byte-streams/ba53c43b(IO-1: "A source read MUST append bytes to the tail of a caller-provided destination buffer (never overwriting existing content) and return the number transferred … -1 at end-of-stream") where it is read as naming a method calledread, and narrowsio-and-byte-streams/f009344c(IO-16) in the same breath. Verified on 2026-09-08 against 3.2.11, 3.4.10 and 4.0.6. The fact:IO.copy_stream— which is literally whatNet::HTTP#send_request_with_body_streamcalls, so it is the code path everybody_stream=upload takes — drives a duck-typed source by checkingrespond_to?(:to_path), thenrespond_to?(:readpartial), then callingreadpartial(16384, buf)with one and the samebufobject on every call, expecting it overwritten; with no#readpartialit falls back toread(16384, buf)with the identical expectation. Ruby's ownIO#read(n, buf)andStringIO#read(n, buf)overwrite the destination on all three interpreters.IO-1requires the exact opposite. ADexpace::IO::BufferedSourcethat satisfiedIO-1under the name#reador#readpartialwould therefore append the whole stream intocopy_stream's single reused buffer and write ever-growing garbage to the socket — a corruption no unit test of#readalone would show, because it only appears when a foreign driver reuses the buffer. What the SDK does instead, perdocs/work/mvp/phase3/phase3a/2026-09-08-phase3a-io-contracts-design.md:IO-1's primitive is#read_into(dest, count:)with the tail-append, the four return values and the-1sentinel, while#read,#readpartial,#getbyte,#readbyteand#eachkeep Ruby's own semantics and areIO-16's bridge — which is what makesdocs/sdk-design-ruby/03-seam-by-seam-idiomatic-mapping.md§3.1's "IO-16's native-stream bridge is satisfied by construction" true rather than merely asserted. Two consequences that travel with the split.IO-16's "-1 at end" is the reference host'sInputStreamconvention and the requirement's own word is host-native, so the bridge signals end of stream withnil(#read,#getbyte) and by raising (#readpartial,#readbyte); the portable vocabulary keeps-1, becauseIO-17's pump reads it and a foreign source implementingIO-1literally would return it. And the raised class must inherit::EOFError: verified thatIO.copy_streamterminates cleanly on anEOFErrorsubclass raised by a duck-typed#readpartialon all three interpreters, soDexpace::EndOfStreamError < ::EOFErroris load-bearing rather than tidy — outside that familycopy_streampropagates and every streaming upload phase 8 performs fails. review ·docs/work/mvp/phase3/phase3a/2026-09-08-phase3a-io-contracts-design.md· high · sha:manual-phase3a-read-into-splitforce_encodingis unusable on the ingress path, because the chunks arriving there are frozen;String#bis the retag idiom. Supersedesio-and-byte-streams/0a4773a6("String#bis the idiom for representing 'these bytes, untagged,' whileforce_encodingis a retag used only where the bytes are known to conform"), which is true and incomplete:force_encodingalso requires aStringyou own. Verified on 2026-09-08 against 3.2.11, 3.4.10 and 4.0.6. The fact:force_encodingon a frozenStringraisesFrozenErroreven when the target encoding is already the string's own —"abc".b.freeze.force_encoding(Encoding::BINARY)raises on all three. And the chunks reachingDexpace::IO::BufferedSource.over(body)are routinely frozen: design §10.2's canonical body representation is Rack's#each-yielding-chunks protocol, a Rack body is commonly a literal array, and every file in this repository carries# frozen_string_literal: true, so["chunk", "more"].eachyields two frozen strings. A retag written the obvious way therefore raises on the most ordinary body there is.String#balways returns a new, unfrozen copy (fz.b.equal?(fz)is false andfz.b.frozen?is false on all three), so it is safe whereforce_encodingis not. What the SDK does instead: a chunk arriving from an upstream or a wrapped body is stored as-is when it is both frozen and alreadyEncoding::BINARY— frozen means it cannot become caller-mutable state, so no copy is owed — and aschunk.botherwise;force_encodingappears nowhere on the ingress path. The companion trap, verified in the same run and the reason every encoding test in phase 3a uses non-ASCII bytes: appending a non-ASCII UTF-8Stringto a BINARY one silently retags the result to UTF-8, while appending an ASCII-only one leaves it BINARY, and#concatbehaves identically — so an ASCII-only fixture passes under exactly the bug the rule exists to prevent.io-and-byte-streams/ff0de47eand/d2b47c89are unaffected and are adopted verbatim. review ·docs/work/mvp/phase3/phase3a/2026-09-08-phase3a-io-contracts-design.md· high · sha:manual-phase3a-frozen-ingress-retag- The single decode boundary is two steps, not one, and the transcode must name its target encoding explicitly. Supersedes
io-and-byte-streams/fbcb4d19("There is exactly one decode boundary, Response#body_string, which applies the media type's charset via String#encode(invalid: :replace, undef: :replace) and falls back to UTF-8 when the charset is absent or unknown, per HTTP-42"), whose rule is right — one boundary, the declared charset, UTF-8 as the fallback — and whose recipe destroys the payload. Verified on 2026-09-08 against 3.2.11, 3.4.10 and 4.0.6. The fact, in two halves. (a) The bytes reachingResponse#body_stringareEncoding::BINARY, becauseio-and-byte-streams/d2b47c89requires the port to retag every response body to BINARY on ingress and never trust a transport's tagging. From BINARY, every byte ≥ 0x80 is an undefined character in the source encoding, soString#encodewithundef: :replacereplaces it:"café".b.encode(::Encoding::UTF_8, invalid: :replace, undef: :replace)returns"caf"followed by two U+FFFD replacement characters — one per byte of the two-byte UTF-8é— on all three interpreters, and is not==to the correct answer. The bytes must be retagged to the declared charset first, which is whatDexpace::IO::TypedReads#read_string(encoding)already does, and only then transcoded. (b) The cited call takes no target argument, andString#encode(invalid: :replace, undef: :replace)with none converts toEncoding.default_internal— a process-global the host application sets. WithEncoding.default_internal = ::Encoding::ISO_8859_1, the same call on the same string returns an ISO-8859-1 result with the accented character destroyed, while the explicit-target form is unaffected; verified both ways on all three. That is the same passes-where-you-look shapedocs/sdk-design-ruby/03-seam-by-seam-idiomatic-mapping.md§3.5 pinsURI::RFC3986_PARSERagainst and thatIO-14avoidsIO#getsand$/for: a library must not let a host global decide what its decode returns. What the SDK does instead, perdocs/work/mvp/phase3/phase3b/2026-09-08-phase3b-body-lifecycle-design.md: resolve the charset fromDexpace::MediaType#charset, which is alreadynilfor an absent or unknown-to-this-Ruby charset soEncoding.findcannot raise andHTTP-42's fallback needs no second validation; retag through 3a's#read_string; then#encode(enc, invalid: :replace, undef: :replace)with both encodings named. The result isvalid_encoding?in every case tested — UTF-8 bytes declared UTF-8, ISO-8859-1 bytes declared ISO-8859-1, and ISO-8859-1 bytes mis-declared as UTF-8, which scrubs to a replacement character rather than raising. On phase 10's inbound list as §3.1's decode sentence, because the sentence in that frozen design chapter is what a later reader will copy.io-and-byte-streams/d2b47c89is unaffected and is adopted verbatim. review ·docs/work/mvp/phase3/phase3b/2026-09-08-phase3b-body-lifecycle-design.md· high · sha:manual-phase3b-decode-retag-then-transcode