Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
133 commits
Select commit Hold shift + click to select a range
c8cde4a
Make docs/knowledge/ queryable: lookup CLI and repo skill (#35)
Wahbeh-Mohammad Aug 26, 2026
e3ba885
feat(core): phase 3 — byte-stream I/O contracts and request/response …
Wahbeh-Mohammad Aug 26, 2026
63ed1b7
Phase 4 — execution context, recovery chains, and the stage-based pip…
Wahbeh-Mohammad Aug 26, 2026
cba4721
Phase 5 — the three resilience pillars and the public authoring surfa…
Wahbeh-Mohammad Aug 27, 2026
743f316
Phase 6 — the serde seam, Server-Sent Events, and pagination (#48)
Wahbeh-Mohammad Aug 27, 2026
bd37a08
Phase 7 — configuration, platform primitives, and the observability f…
Wahbeh-Mohammad Aug 28, 2026
a0d734d
Phase 8 — the transport adapters, the file-backed body, and the RxJS …
Wahbeh-Mohammad Aug 29, 2026
d8217af
chore: Phase 9 — cross-cutting invariants, conformance, and the shrin…
Wahbeh-Mohammad Aug 29, 2026
36c3f96
Fix: three Phase 10 defects and correct the Phase 10 documents (#59)
Wahbeh-Mohammad Aug 30, 2026
12b2242
chore: merge test/node-conformance into tests/, and gate the partitio…
Wahbeh-Mohammad Aug 31, 2026
f93ccd9
chore: split docs/knowledge/ into a harvest-only tree and a notes tre…
Wahbeh-Mohammad Aug 31, 2026
3c01a85
docs: add the community health files and the bun dependabot config.
Wahbeh-Mohammad Sep 2, 2026
b040968
docs: restructure docs/ and add a housekeeping skill to prevent drift
Wahbeh-Mohammad Sep 2, 2026
7b26c1c
chore: add a petstore codegen spike as the witness for the codegen ta…
Wahbeh-Mohammad Sep 2, 2026
d64a107
Close the register backlog, then purge the retirement tables (#66)
Wahbeh-Mohammad Sep 4, 2026
3675b55
chore: Decide every deferred row, then dissolve the register
Wahbeh-Mohammad Sep 4, 2026
1f48926
chore: decide every open item, then dissolve the last register
Wahbeh-Mohammad Sep 4, 2026
de05910
docs: open the audit #67 decision ledger and index it
Wahbeh-Mohammad Sep 4, 2026
ae46aed
docs(serde): stop claiming a top-level wire null can never decode
Wahbeh-Mohammad Sep 4, 2026
217fe70
docs(core): correct six TSDoc blocks that state the opposite of the code
Wahbeh-Mohammad Sep 4, 2026
1fddb21
docs(core): name RequestBodyNotAllowedError on buildRequest, and pin it
Wahbeh-Mohammad Sep 4, 2026
8d777c9
test(transport-conformance): stop claiming TRANSPORT-22 the suite nev…
Wahbeh-Mohammad Sep 4, 2026
0c4c94a
docs: re-anchor deviations.md items 3, 5, 15, 17 and the OBS-29 row
Wahbeh-Mohammad Sep 4, 2026
c983c0c
docs: make three as-built guides match the functions they document
Wahbeh-Mohammad Sep 4, 2026
66ee76d
fix: give CTX-15's no-op instrumentation bundle a no-op span (#69)
Wahbeh-Mohammad Sep 4, 2026
2287649
test: pin REDIR-3's current-hop reading and PAGE-19's relative-refere…
Wahbeh-Mohammad Sep 4, 2026
e341d3d
docs: record the deviations the 2026-09-04 audit found outside a phas…
Wahbeh-Mohammad Sep 4, 2026
fe3e4a0
docs: ledger — record the #69 outcome, the span.ts split and the stri…
Wahbeh-Mohammad Sep 4, 2026
73c64d0
docs: ledger — record the #68 outcome and its round-2 scope
Wahbeh-Mohammad Sep 4, 2026
70c1a3e
docs: re-anchor the last five stale citations in deviations.md
Wahbeh-Mohammad Sep 4, 2026
bd396b6
docs(core): InstrumentationBundle no longer claims nothing consumes i…
Wahbeh-Mohammad Sep 4, 2026
6b88166
Merge branch 'audit/67/68-reanchor-deviations-tsdoc' into audit/remed…
Wahbeh-Mohammad Sep 4, 2026
840f355
Merge branch 'audit/67/69-record-unledgered-deviations' into audit/re…
Wahbeh-Mohammad Sep 4, 2026
e99f9d3
docs: ledger — wave 1 landed; D8/D9 for wave 2
Wahbeh-Mohammad Sep 4, 2026
f03a847
fix: build the redirect error messages from redactUrl (#70)
Wahbeh-Mohammad Sep 4, 2026
e5ea278
feat: carry a redacted url.full on http.redirect.rejected (#70)
Wahbeh-Mohammad Sep 4, 2026
86091f4
test: prove the rejected-redirect path leaks no secret end to end (#70)
Wahbeh-Mohammad Sep 4, 2026
b92fb72
docs: state the redacted-message / raw-property split in the errors g…
Wahbeh-Mohammad Sep 4, 2026
9798d22
feat: redact the Basic and Digest passwords behind credential classes…
Wahbeh-Mohammad Sep 4, 2026
0c06d74
fix: guard a challenge replay whenever the outbound hop was guarded (…
Wahbeh-Mohammad Sep 4, 2026
d5acd9e
docs: rewrite auth.md's credential shape and ledger the two AUTH-8/XC…
Wahbeh-Mohammad Sep 4, 2026
60174dc
docs: ledger — record the #70 outcome and the api-extractor link trap
Wahbeh-Mohammad Sep 4, 2026
fc33e90
docs: ledger — record the #71 outcome
Wahbeh-Mohammad Sep 4, 2026
fab9b70
Merge branch 'audit/67/70-redact-redirect-urls' into audit/remediatio…
Wahbeh-Mohammad Sep 4, 2026
9ce8943
Merge branch 'audit/67/71-auth-credential-hygiene' into audit/remedia…
Wahbeh-Mohammad Sep 4, 2026
4487660
docs: ledger — wave 2 landed, run paused before wave 3
Wahbeh-Mohammad Sep 4, 2026
cc27e81
docs: ledger — pre-take wave 3 decisions D10–D12 and the file partition
Wahbeh-Mohammad Sep 5, 2026
034a20c
fix(auth): read every value of the challenge header, parsed per value
Wahbeh-Mohammad Sep 5, 2026
db430f4
fix(auth): emit cnonce for every -sess Digest algorithm, negotiated q…
Wahbeh-Mohammad Sep 5, 2026
fb2a631
test(rx): pin the SSE ownership transfer by counting the owned resource
Wahbeh-Mohammad Sep 5, 2026
624f53b
fix(auth): an empty Digest realm or nonce is unsatisfiable, not present
Wahbeh-Mohammad Sep 5, 2026
9126a04
docs(rx): state the SSE ownership transfer outright
Wahbeh-Mohammad Sep 5, 2026
f6d2510
test(node): pin the SSE ownership transfer's single release on Node
Wahbeh-Mohammad Sep 5, 2026
19b0602
test(transport): both transports must surface every repeated challeng…
Wahbeh-Mohammad Sep 5, 2026
be1b823
docs: ledger the -sess cnonce departure from AUTH-22's letter
Wahbeh-Mohammad Sep 5, 2026
6152d27
docs: record the challenge-list and -sess cnonce behaviour in auth.md
Wahbeh-Mohammad Sep 5, 2026
74e4fd9
docs: ledger the RxJS SSE ownership transfer as an ASYNC-21 departure
Wahbeh-Mohammad Sep 5, 2026
2cfbe5c
docs(transport-conformance): the suite header no longer claims TRANSP…
Wahbeh-Mohammad Sep 5, 2026
21fa0ba
fix(transport-conformance): the fixture's header array must be mutabl…
Wahbeh-Mohammad Sep 5, 2026
36378b3
fix(retry): surface the final typed error, not a SuppressedError wrapper
Wahbeh-Mohammad Sep 5, 2026
32c9f2b
test(xcut): conformance rows for the surfaced retry error and its trail
Wahbeh-Mohammad Sep 5, 2026
0778a88
docs: the retry pillar throws the last attempt's error, and retryAtte…
Wahbeh-Mohammad Sep 5, 2026
c377fea
test(node): the retry trail case moves from "same wrapper" to "no wra…
Wahbeh-Mohammad Sep 5, 2026
4578841
docs(retry): the trail is not an attempt count — drop the false arith…
Wahbeh-Mohammad Sep 5, 2026
d883cd7
Merge branch 'audit/67/75-rx-sse-ownership' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
a81a156
Merge branch 'audit/67/74-auth-challenges' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
2ea8b3e
Merge branch 'audit/67/72-retry-typed-error' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
c644473
docs: ledger — wave 3 landed; re-anchor deviations item 3 after #72
Wahbeh-Mohammad Sep 5, 2026
25b987a
docs: ledger — pre-take wave 4 decisions D13–D15 and the file partition
Wahbeh-Mohammad Sep 5, 2026
dba7743
fix(core): a path placeholder is satisfied only by an own property (S…
Wahbeh-Mohammad Sep 5, 2026
3629ec5
fix(core): reject an invalid Date in the conditional-request setters …
Wahbeh-Mohammad Sep 5, 2026
8036fbc
fix(core): raise on a zero-length chunk during an exact-length body copy
Wahbeh-Mohammad Sep 5, 2026
ef7df30
fix(core): quote the multipart boundary parameter when it is not a token
Wahbeh-Mohammad Sep 5, 2026
5a49bb1
fix(core): the timeout setter enforces AbortSignal.timeout()'s own ra…
Wahbeh-Mohammad Sep 5, 2026
58a4d3e
fix(core): stop the response logging tap from poisoning itself after …
Wahbeh-Mohammad Sep 5, 2026
fbaf269
fix(core): one idempotency key per logical request across retry attempts
Wahbeh-Mohammad Sep 5, 2026
dad81b4
docs: record the RETRY-44 reading behind the single-application reque…
Wahbeh-Mohammad Sep 5, 2026
01dfdfb
test(node-conformance): cover the five Web Streams bridges, and pin t…
Wahbeh-Mohammad Sep 5, 2026
6c1343d
fix(core): reject an unpaired surrogate where it is supplied (HTTP-29…
Wahbeh-Mohammad Sep 5, 2026
2d3785c
fix(test): type the multipart drain helper as a view over a plain Arr…
Wahbeh-Mohammad Sep 5, 2026
e414e26
fix(core): getAll returns a frozen list on the absent-name path too (…
Wahbeh-Mohammad Sep 5, 2026
54b5b90
fix(core): TeeSink rejects a non-integral tap limit at the constructo…
Wahbeh-Mohammad Sep 5, 2026
d357f61
refactor(core): consolidate EMPTY_VALUE_LIST in http/builder.ts
Wahbeh-Mohammad Sep 5, 2026
63e2aec
docs(core): errors.ts TSDoc matches what the two classes now cover
Wahbeh-Mohammad Sep 5, 2026
18ba420
docs: http.md states the real timeoutMs range
Wahbeh-Mohammad Sep 5, 2026
179696e
docs: deviations — two rows for #76, per D0
Wahbeh-Mohammad Sep 5, 2026
10ebe03
Merge branch 'audit/67/73-idempotency-key-once' into audit/remediatio…
Wahbeh-Mohammad Sep 5, 2026
4212de3
Merge branch 'audit/67/77-body-lifecycle' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
36c3d04
Merge branch 'audit/67/76-domain-model-validation' into audit/remedia…
Wahbeh-Mohammad Sep 5, 2026
8e32ee2
docs: ledger — wave 4 landed; pre-take wave 5 decisions D16–D18
Wahbeh-Mohammad Sep 5, 2026
71c9997
test(retry): pin one retryability answer per I/O error class
Wahbeh-Mohammad Sep 5, 2026
b937175
fix(retry): a zero initial delay never yields a NaN backoff
Wahbeh-Mohammad Sep 5, 2026
426838b
fix(retry): a non-finite delayOverride falls back like a throwing one
Wahbeh-Mohammad Sep 5, 2026
1c3f46a
fix(core): scope send()'s async stores with run(), not enterWith (#80)
Wahbeh-Mohammad Sep 5, 2026
d8d3ad5
docs: item 17 states what the cause-walk matches, and why
Wahbeh-Mohammad Sep 5, 2026
1254707
fix(codec-json): race the abort signal against a pending read or writ…
Wahbeh-Mohammad Sep 5, 2026
b6b4041
feat(core): a public way to instrument a pipeline (#80)
Wahbeh-Mohammad Sep 5, 2026
f08bb19
fix(core): close the response on every paginator exit, including a ma…
Wahbeh-Mohammad Sep 5, 2026
6655f9d
test(retry): keep the new retry cases inside the lint caps
Wahbeh-Mohammad Sep 5, 2026
b757a55
fix(codec-json): a present Tristate can never carry null (SERDE-14)
Wahbeh-Mohammad Sep 5, 2026
a83c206
fix(core): the logging step says when a body capture fails, and names…
Wahbeh-Mohammad Sep 5, 2026
ead5a62
fix(core): report an SSE release failure once, not once per channel (…
Wahbeh-Mohammad Sep 5, 2026
b2e1d9b
docs(core): TSDoc for the reachable operation span, and for what reda…
Wahbeh-Mohammad Sep 5, 2026
0bcc6f2
docs(retry): RetryStepOptions.delayOverride states the failure semantics
Wahbeh-Mohammad Sep 5, 2026
80e16d7
fix(core): a pagination cursor with no UTF-8 form fails inside the er…
Wahbeh-Mohammad Sep 5, 2026
c6bbc3a
test(node-conformance): the caller's async context survives Runtime.s…
Wahbeh-Mohammad Sep 5, 2026
620cf8d
docs: how to instrument a pipeline, and what makes DEXPACE_LOG_LEVEL …
Wahbeh-Mohammad Sep 5, 2026
c44ce53
docs: deviations — OBS-29 closed, and an OBS-35 row for the default k…
Wahbeh-Mohammad Sep 5, 2026
835ac44
docs(core): send() states what it restores in the caller's async cont…
Wahbeh-Mohammad Sep 5, 2026
8093e8a
docs(core): the shipped .d.ts names the splice site and the abort race
Wahbeh-Mohammad Sep 5, 2026
118d56c
docs: a paging strategy owes a well-formed PageInfo (PAGE-4)
Wahbeh-Mohammad Sep 5, 2026
6c7fb65
Merge branch 'audit/67/78-retry-classify-backoff' into audit/remediat…
Wahbeh-Mohammad Sep 5, 2026
b9eef12
Merge branch 'audit/67/80-instrumentation' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
4576658
Merge branch 'audit/67/79-serde-pagination-sse' into audit/remediatio…
Wahbeh-Mohammad Sep 5, 2026
a9c5b43
docs: ledger — wave 5 landed; pre-take wave 6 decisions D19–D20
Wahbeh-Mohammad Sep 5, 2026
f02dd8e
fix(transport): degrade the headers a native client refuses, on both …
Wahbeh-Mohammad Sep 5, 2026
61fce6d
fix(transport-undici): write a file body through its own writeTo (BOD…
Wahbeh-Mohammad Sep 5, 2026
2a75c5b
feat(transport-undici): refuse a proxy type undici cannot honour, at …
Wahbeh-Mohammad Sep 5, 2026
6df9645
docs: record the SOCKS gap, and the two transport rules #81 was about
Wahbeh-Mohammad Sep 5, 2026
9c6f145
docs: re-anchor two citations the same change moved
Wahbeh-Mohammad Sep 5, 2026
ca682c5
test(transport): name the intact file-body fixture's size and range
Wahbeh-Mohammad Sep 5, 2026
0eebc23
docs(transport-fetch): the README's drop set was three names short
Wahbeh-Mohammad Sep 5, 2026
808f6b0
Merge branch 'audit/67/81-undici-parity' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
fe04249
docs: ledger — wave 6a landed (#81)
Wahbeh-Mohammad Sep 5, 2026
3c72ef0
fix(transport-shared): CONTROL_BYTE was excepting LF as well as HTAB
Wahbeh-Mohammad Sep 5, 2026
73af7c5
fix(transport): one classification table for a permanent native failure
Wahbeh-Mohammad Sep 5, 2026
55f06ac
fix(transport): a body-less response reports body === null on both ad…
Wahbeh-Mohammad Sep 5, 2026
deb32e0
fix(transport): a producer failure aborts the native call it raced
Wahbeh-Mohammad Sep 5, 2026
ac26cb0
fix(transport): validate defaultTimeoutMs at both factories
Wahbeh-Mohammad Sep 5, 2026
a889d5d
docs(transport): record the parity work, and the TRANSPORT-20 reading
Wahbeh-Mohammad Sep 5, 2026
901b50e
Merge branch 'audit/67/82-transport-parity' into audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
262fb8f
docs: ledger — wave 6b landed (#82), run complete; re-anchor item 13 …
Wahbeh-Mohammad Sep 5, 2026
c0ff3fd
Merge pull request #98 from dexpace/audit/remediation-67
Wahbeh-Mohammad Sep 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"@dexpace/core": minor
---

Add the seam foundations: the `Transport` contract with its `composeSignal`/`isTimeoutSignal` cancellation helpers and `CancellationError`, the operation-input projection (`OperationDescriptor`, `buildRequest`, `OperationAssemblyError`), and `DexpaceError` as the new root of the error taxonomy above `DomainModelError`.
Add the seam foundations: the `Transport` contract with its `composeSignal`/`isTimeoutSignal` cancellation helpers and `CancellationError`, the operation-input projection (`OperationDescriptor`, `buildRequest`, `OperationAssemblyError`), and `DexpaceError` as the root of the error taxonomy.

`DomainModelError` now extends `DexpaceError` instead of `Error`. This is additive — every existing leaf keeps its parent, its behavior, and its `instanceof DomainModelError` narrowing.
Every existing error leaf keeps its behavior and its message. The taxonomy is two levels: a leaf's own superclass is `DexpaceError` itself, and a family is grouped with an exported type guard rather than an intermediate class.
24 changes: 24 additions & 0 deletions .changeset/2026-08-25-body-lifecycle-review-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@dexpace/core": minor
---

Body lifecycle review fixes.

Security:

- Body media types are validated as header-safe at construction (`byteArrayBody`, `stringBody`, `streamBody`, and every part rendered into a multipart body), using the same predicate as outbound header-value validation (HTTP-26). A CR/LF in a media type was previously interpolated verbatim into a multipart part header, which allowed arbitrary header injection, arbitrary part content, and a forged closing boundary while the declared content length still matched the corrupted bytes (HTTP-51).
- `StreamBody.writeTo` now refuses a chunk that would carry the body past its declared `contentLength` *before* writing it, and aborts the sink rather than closing it on any length mismatch. Overrun bytes previously reached the sink and were reported only afterwards, leaving them on the socket behind a stamped `Content-Length` (HTTP-39/BODY-10).

Correctness:

- A body write failure is no longer masked by the close that follows it. All five `Body` implementations share one writer scope that aborts on failure and never lets a close error replace the primary one (RECOV-12), so retry classification still sees the I/O failure in the cause chain (RETRY-2).
- `TypedResponse.value()` memoizes a parser that throws synchronously; it previously re-ran the handler and re-read the single-use body (HTTP-44).
- `HttpStatusError.preview()` decodes with the charset declared by the response media type, falling back to UTF-8, and never throws a `RangeError` on an unknown label (HTTP-42).
- `withRequestLogging(...).materialize()` gives the new wrapper its own tap buffer instead of aliasing the original's, so one wrapper's write can no longer rewrite another's captured preview (BODY-21).
- `withResponseLogging` treats a zero-length delegate chunk as a stream-contract violation, matching `RetentionWindow` under IO-17 (BODY-25), and `snapshot()` now starts the lazy drain the way `read()` does (BODY-22).
- `Response.close()` marks the response closed only once the release actually succeeds, memoized so concurrent closers share one cancel — the shape `BufferedSink.close()` already uses (BODY-15, HTTP-43).

Public API:

- New `FormBodyValidationError`, reported by `isBodyError`. A form field whose value cannot be rendered is now raised instead of silently dropped from the body.
- `FormUrlEncodedInput` accepts the new `FormUrlEncodedValue` (`string | number | boolean | bigint | null`); primitives render rather than vanish (HTTP-38/BODY-35).
7 changes: 7 additions & 0 deletions .changeset/2026-08-25-body-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@dexpace/core": minor
---

Add the core Body domain interface and implementations (ByteArrayBody, StringBody, FormUrlEncodedBody, StreamBody, MultipartBody, materialize, TypedResponse, HttpStatusError, toHttpError, withRequestLogging, withResponseLogging).

`RequestBuilder.body` and `ResponseBuilder.body` narrow from `unknown` to `Body | undefined` and `ReadableStream<Uint8Array> | null` respectively — a breaking parameter-type change per `styleguide/typescript/10-api-design.md`. Resolving Phase 3b's open D1 finding (`docs/superpowers/specs/2026-07-23-nodejs-sdk-v1-roadmap-design.md`, "Open Findings — Phase 3b Validation Review"): kept as **minor** rather than major because `@dexpace/core` is still pre-1.0 (`0.0.0`), where a 0.x breaking change is conventionally released as minor (semver's own carve-out for initial development, https://semver.org/#spec-item-4). Revisit at 1.0.
5 changes: 5 additions & 0 deletions .changeset/2026-08-25-io-contracts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@dexpace/core": patch
---

Internal: byte-streaming primitives for product-spec §5 (IO-1–IO-42). No public API change.
23 changes: 23 additions & 0 deletions .changeset/2026-08-26-add-the-node-runtime-conformance-suite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
---

Add the Node-runtime conformance suite.

No published package changes.

Deliberately empty — `changeset --empty` — rather than absent. Every file in this change is repository
infrastructure that ships to nobody: `test/node-conformance/`, `.github/workflows/ci.yml`, `bunfig.toml`,
`eslint.config.js`, the root `package.json` scripts, `CLAUDE.md`, and the phase docs. Zero files under
`packages/` were touched, so there is nothing for `@dexpace/core` to bump and a `patch` here would put a line
in the published changelog that means nothing to a consumer reading it.

The empty changeset records that the judgement was made, which is the difference between "this change needs no
release" and "somebody forgot a changeset". Verified before writing it:
`git show --stat --name-only e3d0b18 | grep '^packages/'` returns nothing.

What the change does, for anyone reading this file from the repository rather than the changelog: `bun test`
runs the unit suite on Bun and proves nothing about the runtime the SDK ships to. 319 of 516 unit tests
exercise a runtime-divergent surface — Web Streams, `AbortSignal`, async iteration, `ByteQueue`'s `Uint8Array`
handling — against two assertions of Node coverage that touched none of it. `test/node-conformance/` adds 30
`node --test` cases over the built artifact, wired as `test:node` and run by CI as a matrix over the declared
`engines.node` floor and current LTS. Closes checkpoint §5.9 / roadmap finding E5.
46 changes: 46 additions & 0 deletions .changeset/2026-08-26-execution-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
'@dexpace/core': patch
---

Add the execution-context model for product-spec §7 (`CTX-1`–`CTX-20`, `XCUT-14`). No public API change.

Everything this adds lives under `packages/core/src/context/` and none of it is re-exported from
`src/index.ts` — `packages/core/etc/core.api.md` is byte-identical before and after. `patch` rather than an
empty changeset because files under `packages/` did change: the published tarball carries the new
`dist/context/*.js`, and a consumer stepping through the package in a debugger will see them.

What landed: `ExecutionContext` as a three-member discriminated union — `DispatchContext` (before any
request), `RequestContext` (an outbound request assembled), `ExchangeContext` (a response arrived, terminal) —
with `promoteToRequest`/`promoteToExchange` as the pure promotion chain and `createDispatchContext`/
`createRequestContext`/`createExchangeContext` as the off-chain factories `CTX-5`/`CTX-6` require.
`InstrumentationBundle` plus the `noopInstrumentationBundle` disabled-tracing default. `ContextStore`, a
bounded keyed registry with `install`/`installIfAbsent`/`find`/`close`, and `DuplicateContextKeyError`.

Three design calls worth recording:

- **Call keys are `Symbol()`, not a counter or a UUID.** `CTX-4`'s uniqueness requirement cannot lean on any
field of the instrumentation bundle, because `noopInstrumentationBundle`'s fields are all constants shared
by every context that takes the default. A fresh `Symbol()` per call is distinct across the process and
across all three context flavors by construction, and `ContextInit.key` is the pin that makes two contexts
deliberately share one store slot (`CTX-5`).
- **The store's cap drains in a loop, and holds strong references.** `XCUT-14` names context registries first
among the caller-keyed process-lived maps that MUST carry a hard cap and drain back under it after each
insert — an unbounded one is a memory-exhaustion vector, not merely a leak. The loop (rather than a single
check-then-evict) is what makes an insert burst converge. `Map`, never `WeakMap`/`WeakRef`: a registered
context keeps its whole `Request`+`Response` graph reachable on purpose, so the cap is the backstop rather
than the collector (`CTX-19`).
- **Promotions never touch a store.** `context.ts` does not import `store.ts`, which is what satisfies
`CTX-17`'s negative half structurally — constructing a head context must not auto-register it. Wiring the
store into the promotions would invert the layering and make every promotion a global side effect. The
positive half — the first store entry, installed by the first promotion — is Phase 4c's `Runtime.send()`.

Two known deviations, both already in the deferral register (`docs/work/mvp/2026-09-04-open-items-dissolution.md`):

- `contextStore` is a module-level mutable singleton, which
`docs/knowledge/harvested/variables-and-declarations.md:22` bans. Accepted because threading a store handle through
builder → runtime → every step would be a wide API change for no observable gain; logged in the design's
Deviation Ledger for Phase 10. Tests build their own `new ContextStore()` rather than asserting through the
singleton, which is shared by every file in a `bun test` run.
- `activeSpan` and `tracerFactory` stay typed `unknown`, and `activeSpan` is `undefined` rather than a no-op
span object. `CTX-14`/`CTX-15` ship as the bundle's frozen shape and the disabled default only; real W3C
Trace Context generation waits for the Phase 7 tracing adapter that gets to define `Span`.
14 changes: 14 additions & 0 deletions .changeset/2026-08-26-max-retries-range-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@dexpace/core": patch
---

Tighten `RequestOptionsBuilder.maxRetries` validation: a defined value must now be a non-negative
integer. `Infinity`, `NaN`, and fractional values were previously accepted and now throw
`RequestOptionsValidationError`, the same way a negative value already did.

A retry ceiling is a count of wire sends, so a non-finite one is as out of range as a negative one —
and worse in effect: a negative value still fails a downstream `>= 1` guard, while `Infinity` or
`NaN` makes a retry driver's `attempt >= ceiling` test permanently false and its loop unbounded.
HTTP-35's requirement is that an out-of-range retry count is a loud error at the call site that
supplied it, never a value reinterpreted somewhere downstream; this closes the half of that
requirement the setter did not implement.
51 changes: 51 additions & 0 deletions .changeset/2026-08-26-phase3-conformance-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
"@dexpace/core": minor
---

Phase 3 conformance fixes, from a review of the shipped `io/` and `body/` layers against the phase 3a/3b plans.

Correctness:

- The request-body logging tee now forwards **both** teardown paths to the sink it was handed. Its adapter stream
declared `write` and `close` but no `abort`, and a `WritableStream`'s default abort algorithm is a no-op — so a
delegate failure aborted the adapter and stopped there, leaving the caller's sink open, still locked, and never
told the message was broken. A truncated body could be committed downstream as a complete one. `writeTo` also
releases the writer when a delegate refuses before ever touching the adapter, which is what a `ConsumedBodyError`
on a second write does (BODY-17, RECOV-12).
- `StreamBody.writeTo` no longer cancels the caller's stream when the sink fails. The unknown-length path used
`pipeTo`'s default `preventCancel: false`, which cancels the *source* on a destination failure — taking
cancellation ownership away from the caller on exactly the failure path, and disagreeing with the
declared-length path, which only releases its reader. Both paths now leave the caller's stream alone (BODY-8).
- Every `Body` variant is frozen at construction. `readonly` is erased at run time, so `contentLength` could be
reassigned after construction and desynchronized from the bytes `writeTo` emits — the same declared-length drift
`MultipartBody` shares one framing routine to prevent, left open on the field a transport stamps into
`Content-Length` (HTTP-1, XCUT-15, HTTP-51).
- `Response` regained the private constructor and `createResponse` friend hook that the body-lifecycle rewrite
dropped. `Response` is exported as a value, so a public field-wise constructor let a caller construct around
`build()`'s required-field validation, and it appeared in the published `.d.ts` (HTTP-2).
- `TeeSink.write` validates its count. `IO-3`'s guard existed as three byte-for-byte copies and the tee — the
fourth size-taking surface — had none, so a negative count was rejected only indirectly, and not at all on its
`count === 0` and short-source early returns. The guard is now single-sourced in `io/limits.ts`.
- `withResponseLogging` enforces the zero-length-chunk contract on the exceeds-cap tail path as well as the drain.
A rule held in one regime and not the other made the same violating upstream pass or fail depending only on how
big the body happened to be (BODY-25).

Public API:

- `Response` and the response-body logging wrapper no longer declare `[Symbol.asyncDispose]`; `close()` is the only
teardown interface, matching every other resource-owning class in the package. The symbol postdates the declared
`engines.node` floor (`>=18.17`), where it evaluates to `undefined` and binds the method to the string
`"undefined"`, and its type reached the package only through a dev-only global — so a consumer compiling against
the published `.d.ts` on this package's own declared `lib` failed with
`TS2550: Property 'asyncDispose' does not exist on type 'SymbolConstructor'`. It returns, on all seven resource
owners at once, when the runtime floor moves.
- Every public symbol now carries TSDoc. The committed API report had accumulated 62 `(undocumented)` members,
including 11 of `Response`/`ResponseBuilder`'s own that a wholesale file rewrite had dropped; it is back to zero.

Internal:

- `http/charset.ts`'s `decodeText` is renamed `decodeBodyText`. It shares a name with `io/text-codec.ts`'s
`decodeText` while deliberately disagreeing with it: this one delegates every label to `TextDecoder` (so
`iso-8859-1` follows the WHATWG mapping onto windows-1252) and consumes a leading BOM, which is right for a whole
message body; the other implements true ISO-8859-1 for IO-13's round-trip and sets `ignoreBOM` so a mid-stream
BOM survives as ordinary data (SSE-12). Reaching for the wrong one silently changes bytes.
44 changes: 44 additions & 0 deletions .changeset/2026-08-26-phase3-review-pass-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
"@dexpace/core": minor
---

Phase 3 review pass 2. Five defects, each in the same class as one pass 1 already fixed — the earlier fixes
were correct but did not reach every site the same reasoning applies to.

Correctness:

- `Response.bytes()`, `Response.text()` and `toHttpError()` now acquire the body reader **inside** the try, so
the response is closed even when the read cannot start. `getReader()` itself throws a `TypeError` when an
external consumer already holds the lock — which `BODY-15` explicitly forbids assuming away, and which
`Response.close()` was already hardened for — so the one failure `BODY-16`'s close guarantee most needs to
cover was the one that skipped the close entirely and held the connection open.
- `MultipartBody.writeTo` verifies the bytes it writes against its own declared `contentLength`. The shared
framing routine keeps the framing consistent but takes each part's own `contentLength` on trust, and
`MultipartPart.body` is the public `Body` interface — so a caller implementation reporting one length and
writing another desynchronized the value a transport stamps into `Content-Length` from what reaches the
socket. An overrunning chunk is now refused before it is written, and a short total raises inside the writer
scope so the sink is aborted rather than cleanly closed (HTTP-51, same shape as `StreamBody`'s HTTP-39 check).
- `withRequestLogging` closes the primary sink when a delegate resolves without closing the adapter. It is the
only place that takes a writer on behalf of someone else's `Body`, so a delegate that ignored `writeTo`'s
close-the-sink contract stranded the caller's sink open and locked with nothing thrown to notice it by.
- A foreign primitive source that over-reports its transferred count now raises `SourceContractViolationError`.
It previously surfaced as `EndOfStreamError: delivered 2 of 99 bytes` — a foreign source's broken accounting
reported as an exhausted stream, which is the exact confusion `IO-17` forbids and which the under-report
direction was already guarded against (IO-17).

Documentation:

- `multipartBody`'s `boundary` parameter and `MultipartBodyBuilder.boundary` now state the obligation a
caller-supplied delimiter carries. RFC 2046 requires the sender to pick a boundary that appears in no part,
and that half cannot be checked here — a `StreamBody` part's bytes do not exist until the write, and a partial
scan would read as a complete guarantee. The generated default (32 random characters from Web Crypto) is the
mitigation, and is why it is the default.

Tooling:

- New blocking gate `verify:consumer-types`: compiles a throwaway consumer against the built `.d.ts` using the
`lib` and `target` read from `tsconfig.base.json`, with `types: []`. This is the gate whose absence let pass
1's `Symbol.asyncDispose` defect ship — `typecheck` passes on dev-only ambient globals, `build` emits
regardless, `api` only compares a report, `lint:publish` checks resolution and export shape rather than
whether declarations resolve, and `verify:dual-consumption` runs `node`, not `tsc`. Verified to fail on the
reintroduced defect and pass once reverted.
Loading
Loading