Repository navigation
Phase 8a: synchronous transport — documentation and phase record - #92
Wahbeh-Mohammad merged 22 commits into
Conversation
…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.
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
Nothing was skipped. Round 0 → fixed in round 1
Round 1 → fixed in round 2
Round 2 → fixed in round 3
Round 3 → fixed in round 4
Round 4 (final) — approveWhat the final reviewer verified by experiment, both interpreters
Tips reviewed at the final round: code The run, and the reconciliation after reviewThe 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 Phases 7b (#81–#83), 7c (#84–#86) and 7a (#87–#89) merged while this lane ran, so the stack was then rebased onto |
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-28partially ✅ with its zero-copy clause ⏳ underdocs/first-release.md) plus the phase-levelDexpace::TransportErrorrow 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.docs/sdk-design-ruby/is frozen.docs/sdk-documentation/transport-net_http.mdandconformance.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 forHash#inspectspelling and theTimeoutthread count; the leads name the six shorthand aliases afterrequire "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 defaultContent-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'snet_httpandconformancerows and the skeleton clause (two skeletons remain); the command block; a block of 8a "Constraints that will bite" lines (the one-socket-per-Net::HTTPrule, the producer'sensure, the no-breakread_bodyloop, the closed-queue classification, the chunked-length sentinel); every count re-derived from the combined tree by the reconcile pass — 220 lib files besideversion.rb, 220sig/mirrors, the same nineteenprivate_constanttest-mirror exceptions (8a adds none;dexpace-conformance's three are named), eighteen checklists, nineteen pages, eighteen gates, 55 phase-10 inbound bullets.clean_bundlebullet, and the reconciliation paragraph.docs/product-spec/,docs/sdk-design-ruby/,docs/knowledge/harvested/,docs/deviations.mdand every other phase's documents are untouched (6b's load-sensitiveREDIR-23test deliberately left alone, routed).Reconciliation onto phase 7's
mainThis stack was reviewed on
c53638band rebased onto734e6b3(phases 7b, 7c and 7a) by a reconcile pass: three code conflicts resolved inside the commits that caused them (seam_surface_test.rbtomain'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'sclean_bundleinbound bullet closed by 8a's Task 23; 7a's owedgems/dexpace-core/README.mdparagraph paid here. 8a's own status note (kept verbatim) calls this lane "the first code outside dexpace-core" — true on its base, false onmain, where 7a'sdexpace-serde-jsonlanded 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.yamlgained 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.rbexit 0 (all eight checks);ruby scripts/verify_knowledge_structure.rbOK; 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.