Skip to content

Commit 5479da0

Browse files
Phase 5a: configuration and the clock — documentation and phase record (#65)
* feat: add BuildInfo, UUID, Retryability and HTTPDate to core Phase 5a, Tasks 2 through 5: the four free-standing utilities the configuration chapter files beside the chain. - Dexpace::BuildInfo (CFG-36) resolves the SDK version and the runtime's version, vendor and OS name into frozen constants at load, each falling back to the non-blank "unknown", and composes IDENTITY_TOKENS in the fixed SDK-then-runtime order. Every component is read off a constant that needs no require, so the allowlist does not grow. - Dexpace::UUID.generate (CFG-32) draws sixteen bytes from a ::Random kept in the fiber-local Thread.current[:dexpace_prng] -- one generator per execution context, none shared -- masks the version and variant nibbles and returns the frozen canonical form. Never SecureRandom. - Dexpace::Retryability.retryable_status? (CFG-35's status half, XCUT-5's single classifier) answers for an Integer or anything answering #code: 408, 429 and every 5xx but 501 and 505. - Dexpace::HTTPDate (CFG-29 to CFG-31) formats with Time#httpdate and parses with an owned anchored grammar, never Time.httpdate, which accepts the obsolete RFC 850 and asctime forms; every component is checked back against what Time.utc built, because Time.utc(1994, 11, 31) is silently 1 December (P5-54). The grammar is one Regexp compiled once with a per-pattern timeout. seam_surface_test.rb now counts four stdlib features: http_date.rb requires "time" in the file that uses it. * feat: add Clock, Async.delay and the deadline keyword Phase 5a, Tasks 6 through 8: the injectable time source and what waits on it. - Dexpace::Clock (CFG-15, CFG-16) exposes exactly the three operations the _Clock interface declares -- #now, #monotonic on CLOCK_MONOTONIC's scale, and #sleep -- with Clock::SYSTEM as the platform-backed default and Clock.deadline_in as the one way to compute an instant, so a deadline is never derived from Time.now. #sleep is a per-call ::Thread::Queue#pop(timeout:) that the cancellation token's #on_cancel hook wakes, with the token re-checked after the wake (CFG-17): never Kernel#sleep, which cannot be woken without Thread#raise. The hook is detached in an ensure. - Future#wait, #value and Completer#await take deadline: and clock: beside the positional cancellation (SEAM-18, phase 2's P2-5 pick-up). The deadline is the same timed gate pop, and expiry settles the completer through request_cancel(:deadline_expired) rather than raising past the wait; the loop lives in the private Async::Deadline helper. - Dexpace::Async.delay (CFG-18) is the scheduler-conditional counterpart: a Future settling with true after the duration elapses on the registered Fiber.scheduler, and Dexpace::SeamError without one, since a thread-backed fallback would violate the headline it exists for. It settles with true, not nil, because SEAM-16 makes a nil-valued Settlement unconstructible (P5-52). * feat: add the configuration chain, the slot and their readers Phase 5a, Tasks 9 through 13: the four-tier chain, the process-wide slot, and the two earlier layers that now read them. - Dexpace::Configuration (CFG-1 to CFG-12, CFG-37, CFG-38) is a frozen Data over the override map and the two seams. #string looks up in the strict order -- override, environment, normalised property, default -- that inverts Ruby's convention on purpose (design ledger entry 16); CFG-2's emptiness rule applies to the environment tier alone; and #raw_property reads the property tier by the exact name. #integer, #boolean and #duration route through #string and never raise, with durations in Float seconds and a bare number read as milliseconds. Configuration::Builder is a file of its own beside its model, as phase 1 files every builder (P5-51); an inherited seam passes through by reference, added properties compose over it, and the two seam operations are mutually exclusive on one builder. Keys declares the seven CFG-14 names; Sources holds ENVIRONMENT, NONE and .from_hash. ConfigParsers and DeepValue (CFG-33, CFG-34: NaN equals NaN and hashes alike, signed zeros differ, Integer and Float arrays differ, cycles survive) are private_constants with sig/ mirrors (P5-57). - Dexpace.configure, .configuration and .reset_config! (CFG-13) swap one frozen reference under a ::Thread::Mutex and read it lock-free; the builder runs outside the mutex, so a block that reads the slot cannot deadlock it. - ContextStore.default is built on its first call, under one mutex, reading Keys::MAX_TRACKED_CONTEXTS then and falling back to the constant on a non-positive value (P5-55). Phase 4a's load-time assignment could never see a Dexpace.configure at boot, and an unsynchronised ||= publishes one store per first caller; phase 4a's fresh-process case now asserts no store exists before the first call. - Dexpace::IO.max_materialized_bytes reads Keys::MAX_MATERIALIZED_BYTES with the constant as the fallback, and the five readers of the constant -- TypedReads#guard_materialization!, Body.clamp_cap, StreamBody#replayable? and BufferBody#== -- read the function per call (P5-56), so the live configuration governs every materialisation. * feat: add the proxy model and its never-raising resolver Phase 5a, Tasks 14 and 15: CFG-22 through CFG-28. - Dexpace::Proxy is a frozen Data over eight members, built through .build with the port validated into 0..65535 and every non-proxy entry a HostPattern; #to_s renders type://user:****@host:port and #inspect masks the password and names the challenge handler by class only. #bypass? is true under bypass_all or on any matching pattern. - Proxy::Type is a closed set of three in the pipeline's Stage shape: .of is the only lookup and canonicalises a copy back to its constant, .new and .[] are private, there is no .build and #with refuses, so identity comparison over the constants holds. - Proxy::HostPattern is a one-member Data over the glob with the compiled Regexp a private instance variable set at construction (P5-53): anchored \A...\z, case-insensitive, every character but * and ? literal, per-pattern timeout, never the process-global one. - Proxy.resolve reads the chain through the private ProxyResolution and never raises on its content: the property tier first, https.* over http.* with the port taken from the same layer as the chosen host and credentials from https.* only; the environment second, HTTPS_PROXY then HTTP_PROXY, with an explicit port required since CFG-25 forbids defaulting to 80 or 443. Every malformed input yields nil after one Kernel#warn prefixed "[dexpace]" (P5-8), with a StandardError backstop behind the explicit branches. http.nonProxyHosts wins over NO_PROXY, an escaped separator is literal, and a bare "*" yields nil because a proxy no request will use is not a proxy (CFG-27). Nothing in core calls .resolve, which is how CFG-28's prohibition on implicit reads is met. proxy.rb loads its three nested files from inside its own class body; the entry file's phase-5a block is now complete at ten lines. * feat: regenerate the surface manifest for the configuration layer Phase 5a, Task 16's surface half. The runtime surface snapshot grows by 84 rows, 730 to 814: the public constants and methods of Configuration and its Builder, Keys and Sources, the slot's three module methods, Clock and Clock::SYSTEM, Async.delay, the deadline: and clock: keywords on the future and the completer, Proxy with Type and HostPattern, HTTPDate, UUID, Retryability, BuildInfo and IO.max_materialized_bytes. Every new row was read against the object model; no row was removed, and the three private_constants contribute none. The smoke suite pins the configuration layer beside the seven before it and preloads "time" so its "defines nothing outside Dexpace" case keeps its meaning: http_date.rb requires the feature, which defines Date and DateTime at the top level. * fix: mask the proxy username and take four review nits on the code layer Review round 0 of the phase-5a stack. CFG-22's canonical text is "never emit username/password in cleartext", and Proxy#to_s and #inspect printed the username verbatim beside a masked password (R0-2): both credentials now render as "****" when present and as nothing when absent, so a rendering says only whether a credential is set. Four nits on the same layer: ContextStore.default reads the chain outside its mutex and only the `||=` runs under it, with a lock-free read of the published reference in front, so no caller-supplied seam runs under a non-reentrant lock and a promotion after the first pays no lock (R0-4); Completer#await validates deadline: and clock: before the settled short-circuit, so a settled future ignores an expired deadline and not an invalid one (R0-5); Configuration::Builder.new routes its seed map through #override and its seams through the setters' guard, so a nil seed value or a non-callable seam fails fast under CFG-37 instead of stringifying to "" (R0-6); and a blank HTTPS_PROXY override or property no longer masks HTTP_PROXY, since CFG-2's fall-through covers only the environment tier and a blank is not a URL (R0-8). The RBS mirrors follow; the manifest is unchanged because every added member is private. * test: add the phase-5a doubles and the matrix-fact suite Phase 5a, Task 1 and the test-support half of the plan. - matrix_facts_test.rb asserts, on whichever interpreter runs it, the floor-straddling facts the design rests on: Time.httpdate accepting asctime and RFC 850 while rejecting UTC and a missing comma, Fiber storage leaking across threads while Thread.current is isolated, Random#bytes returning an unfrozen BINARY buffer, Queue#pop with a negative timeout returning nil at once and unmounting the fiber under a scheduler, the RUBY_* constants needing no require, and ENV handing out a frozen String per call. Each is a guard of its own beside the suites, and the three doubles are exercised here too. - FakeClock is the _Clock double: a settable monotonic instant advanced by the test, a recorded sleep, and nothing the interface does not declare. - FakeConfigSource is the configuration-seam double, named so because fake_source.rb is phase 3a's IO-17 double and would have been overwritten by the plan's name (P5-58). - ParkingScheduler is the Fiber.scheduler double that parks a fiber with a deadline and runs its loop in #close; phase 2's ProbeScheduler records hooks and cannot drive a timed pop. It defines #fiber_interrupt because 4.0.6 warns without it. All three are top level, as every double on main is. * test: cover the utilities, the clock and the deadline Phase 5a, Tasks 2 through 8: the suites for BuildInfo, UUID, Retryability, HTTPDate, Clock, Async.delay and the deadline keyword, each opening with the IDs it exercises. - build_info_test.rb: every constant frozen and non-blank, the token order, and the blank guard resolving to "unknown" (CFG-36). - uuid_test.rb: the version-4 shape and the variant nibble over a thousand draws, one generator per context and none shared, and a text scan of uuid.rb for SecureRandom, since securerandom is allowlisted and the require gate would not catch the substitution (CFG-32). - retryability_test.rb: 408, 429 and every 5xx but 501 and 505, over an Integer and a Status, and XCUT-7's subset (CFG-35). - http_date_test.rb: the specification's own example byte for byte, the four zone tokens as one instant, the RFC 850 and asctime rejections, the impossible calendar dates Time.utc would normalise, and the grammar's per-pattern timeout (CFG-29 to CFG-31). - clock_test.rb: the three-method seam, a cancel during a thirty-second wait waking it inside five seconds, the token re-asserted after the wake, and a text scan for Kernel.sleep (CFG-15 to CFG-17). - delay_test.rb: the zero-duration fast path, SeamError without a scheduler, and settlement under ParkingScheduler (CFG-18). - future_deadline_test.rb and close_quietly_cfg21_test.rb: expiry settling the completer with :deadline_expired rather than raising past the wait, non-expiry returning the value, the identical error object through #value, and CFG-21's late close (SEAM-18, CFG-19 to CFG-21). * test: cover the chain, the slot, the wirings and the proxy Phase 5a, Tasks 9 through 15: the suites for Configuration and its Builder, Keys and Sources, the slot, the two wirings into earlier layers, and the proxy model with its resolver. - configuration_test.rb: CFG-1's order with every tier populated, the CFG-2 asymmetry, CFG-3's one-way normalisation, the never-throw accessors over every grammar, and DeepValue driven from inside module Dexpace -- NaN payloads, signed zeros, Integer against Float, and two self-referential arrays (CFG-1 to CFG-7, CFG-33, CFG-34, CFG-38). - builder_test.rb, keys_test.rb, sources_test.rb: the builder's contract and every fail-fast branch, the inherited seam passed by reference and composed over, the seven declared key names, and the hermetic hash seam (CFG-8 to CFG-12, CFG-14, CFG-37). - config_test.rb: the slot's identity default, last-write-wins at the key level, a re-entrant block, sixteen writers against sixteen readers seeing only whole snapshots, and a text pin that both publications happen inside the one mutex, because under the GVL no behavioural case can see the mutex dropped (CFG-13, XCUT-11). - context_store_config_test.rb: the cap read on first use in a fresh process, the non-positive fallback, and sixteen threads reaching .default first getting one store under a seam slowed to 50 ms, which is the case an unsynchronised ||= fails (CTX-11). - io_ceiling_test.rb: the configured ceiling honoured by every reader, including Buffer#snapshot through the materialisation guard and Body.buffer_bounded clamping down and never up (IO-9, BODY-32). - proxy_test.rb, type_test.rb, host_pattern_test.rb: the model's refusals and masked renderings, the closed type set, the anchored case-insensitive glob with its timeout, and the resolver over both tiers -- the same-layer port rule, the credential source, every malformed input answered by its own branch and the backstop only by a raising seam, port 65536 refused by the resolver's own check, and the NO_PROXY and http.nonProxyHosts splitting rules (CFG-22 to CFG-28). * test: make the proxy, ceiling and cap suites hermetic; cover round 0 Review round 0 of the phase-5a stack. The CFG-28 case resolved against the empty slot, whose environment seam is the real ENV, and failed on a host with HTTPS_PROXY set (R0-1, blocking): the lib scan stays, and the default-argument half now installs a fake seam in the slot through Dexpace.configure, asserts .resolve with no argument reads it, and resets in ensure. The ceiling suite seeds the slot's environment seam with an empty fake in every setup and after each mid-case reset, and the cap suite's fresh processes clear MAX_TRACKED_CONTEXTS by default (R0-7). The masking cases flip to the username masked with the password and add the one-credential shapes (R0-2). The race case slows `new` rather than the seams, because the seams now run outside the lock and a slow seam alone no longer discriminates the mutex; two cases observe that the seams run with the mutex unowned and that a later .default runs no seam (R0-4). One case each for a settled future refusing an invalid deadline (R0-5), Builder.new refusing a nil seed value, a blank seed key and a non-callable seam (R0-6), and a blank HTTPS_PROXY from any tier not masking HTTP_PROXY (R0-8). * docs: add the phase-5a checklist and the design's as-built addendum The checklist, written from what was built: thirty-eight own rows, CFG-1 to CFG-38, 35 implemented and 3 implemented in part -- CFG-20, whose fourth clause docs/first-release.md's Unsatisfied-MUSTs entry already names; CFG-34, whose container clause has no Ruby manifestation per section 11.15; CFG-35, whose throwable half is phase 6a's Task 3 -- nothing deferred, nothing not built, nothing not applicable, plus seven cross-reference rows for the IDs of other phases this build reached. It carries the thirty-six single-edit mutations run red under twenty-five headings, the audit groups run, twenty-three departures from the plan's text with their reasons, the findings routed, and the postponed work with its owners. The design gains an "As built" addendum with rows P5-51 to P5-58, numbered from the tree so the parallel phase-5 lanes cannot collide: the Builder's own file, Async.delay settling with true, the one-member HostPattern, HTTPDate's component round trip, the first-call store construction under a mutex, the ceiling's readers moved to the function, the private constants' sig/ mirrors, and the three doubles' names. The addendum records six places the design's text reads differently against the source without rewriting it, and states that no frozen-chapter sentence is contradicted, so docs/first-release.md is untouched. * docs: add the as-built configuration page and point at it docs/sdk-documentation/configuration.md is the ninth as-built page: the four-tier chain and the strict order that inverts Ruby's habit, the never-throw typed accessors, the builder with its two seams and derivation, the process-wide slot, the seven declared keys and the two earlier layers that read them, the injectable clock and its cancellable queue wait, the deadline on the future and the scheduler-conditional delay, the proxy model with its closed type set, host patterns and never-raising resolver, the four utilities, and the private structural equality nothing calls yet. Every example was run verbatim on 4.0.6 and 3.2.11 and printed the same, but for the floor's Hash#inspect spelling and BuildInfo::RUNTIME_VERSION, which the page says. architecture.md, the core README, README.md and docs/README.md point at it, and each names phase 5a among the built phases. * docs: record phase 5a in CLAUDE.md and the roadmap CLAUDE.md's built-phases paragraph gains the configuration layer in the same shape as the seven before it, citing the 5a checklist; the counts the probe reads are re-derived from the tree -- one hundred and twenty phase files under lib/dexpace/, nine private_constants without a test/ mirror, nine checklists written -- and the constraints-that-bite list gains five lines: the chain's precedence and the CFG-2 asymmetry, the queue wait behind Clock#sleep and the deadline, Time#httpdate against the owned parser, the UUID generator's fiber-local Random, and the context store's first-call construction beside the per-call ceiling. The roadmap's status notes gain the 2026-09-17 entry after 4a's, in the same shape: the stack and its base, the seventeen files, the three pick-ups from earlier phases and how each landed -- the deadline as a timed gate pop, the store's cap by option (b), the ceiling's readers moved -- the fourth left to 5b, the checklist's counts, the gate runs, the thirty-six guards, the facts re-verified on every row, the four shape changes against the design, and the addenda that remain a human's because the design chapters are frozen. * docs: record review round 0's repair in the checklist, ledger and page Review round 0 of the phase-5a stack. The CFG-22 row now says both credentials are masked and why, the CFG-28 row that its slot half is proven through a fake seam and not the real ENV, the CFG-24 row that a blank HTTPS_PROXY from any tier is absent for the preference, the CFG-37 row that Builder.new validates its seeds, the CTX-11, XCUT-11 and SEAM-18 rows the seams-outside-the-mutex shape and the validate-before- return order, the IO-9 row the hermetic slot and the assumption phase 3a's and 3b's own suites still make, and the CFG-26 row the plan's self-review item 4 as the reading behind the dropped empty token (R0-9). A second guard table carries the nine mutations run red after the repair, deviations 24-26 and a findings-routed bullet carry the nine findings, and the design's P5-55 row is amended for the chain read outside the lock with a round-0 paragraph beside the addendum. The as-built page's proxy fences render the masked username, gain the one-credential shape and the blank-HTTPS_PROXY case, and its store paragraph says the seams never run under the lock; every fence was re-run on 4.0.6 (154 checks, 0 mismatches) and 3.2.11. CLAUDE.md cites P5-9 for Async.delay's raise-rather-than-degrade decision (R0-3) and describes the store's lock shape; the roadmap's status note gains the round-0 sentence and the re-run counts (1,762 runs; 4,703 of 4,704).
1 parent 31ec0e6 commit 5479da0

9 files changed

Lines changed: 1318 additions & 26 deletions

File tree

‎CLAUDE.md‎

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ three-state PATCH — solved exactly once, and it deliberately does not compete
1313
Work here is **spec-driven, not feature-driven**. `docs/product-spec/` is normative: 645 numbered requirements
1414
across 19 prefixes. Before implementing anything, find the requirement IDs it must satisfy.
1515

16-
**Phases 0, 1, 2, 3a, 3b, 4a, 4b and 4c are built; the domain model, the seam layer, the byte-streaming
17-
layer, the body layer, the execution context, the recovery layer and the stage pipeline are the only domain
18-
code.** Six gems exist under `gems/`, every one at `0.0.0`. `dexpace-core` carries the HTTP domain model — `Dexpace::Request`,
16+
**Phases 0, 1, 2, 3a, 3b, 4a, 4b, 4c and 5a are built; the domain model, the seam layer, the byte-streaming
17+
layer, the body layer, the execution context, the recovery layer, the stage pipeline and the configuration
18+
layer are the only domain code.** Six gems exist under `gems/`, every one at `0.0.0`. `dexpace-core` carries the HTTP domain model — `Dexpace::Request`,
1919
`Response`, `Headers`, `Status`, `Method`, `Protocol`, `MediaType`, `Query`, `RequestOptions`, `HeaderName`, the
2020
`HeaderSyntax`, `PercentEncoding` and `URL` function modules, and the construction contract `Dexpace::Model` /
2121
`Dexpace::Builder` under one error root, `Dexpace::Error`
@@ -62,7 +62,20 @@ pipeline, §8.1's dispatch runtime: `Dexpace::Pipeline` with its nested vocabula
6262
`Transform`, and the two private drivers — beside the flat `Dexpace::AsyncPipeline` with
6363
`.map_response` and the one error `Dexpace::PipelineError`; the two `SEAM-18` bridges are the
6464
pipeline's bridges and it ships none of its own
65-
(`docs/work/mvp/phase4/phase4c/2026-09-09-phase4c-stage-pipeline-checklist.md`); every other
65+
(`docs/work/mvp/phase4/phase4c/2026-09-09-phase4c-stage-pipeline-checklist.md`) — and the
66+
configuration layer, §10's four-tier chain and §8.3's clock: the frozen `Dexpace::Configuration`
67+
`Data` with its `Builder`, `Keys`, `Sources` (`ENVIRONMENT`, `NONE`, `.from_hash`) and the private
68+
`ConfigParsers`, the process-wide slot `Dexpace.configure` / `.configuration` / `.reset_config!`
69+
over `Configuration::EMPTY`, the private structural comparator `Dexpace::DeepValue`, the injectable
70+
`Dexpace::Clock` with `SYSTEM`, `.deadline_in` and the RBS interface `_Clock`, the
71+
scheduler-conditional `Dexpace::Async.delay` beside the `deadline:` / `clock:` keywords on
72+
`Future#wait` / `#value` and `Completer#await`, the proxy model `Dexpace::Proxy` with its closed
73+
`Type` set, `HostPattern` and the private `ProxyResolution` behind `Proxy.resolve`, the RFC 1123
74+
pair `Dexpace::HTTPDate.format` / `.parse`, the v4 `Dexpace::UUID.generate`,
75+
`Dexpace::Retryability.retryable_status?` and the static `Dexpace::BuildInfo` constants — plus two
76+
wirings into earlier layers: `ContextStore.default` reads `Keys::MAX_TRACKED_CONTEXTS` on its first
77+
call, and the five readers of `IO::MAX_MATERIALIZED_BYTES` read `Dexpace::IO.max_materialized_bytes`
78+
per call (`docs/work/mvp/phase5/phase5a/2026-09-09-phase5a-configuration-checklist.md`); every other
6679
gem's `lib/` still holds its namespace module and a `VERSION` constant and nothing else. Nothing talks to a
6780
socket yet. The workspace root
6881
carries the `Gemfile`, `Rakefile`, `Steepfile`, `rbs_collection.yaml`, `.rubocop.yml`, `.yardopts`, `VERSIONS` and
@@ -479,6 +492,42 @@ Each is one line plus the chapter to read before touching the area.
479492
`Dexpace/NoWeakReferences`, refuses `ObjectSpace::WeakMap`, `ObjectSpace::WeakKeyMap` and `WeakRef` in every
480493
gem's `lib/`, because `CTX-19` makes the cap and not the collector the leak backstop (P4-10). Construction
481494
registers nothing; only the two promotions call `#set` (`CTX-17`).
495+
- **The configuration chain's precedence inverts Ruby's convention: an explicit override, then the
496+
environment, then a property, then the default (`CFG-1`)** — a property set through `Dexpace.configure`
497+
is the *third* tier, below a process environment variable of the same normalised name, and the key
498+
normalisation is `downcase.tr("_", ".")` on the property side only (`CFG-3`); `Configuration#string` is
499+
the one lookup every typed accessor routes through, and an empty override or property value is an
500+
answer while an empty environment value is absent (`CFG-2`, which names that tier alone).
501+
`Dexpace.configuration` is a lock-free read of a frozen snapshot; `Dexpace.configure` builds outside the
502+
mutex and swaps inside it, so a block that reads the slot cannot deadlock the non-reentrant mutex
503+
(`CFG-13`, `XCUT-11`; phase 5a's design).
504+
- **`Dexpace::Clock#sleep` is a per-call `::Thread::Queue#pop(timeout:)` woken by the cancellation
505+
token's `#on_cancel` push, never `Kernel#sleep`** — `Kernel#sleep` cannot be woken without
506+
`Thread#raise`, which is banned; the token is re-asserted after the wake, so a cancel during the wait
507+
raises `Dexpace::CancelledError` and a wait that elapsed returns `nil` (`CFG-15`, `CFG-17`). The same
508+
timed pop is the deadline on `Future#wait` / `#value` / `Completer#await`, which expire by
509+
`request_cancel(:deadline_expired)` — settling the completer, never raising past it — and
510+
`Dexpace::Async.delay` raises `SeamError` without a `Fiber.scheduler` rather than blocking a thread
511+
(`CFG-18`; phase 5a's P5-9, and P5-52 for the `true` it settles with).
512+
- **`Time#httpdate` formats and `Dexpace::HTTPDate.parse` parses; `Time.httpdate`, `Time.parse` and
513+
`Time.rfc2822` are never called in core** — the stdlib parser accepts the obsolete RFC 850 and asctime
514+
forms `CFG-31` refuses, and `Time.utc` silently normalises `31 Nov` and `29 Feb 1995` to the next day, so
515+
the parser round-trips every component it built (`CFG-30`, `CFG-31`; phase 5a's P5-54). The formatter's
516+
English names are CRuby's own tables and never the locale — verified under a `de_DE.UTF-8` `LOCPATH` on
517+
3.2.11, 3.4.10 and 4.0.6.
518+
- **`Dexpace::UUID.generate` draws from a `::Random` in `Thread.current[:dexpace_prng]`, never
519+
`SecureRandom`** — `securerandom` is allowlisted, so the require gate would not catch the substitution;
520+
`uuid_test.rb`'s text scan does. `Thread.current[]` is fiber-local, which is the point: one generator per
521+
execution context and none shared (`CFG-32`).
522+
- **`ContextStore.default` is built on its FIRST call, under one `::Thread::Mutex`, reading the configured
523+
cap then — and reading it OUTSIDE the lock** — phase 4a's load-time assignment could never see a
524+
`Dexpace.configure` at boot, and an unsynchronised `@default ||= new` publishes one store per first
525+
caller under a slow construction (sixteen for sixteen; `context_store_config_test.rb`); the two seams are
526+
caller-supplied callables and never run under the non-reentrant mutex, only the `||=` does, and the
527+
published reference is read lock-free after that. A configure after the first promotion does not resize
528+
the store, and neither does `reset_config!` (`CTX-11`; phase 5a's P5-55). The materialisation ceiling is the
529+
other way round: `Dexpace::IO.max_materialized_bytes` is read per call, so the live configuration governs
530+
every materialisation and `IO::MAX_MATERIALIZED_BYTES` is only the default and the fallback (P5-56).
482531

483532
## Public API surface
484533

@@ -578,11 +627,13 @@ probe compares each against the live tree, and a count written anywhere else in
578627
and `dexpace-conformance`. Each has a gemspec reading `VERSIONS`, a `sig/` mirroring its `lib/` one file
579628
per file, a smoke suite, a README, a LICENSE copy and a per-gem `Rakefile`. `dexpace-core`'s `lib/` holds
580629
the phase-1 HTTP domain model, the phase-2 seam layer, the phase-3a byte-streaming layer, the phase-3b
581-
body layer, the phase-4a execution context, the phase-4b recovery layer and the phase-4c stage pipeline —
582-
one hundred and three phase-1, phase-2, phase-3a, phase-3b, phase-4a, phase-4b and phase-4c files under
583-
`lib/dexpace/` beside phase 0's `version.rb`, every one mirrored in `sig/`, and every one of the one hundred
584-
and three but the six `private_constant`s `hooks.rb`, `bounded_map.rb`, `context/call_key.rb`,
585-
`recovery/ownership.rb`, `pipeline/sync_driver.rb` and `pipeline/async_driver.rb` mirrored in `test/`; every
630+
body layer, the phase-4a execution context, the phase-4b recovery layer, the phase-4c stage pipeline and
631+
the phase-5a configuration layer — one hundred and twenty phase-1, phase-2, phase-3a, phase-3b, phase-4a,
632+
phase-4b, phase-4c and phase-5a files under `lib/dexpace/` beside phase 0's `version.rb`, every one
633+
mirrored in `sig/`, and every one of the one hundred and twenty but the nine `private_constant`s
634+
`hooks.rb`, `bounded_map.rb`, `context/call_key.rb`, `recovery/ownership.rb`, `pipeline/sync_driver.rb`,
635+
`pipeline/async_driver.rb`, `configuration/parsers.rb`, `deep_value.rb` and `proxy/resolution.rb` mirrored
636+
in `test/`; every
586637
other
587638
gem is a phase-0 skeleton whose `lib/` holds the namespace module and a `VERSION` constant and nothing
588639
else. Every adapter gemspec declares `dexpace-core` and no third-party gem yet
@@ -593,15 +644,16 @@ probe compares each against the live tree, and a count written anywhere else in
593644
`phase1/`, `phase2/`, `phase3/`, `phase4/`, `phase5/`, `phase6/`, `phase7/`, `phase8/`, `phase9/` and `phase10/`. `phase0/`, `phase1/` and `phase2/` each
594645
carry that phase's design, plan and checklist; `phase3/` carries its segmentation design,
595646
`docs/work/mvp/phase3/2026-09-08-phase3-segmentation-design.md`, and two sub-phase directories —
596-
`phase3/phase3a/` and `phase3/phase3b/`, each holding that sub-phase's design, plan and checklist — eight
647+
`phase3/phase3a/` and `phase3/phase3b/`, each holding that sub-phase's design, plan and checklist — nine
597648
checklists written so far, each at implementation; `phase4/`
598649
carries its segmentation design,
599650
`docs/work/mvp/phase4/2026-09-08-phase4-segmentation-design.md`, and three sub-phase
600651
directories — `phase4/phase4a/`, `phase4/phase4b/` and `phase4/phase4c/`; each holds that sub-phase's
601652
design, plan and checklist. `phase5/` carries its segmentation design,
602653
`docs/work/mvp/phase5/2026-09-09-phase5-segmentation-design.md`, and three sub-phase
603-
directories — `phase5/phase5a/`, `phase5/phase5b/` and `phase5/phase5c/`; each holds a design
604-
and a plan. `phase6/` carries its segmentation design,
654+
directories — `phase5/phase5a/`, `phase5/phase5b/` and `phase5/phase5c/`; `phase5a/` holds that
655+
sub-phase's design, plan and checklist, and the other two each hold a design and a plan. `phase6/`
656+
carries its segmentation design,
605657
`docs/work/mvp/phase6/2026-09-09-phase6-segmentation-design.md`, and three sub-phase
606658
directories — `phase6/phase6a/` (retry), `phase6/phase6b/` (redirect) and `phase6/phase6c/`
607659
(authentication); each holds a design and a plan. Phase 6 is the largest **build** phase in the
@@ -663,6 +715,6 @@ probe compares each against the live tree, and a count written anywhere else in
663715
`gates:sole_parse`) and a ninth probe check for chapter attribution — none of the four built yet —
664716
and closes or narrows five `docs/first-release.md` lines while publishing nothing: every gem stays
665717
at `0.0.0`.
666-
Every checklist but phase 0's, phase 1's, phase 2's, phase 3a's, phase 3b's, phase 4a's, phase 4b's and
667-
phase 4c's is still to be written at execution time.
718+
Every checklist but phase 0's, phase 1's, phase 2's, phase 3a's, phase 3b's, phase 4a's, phase 4b's,
719+
phase 4c's and phase 5a's is still to be written at execution time.
668720
- There are 40 harvested topics under `docs/knowledge/harvested/`; the harvest ran here on 2026-09-05.

‎README.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ not compete with `faraday` or `httpx` on the easiest way to fetch a JSON endpoin
1515

1616
## Status
1717

18-
**Phases 0, 1, 2, 3a, 3b, 4a, 4b and 4c are built.** Nothing is published. The repository holds six gems under
18+
**Phases 0, 1, 2, 3a, 3b, 4a, 4b, 4c and 5a are built.** Nothing is published. The repository holds six gems under
1919
`gems/`, every one at `0.0.0`. `dexpace-core` carries the HTTP domain model — the frozen,
2020
validated wire types every later phase stands on (`docs/sdk-documentation/http.md`) — the seam
2121
layer: the provider registry, the transport and codec seams, the core-owned async pivot,
@@ -34,7 +34,11 @@ here on (`docs/sdk-documentation/recovery.md`) — and the stage pipeline: sixte
3434
stages, a builder with pillar rules and surgical edits, a forward-only per-call cursor with a
3535
pillar-only fork and stage-scoped state, a sync runtime and an async mirror that are both
3636
transports, and the adapter that installs a recovery transform
37-
(`docs/sdk-documentation/pipelines.md`); nothing talks to a socket yet. The other five are still skeletons — a namespace, a `VERSION`, a gemspec, a
37+
(`docs/sdk-documentation/pipelines.md`) — and the configuration layer: the four-tier chain with its
38+
never-throw typed accessors, builder and process-wide slot, the injectable clock with its
39+
cancellable wait and the deadline the future gained, the proxy model and its never-raising
40+
resolver, and the RFC 1123 date, UUID, retryability and build-info utilities
41+
(`docs/sdk-documentation/configuration.md`); nothing talks to a socket yet. The other five are still skeletons — a namespace, a `VERSION`, a gemspec, a
3842
signature mirror and a smoke suite:
3943

4044
| Gem | Namespace | Runtime dependencies today |

‎docs/README.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,10 @@ a different namespace and the probe does not confuse them.
102102
Six gems exist under `gems/`, all at `0.0.0` and none published — each a gemspec reading the
103103
root `VERSIONS` file, a `sig/` mirror and a smoke suite. `dexpace-core` holds phase 1's HTTP
104104
domain model, phase 2's seam layer, phase 3a's byte-streaming layer, phase 3b's body layer, phase
105-
4a's execution context, phase 4b's recovery layer and phase 4c's stage pipeline; the other five are
105+
4a's execution context, phase 4b's recovery layer, phase 4c's stage pipeline and phase 5a's
106+
configuration layer; the other five are
106107
phase 0's skeletons, a namespace and a `VERSION`. The as-built documentation lands in
107-
[`sdk-documentation/`](./sdk-documentation/) as each gem gains code; the eight pages written so far are [`sdk-documentation/quality-gates.md`](./sdk-documentation/quality-gates.md), because the
108+
[`sdk-documentation/`](./sdk-documentation/) as each gem gains code; the nine pages written so far are [`sdk-documentation/quality-gates.md`](./sdk-documentation/quality-gates.md), because the
108109
gate set is the thing phase 0 built, [`sdk-documentation/http.md`](./sdk-documentation/http.md),
109110
because the domain model is the thing phase 1 built,
110111
[`sdk-documentation/seams.md`](./sdk-documentation/seams.md), because the seam layer is the thing
@@ -114,9 +115,11 @@ because the body layer is the thing phase 3b built,
114115
[`sdk-documentation/execution-context.md`](./sdk-documentation/execution-context.md), because the
115116
execution context is the thing phase 4a built,
116117
[`sdk-documentation/recovery.md`](./sdk-documentation/recovery.md), because the recovery layer and
117-
the error trail are the things phase 4b built, and
118+
the error trail are the things phase 4b built,
118119
[`sdk-documentation/pipelines.md`](./sdk-documentation/pipelines.md), because the stage pipeline is
119-
the thing phase 4c built.
120+
the thing phase 4c built, and
121+
[`sdk-documentation/configuration.md`](./sdk-documentation/configuration.md), because the
122+
configuration layer and the clock are the things phase 5a built.
120123

121124
## Keeping this file true
122125

‎docs/sdk-documentation/architecture.md‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
skeletons and the gate set, phase 1 the HTTP domain model in `dexpace-core`, phase 2 the seam
55
layer in the same gem, phase 3a the byte-streaming layer beneath every body, phase 3b the body
66
layer on top of it, phase 4a the execution context one in-flight call carries, phase 4b the
7-
recovery layer and the error trail, and phase 4c the stage pipeline; the adapters are still to
7+
recovery layer and the error trail, phase 4c the stage pipeline, and phase 5a the configuration
8+
layer and the clock; the adapters are still to
89
come, so this page is still a stub: it names the pages this tree will eventually hold and where
910
each one's content will come from, so the plan for the documentation exists before the
10-
documentation does. Eight pages are real already, because their subjects are:
11+
documentation does. Nine pages are real already, because their subjects are:
1112
[`quality-gates.md`](./quality-gates.md), [`http.md`](./http.md), [`seams.md`](./seams.md),
1213
[`io.md`](./io.md), [`body.md`](./body.md), [`execution-context.md`](./execution-context.md),
13-
[`recovery.md`](./recovery.md) and [`pipelines.md`](./pipelines.md).
14+
[`recovery.md`](./recovery.md), [`pipelines.md`](./pipelines.md) and
15+
[`configuration.md`](./configuration.md).
1416
Once `dexpace-core` and the first adapters ship, this page becomes the same kind of front door the
1517
sibling Node SDK's `docs/sdk-documentation/architecture.md` is — package by package, seam by seam
1618
— and the entries below turn from plain text into real links, one at a time, as each page is
@@ -76,6 +78,17 @@ phase 4c shipped; derives from `docs/sdk-design-ruby/05-pipeline-architecture.md
7678
read together with the pipeline deviations recorded in
7779
`docs/sdk-design-ruby/10-deliberate-deviations-from-the-reference-contract.md`.
7880

81+
[configuration.md](./configuration.md) — the configuration layer and the clock: the four-tier
82+
chain and the strict order that inverts Ruby's habit, the never-throw typed accessors, the
83+
builder, the two seams and derivation, the process-wide slot, the seven declared keys and the two
84+
earlier layers that read them, the injectable clock and its cancellable queue wait, the deadline
85+
on the future and the scheduler-conditional delay, the proxy model with its closed type set, host
86+
patterns and never-raising resolver, and the RFC 1123 date, UUID, retryability and build-info
87+
utilities. Written against the layer phase 5a shipped; derives from
88+
`docs/sdk-design-ruby/08-instrumentation-and-configuration.md` §8.2 and §8.3, read together with
89+
entries 16 and 17 of
90+
`docs/sdk-design-ruby/10-deliberate-deviations-from-the-reference-contract.md`.
91+
7992
auth.md — tiers, credentials, schemes, challenges, and the redirect-safe re-issue rule. Derives
8093
from `docs/sdk-design-ruby/06-retry-redirect-and-authentication.md`.
8194

0 commit comments

Comments
 (0)