Repository navigation
Phase 4a: execution context — the context layer - #57
Merged
Merged
Conversation
This was referenced Sep 16, 2026
Phase 4a (issue #14). One module, Dexpace::Context, that the three flat Data flavours include: CTX-1's one-way chain is DispatchContext -> RequestContext -> ExchangeContext, with #promote_to_request and exchange stage terminal by the absence of a third (P4-1). Each promotion builds the successor through its .build, carries the bundle, the call key and the store forward by identity, adds exactly one artefact, and registers the successor with store.set -- the first store entry a chain ever has, because construction registers nothing (CTX-2, CTX-3, CTX-17). #close is store.release(self): a frozen Data cannot carry a latch, and CTX-9's identity-conditional eviction makes it idempotent through the store instead (P4-4). CTX-16's operation name is nil or a non-empty frozen String, carried forward unchanged, advisory only. Dexpace::CallKey (private_constant) mints CTX-4's key, a frozen "traceId:spanId:n" with one process-wide counter under one ::Thread::Mutex, so two default-constructed contexts are never == and a pinned call_key: is the one way to make them so (CTX-5, CTX-6). Dexpace::ContextStore HAS the private_constant Dexpace::BoundedMap -- the one bounded-keyed-map implementation XCUT-14 and AUTH-19 will share, a strong Hash behind one mutex with XCUT-14's drain loop inside the same synchronize as the insert -- and exposes CTX-8's #set and after the mutex is released), CTX-18's #[] and the identity-checked assigned .default. The cap must be a positive Integer, since a negative one would spin the drain forever. Dexpace::Instrumentation is roadmap obligation 1 discharged: Bundle, a frozen Data of eight members with #valid? derived (P4-6) and NONE carrying OBS-26's sentinels; TraceIdFlavour, a closed set of NONE, W3C and DATADOG with the trace-id sentinel on the flavour and the span-id sentinel on Bundle (P4-7); and NO_SPAN, NO_TRACER and NO_TRACER_FACTORY, three frozen singletons whose classes are private so phase 5c can widen them without an NFR-4 diff, the factory's (P4-8). Every sig/ mirror declares every method for the strict core Steep target, the two private constants included as hooks.rbs is; _ContextHost is the self-type Context needs (P4-40). The entry file requires the twelve files in dependency order.
Dexpace/NoWeakReferences is CTX-19 as a lint rule (P4-10): a constant reference to ObjectSpace::WeakMap or ::WeakKeyMap -- qualified, cbase-qualified, or bare inside `module ObjectSpace` -- to WeakRef, or a `require "weakref"` in any form tools/require_scan.rb reads, is an offense naming CTX-19 and the cap CTX-11 makes the leak backstop. A source-text cop, because RuboCop parses and never evaluates, so the WeakKeyMap rows parse at TargetRubyVersion 3.2 where the constant does not exist. It is the eighth custom cop, not the plan's seventh: CLAUDE.md, quality-gates.md, phase 2's checklist and cops_test.rb all count Dexpace/NoKeywordSplat, which phase 0's plan says is uncounted. Its cases live in a nested class of cops_test.rb under the 100-line cap, fourteen rejected and fifteen accepted on RuboCop 1.91.0; the .rubocop.yml entry scopes it to every gem's lib/, since the require allowlist covers core alone. The runtime surface manifest of dexpace-core grows from 515 to 580 lines through `rake surface:regenerate`, once, and every one of the 65 added rows was read against the design's object model: the seven flat constants and the instrumentation subsystem, their .build and .of and .default singletons, the Data readers of the five value types (which the shipped walker holds, closing the plan's finding against phase 0), Bundle's #valid? and #remote?, TraceIdFlavour's two predicates and four constants, ContextStore's five methods and its cap, and the three no-op singletons typed by their private classes. Nothing removed; no private_constant present. Context's construction validation is a private instance method rather than the plan's public Context.validate!, so the module's public surface is the one method the design gives it. The core smoke suite's constant list gains the seven public constants and asserts BoundedMap and CallKey are as unreachable as Hooks. Both are files a gate reads at run time, so they travel with the code.
Review round 0, R0-4 and R0-5. dispatch_context.rbs still named the plan's public Context.validate!, which the build made the private instance method #validate_context! (checklist deviation 5); the comment now names what lib defines. The cop's head comment claimed to match `require "weakref"` "in the forms tools/require_scan.rb reads", but the scanner's loader list includes autoload and the cop's matcher does not: `autoload :WeakRef, "weakref"` produces no offense on its own line. The comment now states the three require spellings the matcher reads and why the autoload form needs no row -- it names the constant as a Symbol, and the reference that later triggers it is the one on_const flags, verified against a scratch adapter file on 4.0.6.
Context#validate_context! accepted any non-nil object as a pinned call_key and the two flavours that carry an operation_name checked only for "": a Symbol passed silently, keying a slot no String lookup could find, and an Integer escaped as a NoMethodError from #empty? where every core model raises a field-named InvalidArgumentError. The design says a given key "must be a non-empty String" and that operation_name is nil or a non-empty frozen String, and sig/ already types both that way. Add the "must be a String" guard to #validate_context!, between the required! check and the empty check, and hoist CTX-16's two-state rule into one private Context#validate_operation_name! the RequestContext and ExchangeContext initializers share, so the message form lives in one place the way Model.required! keeps SEAM-29's. The .build paths pass a frozen Symbol or Integer through Model.frozen_string untouched, so the guard in initialize covers .build, #with and both promotions alike. No public signature changes; context.rbs declares the private helper.
The three phase-4 lanes each added their layer's resolution case to dexpace_test.rb; alone each kept the class under Metrics/ClassLength's hundred lines, together they reach 116. The six "requiring dexpace alone makes the whole <layer> resolve" cases move into a nested DexpaceTest::Layers, the split every larger suite in this gem already uses, so the honest RuboCop run is clean again. No case changes.
Wahbeh-Mohammad
force-pushed
the
14-phase-4a-execution-context
branch
from
September 16, 2026 20:04
d8cf132 to
c098a95
Compare
Contributor
Author
Rebase-and-reprove record for the phase 4a stack (2026-09-16)The stack was cut from What conflicted, and how each was resolved
Re-proven on the rebased tips
|
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #14. First of three phase 4a PRs — the code. Its tests are #58 and the phase record is #59.
Merge note. This stack was cut from
mainat419aacein parallel with phase 4b; 4b (#53–#55) and 4c (#60–#62) merged first, so the three branches were rebased onto theirmainand re-proven before merging — the record is in the comment below. The numbers in this body are from the tips as reviewed; the rebased tips add one fix commit (the smoke suite split forMetrics/ClassLength) and the regenerated manifest, and every gate is green on them.What lands
The correlation model in
dexpace-core— 30 files, +1,467 / −8 — three immutable context flavours forming a one-way promotion chain, one call-unique key, one bounded process-wide store, and the instrumentation bundle roadmap cross-phase obligation 1 fixes in core for phase 5 to populate. Nothing here talks to a socket; 4a adds norequireof any kind beyondrequire_relative, and the gemspec keeps zeroadd_dependencylines.Dexpace::Context(the module the three flavours include,#closeits one public method) and the chain —DispatchContext→#promote_to_request→RequestContext→#promote_to_exchange→ExchangeContext, which is terminal and owns no method (CTX-1–CTX-3). Every flavour isData+Model+private_class_method :new+ a validating.build; the bundle and the call key travel by reference (CTX-2, asserted withassert_same); a promotion registers the promoted context in its store slot (CTX-17— construction registers nothing);operation_nameis carried and advisory (CTX-16). A pinnedcall_keyoroperation_namemust be aString— a review round found a Symbol accepted silently and an Integer leakingNoMethodError.Dexpace::CallKey(private_constant) —CTX-4's rendered prefix plus one process-wide monotonic counter shared by all three flavours, so two default-constructed contexts are never==(CTX-5/CTX-6) and two keys minted from the sameBundle::NONEdiffer (CTX-15). The key participates in value equality, which is what makesCTX-9's trap constructible at all (R2).Dexpace::BoundedMap(private_constant, the one implementationCTX-11,XCUT-14and phase 6'sAUTH-19share) andDexpace::ContextStorewith.default(assigned at file load, not memoised — a review round found the memoised form survived every suite),#set,#put(reject-on-duplicate,CTX-8, raisingContextConflictErrorcarrying the key),#[],#release(eviction conditional on reference identity,CTX-9, which is what makes#closeidempotent without a latch — a frozenDatacannot carry one, P4-4),#size, andMAX_TRACKED_CONTEXTS = 1024(P4-9,AUTH-19's number). Insert and drain sit in onesynchronize, so the drain body runs at most once per insert (R4); oldest-registration-first is the documentedCTX-13victim, and a promotion re-setting a key does not refresh its position.Dexpace::Instrumentation::Bundle— eight stored members plus the derived#valid?(OBS-26's all-zero rule, P4-6) and#remote?,Bundle::NONE,INVALID_SPAN_ID;TraceIdFlavourwithNONE/W3C/DATADOG/ALLand.of, whose invalid trace id varies by flavour because Datadog renders decimal (P4-7);NO_SPAN,NO_TRACERandNO_TRACER_FACTORY, three frozen singletons,#tracerinopentelemetry-api1.11.0's exact five-parameter shape (deprecated_name = nil, deprecated_version = nil, name: nil, version: nil, attributes: nil, all ignored) so a real provider is call-compatible (P4-8;CTX-20,OBS-25). FourRegexp.new(…, timeout:)patterns, lowercase-only, no case fold.Dexpace::ContextConflictError, the fourth error in phase 2's shape, carrying#call_key.Dexpace/NoWeakReferences—CTX-19's prohibition as a lint overgems/*/lib/**/*.rb:ObjectSpace::WeakMap,ObjectSpace::WeakKeyMap,WeakRefand the threerequire "weakref"spellings, with its cases in a nested class ofcops_test.rb(29 rows). It is the eighth, not the plan's seventh —mainalready had seven — and the tree,CLAUDE.mdandquality-gates.mdsay so.sig/mirrorslib/one file per file — twelve mirrors,bounded_map.rbsandcall_key.rbsamong them withhooks.rbs's comment on why aprivate_constantstill needs one for the strictcoretarget — plus a fourth RBS interface,_ContextHost, the self-type strict Steep needs forContext#close(ledger row P4-60; first numbered P4-40 while the lanes ran in parallel, renumbered on the docs branch because 4b's addendum took P4-40–P4-49 and P4-50–P4-59 are 4c's). YARD 100% (402 methods). The surface manifest regenerated once (515 → 580 rows, all 65 read against the object model,Datareaders included, nothing private leaked). The core smoke suite's constant list is on this branch because the code change invalidates it.Decisions taken in the open, against the plan's text
Seventeen are itemised in the checklist's "Deviations from the plan"; the ones a reader should know: the construction validation is the private
Context#validate_context!(the plan's publicContext.validate!would have been anNFR-4-locked name the design never lists);BoundedMap.new(cap:)refuses anything but a positive Integer (a negative cap spins the drain);CallKey.mintchecks its bundle so.build(bundle: nil)raisesSEAM-29's message rather thanNoMethodError;Bundleowns its String members andtrace_statein#initializeso#withis validated identically, requiresspan:/tracer_factory:, and freezesINVALID_SPAN_IDexplicitly ("0" * 16is unfrozen even under the magic comment — a test caught it);DATADOG's bound is(1 << 64) - 1because rbs typesInteger#**asNumeric; the plan'sMetrics/ParameterListsinline disables were not needed — phase 0 as built already setsCountKeywordArgs: false, citing this phase's own measurement.Layering
Each tip of the stack is green under every gate on its own tree. This branch is green on the SimpleCov floor too — 97.61% on 4.0.6 and 97.58% on 3.2.11 (1,154 runs, 0 failures), because the smoke suite and phase 2's suites reach most of the layer. The tests PR takes the same tree to 99.96%.
Verification
while→ifdrain equivalent:#releasecomparing==instead ofequal?(theCTX-9trap); the drain evicting two per insert, and deleted;#putas a plain overwrite; a promotion rebuilding the bundle (onlyassert_samecatches it); the key derived from the bundle; one counter per flavour; the store'sHashswapped forObjectSpace::WeakMap(red on 3.2.11 with its floor-specificNoMethodError); the all-zero pair valid; an uppercase span id accepted;.defaultmemoised; a release-then-set promotion; the cop'sIncludenarrowed to core (caught by the gate suite on the tests branch, sinceCopCasecannot see.rubocop.yml).synchronizeblock read —BoundedMap's insert-and-drain,CallKey's increment, the store's lookup/release: each a hash write or a counter, never across a callback. The 32-threadCTX-8race and the 16×1000CTX-7burst ran 15/15 undertimeoutwith 1 winner / 31ContextConflictErrorand 16,000 entries every time; four seeds on two interpreters, identical run counts.GC.starts (CTX-19) and drop to 0 under theWeakMapswap;ObjectSpace::WeakKeyMapundefined on 3.2.11 while the cop still rejects the spelling there; theFiber[]boundary in a childFiber, a new::Threadand anEnumerator's fiber with the guard thatThread.current[:probe]is visible in none;#tracercalled five ways with#parametersmatching the gem;#withre-validating on all fiveDatatypes on 3.2.11 (Data#withskipsinitializethere;Model#withis what re-validates);CTX-13with cap 3 through real promotions.Known follow-ups from the review (not blocking)
CTX-13case. Docs branch; noted on Phase 4a: execution context — documentation and phase record #59.TraceIdFlavour::DATADOGaccepts a leading-zero decimal rendering such as"007";OBS-27says "rendered as a decimal string" and does not forbid it.Bundleexposes both theDatareader#remoteand the predicate#remote?; both are in the manifest andNFR-4-locked. Hiding the reader was considered and not done, to keep theDataconvention uniform.require "weakref"spellings its comment names and notautoload :WeakRef, "weakref"— harmless, since any later use must nameWeakRef, whichon_constflags (R0-5, closed by narrowing the comment; widening the matcher is two rows if wanted).