Skip to content

Phase 8a: synchronous transport — documentation and phase record - #92

Merged
Wahbeh-Mohammad merged 22 commits into
mainfrom
30-phase-8a-synchronous-transport-and-conformance-docs
Sep 21, 2026
Merged

Wahbeh-Mohammad merged 22 commits into
mainfrom
30-phase-8a-synchronous-transport-and-conformance-docs

Conversation

@Wahbeh-Mohammad

Copy link
Copy Markdown
Contributor

Closes #30. Third PR of phase 8a's stack — the phase record and the documentation — on top of #91. Umbrella #29 stays open: 8b and 8c remain.

What lands

14 files on top of phase 7's main (the 8a record plus the reconcile pass's dated paragraphs).

  • docs/work/mvp/phase8/phase8a/2026-09-11-phase8a-synchronous-transport-and-conformance-checklist.md — twenty-three own rows (twenty-two ✅, TRANSPORT-28 partially ✅ with its zero-copy clause ⏳ under docs/first-release.md) plus the phase-level Dexpace::TransportError row and sixteen cross-reference rows (PAGE-36's adapter half, OBS-21/OBS-25, HTTP-17/18, XCUT-18, CFG-15/16/22–28, …); the roadmap's legend verbatim; sections: requirement rows, what was built, guards run red (47), audit groups run, deviations from the plan (43), findings routed, postponed work.
  • The design's As-built addendum, ledger rows P8-51–P8-65 beside the design's own P8-1–P8-15 (the charter's bands hold: 8b's as-built rows start at P8-71, 8c's at P8-91). The consolidation into design §10 is a human's — docs/sdk-design-ruby/ is frozen.
  • docs/sdk-documentation/transport-net_http.md and conformance.md (new; every block executed on 4.0.6 under net-http 0.9.1 and 3.2.11 under 0.4.1, identical but for Hash#inspect spelling and the Timeout thread count; the leads name the six shorthand aliases after require "dexpace/conformance"), architecture.md, the two gems' READMEs, README.md, docs/README.md.
  • docs/knowledge/notes/transport-adapter.md — one new reference entry: the net-http 0.9.x facts beside the design's 0.6.0-only measurements (no default Content-Type; TCPSocket.open(open_timeout:) connect), and that net-http 0.9.1 is 4.0.6's default gem under Bundler (round 2's R2-3 corrected the opposite claim).
  • docs/first-release.md — existing entries only: P6-4's adapter-wrapping blocker ticked; TRANSPORT-28's zero-copy decline confirmed on all three versions; the conformance-suite and P8-9 blockers dated; the Minitest 6 entry narrowed.
  • CLAUDE.md — the built-phases sentence gains 8a ("the first of phase 8's three sub-phases"); "Nothing talks to a socket yet" becomes the synchronous transport's sentence; the gem table's net_http and conformance rows and the skeleton clause (two skeletons remain); the command block; a block of 8a "Constraints that will bite" lines (the one-socket-per-Net::HTTP rule, the producer's ensure, the no-break read_body loop, the closed-queue classification, the chunked-length sentinel); every count re-derived from the combined tree by the reconcile pass — 220 lib files beside version.rb, 220 sig/ mirrors, the same nineteen private_constant test-mirror exceptions (8a adds none; dexpace-conformance's three are named), eighteen checklists, nineteen pages, eighteen gates, 55 phase-10 inbound bullets.
  • The roadmap's phase-8a status note (append-only, with its four review-round paragraphs), its dated phase-10 inbound bullets, the dated closure of 7a's clean_bundle bullet, and the reconciliation paragraph.

docs/product-spec/, docs/sdk-design-ruby/, docs/knowledge/harvested/, docs/deviations.md and every other phase's documents are untouched (6b's load-sensitive REDIR-23 test deliberately left alone, routed).

Reconciliation onto phase 7's main

This stack was reviewed on c53638b and rebased onto 734e6b3 (phases 7b, 7c and 7a) by a reconcile pass: three code conflicts resolved inside the commits that caused them (seam_surface_test.rb to main's version exactly — see the code PR), six prose collisions (CLAUDE.md, README.md, docs/README.md, architecture.md, the roadmap, docs/first-release.md's auto-merge verified hunk by hunk) resolved by keeping every lane's content in merge order with every count re-derived from the tree; Task 19c's codec half un-guarded and green against 7a's real codec; 7a's clean_bundle inbound bullet closed by 8a's Task 23; 7a's owed gems/dexpace-core/README.md paragraph paid here. 8a's own status note (kept verbatim) calls this lane "the first code outside dexpace-core" — true on its base, false on main, where 7a's dexpace-serde-json landed first; the checklist's dated "Reconciled 2026-09-21" note and the roadmap's reconciliation paragraph say so. One 7a bullet stays open as written: rbs_collection.yaml gained no row from 8a (net-http is a default gem with rbs's own stdlib signatures).

Verification

Docs tip (rebased): bundle exec rake (eighteen gates) green on 4.0.6; ruby .claude/skills/housekeeping/probe.rb exit 0 (all eight checks); ruby scripts/verify_knowledge_structure.rb OK; the housekeeping and knowledge tooling suites green; both new pages' blocks run on 4.0.6 and 3.2.11.

Known follow-ups

R0-12 (deferred, commit subjects) and the routed items — see the code PR's list.

…se 8a)

dexpace-transport-net_http: Dexpace::Transport::NetHTTP with .build over a fresh-per-call Net::HTTP
and .using over a caller's own, the Adapter, the per-response ResponsePump with its head-adaptation
handshake, RequestMapper and ResponseMapper, Deadline, Failures, TLSSettings, ProxyRoute, the
require-time registration under :net_http, and the gemspec's net-http >= 0.4.

dexpace-conformance: the assertion protocol phase 0 postponed (Failure, Vacuous, Assertion, Result,
Report), the twenty-eight-assertion TransportSuite, TransportCase with its SettleOnly guard,
BorrowedPair, the WireServer fixture and its Scripts, MinitestDriver, the opt-in RSpecDriver, and the
RecordingSpan and Allocations doubles.

dexpace-core: Dexpace::TransportError < ::IOError, Configuration::Keys::REQUEST_TIMEOUT and
Instrumentation::Events::TRANSPORT_HEADER_DROPPED, with the pins the two constants moved.

Repository: tempfile on the require allowlist with its fixture, BUNDLE_PATH scoped in the
clean-bundle gate, socket and tempfile on the conformance Steep target, the surface manifests
regenerated once, the net-http Timeout-thread warm-up both adapter gems' test helpers require, and
the bare-require helper core's repaired seam-surface test needs.
Phase 8a, review round 0's three code-branch nits. R0-6: the adapter's
ResponseMapper::LENGTH and the conformance fixture's RequestReader length
check were regexp literals a wire value reaches; both are now
::Regexp.new('\A[0-9]{1,15}\z', timeout: 1.0).freeze, core's spelling for
every such pattern (PacingParsers, P6-61), so no header hands #to_i an
unbounded digit run -- a sixteen-digit Content-Length is the unknown-length
sentinel like any other value the grammar refuses. R0-7: the rewritten swap
pin in core's transport_test.rb compared a Proc with an Array and could not
fail; it now snapshots Transport.registered_keys before the swap and asserts
the same list afterwards. R0-10: Allocations.delta re-enabled the collector
unconditionally in its ensure; it now records what GC.disable answered and
re-enables only when it found the collector running, so a host that had GC
off around the call finds it off afterwards. The request_reader sig gains
the private LENGTH declaration the Steep target needs.
Cancellation::Source runs an already-cancelled hook inline, so the
pump's own on_cancel { close } could run #release from inside the
constructor before @subscription was assigned, and #release then
raised NoMethodError on nil.detach after joining a producer that had
already put the request on the wire (review round 1, R1-2). On the
borrowing construction that raise reached Adapter#dispatch's rescue,
which pushed the permit a second time onto a full SizedQueue.

ResponsePump#initialize now asks the token first: a cancelled token
gets a closed pump with no thread started, no socket opened and the
borrowed permit handed straight back, exactly once. Otherwise the
thread is started and the subscription registered after it, as before,
with both slots initialised to nil so the inline #release a cancel
racing the registration triggers finds them; #release joins and
detaches nil-safely, and #produce reads the latch before it exchanges,
so a pump closed before its producer was scheduled never touches the
socket. The RBS types both ivars optional and declares the new private
start_producer. Recorded as P8-64.
Net::HTTP.new's default p_addr is :ENV, which consults URI#find_proxy
on #start, and uri's find_proxy warns "The environment variable
HTTP_PROXY is discouraged" whenever HTTP_PROXY is set and the lower-case
http_proxy is not -- before its loopback exemption and before NO_PROXY
is read (uri 0.12.5 through 1.1.1, every row). DexpaceTestCase makes
every warning fatal, so on such a host test/support/net_http_warmup.rb,
required by both adapter gems' test_helper, aborted every suite of both
gems and rake test:gems at load (review round 2's R2-1, measured on
4.0.6 under HTTP_PROXY=http://127.0.0.1:9 with no http_proxy).

Build the warm-up client the way the adapter builds its own through
ProxyRoute: an explicit nil for the four proxy positionals. The comment
records the mechanism and the rule the tests branch applies to every
raw Net::HTTP a suite starts, so the suites are hermetic under
HTTP_PROXY, http_proxy, HTTPS_PROXY and NO_PROXY alike; the two R17
controls that reach :ENV on purpose set http_proxy themselves.
ResponseMapper's YARD already says what an HTTP/1.0 head does and whose
gap that is. Say the same for the status: phase 1's Status guards
100-599 (HTTP-10's reading), Net::HTTP parses any three digits and
delivers a 999 or a 600 as an HTTPUnknownResponse, and such a head
raises Dexpace::InvalidArgumentError after the head with the connection
released (measured 2026-09-20; review round 2's R2-2). Whether
TRANSPORT-24's "any code" reaches 600-999 is a phase-1 model question,
routed to phase 10's inbound list on the docs branch. A comment only:
no behaviour, gate or surface changes.
Review round 3's R3-1. ResponseMapper#parse_length! took any single,
grammar-matching Content-Length as the body's length, but net-http reads
a response carrying Transfer-Encoding: chunked under the chunked framing
whatever else the head says -- read_body_0 asks #chunked? before
#content_length on 0.4.1, 0.6.0 and 0.9.1 alike -- so the value was RFC
9112 section 6.3's overridden one and never the number of bytes the pump
would deliver. Every reader that copies exactly `content_length` bytes
(ResponseBody#each, #write_to, #to_replayable) therefore truncated a
chunked body silently when the header was short ("0123456789" delivered
as "01234" under Content-Length: 5) or raised StreamError.short_transfer
when it was long, while #body_string, which drains to end of stream,
read all of it. TRANSPORT-27 maps an invalid Content-Length to the
unknown-length sentinel, and a Content-Length the framing overrides is
the invalid case R4's grammar could not see.

The guard gains `!native.chunked?` -- Net::HTTPHeader#chunked?, the
predicate read_body itself consults -- so such a header is the -1
sentinel like any other value the mapper refuses: the pump reads to end
of stream and every reader delivers the whole body. The native delete in
that branch changes nothing read_body consults under a chunked encoding
and is kept for the one-branch shape. The wire value still reaches the
caller's Dexpace::Headers, as before. R4's comment states the case.
Measured through the real adapter against WireServer on 4.0.6: both
directions now answer -1 and deliver ten bytes through #each, #write_to
and #to_replayable.
…conformance gem

The adapter's ten suites (adapter, response pump, request and response mappers, deadline, failures,
TLS settings, proxy route, the conformance driver against the real adapter, the generator slice
with its guarded codec half, and the matrix facts printing each row's active net-http version) with
AdapterFixtures and NetHTTPRecordingSink; the conformance gem's nineteen suites with the
RawWireTransport double and its twenty named defects, NonConformingTransport, StubTransport and
AssertionProbe; core's TransportError suite and the bare-require registry tests; and the allowlist
gate test's comment naming tempfile.
Phase 8a, review round 0. R0-1: the TRANSPORT-22 adaptation-failure fixture
declares Content-Length: 100, sends two bytes and holds the connection, so
only Adapter#dispatch's close can release it -- mutation H (the guard
removed) now fails the bounded await and strands the producer the test
base's thread count catches. R0-2: the two clamp assertions in
deadline_test.rb carry an explicit 0.0 delta, since Minitest's default
0.001 is MIN_TIMEOUT_SECONDS exactly and swallowed mutation N. R0-3: the
subscription test reads the Cancellation::Source's hook list, one longer
while the pump is open and back to its size after the close, the way
core's own cancellation suite observes a bounded registration; mutation E
(the detach removed) now fails. R0-8: the plain-http TLS test's comment
and name say what it proves -- a tls: hash is inert on a plaintext call --
and record mutation X as unobservable. R0-9: the origin-401 test configures
a credentialled proxy through the chain and lets the fixture play it: one
request carrying the preemptive Basic, a 401, and no second request.

Three guards for the code branch's round-1 lines: the sixteen-digit
Content-Length that ResponseMapper's bounded grammar maps to the sentinel
and RequestReader's frames as no body, each grammar's per-pattern timeout,
and a collector the host had disabled staying disabled after
Allocations.delta. Each was run red against its reverse mutation.
Phase 8a, review round 1. The suite's header said every configuration is a
hermetic from_hash source; since round 1 the origin-401 test drives the
whole adapter through Dexpace.configure's override tier -- above the
environment -- and resets it in an ensure, and the header now says so.
Review round 1's R1-1: the two tests that drive the whole adapter
through Dexpace.configure overrode HTTP_PROXY alone over
Configuration::EMPTY, whose environment tier is the real ENV, so a
host HTTPS_PROXY won the resolver's preference and a host NO_PROXY
covering the target bypassed the fixture. Both now override all three
keys the resolver reads, and the origin-401 test's comment gives the
real reason for its TEST-NET-1 target. Measured while fixing it: with
HTTPS_PROXY=http://127.0.0.1:9 exported, fifteen of the adapter
suite's thirty-four tests routed their loopback fixture through that
proxy, because every owning adapter resolves its proxy through the
process-wide configuration. NetHTTPHermeticProxy blanks the three
keys through the override tier around every test that includes
AdapterFixtures and around the conformance driver, never touching
ENV, and the two Net::HTTP :ENV controls clear no_proxy for the
library's own find_proxy. The gem's suite is green under
HTTPS_PROXY=http://127.0.0.1:9 NO_PROXY=192.0.2.1 and under a clean
environment alike.

R1-2: two tests in a fourth nested class of the pump suite build a
pump over an already-cancelled source, against a fixture that holds
before headers: the pump comes back closed, its first read is the
cancellation, no connection reached the fixture and, on the borrowing
construction, the permit is back exactly once. Both are red against
the unfixed pump (NoMethodError on nil.detach plus a leaked producer)
and against the nil-safe release alone (one connection, a producer
outliving the join, the permit held). The bounded-join source scan
admits the nil-safe spelling and still refuses an unbounded join.
With the pump refusing a cancelled token on its own (P8-64), the
adapter suite's "already-cancelled token is refused before anything
reaches the wire" case no longer told Adapter#perform's
cancellation.check! from the pump's: removing the check left the same
CancelledError and the same zero connections. P8-52's claim is that
the check runs before anything is MAPPED, so the case now sends a
request carrying a managed header through an adapter with a recording
sink and asserts that no TRANSPORT_HEADER_DROPPED event was logged.
Mutation B is red again: the Expect drop is logged when the check is
gone.
Review round 2 measured that under an upper-case HTTP_PROXY with no
lower-case http_proxy, every raw Net::HTTP the suites start through
Net::HTTP.new's :ENV default hit uri's "HTTP_PROXY is discouraged"
warning, which the test base makes fatal: 3 failures and 17 errors in
the adapter gem and 6 and 14 in the conformance gem once the warm-up
alone was fixed. Pass the four proxy positionals as nil, exactly as
the adapter's ProxyRoute does, in AdapterFixtures#client_for, the
conformance driver's borrow factory, and the Net::HTTP.start calls in
wire_server_test.rb and scripts_test.rb. The two R17 controls keep
:ENV on purpose and set the lower-case name themselves.

Two guards make the property host-independent: adapter_test.rb's
ProxyTest drives a client_for client through the borrowed adapter with
HTTP_PROXY exported and http_proxy cleared, and wire_server_test.rb's
new PlainClientTest does the same through Fetch#get. A fixture reverted
to :ENV turns each red with the warning itself, whatever the host's
environment says.
NetHTTPHermeticProxy blanks the SDK's three configuration keys and
nothing else; the library's own :ENV default is the other reader of the
environment, and the fixtures keep off it by passing an explicit nil
proxy. The module's comment now names both halves, the uri warning that
makes the second one necessary, the fixture sites that honour it, and
the two guards that hold it on any host (review round 2's R2-1).
…join

Review round 3's R3-1, R3-2 and R3-3, on the tests layer.

R3-1: response_mapper_test.rb's BodyTest gains a case over a native
head carrying both Transfer-Encoding: chunked and a Content-Length of 5,
asserting the -1 sentinel, the raw header in the caller's Headers and
the whole ten-byte body through #body_string; a coding LIST ("gzip,
chunked") answers -1 too, because the predicate is net-http's own
#chunked?. adapter_test.rb's WireTest drives the same head through the
real adapter against WireServer in both directions -- a short header
through #each, which truncated to "01234", and a long one through
#to_replayable, which raised StreamError.short_transfer -- and asserts
the whole body from each. With the code fix reverted the unit case
fails on the sentinel and the wire case errors with the StreamError.

R3-2: response_pump_test.rb's TeardownTest gains the two cases the
closed-pump guard at the top of #readpartial lacked: a one-byte read
leaves residue in the pump, then a close (resp. a cancel through the
token) and the next one-byte read raises ClosedError (resp.
CancelledError with the reason) instead of serving the residue. Every
earlier test drained with a large maxlen or through #body_string, where
the residue is empty and #fill's own check fires; with the #readpartial
guard removed both new cases fail with "nothing was raised".

R3-3: wire_server_test.rb's "promptly" test takes its thread baseline
BEFORE WireServer.start and asserts the count is back to it after
#close (both threads gone, not merely one), with the two-threads-alive
count asserted before the close; and a source-scan case, the shape the
pump's JOIN_DEADLINE test uses, asserts the accept-thread join, the
per-handler join and that no join in the fixture is unbounded -- on MRI
IO#close on the accepted socket usually lets the handler finish before
#close returns, so the count alone tells the join from its absence only
when the handler loses that race (three of five runs here); the scan
tells them apart every time.
… tree

The slice test's third case names phase 7a's Dexpace::Serde::JSON::Codec,
which 8a's base did not carry, so it was written under a `skip` guard
that could never run. Reconciling the 8a stack onto the tree that holds
the whole of phase 7 removes the guard and runs the case for real.

Two repairs by the minimum, both to the test alone: an explicit
`require "dexpace/serde/json"` beside the existing cross-gem require of
the conformance gem, because nothing else on the adapter's load path
defines the codec; and the three lines that read the mapped error's
headers and body, written against 4b's design names (`#headers`,
`#body`, `#read_fully`), now read `#response.headers` and
`#response.body_string` -- the as-built ProtocolError carries the
buffered response, and body_string over BODY-30's BufferBody is what
makes "readable twice after the socket is gone" a real assertion.

The suite's skip count drops from two to one: the remaining skip is
conformance_test.rb's TRANSPORT-18 vacuity, reported as a Minitest skip
by design §9.3.
…s-built pages and the phase record

The checklist (one row per ID, the roadmap's legend), the design's As-built addendum, the knowledge
note's Reference entry, docs/sdk-documentation/transport-net_http.md and conformance.md with every
example run on 4.0.6 and 3.2.11, architecture.md, both gem READMEs, README.md, docs/README.md,
CLAUDE.md's re-derived claim sentences and constraints, first-release.md's touched entries, the
roadmap's dated status note and three dated inbound bullets for phase 10.
Phase 8a, review round 0's documentation findings and the round-1 record.
R0-4: every block of docs/sdk-documentation/transport-net_http.md is now
runnable from the page alone -- the keep-alive script is written out, the
block that shows a close builds its own adapter, the header-policy block
sends the four POSTs whose heads it reads back, the budget and failure
blocks show their rescues, the refused connection targets a fixture's
closed port, the proxy block resets the configuration -- and the lead
states the four helpers and the per-block setup once. The eleven blocks
were run in order as one script under -w on 4.0.6 (net-http 0.9.1) and on
3.2.11 under both 0.4.1 and 0.9.1, zero mismatches. R0-11: the checklist
names wire_server/recorded_request.rb as the public file proven through the
fixture's suite and not a mirror, P8-59 lists MinitestDriver.drive and
.method_name_for beside .build_case, and the knowledge note's list of the
facts that did not move is introduced as the six it is.

The checklist gains guard rows 31-36 (round 0's three survivors H, N and E
made red, and the three guards for round 1's own code lines), each run on
4.0.6 and 3.2.11, a paragraph recording X as unobservable, departures 37
and 38, and the corrected TRANSPORT-3, -6, -22, -27 and -30 rows; the
design's As-built addendum gains P8-63 -- the inbound Content-Length
grammar as a bounded, timed Regexp.new, superseding the cross-cutting
constraint's "carries no timeout:" sentence -- and the roadmap's status
note follows the band to P8-63.
…lead

Review round 1's four findings, recorded where each lands. P8-64
joins the design's As-built addendum: the pump asks the token before
it starts a producer, so a token already cancelled at construction
gets a closed pump with no thread, no socket and the borrowed permit
straight back, where the reviewed pump raised NoMethodError on a nil
subscription from inside its constructor. The checklist's TRANSPORT-3
and TRANSPORT-30 rows and its CFG-22..CFG-28 cross-reference row say
which two tests drive the process-wide configuration and that every
key the resolver reads is overridden there, and that every
owning-adapter test runs under NetHTTPHermeticProxy because a host
HTTPS_PROXY routed fifteen of the adapter suite's thirty-four tests
through it; guards 37-41 record the round's mutations, including the
round-0 mutation B that P8-64 had made equivalent and its rewritten
guard; departures 39-42 itemise the changes; the roadmap's status note
carries the band to P8-64 and phase 10's inbound list gains 6a's
interleaving-dependent RETRY-42 / RECOV-28 test beside the REDIR-23
bullet, by date and content. The transport page's lead names all six
of its shorthand names, WireServer and Scripts included, and says what
a host proxy variable does to its socket blocks. CLAUDE.md's pump
constraint gains the P8-64 clause.
Completing fix round 2 after the session boundary: re-running the
round's mutations at the tests tip found guard row 39 stale. With
NetHTTPHermeticProxy's setup in place, reducing the two chain-driven
tests' overrides to HTTP_PROXY alone survives under
HTTPS_PROXY=http://127.0.0.1:9 NO_PROXY=192.0.2.1 (9/9 and 34/34
green), because Dexpace.configure composes over the live slot and the
module's blanks still stand beneath the test's own override; the
reduction is red only with the module's setup also removed, which is
the round-1 failure itself. Row 39 now says exactly that, the
paragraph after the table counts 37, 38 and 40 as the load-bearing
rows with 39 red on top of 40, and the roadmap's status note counts
round 2's guard rows as 37-41 rather than 37-40. The per-test lines
stay: each test is hermetic read on its own, and the equivalence is
recorded rather than made red.
Review round 2's four findings, recorded where each belongs.

R2-1: the CFG-22..CFG-28 cross-reference row says which half of the
proxy hermeticity NetHTTPHermeticProxy covers and why the library's own
:ENV default must never be reached from a fixture -- uri's find_proxy
warns on an upper-case HTTP_PROXY before its loopback exemption and the
test base makes that fatal -- with the sites, the six spellings both
gems' suites are green under, and the two host-independent guards;
guard rows 42-44 and deviation 43 carry the mutations and the reason;
CLAUDE.md's Net::HTTP constraint line and the transport page's lead
gain the clause.

R2-2: the TRANSPORT-24 row no longer claims totality over every
three-digit code Net::HTTP parses. The mapping is total over 100-599,
phase 1's Status guard, and a 999 or 600 head raises
InvalidArgumentError after the head with the connection released, as
an HTTP/1.0 head does; measured with 999, 600, 099, 599 and 99. The
design's As-built addendum states the bound, the as-built page's
"not lenient" paragraph gains it, and phase 10's inbound list gains a
dated bullet in the shape of the "http/1.0" one: a phase-1 model
question, not this adapter's to widen.

R2-3: the knowledge note's 0.9.x entry and the checklist's matrix
paragraph said net-http is no longer a default gem on 4.0.6. It is
(the default specification is shipped, BUNDLED_GEMS::SINCE has no row);
what they saw was this machine's installed copy beside it, which
RubyGems prefers outside Bundler and Bundler does not. Corrected inside
8a's own entry, never a harvested file.

R2-4: the roadmap's status note counts forty-three departures
(thirty-six from the build, two from round 1, four from round 2, one
from round 3) and names round 3's guard rows and the fifth dated
inbound bullet.
The documentation half of review round 4, the manager-sanctioned
targeted round after round 3's four findings.

R3-1: the checklist's TRANSPORT-27 row states the case R4's grammar
could not see -- a well-formed Content-Length beside Transfer-Encoding:
chunked is the unknown-length sentinel, because net-http frames such a
response by the chunked coding and the value was never the number of
bytes the pump delivers -- with the truncation and the StreamError the
reviewed mapper produced, the fix, the unit and wire cases and guard 45;
the design's As-built addendum gains P8-65, widening R4's list of -1
cases with the reason; the transport page's inbound-mapping paragraph
gains the clause and a runnable example (55 checks over the page, the
reviewer's 53 plus these two, the only mismatches the ephemeral port the
lead already names); the checklist's Deviations gain item 44 and both
P8 range sentences move to P8-65.

R3-2 and R3-3: guard rows 46 and 47 record the two mutations round 3
found surviving -- the closed-pump guard at the top of #readpartial,
now red through two small-read cases, and WireServer#close's handler
join, red on every run through the source scan and on most runs
through the tightened thread count -- with a round-4 paragraph saying
why the scan is the guard and the count is the property.

R3-4: the post-table paragraph names the other half of the 38pre pair
(the latch read in #produce removed with the pre-check kept) as
surviving the same way and for the same reason, and says why both
halves are kept.

The roadmap's 8a status note counts forty-four departures, names rows
45-47 and P8-65. The probe is clean.
Phase 8a was built off c53638b concurrently with 7a, 7b and 7c, and the
three phase-7 stacks merged first. This commit carries what no rebased
8a commit could express: the dated reconciliation paragraph in the
roadmap, the dated "Reconciled" note at the head of 8a's checklist with
the in-row note on the un-guarded generator-slice row, one new bullet on
phase 10's inbound list (two child-process idioms now prove one
property, seam_surface_test.rb's private helper beside 8a's shared
BareRequire module, after main's version of that file was kept byte for
byte), the dated closure of 7a's gates:clean_bundle bullet, which 8a's
Task 23 closes on this tree, and the serialization-layer paragraph 7a's
own reconciliation recorded as owed in gems/dexpace-core/README.md,
beside a sentence for 8a's three core additions.

Every count in it is re-derived from the combined tree: 220 lib files
beside version.rb with 220 sig mirrors, the same nineteen private-
constant test-mirror exceptions, eighteen checklists, nineteen as-built
pages, the core manifest at 1,335 rows, fifty-five inbound bullets.
@Wahbeh-Mohammad Wahbeh-Mohammad added type:feature New capability or enhancement area:core Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-* labels Sep 21, 2026
@Wahbeh-Mohammad

Copy link
Copy Markdown
Contributor Author

Review record for the phase 8a stack (#90 → #91 → #92)

5 independent reviews — the four-review cap, then one manager-sanctioned targeted round because round 3's R3-1 was a real code defect (6c's precedent) — each by a fresh agent with no memory of the previous one, each re-running every gate itself on every tip (4.0.6 every gate individually at the code tip and the full rake at the tests and docs tips; the matrix set on 3.2.11, 3.3.12 and 3.4.10) and applying its own mutations on both interpreters, with a fix round by a fresh agent between each. The stack was cut from main at c53638b.

Round Verdict Blocking Should-fix Nits Mutations (caught) Disposition
0 changes required 0 4 8 50 (44) all 12 addressed in fix round 1
1 changes required 0 2 2 48 (46) all 6 addressed in fix round 2
2 changes required 0 3 1 55 (51) all 6 addressed in fix round 3
3 changes required 0 2 2 52 (47) all 6 addressed in fix round 4
4 (final) approve 0 0 0 39 (35) carried into the PR bodies

Nothing was skipped.

Round 0 → fixed in round 1

  • R0-1 should-fix — gems/dexpace-transport-net_http/test/dexpace/transport/net_http/adapter_test.rb:349: TRANSPORT-22 adaptation-failure test does not discriminate: the guard can be deleted and it stays green. Fixed on tests (c2aa7ed): adapter_test.rb TRANSPORT-22: Content-Length: 100 with two bytes sent and the connection held, so only the dispatch rescue's close_quietly(pump) can release it. Mutation H now fails twice over on 4.0.6 and 3.2.11: await_closed_connection(timeout: 2) answers ni
  • R0-2 should-fix — gems/dexpace-transport-net_http/test/dexpace/transport/net_http/deadline_test.rb:59: The TRANSPORT-6 clamp assertion is vacuous: assert_in_delta's default delta equals MIN_TIMEOUT_SECONDS. Fixed on tests (c2aa7ed): deadline_test.rb: both clamp assertions carry an explicit 0.0 delta (6a's spelling) with the default-delta hazard stated in the comment. Mutation N now fails on both rows: 'Expected |0.001 - 0.0005| (0.0005) to be <= 0.0'. Checklist guard row 32 and the TRANSP
  • R0-3 should-fix — gems/dexpace-transport-net_http/test/dexpace/transport/net_http/response_pump_test.rb:145: P8-52's detach is untested: the subscription-detached test passes with @subscription.detach removed. Fixed on tests (c2aa7ed): response_pump_test.rb: the test reads the Cancellation::Source's @hooks size (the idiom core's cancellation_test.rb:219 uses) — before + 1 while the pump is open, back to before after pump.close — then cancels late and asserts closed?. Mutation E (detach remov
  • R0-4 should-fix — docs/sdk-documentation/transport-net_http.md:80: Transport page examples are not reproducible from the page: undeclared keep-alive server/url, an adapter closed then reused, wire requests never shown. Fixed on docs (e928dac): transport-net_http.md: the lead states the four helpers exactly (req with its defaults over Request.build, headers over Headers.builder, NetHTTP, EMPTY), that adapter is the first block's owning adapter never closed, that every socket block starts its own se
  • R0-5 nit — docs/work/mvp/phase8/phase8a/2026-09-11-phase8a-synchronous-transport-and-conformance-checklist.md:57: Two skips at the tests tip and TRANSPORT-18 marked ✅-vacuous, against the brief's one skip and one N/A. Deferred to pr body on docs (``): Tree unchanged, as the reviewer allowed: MinitestDriver maps Vacuous to a Minitest skip by design §9.3 (a vacuity must stay visible in a green run), 6b's REDIR-25 precedent marks a vacuous row ✅, and P8-55/departure 25 record it; the second skip is the manager
  • R0-6 nit — gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/response_mapper.rb:23: LENGTH is a regexp literal a wire value reaches; core's convention is Regexp.new(source, timeout:) even for linear patterns. Fixed on 30-phase-8a-synchronous-transport-and-conformance (b3684ec): ResponseMapper::LENGTH and a new private RequestReader::LENGTH are ::Regexp.new('\A[0-9]{1,15}\z', timeout: 1.0).freeze (declared in request_reader.rbs); parse_length! and read_body use them, so a sixteen-digit run is the -1 sentinel (native header deleted,
  • R0-7 nit — gems/dexpace-core/test/dexpace/transport_test.rb:52: The rewritten swap pin asserts a lambda is not the same object as an Array. Fixed on 30-phase-8a-synchronous-transport-and-conformance (b3684ec): transport_test.rb 'swap scopes an override to its block': keys_before = Transport.registered_keys before the swap, assert_equal(keys_before, Transport.registered_keys) after; Registry#registered_keys is @state.factories.keys, a fresh array over an immutable Da
  • R0-8 nit — gems/dexpace-transport-net_http/test/dexpace/transport/net_http/tls_settings_test.rb:74: The 'tls: is not applied to a plain-http request' test cannot observe what it claims; its comment's reasoning is wrong. Fixed on tests (c2aa7ed): tls_settings_test.rb: renamed 'a tls: hash is inert on a plain-http request' and the comment says what is proven (the plaintext call answers ok with a bogus ca_file and VERIFY_PEER configured), that #apply's reach is unobservable without a handshake, that the
  • R0-9 nit — gems/dexpace-transport-net_http/test/dexpace/transport/net_http/proxy_route_test.rb:120: TRANSPORT-30's 401 clause test runs with no proxy configured, so the assertion is trivially true. Fixed on tests (42d4d16): proxy_route_test.rb: the 401 test configures HTTP_PROXY = http://u:pw@127.0.0.1: through Dexpace.configure's override tier (reset_config! in ensure), targets TEST-NET-1 (find_proxy exempts loopback), lets the fixture play the proxy with Scripts.vendor
  • R0-10 nit — gems/dexpace-conformance/lib/dexpace/conformance/allocations.rb:67: Allocations.delta re-enables GC unconditionally in its ensure. Fixed on 30-phase-8a-synchronous-transport-and-conformance (b3684ec): Allocations.delta: was_disabled = ::GC.disable then a begin/ensure with ::GC.enable unless was_disabled; the iterations validation raises before the disable so the ensure never runs on a nil. Guard on the tests branch ('a collector the host had disabled st
  • R0-11 nit — docs/work/mvp/phase8/phase8a/2026-09-11-phase8a-synchronous-transport-and-conformance-checklist.md:32: Three small documentation inaccuracies: the 'every public one a test/ mirror' sentence, P8-59's list, the note's 'Two facts'. Fixed on docs (e928dac): (a) checklist header: every public lib file but wire_server/recorded_request.rb has a test mirror; RecordedRequest is proven through conformance/wire_server_test.rb. (b) design P8-59 lists MinitestDriver.build_case and .drive (@api private) and .method_name_fo
  • R0-12 nit — CLAUDE.md:1: Commit subjects exceed the 74-character rule. Deferred to pr body on code (``): The three over-long subjects are the implementer's commits (75/84/103 chars), which a fixer never amends or rewrites; every round-1 subject is within the rule (72, 68, 68, 62). main's history is squash-merged PR titles, so the subjects are rewritten at merge;

Round 1 → fixed in round 2

  • R1-1 should-fix — gems/dexpace-transport-net_http/test/dexpace/transport/net_http/proxy_route_test.rb:130: The two chain-driven proxy tests depend on the real environment's HTTPS_PROXY/NO_PROXY: an HTTPS_PROXY on the host defeats the overridden HTTP_PROXY. Fixed on tests (8e0e35d): Pre-existing (interrupted fixer): proxy_route_test.rb's origin-401 test and adapter_test.rb's TRANSPORT-30 test override HTTP_PROXY, HTTPS_PROXY (blank) and NO_PROXY (blank); NetHTTPHermeticProxy (test/support) blanks the three keys through the override tier a
  • R1-2 should-fix — gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/response_pump.rb:49: ResponsePump#release dereferences a nil @subscription when the token is already cancelled at construction: Source#on_cancel yields the hook inline before the ivar is assigned. Fixed on 30-phase-8a-synchronous-transport-and-conformance (4ae90d4): Pre-existing (interrupted fixer): ResponsePump#initialize sets @thread/@subscription nil then start_producer(cancellation): a cancelled token gets close + @permit&.push(:permit) with no thread; otherwise the thread then the subscription; #produce returns if cl
  • R1-3 nit — docs/sdk-documentation/transport-net_http.md:22: The transport page's lead names four shorthand names but the blocks also use bare WireServer and Scripts (and need require "dexpace/conformance"). Fixed on docs (e4b03d7): Pre-existing: transport-net_http.md's lead names six shorthand names -- NetHTTP, WireServer and Scripts (Dexpace::Conformance::WireServer / ::Scripts after require "dexpace/conformance"), req, headers, EMPTY -- and says what a host proxy variable does to the s
  • R1-4 nit — docs/work/mvp/2026-09-05-ruby-sdk-v1-roadmap-design.md:2137: 6a's RETRY-42 / RECOV-28 eight-thread budget test is interleaving-dependent and errored in 2 of 5 whole-process 3.2.11 runs; unchanged by 8a, unrouted. Fixed on docs (e4b03d7): Pre-existing: the roadmap's phase-10 inbound list gains a dated 2026-09-20 bullet beside the REDIR-23 one naming DexpaceResilienceRecoveryRetryTest 'RETRY-42 / RECOV-28' with the measured 2-of-5 rate, the interleaving mechanism and what phase 10 may do; the ch

Round 2 → fixed in round 3

  • R2-1 should-fix — test/support/net_http_warmup.rb:22: Every raw Net::HTTP the suites build takes the :ENV proxy default, and an uppercase HTTP_PROXY on the host aborts both gem suites and rake test:gems at load. Fixed on 30-phase-8a-synchronous-transport-and-conformance (962ec69), 30-phase-8a-synchronous-transport-and-conformance-tests (090f834, 360dad4), 30-phase-8a-synchronous-transport-and-conformance-docs (e3fc02d) (962ec69): Reproduced exactly (load-time abort from net_http_warmup.rb:24 under HTTP_PROXY alone; 182 runs / 3 failures / 17 errors with the warm-up alone fixed). Code: test/support/net_http_warmup.rb builds its client with `::Net::HTTP.new(host, port, nil, nil, nil, nil
  • R2-2 should-fix — docs/work/mvp/phase8/phase8a/2026-09-11-phase8a-synchronous-transport-and-conformance-checklist.md:63: TRANSPORT-24's row claims status mapping 'total over the 3-digit codes Net::HTTP parses'; a 999 or 600 head raises Dexpace::InvalidArgumentError, unrecorded and unrouted. Fixed on 30-phase-8a-synchronous-transport-and-conformance-docs (e3fc02d); 30-phase-8a-synchronous-transport-and-conformance (58ca1c3, a YARD comment only) (e3fc02d): Re-measured through the real adapter against WireServer on 4.0.6: 999, 600 and 099 -> Dexpace::InvalidArgumentError "code must be an integer status code between 100 and 599", connection closed (await_closed_connection 1) and threads unchanged in every case; 52
  • R2-3 should-fix — docs/knowledge/notes/transport-adapter.md:80: The new knowledge-note entry and the checklist say net-http 'is no longer a default gem' on 4.0.6; it is (the interpreter ships specifications/default/net-http-0.9.1.gemspec), and the suite's own matrix line says so. Fixed on docs (e3fc02d): Re-measured on 4.0.6 under the bundle: net-http 0.9.1 default_gem? true, specifications/default/net-http-0.9.1.gemspec present, Gem::BUNDLED_GEMS::SINCE['net-http'] nil. docs/knowledge/notes/transport-adapter.md's 8a entry (sha:manual-phase8a-net-http-0-9) now
  • R2-4 nit — docs/work/mvp/2026-09-05-ruby-sdk-v1-roadmap-design.md:4060: The status note says 'thirty-six departures from the plan's text itemised'; the checklist now itemises forty-two. Fixed on docs (e3fc02d): The roadmap's 8a status note now reads "forty-three departures from the plan's text itemised -- thirty-six from the build, two from review round 1, four from round 2 and one from round 3" (the checklist's Deviations section numbers 1-43 after this round's item

Round 3 → fixed in round 4

  • R3-1 should-fix — gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/response_mapper.rb:78: A Content-Length beside Transfer-Encoding: chunked is taken as the body's length: #each/#write_to/#to_replayable truncate or raise StreamError while body_string reads the whole chunked body. Fixed on 30-phase-8a-synchronous-transport-and-conformance (20c00d0), 30-phase-8a-synchronous-transport-and-conformance-tests (4776940), 30-phase-8a-synchronous-transport-and-conformance-docs (6908c86) (20c00d0): Reproduced exactly through the real adapter against WireServer on 4.0.6/net-http 0.9.1 before changing anything (CL 5: content_length 5, body_string ten bytes, #each/#write_to/#to_replayable five; CL 50: StreamError short_transfer). Verified in both interprete
  • R3-2 should-fix — gems/dexpace-transport-net_http/lib/dexpace/transport/net_http/response_pump.rb:91: The closed-pump guard at the top of ResponsePump#readpartial has no test: removing it leaves response_pump_test green, and a small reader would be served buffered residue after a cancel. Fixed on 30-phase-8a-synchronous-transport-and-conformance-tests (4776940); checklist on 30-phase-8a-synchronous-transport-and-conformance-docs (6908c86) (4776940): response_pump_test.rb TeardownTest gains 'a closed pump refuses a small read even with residue buffered, as ClosedError' (Scripts.fixed('abcdef'), head, readpartial(1) == 'a', close, readpartial(1) raises ClosedError) and 'a pump closed by a cancel refuses a s
  • R3-3 nit — gems/dexpace-conformance/lib/dexpace/conformance/wire_server.rb:132: WireServer#close's per-handler join is unguarded: the 'promptly' test asserts Thread.list.size shrank, which the accept thread alone satisfies. Fixed on 30-phase-8a-synchronous-transport-and-conformance-tests (4776940); checklist on 30-phase-8a-synchronous-transport-and-conformance-docs (6908c86) (4776940): Both of the reviewer's suggestions: the 'promptly' test takes baseline = Thread.list.size BEFORE WireServer.start, asserts baseline + 2 alive after the handler entered its hold and assert_equal(baseline, Thread.list.size) after #close; and a new ClosingTes
  • R3-4 nit — docs/work/mvp/phase8/phase8a/2026-09-11-phase8a-synchronous-transport-and-conformance-checklist.md:105: The checklist's 38pre paragraph records one half of an equivalent pair; the other half (the latch read in #produce alone) survives the same way and is unrecorded. Fixed on docs (6908c86): The checklist's post-table paragraph after the guard table now names the other half of the belt-and-braces pair -- the latch read at the top of #produce removed with the cancelled? pre-check kept (review round 3's R3a) -- as surviving both AlreadyCancelledTest

Round 4 (final) — approve

What the final reviewer verified by experiment, both interpreters

  • Base check — main is 734e6b3; c53638b is an ancestor of main and git merge-base main <docs tip> is c53638b; 6b's redirect/step.rb and 6c's auth/step.rb present at the base; reviewed with the base pinned at c53638b as the brief inst
  • R3-1 through the real adapter against WireServer, fix in place and reverted (exp_r31.rb) — Committed tip, every row: Content-Length 5 and 50 beside Transfer-Encoding: chunked both give content_length -1 and "0123456789" through #each, #write_to (10 bytes), #to_replayable and body_string. Guard reverted (4.0.6)
  • R3-2 through the public path (exp_r32.rb): BufferedSource#getbyte, a cancel from the token, getbyte again — Intact: first byte "a", the second read raises Dexpace::CancelledError reason=:reviewer. Under mutation 46: the second read returns 98 ("b") -- the buffered residue served after the cancel, exactly the finding's scenario
  • R3-3: mutation 47 five times on 4.0.6 and three times on 3.2.11/0.4.1 — The source-scan case red on every run; the tightened baseline count and the leaked-thread teardown green in all five 4.0.6 runs and all three 3.2.11 runs (the fixer saw them red in 3/5 and 1/3) -- the race the checklist
  • R3-4: the belt-and-braces pair, each half alone and together — R3a (latch read in #produce removed, pre-check kept): 21 runs, 0 failures. 38pre (pre-check removed, latch read kept): 21 runs, 0 failures. Both removed (row 38): 3 failures (the two AlreadyCancelledTest cases and a leak
  • Exp 1: the active Net::HTTP::VERSION per row — 4.0.6 --local: 0.9.1 (default gem, specifications/default); 3.2.11 pinned: 0.4.1 (default gem); 3.2.11 --local with BUNDLE_PATH at the scratch bundle: 0.4.1 (the interpreter directory's installed 0.9.1 is invisible to an
  • Exp 2: POST with a body and no Content-Type, raw library with Warning.warn raising, then the adapter (exp_misc.rb) — 3.2.11/0.4.1: the raw library raised 'WARNED: .../net/http/generic_request.rb' before the request reached the wire; 4.0.6/0.9.1: no warning and no Content-Type line on the raw wire. Adapter on both rows: 'Content-Type: a
  • Exp 3: cancel from another thread once hang_after_headers flushed the head, through the adapter — Dexpace::CancelledError reason=:reviewer on both rows, never a 200.
  • Exp 4: DNS failure through the adapter (no-such-host.invalid) — 3.2.11: TransportError retryable=true phase=:connect cause=SocketError (Socket::ResolutionError undefined); 4.0.6: cause=Socket::ResolutionError.
  • Exp 8: 4 MiB round trip — 4194304 bytes, SHA256 equal to a raw Net::HTTP fetch of the same script on 4.0.6 and 3.2.11 (body_string's decode boundary tags the text/plain body UTF-8, the bytes identical).
  • Exp 12: the registry after require 'dexpace/transport/net_http' — registered_keys [] before, [:net_http] after; NetHTTP.default fresh per call; Transport.resolve a memoized Adapter; TransportError < IOError including Dexpace::Error and Suppressible, retryable? true, not a StreamError.
  • Exp 6 (through the conformance page): Thread.list.size around a full TransportSuite.run through the real adapter — 27 passed, 0 failed, 1 vacuous (TRANSPORT-18), threads at the end 1.
  • Docs pages — transport-net_http.md 55 checks with the five known port/prose differences, the new R3-1 block's [["5"], -1] and "0123456789" exact; conformance.md 27 checks, 0 mismatches; both on 4.0.6 at the docs tip.
  • CLAUDE.md counts re-derived from the docs-tip tree with git ls-tree — 185 core lib files under lib/dexpace/ beside version.rb, 187 sig = 187 lib .rb, net_http 9 lib files beside version.rb, conformance 23, 15 checklists, 11 phase directories, 6 gems -- every count sentence in the docs-tip
  • Export hygiene — After every mutation the tests export's gems/ tree diffs empty against a pristine git archive of 4776940 (diff -rq); the worktree's git status is empty at every switch; no .orig file left behind.

Tips reviewed at the final round: code 20c00d0, tests 4776940, docs 6908c86 on main c53638b.

The run, and the reconciliation after review

The run was cut off by a session boundary once (mid fix round 2, after its commits had landed; the round was completed by a fresh fixer briefed to verify and re-prove rather than redo) and paused once by the user (after fix round 4; review round 4 restarted from scratch). The four reviews above ran on the stack as built off c53638b; the four-review cap was reached with round 3 at 0 blocking / 2 should-fix / 2 nits, and because R3-1 was a real defect — a Content-Length beside Transfer-Encoding: chunked taken as the body's length, so #each / #write_to / #to_replayable silently truncated a chunked body while body_string read all of it — the manager added one targeted fix/review round (fix 4 20c00d0 / 4776940 / 6908c86; review 4 approve, 0 / 0 / 0, 39 mutations).

Phases 7b (#81–#83), 7c (#84–#86) and 7a (#87–#89) merged while this lane ran, so the stack was then rebased onto main 734e6b3 by a reconcile pass (wf_faa9a3b6-75c): three code conflicts resolved inside the commits that caused them (lib/dexpace.rb's blocks; the LAYERS table; seam_surface_test.rb resolved to main's version exactly, since 7a and 8a had converted the same two pins in different shapes — 8a's BareRequire module stays for its own suites and the unification is a phase-10 bullet), the manifests auto-merged to exactly the regenerated rows (core 1,335; net_http 17; conformance 100), six prose collisions resolved with every count re-derived from the tree, Task 19c's codec half un-guarded and green against 7a's real codec (two design-name repairs in the test; skip count now exactly one), 7a's clean_bundle bullet closed by 8a's Task 23, 7a's owed core-README paragraph paid. Re-proven on the rebased tips by the pass and again by the manager: all eighteen gates on 4.0.6 at every tip, honest RuboCop clean (669 files), probe clean, the matrix set on 3.2.11 / 3.3.12 / 3.4.10 at the tests tip (3,698 runs / 72,656 assertions / 1 skip / 99.88 %) and on 3.2.11 at the code tip (93.02 %). Rebased tips: code bb909ba, tests 705d864, docs 7c2e82e.

@Wahbeh-Mohammad
Wahbeh-Mohammad changed the base branch from 30-phase-8a-synchronous-transport-and-conformance-tests to main September 21, 2026 07:29
@Wahbeh-Mohammad
Wahbeh-Mohammad merged commit a7cfeb6 into main Sep 21, 2026
5 checks passed
@Wahbeh-Mohammad
Wahbeh-Mohammad deleted the 30-phase-8a-synchronous-transport-and-conformance-docs branch September 21, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-* type:feature New capability or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 8a: Synchronous Transport and Conformance

1 participant