Skip to content

Phase 3b: body lifecycle — documentation and phase record - #52

Merged
Wahbeh-Mohammad merged 12 commits into
mainfrom
12-phase-3b-body-lifecycle-docs
Sep 16, 2026
Merged

Wahbeh-Mohammad merged 12 commits into
mainfrom
12-phase-3b-body-lifecycle-docs

Conversation

@Wahbeh-Mohammad

Copy link
Copy Markdown
Contributor

Closes #12. Third of three phase 3b PRs — the documentation and the phase record for #50 and #51. Targets #51's branch.

What lands

9 files, +1,160 / −36, all Markdown.

  • The checklist, docs/work/mvp/phase3/phase3b/2026-09-08-phase3b-body-lifecycle-checklist.md, written from the build: 51 rows — the 49 own IDs (BODY-1–BODY-37, HTTP-36–HTTP-45, HTTP-51, HTTP-52: 46 ✅, 4 ✅ in part with the remainder owned — BODY-12's clause 2 with TRANSPORT-28, BODY-34's source and predicate with phase 5a Task 13 / 5b Tasks 14–15, BODY-30's and HTTP-52's response-side clause — and 1 ⏳, BODY-36) plus the two cross-reference rows the design names, HTTP-46 (the body half of by-value equality, now with a subject) and HTTP-3 (the multipart body its builder list names). Plus what was built, the guards run red — the fifty-five-mutation battery with its messages, and the rows each review round added — the audit groups run, twenty-two deviations from the plan's text (none lowers a gate), the findings routed (non-ASCII multipart names → phase 10's inbound list; the Body.string stdlib leak → phase 1 Task 1; 3a's #clear_tap and one-byte-per-read findings recorded as closed by 3a as built), Task 13's view-retention measurement re-run on the merged tree, and the postponed work re-read (nothing new postponed).
  • The phase 3b design's ledger gains an "As built" addendum: P3-14, P3-15, P3-22 and P3-27 as built, the 3a contract row without #clear_tap, R10's cost paragraph now that 3a's fill fix landed, and the three review rounds' repairs. docs/sdk-design-ruby/ §3.1, §5.1 and §10 are frozen; their addenda and the consolidation are a human's, stated in the roadmap note as 3a did.
  • docs/sdk-documentation/body.md — new as-built page for the body layer (fourteen fences, sixty annotations, every one run verbatim on 4.0.6 and 3.2.11 by the reviewer); architecture.md, the core README, README.md and docs/README.md updated to point at it.
  • CLAUDE.md count sentences re-derived from the tree (sixty-three phase-1/2/3a/3b files beside version.rb, the sig/ and test/ mirrors, five checklists written so far); the roadmap gains the phase 3b status note, corrected in round 1 to the row arithmetic the table's cells actually have.

Verification

  • bundle exec rake on Ruby 4.0.6 at this tip: exit 0, all seventeen gates green (1,153 / 6,076, 99.96%, YARD 0 undocumented).
  • ruby .claude/skills/housekeeping/probe.rb: no drift, all eight checks; verify_knowledge_structure.rb OK; the housekeeping suite (108 runs) and the knowledge suite (92 runs) green.
  • Empty diff under docs/product-spec/, docs/sdk-design-ruby/, docs/knowledge/ and phase 3a's two documents.
  • The final reviewer re-derived every count the status note and CLAUDE.md state against the checklist and the tree, counted the 51 rows from the table's cells, and ran every body.md fence by hand on both interpreters.

Known follow-up from the final review (not blocking)

  • R2-1 — Body.stream over an already-closed stream leaks a raw ::IOError ("closed stream") from the BODY-9 probe's io.pos (stream_body.rb:90, whose rescue is ::SystemCallError only). No requirement text is violated — the failure is loud and at construction — and it is the same argument-boundary class the checklist's "Findings routed" already sends to phase 1's plan, Task 1 for Body.string; the bullet should name the probe's ::IOError beside it so phase 1's sweep covers both.

