Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4bec966
feat: the synchronous net-http transport and the conformance gem (pha…
Wahbeh-Mohammad Sep 20, 2026
ab034f1
fix: bounded Content-Length regexps, the swap pin and GC state (round 1)
Wahbeh-Mohammad Sep 20, 2026
ff825f0
fix: a pump built over a cancelled token closes without a producer
Wahbeh-Mohammad Sep 20, 2026
0fc263e
fix: the warm-up client takes an explicit nil proxy, never :ENV
Wahbeh-Mohammad Sep 20, 2026
875c9b6
docs: state the status range the response mapper is total over
Wahbeh-Mohammad Sep 20, 2026
bb909ba
fix: a Content-Length beside a chunked encoding is the -1 sentinel
Wahbeh-Mohammad Sep 20, 2026
d63a1f6
test: phase 8a suites and doubles for the net-http transport and the …
Wahbeh-Mohammad Sep 20, 2026
e0c1a4b
test: make three surviving mutations red and guard the round-1 fixes
Wahbeh-Mohammad Sep 20, 2026
a31e1db
test: say which proxy-route test sets the process-wide configuration
Wahbeh-Mohammad Sep 20, 2026
a723d55
test: hermetic proxy keys and the pump built over a cancelled token
Wahbeh-Mohammad Sep 20, 2026
e21b4e4
test: keep the adapter's token check discriminable from the pump's
Wahbeh-Mohammad Sep 20, 2026
54136ca
test: every raw fixture client takes an explicit nil proxy (R2-1)
Wahbeh-Mohammad Sep 20, 2026
ed71b2b
test: say which half of the proxy hermeticity the module covers
Wahbeh-Mohammad Sep 20, 2026
a36a9d2
test: guard the chunked length, the closed-pump read and the handler …
Wahbeh-Mohammad Sep 20, 2026
705d864
test: un-guard the generator slice's codec half now that 7a is on the…
Wahbeh-Mohammad Sep 21, 2026
ba59174
docs: phase 8a checklist, as-built ledger rows P8-51-P8-62, the two a…
Wahbeh-Mohammad Sep 20, 2026
1349255
docs: round-1 record, self-contained transport examples, P8-63
Wahbeh-Mohammad Sep 20, 2026
0bddef5
docs: round-2 record, P8-64, hermetic proxy keys, the transport page …
Wahbeh-Mohammad Sep 20, 2026
3823754
docs: guard row 39 is red only on top of row 40, and say so
Wahbeh-Mohammad Sep 20, 2026
490a791
docs: round-3 record -- fixture proxies, status range, default gem
Wahbeh-Mohammad Sep 20, 2026
b458899
docs: round-4 record -- the chunked length, P8-65, guards 45-47
Wahbeh-Mohammad Sep 20, 2026
7c2e82e
chore: reconcile the 8a stack onto main after phase 7
Wahbeh-Mohammad Sep 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 124 additions & 26 deletions CLAUDE.md

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ not compete with `faraday` or `httpx` on the easiest way to fetch a JSON endpoin

## Status

**Phases 0, 1, 2, 3a, 3b, 4a, 4b, 4c, 5a, 5b, 5c, 6a, 6b, 6c, 7b, 7c and 7a are built.** Nothing is published. The repository holds six gems under
**Phases 0, 1, 2, 3a, 3b, 4a, 4b, 4c, 5a, 5b, 5c, 6a, 6b, 6c, 7b, 7c, 7a and 8a are built.** Nothing is published. The repository holds six gems under
`gems/`, every one at `0.0.0`. `dexpace-core` carries the HTTP domain model — the frozen,
validated wire types every later phase stands on (`docs/sdk-documentation/http.md`) — the seam
layer: the provider registry, the transport and codec seams, the core-owned async pivot,
Expand Down Expand Up @@ -84,13 +84,22 @@ handlers phase 3b's `TypedResponse` was built to take (`docs/sdk-documentation/s
`json >= 2.19.9` floor in its gemspec and asserted at require time, and the seam registration under
`:json` — the first gem here with a third-party dependency, and the first the three zero-dependency
gates have run against with one; the operation-lifecycle and transport groups of the HTTP-tracer
vocabulary are emitted by nothing yet, and nothing talks to a socket yet. The other four are still
vocabulary are emitted by nothing yet.
`dexpace-transport-net_http` carries the synchronous transport — the first thing here that talks
to a socket: two constructions over a fresh-per-call or a borrowed `Net::HTTP`, the header policy
the wire carries, a per-response producer thread behind every streamed body, one total budget
across three native knobs, a classifier that asks the cancellation token first and wraps every
other failure retryable, TLS settings and the proxy over the configuration chain
(`docs/sdk-documentation/transport-net_http.md`) — and `dexpace-conformance` carries the
conformance suite: the assertion protocol, the twenty-eight-assertion transport suite with its
vacuous and waived rows, the plaintext wire fixture, the Minitest and RSpec drivers and the two
observability doubles (`docs/sdk-documentation/conformance.md`). The other two are still
skeletons — a namespace, a `VERSION`, a gemspec, a signature mirror and a smoke suite:

| Gem | Namespace | Runtime dependencies today |
|---|---|---|
| `dexpace-core` | `Dexpace` | none |
| `dexpace-transport-net_http` | `Dexpace::Transport::NetHTTP` | `dexpace-core` |
| `dexpace-transport-net_http` | `Dexpace::Transport::NetHTTP` | `dexpace-core`; `net-http >= 0.4` |
| `dexpace-transport-async_http` | `Dexpace::Transport::AsyncHTTP` | `dexpace-core` |
| `dexpace-serde-json` | `Dexpace::Serde::JSON` | `dexpace-core`; `json >= 2.19.9` |
| `dexpace-async-thread` | `Dexpace::Async::Thread` | `dexpace-core` |
Expand Down
3 changes: 3 additions & 0 deletions Steepfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,8 @@ end
target :conformance do
check "gems/dexpace-conformance/lib"
signature "gems/dexpace-conformance/sig", "gems/dexpace-core/sig"
# rbs's own stdlib signature sets for the two features this gem's lib/ requires beyond core's
# allowlist: `socket` for the wire fixture (P8-14) and `tempfile` for TRANSPORT-28's file body.
library "socket", "tempfile"
configure_code_diagnostics(D::Ruby.default)
end
19 changes: 13 additions & 6 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,13 @@ domain model, phase 2's seam layer, phase 3a's byte-streaming layer, phase 3b's
4a's execution context, phase 4b's recovery layer, phase 4c's stage pipeline, phase 5a's
configuration layer, phase 5b's logging facade and redaction, phase 5c's tracing and metrics
layer, phase 6a's retry layer, phase 6c's authentication layer, phase 6b's redirect layer, phase
7b's server-sent-events layer, phase 7c's pagination layer and phase 7a's serialization layer;
`dexpace-serde-json` holds phase 7a's JSON codec and declares `json >= 2.19.9`; the other four are
phase 0's skeletons, a namespace and a `VERSION`. The as-built documentation lands in
[`sdk-documentation/`](./sdk-documentation/) as each gem gains code; the seventeen pages written so far are [`sdk-documentation/quality-gates.md`](./sdk-documentation/quality-gates.md), because the
7b's server-sent-events layer, phase 7c's pagination layer, phase 7a's serialization layer and
phase 8a's transport error; `dexpace-serde-json` holds phase 7a's JSON codec and declares
`json >= 2.19.9`; `dexpace-transport-net_http` holds phase 8a's synchronous transport and declares
`net-http >= 0.4`, and `dexpace-conformance` phase 8a's assertion protocol, transport suite, wire
fixture, two drivers and two doubles; the other two are phase 0's skeletons, a namespace and a
`VERSION`. The as-built documentation lands in
[`sdk-documentation/`](./sdk-documentation/) as each gem gains code; the nineteen pages written so far are [`sdk-documentation/quality-gates.md`](./sdk-documentation/quality-gates.md), because the
gate set is the thing phase 0 built, [`sdk-documentation/http.md`](./sdk-documentation/http.md),
because the domain model is the thing phase 1 built,
[`sdk-documentation/seams.md`](./sdk-documentation/seams.md), because the seam layer is the thing
Expand Down Expand Up @@ -137,9 +140,13 @@ the two `standard` constructors are the things phase 6b built,
the serde-boundary gate are the things phase 7b built,
[`sdk-documentation/pagination.md`](./sdk-documentation/pagination.md), because the pagination
layer — the page value, the strategies, the two views, the two engines and the fetcher front-end —
is the thing phase 7c built, and
is the thing phase 7c built,
[`sdk-documentation/serde.md`](./sdk-documentation/serde.md), because the serialization layer and the
JSON codec are the things phase 7a built.
JSON codec are the things phase 7a built, and
[`sdk-documentation/transport-net_http.md`](./sdk-documentation/transport-net_http.md) and
[`sdk-documentation/conformance.md`](./sdk-documentation/conformance.md), because the synchronous
transport and the conformance suite are what phase 8a built — the first code outside
`dexpace-core` after 7a's codec.

## Keeping this file true

Expand Down
40 changes: 28 additions & 12 deletions docs/first-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ stated in the release notes rather than discovered at `bundle install`.
phase 8a and 8c each run it against a real adapter. **With one stated exception**: the 3.2 row runs
the suite against `dexpace-transport-net_http` only, because `dexpace-transport-async_http` cannot
be installed there (see the supported-Ruby note above, `P8-36`, and 8c's plan, Task 3). "Passing across 3.2
through 4.0" therefore means: every gem on every row it can be installed on
through 4.0" therefore means: every gem on every row it can be installed on. **Status 2026-09-20**:
the suite exists — twenty-eight assertions over twenty-two `TRANSPORT` IDs, `HTTP-17`/`HTTP-18` and
`PAGE-36` — and 8a ran it against `dexpace-transport-net_http` on 3.2.11 (net-http 0.4.1 and 0.9.1),
3.3.12, 3.4.10 and 4.0.6, every assertion green and `TRANSPORT-18` vacuous by measurement; the
`async-http` half waits for 8c
- [ ] **Before release, `docs/sdk-documentation/` must state what a green `dexpace-conformance` run does
and does not prove, and the run's own report preamble must name the same omissions.** Filed
2026-09-12 by phase 8a's design (`P8-9`). The wire fixture speaks plaintext only and exercises no
Expand All @@ -69,7 +73,9 @@ stated in the release notes rather than discovered at `bundle install`.
passes is entitled to know that TLS verification, connect-timeout classification and any waived ID
were not among the things it passed — which is the difference between a conformance suite and a
badge. Cites `TRANSPORT-4`, `TRANSPORT-14`, `TRANSPORT-20`, `NFR-2`; the suite itself is phase
8a's Tasks 4–8 and 20 and phase 9's Tasks 2–12a
8a's Tasks 4–8 and 20 and phase 9's Tasks 2–12a. **Status 2026-09-20**: `TransportSuite::PREAMBLE`
names the two omissions in every report, and `docs/sdk-documentation/conformance.md` states them
beside what a green run proves; the box ticks when 8c's waiver is stated beside them
- [ ] **Before release, `docs/sdk-documentation/` must document the `include Dexpace` constant-shadow
hazard.** Filed 2026-09-08 by phase 3a's design; recorded here 2026-09-13. Phase 1 measured
`Dexpace::Method` shadowing `::Method` and concluded "**verified inert outside core**"; the observation
Expand Down Expand Up @@ -201,10 +207,15 @@ stated in the release notes rather than discovered at `bundle install`.
`dexpace-conformance` fixture, or a downstream SDK's `SEAM-26` operation projection asking for one**.
Until that event, it is a release decision and not a phase's: either the release notes state that
`HTTP-22`, `HTTP-48`, `HTTP-49` and `HTTP-50` are unbuilt, or the four are built before the tag
- [ ] **Phase 8's first transport adapter must wrap every stdlib I/O and timeout error it lets
- [x] **Phase 8's first transport adapter must wrap every stdlib I/O and timeout error it lets
escape** — `Errno::ETIMEDOUT`, `SocketError`, `Timeout::Error` and their kin — in something
answering `#retryable?` (`Dexpace::TransportError` or equivalent), defaulting to `true` per
`XCUT-4` branch (b). `RETRY-2`'s classification is a capability-only query (`XCUT-6`;
`XCUT-4` branch (b). **Ticked 2026-09-20**: `Dexpace::TransportError` landed with 8a's Task 2 in
the shape below, and `dexpace-transport-net_http`'s `Failures.wrap` is a catch-all over every
`StandardError` that is not already a `Dexpace::Error` — the twelve families the design names each
proven wrapped with the original as `#cause`, on net-http 0.4.1, 0.6.0 and 0.9.1
(`gems/dexpace-transport-net_http/test/dexpace/transport/net_http/failures_test.rb`). The
`async-http` families are 8c's to prove against the same class. `RETRY-2`'s classification is a capability-only query (`XCUT-6`;
`CFG-35`'s throwable half is phase 6a's Task 3, `Policy.throwable_retryable?`), so a bare
unwrapped stdlib error classifies as **not retryable**, which is a silent
retry-eligibility regression for exactly the class of failure `RETRY-4` calls "always
Expand Down Expand Up @@ -332,10 +343,11 @@ and the phase whose checklist carries the ⏳ row citing the entry here.
`dexpace-core` by `SEAM-1`/`NFR-1`. Trigger: core's dependency budget changes — an event, and no phase in
v1 can produce it. ⏳ row: phase 3b, which owns the ID. Its companion **`BODY-12` clause 2 (SHOULD)** —
the transport dispatching a true zero-copy kernel path for a `Dexpace::FileBody` — is not a deferral but
an **UNSCHEDULED** decision (2026-09-12, phase 8a's design, R5; confirmed at execution by 8a's Task 25)
and is stated here so the release notes carry it: `Net::HTTP` streams a body through `::IO.copy_stream`
into a `Net::BufferedIO` whose `is_a?(::IO)` is false, so the kernel path is unreachable without
rewriting the library's own write path. Clause 1 was discharged by phase 3b (`::IO.copy_stream` with the
an **UNSCHEDULED** decision (2026-09-12, phase 8a's design, R5; **confirmed at execution 2026-09-20 on
net-http 0.4.1, 0.6.0 and 0.9.1** — the kernel path is still unreachable without bypassing the library's
own write path) and is stated here so the release notes carry it: `Net::HTTP` streams a body through
`::IO.copy_stream` into a `Net::BufferedIO` whose `is_a?(::IO)` is false, so the kernel path is
unreachable without rewriting the library's own write path. Clause 1 was discharged by phase 3b (`::IO.copy_stream` with the
`(length, offset)` window) and is not owed.
- **`PIPE-36` (SHOULD), pillar-step stage locking.** Post-MVP per the design's own coverage
index; nothing in v1 implements any part of it, and 4c's design names `#stage`'s precedence table (its
Expand All @@ -359,9 +371,10 @@ and the phase whose checklist carries the ⏳ row citing the entry here.
⏳ rows: phase 5c (`OBS-32`) and phase 5b (`OBS-37`).
- **`TRANSPORT-28`'s zero-copy clause (SHOULD), per-adapter.** The two
MVP transports do not need it to satisfy the transport contract; `8a`'s R5 finds `TRANSPORT-28`'s
reachable half satisfiable on `Net::HTTP` and only its zero-copy clause outstanding. Trigger: a transport
adapter beyond the two the MVP ships. ⏳ row: phase 8a — `TRANSPORT-28`'s
zero-copy clause. **`TRANSPORT-30` was in this entry and is no longer** *(narrowed 2026-09-13)*:
reachable half satisfiable on `Net::HTTP` and only its zero-copy clause outstanding — built and proven
2026-09-20: a file body's `offset:` and `count:` window reaches the wire exactly and the body is
replayable, on every supported row. Trigger: a transport adapter beyond the two the MVP ships. ⏳ row:
phase 8a — `TRANSPORT-28`'s zero-copy clause. **`TRANSPORT-30` was in this entry and is no longer** *(narrowed 2026-09-13)*:
`8a`'s `R17` found the deferral resting on a premise that was false in both directions — phase 5a
ships `CFG-22`–`CFG-28`'s proxy resolver and routes proxy *use* to phase 8, and `Net::HTTP.new`'s
`p_addr` defaults to `:ENV`, so the adapter was already proxying from the environment with a
Expand Down Expand Up @@ -592,8 +605,11 @@ the trigger, then the one job to do when it fires.
`test.rb`, `autorun.rb`, `spec.rb` and `benchmark.rb` all remain. Every assertion name this repository uses
survives: 22 were checked, from `assert_equal` to `assert_in_delta`, and all 22 are still defined on
`Minitest::Assertions` in 6.0.0. What disappears is `Minitest::Mock` and `Object#stub` — which **phase 8a's
plan uses twice** (`Dexpace::Conformance::TransportSuite.stub(:assertions, assertions)`, in its driver test
plan used twice** (`Dexpace::Conformance::TransportSuite.stub(:assertions, assertions)`, in its driver test
and again in its `test/` fence) and which two corpus rules name (`testing/e27df4c7`, `testing/70473c9d`).
*Narrowed 2026-09-20 at 8a's execution*: the built suites use **no** `.stub` anywhere — `TransportSuite.run`
takes an `assertions:` keyword and the drivers' tests hand in a plain object answering `#assertions` — so
the `stub` half of the pin's first reason is gone; the second reason below stands on its own.
The pin keeps the same framework major and a working `stub` on every matrix row, at the cost of the 4.0 row
not exercising the Minitest its interpreter ships; without it that row runs **red**, which would falsify the
standing blocker above — the `dexpace-conformance` suite passing across 3.2 through 4.0 — and is exactly the
Expand Down
34 changes: 34 additions & 0 deletions docs/knowledge/notes/transport-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,40 @@ stable key.
process-globally was rejected for the reason the port refuses `Regexp.timeout`: a library must not
mutate a host global.
<sub>review · `docs/work/mvp/phase8/phase8a/2026-09-11-phase8a-synchronous-transport-and-conformance-design.md` · high · sha:manual-phase8a-content-type-warning</sub>
- **`net-http` 0.9.x, Ruby 4.0's, differs from the 0.4.x and 0.6.x the other supported Rubies ship in two
places the adapter meets, and neither changes what the adapter does.** Beside `transport-adapter/0921e946`
and this file's content-type entry above, whose measurements were taken on 0.6.0; verified on 2026-09-20 on
0.4.1 (3.2.11 and 3.3.12), 0.6.0 (3.4.10) and 0.9.1 (4.0.6). On every one of those rows `net-http` is a
**default** gem and on none a bundled one: 4.0.6 ships `specifications/default/net-http-0.9.1.gemspec` and
`Gem::BUNDLED_GEMS::SINCE` has no `net-http` row, and under Bundler `Gem.loaded_specs["net-http"].default_gem?`
answers true. (The development machine also holds an *installed* copy of the same 0.9.1 beside the default
one in the 4.0.6 and 3.2.11 gem directories — an artefact of an earlier networked install, which RubyGems
prefers outside Bundler and Bundler does not; an earlier wording of this entry read that artefact as the
gem having left the default set, corrected 2026-09-20 by review round 2's R2-3. The distinction is the
hard rule's load-bearing one: a default gem needs no `Gemfile` entry, a bundled gem does.)
**One**: `Net::HTTPGenericRequest#supply_default_content_type` is gone from 0.9.1 — a body-bearing request
with no `Content-Type` emits no warning under `-w` and reaches the wire with no `Content-Type` at all,
while `#set_body_internal` still gives a body-less `POST` `body = ''` and `Content-Length: 0` on every
version. So `P8-4`'s first reason (the warnings-fatal build) holds on three of the four rows and its
second (a form type as a claim about the bytes) on the same three; `TRANSPORT-10`'s own rule holds on
all four, and the adapter's stamp is identical on all four — which is why its `-w` `POST` test reads the
wire's `Content-Type` line rather than only the absence of a warning. **Two**: `Net::HTTP#connect` is
`Timeout.timeout(@open_timeout, Net::OpenTimeout) { TCPSocket.open(...) }` on 0.4.1 and 0.6.0 and
`TCPSocket.open(..., open_timeout: @open_timeout)` on 0.9.1, and the first `Timeout.timeout` in a process
starts Ruby's process-wide singleton timeout thread, which lives for the rest of the process. A suite
that counts threads around every test — phase 0's `DexpaceTestCase` does — charges that thread to the
first test that connects on a 3.2, 3.3 or 3.4 row and to no test on a 4.0 row; phase 8a parks it by
opening one connection at test-helper load (`test/support/net_http_warmup.rb`), which is a test-support
arrangement and not the SDK calling the primitive §8.3 bans. The facts that did **not** move across the
three versions and are worth stating because the design measured them once: `max_retries` defaults to 1
with `PUT` and `DELETE` in the retried set, `#read_timeout=` reaches a live socket, `#[]=` on
`Accept-Encoding` flips `decode_content` off and `#add_field` does not, a caller `Host` is honoured
verbatim, `#to_hash` preserves bytes and multiplicity, and `send_request_with_body_stream` copies into a
`Net::BufferedIO` on every one. The gemspec therefore pins `net-http >= 0.4` with no upper bound
(phase 8a's `P8-60`), and `gems/dexpace-transport-net_http/test/dexpace/transport/net_http/matrix_facts_test.rb`
prints the active version per row and asserts the two version-bound facts as the disjunction the adapter
is correct under. Cites `TRANSPORT-10`, `TRANSPORT-26`, `TRANSPORT-2`, `NFR-2`, `NFR-6`.
<sub>review · `docs/work/mvp/phase8/phase8a/2026-09-11-phase8a-synchronous-transport-and-conformance-checklist.md` · high · sha:manual-phase8a-net-http-0-9</sub>
- **`TRANSPORT-12`'s "stricter wire grammar" is per-protocol, not per-adapter, and on the looser protocol
wire-boundary re-validation (phase 8a Task 16, phase 8c Task 9) is the only defence.** Annotates
`transport-adapter/cb7901ef`. Verified 2026-09-11 on
Expand Down
Loading
Loading