Repository navigation
Commit 5479da0
authored
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
- docs
- sdk-documentation
- work/mvp
- phase5/phase5a
- gems/dexpace-core
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
17 | | - | |
18 | | - | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| |||
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
65 | | - | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
66 | 79 | | |
67 | 80 | | |
68 | 81 | | |
| |||
479 | 492 | | |
480 | 493 | | |
481 | 494 | | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
482 | 531 | | |
483 | 532 | | |
484 | 533 | | |
| |||
578 | 627 | | |
579 | 628 | | |
580 | 629 | | |
581 | | - | |
582 | | - | |
583 | | - | |
584 | | - | |
585 | | - | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
586 | 637 | | |
587 | 638 | | |
588 | 639 | | |
| |||
593 | 644 | | |
594 | 645 | | |
595 | 646 | | |
596 | | - | |
| 647 | + | |
597 | 648 | | |
598 | 649 | | |
599 | 650 | | |
600 | 651 | | |
601 | 652 | | |
602 | 653 | | |
603 | | - | |
604 | | - | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
605 | 657 | | |
606 | 658 | | |
607 | 659 | | |
| |||
663 | 715 | | |
664 | 716 | | |
665 | 717 | | |
666 | | - | |
667 | | - | |
| 718 | + | |
| 719 | + | |
668 | 720 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
38 | 42 | | |
39 | 43 | | |
40 | 44 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | | - | |
| 105 | + | |
| 106 | + | |
106 | 107 | | |
107 | | - | |
| 108 | + | |
108 | 109 | | |
109 | 110 | | |
110 | 111 | | |
| |||
114 | 115 | | |
115 | 116 | | |
116 | 117 | | |
117 | | - | |
| 118 | + | |
118 | 119 | | |
119 | | - | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
120 | 123 | | |
121 | 124 | | |
122 | 125 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | | - | |
| 11 | + | |
11 | 12 | | |
12 | 13 | | |
13 | | - | |
| 14 | + | |
| 15 | + | |
14 | 16 | | |
15 | 17 | | |
16 | 18 | | |
| |||
76 | 78 | | |
77 | 79 | | |
78 | 80 | | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
79 | 92 | | |
80 | 93 | | |
81 | 94 | | |
| |||
0 commit comments