Phase 3b (issue #12). One public module, Dexpace::Body, supplies
HTTP-36's whole vocabulary over one hook, #write_to(sink): BODY-1's
false default, BODY-35's -1 sentinel, P3-23's read side (#source
raising by name, #close a no-op), BODY-3's #to_replayable, §10.2's
#each derived from #write_to (P3-21), HTTP-46's identity default, the
eight factories HTTP-38 asks for in one place, BODY-32's cap clamp,
BODY-30/HTTP-52's .buffer_bounded at MAX_BUFFERED_ERROR_BODY_BYTES
draining through #source and closing the original in an ensure, the
two private exact-length copy routines (HTTP-39/BODY-10, BODY-13,
BODY-25; P3-20) and the two latches BODY-6, BODY-7 and BODY-9 share.

The seven request-body variants, flat under Dexpace:: and filed under
lib/dexpace/http/body/ (P3-14): BytesBody and the eager Body.string;
BufferBody, materialize-once's product, reading through a fresh view so
BODY-30's copy stays repeatable; StreamBody with BODY-9's mark/reset
given a Ruby subject -- `pos` then `seek(pos)`, rewinding to the
construction position, never 0 (P3-16) -- and close: true forcing
single-use because BODY-8's own sentence says so; ChunkedBody,
unconditionally single-use with no replayable: keyword (P3-19);
FormBody over the +-for-space encoder that now sits beside the RFC
3986 one in PercentEncoding; FileBody with six fail-fast clauses,
::IO.copy_stream's window and deliberately no #to_path (P3-17);
MultipartBody with one framing routine for the bytes and the length
(count_only:, P3-29), RFC 2046's boundary grammar, HTTP-51's quote
escape and CR/LF rejection, a bare-token subtype, and HTTP-3's
Builder. The response side: ResponseBody, HTTP-41/BODY-14's single-use
handle with Closeable's latch, a block form and BODY-33's preview;
RequestLoggingBody, a fresh TeeSink per write (BODY-17..BODY-21,
BODY-37); ResponseLoggingBody, the drain-once wrapper in two regimes
behind one close-once guard (BODY-22..BODY-29), whose over-cap tail is
BufferedSource.wrapping-owned so its close reaches the wrapper's latch
(R10), with Dexpace.close_quietly's first call site. Every sig/ mirror
declares every method for the strict core Steep target.
Response gains HTTP-43's #close -- a pure forward, because a Data
instance holds no latch and the requirement delegates idempotence to
the body's -- HTTP-42's #body_string, the SDK's one decode boundary in
three load-bearing steps (resolve the charset through MediaType#charset,
RETAG the BINARY bytes through 3a's #read_string, transcode with the
target NAMED, because a target-less #encode follows the host's
Encoding.default_internal and the retag-less recipe design §3.1 states
replaces every non-ASCII byte), and #body_bytes, which decodes nothing;
both close the body in an ensure (BODY-16). TypedResponse is HTTP-44's
lazy wrapper: five raw accessors forwarded without a lock, #value
memoised through an explicit state machine so a nil or false success
runs the handler once and a failure is re-raised as the same object,
HTTP-45's serialization a mutex across the state flip only, and a
handler raising outside StandardError still settling the state; the
handler is any #call(response), the RBS interface _ResponseHandler,
which phase 7 supplies into (R7). sig/ narrows Request#body and
Response#body from untyped to Dexpace::Body? on the reader, .build,
.new and #initialize (P3-15) -- the narrowing phase 1 postponed to
phase 3, free because no release tag exists to diff against. The entry
file requires the twelve files in dependency order.
The runtime surface manifest of dexpace-core grows from 366 to 515
lines through `rake surface:regenerate`, once, and every added row was
read against the plan's list: fourteen constants (R6's twelve plus
MultipartBody::Part and ::Builder), MAX_BUFFERED_ERROR_BODY_BYTES,
FormBody::MEDIA_TYPE, the four framing constants, Response's three
readers with .resolve_charset, the two form-encoder functions,
ResponseBody.new's block-form override, and Body's equality default;
nothing removed and no private_constant present. The core smoke suite's
constant list gains the body layer and pre-requires securerandom beside
uri and strscan, since its top-level snapshot would otherwise attribute
::SecureRandom to the entry file; phase 2's seam_surface_test pin on
core's non-relative requires names securerandom beside strscan and uri,
the one require phase 3b adds, on the allowlist. Both are files a gate
reads at run time, so they travel with the code.
tools/measure_view_retention.rb is Task 13's measurement on the BODY-23
drain: it prints numbers, asserts nothing, needs no bundled gem, and its
numbers are in the checklist beside the plan's decision 5.
Review round 0 of phase 3b found four defects in the body layer and two
nits, none a red gate. This is the code half of the repair.

R0-1: Response#body_string, #body_bytes and Body.buffer_bounded took the
handle #source returns and never closed it. For a ResponseBody that is
the same stream close the body's own performs, but for a BufferBody and
a fits-cap ResponseLoggingBody the handle is a fresh #peek view of a
buffer that outlives the call, and neither body's #close reaches it, so
each read left one registered view behind -- the Array#delete growth
Task 13 measured, against the design's own "every view core takes,
core closes". The two readers now share a private #read_through that
closes the handle in its own ensure and then the body in the method's;
copy_bounded closes the source it took the same way, guarded with
respond_to? as the bodies' #release methods are, because
Dexpace::IO::_Source declares no #close. The over-cap composite's close
is the Tail's, which deregisters its prefix view too.

R0-3: the over-cap Tail forwarded the live delegate as
BufferedSource#read(count), IO-16's bulk read, which fills until count
bytes or EOF, so on an open connection the composite delivered the rest
of the body in .wrapping's 64 KiB segments or at EOF where the delegate
alone returns what has arrived. It now forwards through #read_into,
IO-1's one-fill primitive -- the only member the regime probe asks of
the delegate's source (plan decision 9), so the delegate's source
contract stays _Source's single method -- with a zero return raising
BODY-25's violation through 3a's helper. The forwarding sits in a
private #read_tail beside #take_pending for Metrics/AbcSize.

R0-4: MultipartBody#== and #hash ignored the subtype, so two bodies
framing the same parts as multipart/form-data and multipart/mixed
compared equal under HTTP-46 while their media types differed. Both now
fold the subtype in.

R0-5: ResponseBody admitted a bare #read_into source and then used
#peek, #each, #read and #read_string on it. The construction check now
asks for #read_into and #peek, the narrowest member only a
BufferedSource-shaped reader answers, with a message naming both.

R0-6: the entry file's twelve require_relatives follow the plan's Task
14 Step 1 order again (file_body after buffer_body); both orders were
dependency-safe.

sig/ declares the two private methods for the strict core target. The
runtime surface manifest is unchanged: nothing public was added.
ResponseLoggingBody took @delegate.source three times -- for the prefix
fill, for the regime probe and for the over-cap tail -- which is correct
only over BODY-14's same-handle ResponseBody. The wrapper's stated
delegate contract is #source, #content_length, #media_type and #close,
and BufferBody, inside that contract, answers #source with a fresh
non-consuming #peek view per call by design (P3-23). Over it the
over-cap composite replayed the prefix twice and then the whole body
(15 bytes for a 10-byte body, also through Response#body_bytes), and the
fits-cap regime left the fill view registered in the delegate's buffer
against the design's "every view core takes, core closes" (review round
1, R1-1).

The drain now asks once and holds the handle in @upstream; fill_prefix,
capture_complete? and the Tail read from that one handle, and #release
closes it ahead of the delegate, the delegate's close in an ensure so
BODY-15's transport release is attempted whatever the handle's close
did. One respond_to?(:close) guard covers both handles that cannot be
closed -- a Dexpace::IO::_Source declares no #close, and before a drain
has asked there is no handle at all. The RBS mirror gains the ivar and
the two narrowed private signatures.
Twelve suites mirroring the twelve lib files one for one, 302 runs,
each opening with the IDs it exercises and nesting a class per
behaviour group for the 100-line cap: body_test (the contract's
defaults, #each derived from #write_to, the two copy routines, the
factory table, .buffer_bounded driven over a ResponseBody, a
ResponseLoggingBody in both regimes and a BufferBody plus two
#source-answering doubles, BODY-31's status-blind guarantee pinned
mechanically), the ten variant suites (BODY-9's probe on a real
IO.pipe raising a real Errno::ESPIPE, the two writes made to overlap
through a parking sink with the seek delta counted, BODY-11's six
clauses one test each with /proc/self/fd counted, §7.1's residue
asserted as a fact, HTTP-51's declared-length property, BODY-27's two
close paths in either order, BODY-28's captured buffer surviving the
close and the tail raising ClosedError after it, the fiber proof that
terminates) and typed_response_test (the @value ||= bug by counting
invocations, the raw accessors answering while another thread holds
the parse lock, joined with a timeout). Three phase-1 suites gain
sections: response_test's five groups over HTTP-42's three steps with
the hostile Encoding.default_internal helper and the three-body
surface, request_test's HTTP-46 body half, percent_encoding_test's
FormTest asserting the two encoders differ on exactly space, ~ and *.
FakeBody answers #write_to and #source; FakeResponseBody is the
delegate whose #close raises, which BODY-27 and BODY-28 need and no
StringIO will do. A raw #write recorder in chunked_body_test is what
makes #emit_exactly's own retag observable, the mutation the
adversarial review found missed.
SimpleCov at the tests tip reported 2957 of 2960 lines, and two of the
three misses were phase 3b's: the block-shaped sink keeping a chunk
that is already frozen BINARY rather than copying it -- BytesBody#each
is the path, and the test asserts the yielded chunk is the body's own
frozen bytes -- and clamp_cap refusing a cap that is neither an
Integer nor Float::INFINITY, which ResponseBody#preview now proves
with a Float and a String. The third miss is phase 2's registry claim
swap, the race-only branch every phase since has recorded.
The tests half of review round 0's repair, sixteen rows across five
suites, every one seen red against the corresponding single-edit
mutant on 4.0.6 and restored.

R0-2 (BODY-32): "silently clamp the cap down to the ceiling" had no
symptom on a ten-byte body, so deleting the clamp survived the whole
suite. The witness is the COUNT the first read asks the source for:
buffer_bounded over the FakeSource-backed double, and #preview over a
readpartial-recording stream under .wrapping, both assert exactly
MAX_MATERIALIZED_BYTES for ceiling+1, ceiling*2 and Float::INFINITY.
The deletion now fails one test in each suite; the .max spelling five.

R0-1: twenty body_string/body_bytes calls over a BufferBody and over a
fits-cap ResponseLoggingBody leave zero views registered in the
buffer, one body_bytes over an over-cap wrapper leaves the Tail's
prefix view deregistered, and twenty buffer_bounded copies over each
leave zero too; a handle whose close raises still leaves the body
closed, which pins the two ensures against one flat ensure.

R0-3: a real IO.pipe with its writer held open -- ten bytes written,
two captured, one probed -- and a readpartial(1000) on the tail from a
thread joined with a five-second timeout returns the seven bytes that
are there; the ensure closes the writer so a hang is a failure, never
a stuck run. A read_into-only source proves the tail asks the delegate
for nothing beyond IO-1's primitive, and a zero read on the tail is
BODY-25's violation naming IO-17.

R0-4: two multipart bodies over the same parts and boundary with
subtypes form-data and mixed are not ==, not eql?, and hash apart.

R0-5: a bare _Source (FakeSource) is refused with a message naming
#peek; a view of a BufferedSource is accepted and previews.
Every ResponseLoggingBody row drove a ResponseBody, whose #source is
BODY-14's same handle every call, so a wrapper that asked its delegate
three times looked right. These rows drive the other body the contract
admits -- a BufferBody, whose #source is a fresh #peek view per call
(P3-23) -- and a fresh-view double that counts the asks (the existing
CountingDelegate memoises its source and cannot see a second one):
the over-cap composite delivers the body exactly once, the fits-cap
fill's view and the over-cap composite's handle are deregistered by the
wrapper's close, #source is asked once in either regime and never on an
undrained close, the handle is closed ahead of the delegate and the
delegate is still closed when the handle's close raises, and a raising
handle close after a full capture is not a drain error (review round 1,
R1-1). response_test.rb pins the same composite through
Response#body_bytes with both registries empty afterwards.
The checklist at
docs/work/mvp/phase3/phase3b/2026-09-08-phase3b-body-lifecycle-checklist.md,
written from what was built: fifty-one rows -- the forty-nine own IDs
and HTTP-46's and HTTP-3's cross-reference rows -- 46 implemented, 3 in
part with the remainder deferred to a named owner, 1 deferred
(BODY-36), nothing not built; the guards run red with their messages
and the fifty-five-mutation battery; the audit groups; sixteen
departures from the plan's text; the findings routed; the postponed
work re-read. Task 13's measurement is recorded there beside the
plan's decision-5 table, re-run on the merged tree rather than copied.
The 3b design's ledger gains an "As built, 2026-09-16" addendum. The
roadmap gains the phase-3b status note and a thirty-seventh inbound
bullet for phase 10: MultipartBody refuses a non-ASCII part name or
filename because its part-header sweep is the outbound header grammar.
docs/sdk-documentation/body.md is the as-built page for the layer,
every example run on 4.0.6 and 3.2.11 with identical output.
CLAUDE.md's built-phases paragraph, gem and phase-directory sentences
and three constraints-that-bite lines are rewritten from the tree
(sixty-three lib files, five checklists); README.md, docs/README.md,
the core README and architecture.md point at body.md.
docs/first-release.md needed no line changed. The probe reports no
drift and the knowledge structure verifies.
The documentation half of review round 0's repair. Nothing here is a
new claim about the code: every sentence restates what the code and
tests branches now do, with the numbers re-derived from the runs.

The checklist: BODY-14, BODY-16, BODY-24, BODY-25, BODY-30, BODY-32,
HTTP-41 and HTTP-46 say what changed and where it is proven; the
"What was built" paragraph carries the new counts (1,145 runs / 6,044
assertions, 2,973 / 2,974 lines, 316 body-suite runs, the code tip at
84.33% on 4.0.6 and 84.54% on 3.2.11) and the entry file's require
order; "Guards run red" now says honestly that fifty-four of the
fifty-five first-battery mutants were caught by a test and the clamp
deletion was not, and adds the twelve-row second table with the four
neighbouring re-runs; "Deviations from the plan" gains items 17-21,
one per fence the review found wanting, with the review's readpartial
spelling refused and why.

The design's As-built addendum gains a "Review round 0" paragraph:
rule 4 (every view core takes, core closes) applied to the two readers
and the bounded copy; R10's composite forwarding through read_into so
the delegate's source contract is _Source on the probe and the tail
alike; HTTP-46 over the subtype; the construction check as the sig's
type.

docs/sdk-documentation/body.md: the readers close the handle first,
the over-cap tail delivers a live connection as bytes arrive,
multipart bodies compare over boundary, subtype and parts, and the
ResponseBody constructor asks for the reader's vocabulary. The
roadmap's 2026-09-16 note carries the new counts and one sentence on
the round.
The checklist's BODY-23, BODY-24 and BODY-27 rows now state the
one-handle rule -- ResponseLoggingBody asks its delegate for #source
once and its release closes that handle ahead of the delegate -- with
the BufferBody-delegate shape that showed the three asks, and cite the
DelegateHandleTest group; deviation 22 carries the departure from the
plan's Task 11 fence, and a third battery table the eight mutants run
red on 4.0.6 and 3.2.11 plus the six neighbours re-run. The row
arithmetic reads 46 / 4 in part / 1 as the status cells have it, here
and in the roadmap's 3b note (R1-2). Counts follow the tree: 1,153 runs
/ 6,076 assertions, 2,977 / 2,978 lines, 323 body-suite runs, the code
tip at 84.21% on 4.0.6 and 84.39% on 3.2.11. The design's ledger gains
a "Review round 1" paragraph under the as-built addendum, and body.md's
three-bodies paragraph says a wrapper takes its delegate's handle once.
@Wahbeh-Mohammad Wahbeh-Mohammad added type:docs Documentation only area:core Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-* labels Sep 16, 2026
@Wahbeh-Mohammad
Wahbeh-Mohammad changed the base branch from 12-phase-3b-body-lifecycle-tests to main September 16, 2026 12:18
@Wahbeh-Mohammad
Wahbeh-Mohammad merged commit 419aace into main Sep 16, 2026
5 checks passed
@Wahbeh-Mohammad
Wahbeh-Mohammad deleted the 12-phase-3b-body-lifecycle-docs branch September 16, 2026 12:19
@Wahbeh-Mohammad

Copy link
Copy Markdown
Contributor Author

Review record for the phase 3b stack (#50 → #51 → #52)

Three independent reviews, each by a fresh agent with no memory of the previous one, each re-running the gates itself on every tip and both interpreters; two fix rounds between them. Every finding got a stable id, a severity, a file:line and the command output that was its evidence. approve required zero blocking and zero should-fix; the run was capped at four reviews and stopped at the third because it approved.

Round Verdict Blocking Should-fix Nits Disposition
0 changes required 0 4 2 all 6 fixed
1 changes required 0 1 1 all 2 fixed
2 (final) approve 0 0 1 carried into the PR bodies

No finding was ever blocking; nothing was skipped or disputed. Every should-fix came from the plan's own fences, not from the implementer, and all five were in one family — a handle or view taken and not closed, or a read that blocked where a one-fill read was owed.

Round 0 → fixed in round 1

  • R0-1 should-fix — Response#body_string / #body_bytes and Body.buffer_bounded took a #peek view through body.source and never closed it: five body_string + five body_bytes over a BufferBody left ten registered views in its buffer; one body_string over a fits-cap ResponseLoggingBody left one. Fixed on Phase 3b: body lifecycle — the body layer #50: both readers share a private #read_through that closes the handle #source returned in its own ensure, then the body; copy_bounded closes the source it took. Zero views in all five shapes on both interpreters afterwards.
  • R0-2 should-fix — BODY-32's "silently clamp the cap down to the ceiling" was unproven: deleting the clamp outright survived the whole suite on 4.0.6 and 3.2.11, because every fixture that reached it was 3–10 bytes and the Float::INFINITY branch returned before the clamp. Fixed on Phase 3b: body lifecycle — tests and fakes #51: the witness is the count the first read asks the stream for (MAX_MATERIALIZED_BYTES for caps of ceiling+1, ceiling×2 and infinity; 7 for cap 7). The mutant now fails two tests on both interpreters.
  • R0-3 should-fix — the over-cap Tail forwarded partial reads to the delegate as BufferedSource#read(count), which blocks until count bytes or EOF: over a live IO.pipe with 7 bytes available, tail.readpartial(1000) blocked for 2 s and returned only after the writer closed. Fixed on Phase 3b: body lifecycle — the body layer #50 through #read_into — IO-1's one-fill primitive — rather than the reviewer's suggested #readpartial, keeping plan decision 9's single-method delegate contract; a #read_into-only recorder in TailTest is the row that refuses the other spelling.
  • R0-4 should-fix — MultipartBody#== / #hash ignored the subtype, so form-data and mixed bodies over the same parts and boundary compared equal while their media types differed. Subtype folded into both.
  • R0-5 ResponseBody.new admitted a bare _Source (#read_into only) and then failed with NoMethodError on #preview / #each — tightened to require #peek as well, with a message naming both. R0-6 lib/dexpace.rb's twelve require_relatives were not in the plan's Task 14 order — reordered.

Round 1 → fixed in round 2

  • R1-1 should-fix — ResponseLoggingBody called @delegate.source three times (fill, probe, tail), so over a delegate whose #source is a fresh view per call — BufferBody, inside the wrapper's own stated delegate contract — the over-cap composite returned "012300123456789" (15 bytes for a 10-byte body), also through Response#body_bytes, and the fits-cap regime leaked a view. Fixed on Phase 3b: body lifecycle — the body layer #50: one handle held in @upstream for the fill, the probe and the Tail, closed in #release ahead of the delegate. DelegateHandleTest (seven rows) on Phase 3b: body lifecycle — tests and fakes #51; six single-edit mutants of the new lines caught on both interpreters.
  • R1-2 the checklist's and the roadmap note's row arithmetic ("46 ✅, 3 ✅ in part, 1 ⏳") summed to 50 against a 51-row table — corrected to 46 / 4 in part / 1 = 51, counted from the cells.

Round 2 (final) — open, carried into the PR bodies

  • R2-1 nit — Body.stream over an already-closed StringIO or pipe end leaks a raw ::IOError ("closed stream") from the BODY-9 probe's io.pos (stream_body.rb:90, whose rescue is ::SystemCallError only) rather than InvalidArgumentError. No requirement text is violated and the failure is loud and at construction; it is the same argument-boundary class the checklist already routes to phase 1's plan, Task 1 for Body.string("caf\xE9".b), and the routing bullet should name it too. Listed on Phase 3b: body lifecycle — the body layer #50 and Phase 3b: body lifecycle — documentation and phase record #52.

What the final reviewer verified, in its own runs

  1. Gates — Phase 3b: body lifecycle — the body layer #50 tip 5de567f: all seventeen individually on 4.0.6, all green including the SimpleCov floor (784 runs, 84.21%; the tolerated red the layering rule permits was not needed), the matrix set on 3.2.11 (84.39%); the honest RuboCop run 213 files, no offenses, and Dexpace/QualifiedCoreConstant alone clean over 75 gem lib/ files. Phase 3b: body lifecycle — tests and fakes #51 tip af43beb: full bundle exec rake green on 4.0.6 (1,153 runs / 6,076 assertions, 2,977 / 2,978 lines, 99.96%; test:gates 129; YARD 369 methods, 0 undocumented), the matrix set on 3.2.11 (99.96%) and 3.4.10; the gem suite on seeds 1, 99991, 424242 on 4.0.6 and 31337 on 3.2.11, 1,136 core runs on every seed. Phase 3b: body lifecycle — documentation and phase record #52 tip 479ae9c: full rake green, probe exit 0, knowledge verifier OK.
  2. Every prior finding individually — R1-1 re-probed by hand at the code tip on both interpreters (10 bytes, not 15, over a BufferBody delegate; zero views after close in both regimes; the recorder showing #source asked exactly once and never on an undrained close); R1-2 counted mechanically from the table's cells. Round 1 had done the same for all six round-0 findings.
  3. The mutation battery — 38 single-edit mutants applied by the reviewer itself and every one caught by a test on 4.0.6 (one re-spelled because its first form tripped NFR-6's warning gate at load rather than the assertion); 18 interpreter-sensitive ones repeated on 3.2.11 with identical failure counts. Across the run: the implementer's 55, then 27, 37 and 38 by the three reviewers; the one survivor ever (round 0's BODY-32 clamp deletion) is why the clamp witness exists.
  4. Concurrency discipline — all ten synchronize blocks read in full (StreamBody's consume-once latch and replay claim, ResponseLoggingBody's drain flag and tail claim, TypedResponse's parse claim and settle): each a flag flip, never across a write, a drain or a delegate call. Twenty hang-shaped tests looped ten times each under timeout 120, 200/200. TypedResponse by hand under eight threads with a sleeping handler (once, one object), a nil handler (once over three accesses), a raising handler (same object, #cause intact), a custom Exception subclass (state :failed, a later thread joined within 5 s), and the raw accessors answering within 1 s while another thread held the parse mutex.
  5. The layer by experiment, identical on 4.0.6 and 3.2.11: a real IO.pipe end not rewindable with no Errno::ESPIPE escaping and a Tempfile pre-read to byte 4 replaying from 4 twice; the overlap test's determinism read (a BlockingSink parks the first write until the second is attempted, seek delta exactly 1); the three-step decode — ISO-8859-1 declared, no charset → UTF-8, invalid bytes under a hostile Encoding.default_internal → U+FFFD and still valid, thirteen further charsets including Ruby's dummy encodings — and the one-step recipe design §3.1 states shown to mangle; twenty generated multipart boundaries in grammar and unique from SecureRandom, quote/backslash escaped, CR/LF and over-long boundaries refused, 30/30 random part lists with content_length == bytes written == write_to's return, and a length query leaving a single-use part untouched; both ResponseLoggingBody regimes with the probe byte outside the capture and the IO-42/BODY-28 pair; buffer_bounded over a 2.5 MiB counting source asking for exactly 1,048,576 bytes in one read and never beyond; FileBody's descriptor count flat at 7 over ten writes, its window replaying twice, its six BODY-11 clauses refused by name; P3-23's #close / #body_string / #body_bytes over ResponseBody, both wrapper regimes and BufferBody.
  6. Encoding fixtures classified — fifteen BINARY-tag assertions in the 3b suites: twelve over non-ASCII fixtures (including FakeChunked.frozen_utf8 through #each and through a raw sink, and a 120,000-byte \xC3\xA9 file), three ASCII-only that prove only the empty-result fallback.
  7. Layering — main ⊂ 5de567f ⊂ af43beb ⊂ 479ae9c; twelve commits, 0 merges, subjects 45–72 chars with the right prefixes, no attribution lines; every file in each diff classified (34 code, 17 tests, 9 docs); sig/ mirrors lib/ 65/65; empty diffs under the frozen trees, 3a's two documents, lib/dexpace/io, closeable.rb, tools/rbs_surface.rb and the gate fixtures.
  8. Global constraints by script over all 35 changed .rb — both header lines; require "securerandom" the only plain require added under core lib/; no is_a?(IO), no bare shadowed constant, no new error class, #content_length never nil, no timeout/deadline parameter, keywords everywhere but the two private copy routines, no ObjectSpace, no resource acquired inside an Enumerator block; the surface manifest's +149 rows exactly the plan's Task 14 list plus the recorded extras (Body#==/#eql?/#hash, Response.resolve_charset, the two form encoders, ResponseBody.new) with nothing private leaked; gates:rbs_surface clean without touching its allowlist.
  9. Plan and spec coverage — the 14 tasks' files and outputs; each Scope bullet of Phase 3b: Body Lifecycle #12 mapped; all 51 rows (49 own + HTTP-46 + HTTP-3) with the cited test read for 35 of them — proven except BODY-4, BODY-12 and BODY-34, each partially proven exactly as its row says with the owner named at a path that exists (phase 6; TRANSPORT-28 in docs/first-release.md; phase 5a Task 13 / 5b Tasks 14–15); the twenty-two recorded deviations checked against the code; 3a's #clear_tap and one-byte-per-read findings recorded as closed by 3a as built; Task 13 re-measured (1,000 views close in 0.0124 s against the checklist's 0.0125; git diff main..code -- lib/dexpace/io empty).
  10. Phase record — body.md's 14 fences executed statement by statement, 60 annotations, 0 mismatches on both interpreters; CLAUDE.md counts re-derived (six gems, 63 lib/ files beside version.rb, 65 sig mirrors, hooks.rb the one file without a test mirror, 11 phase directories, 5 checklists, 40 harvested topics); the 323 body-suite runs matching the checklist's per-suite list; every routing target the checklist names present at its cited line.
  11. Report discrepancies — none; every gate, run-count, coverage and mutation claim in the round-2 fix report reproduced exactly. (Round 1 had noted the implementer's "every mutation caught" claim was superseded by R0-2, and the checklist was corrected to say fifty-four of fifty-five.)

Run: workflow wf_5c5d4224-094, 6 agents (1 implementer, 3 reviewers, 2 fixers), 2.7M tokens, 4.4 h.

@Wahbeh-Mohammad Wahbeh-Mohammad mentioned this pull request Sep 25, 2026
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:docs Documentation only

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 3b: Body Lifecycle

1 participant