Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/2026-08-26-execution-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@dexpace/core": patch
---

Internal: execution context promotion chain and bounded store for product-spec §7 (CTX-1..20). No public API change.
83 changes: 80 additions & 3 deletions docs/open-items.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Open Items

Running register of everything known to be unmet, unverified, misreported, or deliberately deferred across the
implemented portion of this project. Reviewed state: **scaffold milestone** (committed, `0ebdc79`) and
implemented portion of this project. Reviewed state: **scaffold milestone** (committed, `0ebdc79`),
**Phase 1 — Core HTTP Domain Model** (branch `2-phase-1-core-http-domain-model`, uncommitted at time of
review). Last reviewed **2026-07-30**.
review), and **Phase 4a — Execution Context** (branch `7-phase-4a-execution-context`, three review passes).
Last reviewed **2026-08-26**.

A requirement absent from this file is either satisfied or belongs to a phase that has not started. The point
of the file is that nothing is unmet *silently* — every gap below is either scheduled against a named phase or
Expand Down Expand Up @@ -84,6 +85,63 @@ add an import scan over `packages/core/src` allowing only relative specifiers an

---

### A5 — CTX-8: the duplicate-key error's *message* does not identify the key — **DECIDE**

Appendix C states CTX-8 more strictly than `product-spec/07` §7.3 does. §7.3 says the reject-on-duplicate
insert "fails all others with an error naming the key"; appendix C
(`appendix-c-consolidated-normative-requirement-index.md:176`) says "an error **whose message** identifies the
key."

`DuplicateContextKeyError`'s message is `` `context key already registered: ${String(key)}` ``. Call keys are
`Symbol()`s whose description is the flavor, not the identity, so every default-constructed context of a given
flavor renders identically:

```
context key already registered: Symbol(dispatch-context)
```

The message therefore names the *kind* of key, not *which* key. The error does carry the offending symbol as a
`readonly key: symbol` field — strictly more identifying than any string, and asserted in
`store.test.ts` — so the requirement's intent is met by the field while its letter is not met by the message.

Phase 4a's design already ledgers the `Symbol()` key choice with the cost "debuggability (opaque when logged or
printed)", but that row does not connect itself to CTX-8's message clause, so nothing currently records this as
a known partial deviation.

Two ways out, both defensible:
1. **Give default keys a distinguishing description** — `Symbol('dispatch-context#' + n)` from a module-scoped
counter. The counter would label only the description; `Symbol()` remains the identity, so CTX-4/5/6's
uniqueness is untouched and the ledger's rejection of a `traceId:spanId`+counter *string key* still stands.
Costs a second module-level mutable binding (`docs/knowledge/variables-and-declarations.md:22`), on top of
the `contextStore` singleton that already takes that deviation.
2. **Record a deliberate partial deviation** in the Phase 4a design's Deviation Ledger, on the grounds that a
symbol has no unique rendering and the typed `.key` field identifies the key more precisely than a message
can.

Either way the Phase 4 checklist's CTX-8 row should stop reading as an unqualified ✅.

### A6 — CTX-12 / XCUT-14: the drain **loop**'s shape is unverifiable, and untested — **WATCH**

`ContextStore.#drain` is a post-insert loop, as CTX-12 (SHOULD) and XCUT-14 (MUST) require. No test proves it
is a loop, and none can: `install` and `installIfAbsent` each set exactly one key before draining, so the map
is never more than one over the cap at drain entry and a second pass is unreachable. Replacing the loop body
with a single check-then-evict breaks nothing — confirmed by mutation testing across the module (that mutant is
the only meaningful survivor of 23).

The Phase 4a plan's Self-Review claims CTX-12 is covered by "a property-style burst test [that] asserts the
size never overshoots after any single insert". That test is real and passing, but it pins the **bound**, not
the drain's shape.

Not a defect today: the loop is present, the bound holds, and on a single-threaded runtime the two shapes are
behaviorally identical. `#drain` and the drain `describe` block both now carry a note saying so, so the loop is
not "simplified" away by a later reader.

**Trigger:** a runtime where inserts can stack more than one overshoot before a drain runs (worker threads, a
future concurrent store), or any change that lets the map exceed `cap + 1`. At that point the shape becomes
observable and owes a real test.

---

## B. Gates and tooling

### B1 — NFR-10 / NFR-17: CI never runs on the declared minimum runtime — **ACT** (trigger has now fired)
Expand Down Expand Up @@ -144,6 +202,21 @@ not deliberate reflection abuse (`Object.create(Request.prototype)`), and states
`sdk-design-nodejs/10-deliberate-deviations-from-the-reference-contract.md` when that phase is reached." Listed
here so the promise survives until then.

### C3 — The Phase 4 checklist under-reports Phase 4a — **ACT**

`docs/superpowers/plans/2026-07-26-phase4-execution-context-and-pipelines-checklist.md` still carries its
banner: "the plans are reviewed and corrected as of 2026-07-26 but **not yet executed**. Every ✅ means 'the
plan builds and tests it,' not 'it is on `main`.'" Phase 4a's rows are now built, tested, and committed on
`7-phase-4a-execution-context`, so the banner understates them while 4b and 4c remain unbuilt.

The same checklist maps only `CTX-*`. It has no `XCUT-14` row, even though
`docs/superpowers/specs/2026-07-28-phase9-cross-cutting-conformance-design.md:66` names "4a's context registry"
as an XCUT-14 site and appendix B's only conformance row that `ContextStore` satisfies is B.8's
"Caller/server-keyed maps bounded with drain-to-cap loop (XCUT-14)" — appendix B has no CTX section at all. The
ID is now cited in `store.ts` and `store.test.ts`; the checklist is the remaining gap.

Split the banner per sub-phase, and add an `XCUT-14` row pointing at 4a Task 4 (qualified by A6 above).

---

## D. Scheduled deferrals
Expand All @@ -161,7 +234,11 @@ No action now. Each is already owned by a named phase; this table exists so none
| Seam contracts (byte-stream, transport, codec, projection) | SEAM-2 – SEAM-30 | 2–8 | |
| Adapter packages, peer-dependency dedup | NFR-2 | 8 | |
| Shrink-survival regression guard | NFR-9 | 9 | |
| Concurrency-model agnosticism check | NFR-11 | 4 | No async code exists yet |
| Concurrency-model agnosticism check | NFR-11 | 4c | Retargeted from "Phase 4" by the 4a design: everything in 4a is synchronous, and 4c's stage pipeline is where async-facing surface appears |
| `CTX-17`'s positive half — the first store entry installed by the first promotion | CTX-17 | 4c | 4a satisfies only the negative half (constructing a head context must not auto-register it), which holds structurally because `context.ts` never imports `store.ts`. Wiring the store into the promotions would invert the layering and make every promotion a global side effect |
| Real W3C Trace Context generation behind `InstrumentationBundle` | CTX-14, CTX-15 | 7 | 4a ships the bundle's frozen shape and the no-op default only. `activeSpan`/`tracerFactory` stay typed `unknown`, and `activeSpan` is `undefined` rather than a no-op span object, until a tracing adapter defines `Span` |
| `contextsEqual()`, value equality over `ExecutionContext` | CTX-5 (equality framing) | none | Built only if 4b or 4c needs one. `CTX-5`'s operative half — pinning an explicit shared key — ships via `ContextInit.key` |
| `FakeTransport` test double | — | 4c | 4a never touches `Transport`; `PIPE-9`'s empty-pipeline dispatch is the likely first real consumer |
| Self-identifying version metadata (real `User-Agent`) | NFR-15 | 7/8 | |
| Publish + provenance CI job | NFR-16 | release | `prepublishOnly` wired; nothing published yet |
| NFR-8 re-confirmed as a documented non-applicability | NFR-8 | 10 | No reflection-driven discovery surface exists by design |
Expand Down
261 changes: 261 additions & 0 deletions packages/core/src/context/context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
// SPDX-License-Identifier: MIT
// packages/core/src/context/context.test.ts
// Exercises: CTX-1 (one-way promotion, incl. the compile-time no-promote-back check), CTX-2 (additive,
// non-mutating, carries forward instrumentation + key), CTX-3 (one shared call key across the whole
// chain), CTX-5/CTX-6 (off-chain construction, fresh key per default call at population scale, explicit
// key pinning), CTX-7 (immutable), CTX-15 (keys stay call-unique though every bundle field is identical),
// CTX-16 (operationName absent at dispatch, introduced at request, carried forward, never keyed on)
import {describe, expect, test} from 'bun:test';
import {Protocol} from '../http/protocol.js';
import {Request} from '../http/request.js';
import {Response} from '../http/response.js';
import {Status} from '../http/status.js';
import {
type DispatchContext,
createDispatchContext,
createExchangeContext,
createRequestContext,
promoteToExchange,
promoteToRequest,
} from './context.js';
import {noopInstrumentationBundle} from './instrumentation.js';

function aRequest(): Request {
return Request.newBuilder().url('https://example.com').build();
}

function aResponse(request: Request): Response {
return Response.newBuilder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.status(Status.of(200))
.build();
}

describe('promotion chain (CTX-1, CTX-2, CTX-3)', () => {
test('dispatch exposes exactly its expected artifacts', () => {
const dispatch = createDispatchContext();
expect(dispatch.kind).toBe('dispatch');
expect(dispatch.key).toBeDefined();
expect(dispatch.instrumentation).toBe(noopInstrumentationBundle);
});

test('promoting dispatch to request adds exactly the request, carrying key and instrumentation forward by reference', () => {
const dispatch = createDispatchContext();
const request = aRequest();
const requestCtx = promoteToRequest(dispatch, request, 'GetWidget');

expect(requestCtx.kind).toBe('request');
expect(requestCtx.key).toBe(dispatch.key);
expect(requestCtx.instrumentation).toBe(dispatch.instrumentation);
expect(requestCtx.request).toBe(request);
expect(requestCtx.operationName).toBe('GetWidget');
});

test('the source context is unchanged by promotion', () => {
const dispatch = createDispatchContext();
const before = {...dispatch};
promoteToRequest(dispatch, aRequest());
expect(dispatch).toEqual(before);
});

test('promoting request to exchange adds exactly the response, carrying everything else forward', () => {
const request = aRequest();
const requestCtx = promoteToRequest(
createDispatchContext(),
request,
'GetWidget',
);
const response = aResponse(request);
const exchangeCtx = promoteToExchange(requestCtx, response);

expect(exchangeCtx.kind).toBe('exchange');
expect(exchangeCtx.key).toBe(requestCtx.key);
expect(exchangeCtx.instrumentation).toBe(requestCtx.instrumentation);
expect(exchangeCtx.operationName).toBe('GetWidget');
expect(exchangeCtx.request).toBe(request);
expect(exchangeCtx.response).toBe(response);
});

test('the whole chain shares one call key across all three flavors', () => {
const dispatch = createDispatchContext();
const requestCtx = promoteToRequest(dispatch, aRequest());
const exchangeCtx = promoteToExchange(
requestCtx,
aResponse(requestCtx.request),
);
expect(requestCtx.key).toBe(dispatch.key);
expect(exchangeCtx.key).toBe(dispatch.key);
});
});

describe('promotion is one-way (CTX-1)', () => {
test('no promotion function accepts an ExchangeContext, so there is no way back', () => {
const requestCtx = promoteToRequest(createDispatchContext(), aRequest());
const exchangeCtx = promoteToExchange(
requestCtx,
aResponse(requestCtx.request),
);

// CTX-1's "the exchange type exposes no method promoting back" is a compile-time guarantee in this
// design, not a runtime one: promoteToRequest/promoteToExchange are free functions typed to accept
// only DispatchContext/RequestContext respectively, and there is no third promotion function. These
// two @ts-expect-error lines are the assertion -- `bun run typecheck` FAILS if either promotion ever
// widens to accept a terminal context, which a prose-only comment would not catch.
// @ts-expect-error -- ExchangeContext is terminal; it is not a DispatchContext
promoteToRequest(exchangeCtx, aRequest());
// @ts-expect-error -- ExchangeContext is terminal; it is not a RequestContext
promoteToExchange(exchangeCtx, aResponse(requestCtx.request));

expect(exchangeCtx.kind).toBe('exchange');
});
});

describe('off-chain construction (CTX-5, CTX-6)', () => {
test('default construction mints a fresh, distinct key every call', () => {
const a = createDispatchContext();
const b = createDispatchContext();
expect(a.key).not.toBe(b.key);
});

test('N default-constructed contexts across all three flavors are pairwise key-distinct', () => {
// CTX-5's "globally distinct across the whole process and all three flavors" is a property over the
// whole population, not just a pair -- a keying scheme that collided every Nth call would pass the
// pairwise test above. Every bundle field is identical here (all use noopInstrumentationBundle), so
// this is also CTX-15's "call-key derivation MUST remain call-unique even when every bundle field is
// identical" at scale.
const request = aRequest();
const keys = new Set<symbol>();
for (let i = 0; i < 1000; i += 1) {
keys.add(createDispatchContext().key);
keys.add(createRequestContext(request).key);
keys.add(createExchangeContext(request, aResponse(request)).key);
}
expect(keys.size).toBe(3000);
});

test('an explicit key can be pinned so two contexts share one slot', () => {
const key = Symbol('shared');
const a = createDispatchContext({key});
const b = createDispatchContext({key});
expect(a.key).toBe(b.key);
});

test('an explicit instrumentation bundle is carried onto the context verbatim', () => {
const instrumentation = {
...noopInstrumentationBundle,
traceId: 'a'.repeat(32),
isValid: true,
};
expect(createDispatchContext({instrumentation}).instrumentation).toBe(
instrumentation,
);
});

test('a caller-supplied instrumentation bundle is frozen by the factory (CTX-7)', () => {
// Object.freeze on the context is shallow, so without this the bundle behind `instrumentation` stays
// writable and the caller can mutate a "immutable" context out from under the whole chain.
const instrumentation = {
...noopInstrumentationBundle,
traceId: 'a'.repeat(32),
};
const dispatch = createDispatchContext({instrumentation});

expect(Object.isFrozen(dispatch.instrumentation)).toBe(true);
expect(Object.isFrozen(instrumentation)).toBe(true); // frozen in place, so the reference stays shared
});

test('createRequestContext and createExchangeContext also default to a fresh key per call', () => {
const request = aRequest();
const a = createRequestContext(request);
const b = createRequestContext(request);
expect(a.key).not.toBe(b.key);

const c = createExchangeContext(request, aResponse(request));
const d = createExchangeContext(request, aResponse(request));
expect(c.key).not.toBe(d.key);
});
});

describe('operationName (CTX-16)', () => {
test('is absent at the dispatch stage', () => {
expect('operationName' in createDispatchContext()).toBe(false);
});

test('defaults to undefined when not supplied at promotion', () => {
const requestCtx = promoteToRequest(createDispatchContext(), aRequest());
expect(requestCtx.operationName).toBeUndefined();
});

test('is carried forward unchanged across the request-to-exchange promotion', () => {
const requestCtx = promoteToRequest(
createDispatchContext(),
aRequest(),
'GetWidget',
);
const exchangeCtx = promoteToExchange(
requestCtx,
aResponse(requestCtx.request),
);
expect(exchangeCtx.operationName).toBe('GetWidget');
});

test('is advisory only -- it never influences the call key', () => {
// CTX-16: "never influencing the request, dispatch decision, or store key." Two otherwise-identical
// promotions differing only in operationName keep their source keys; and pinning one key across two
// different operation names still yields one slot, proving the name is not folded into it.
const key = Symbol('shared');
const a = promoteToRequest(
createDispatchContext({key}),
aRequest(),
'GetWidget',
);
const b = promoteToRequest(
createDispatchContext({key}),
aRequest(),
'DeleteWidget',
);
expect(a.key).toBe(b.key);
expect(a.operationName).not.toBe(b.operationName);
});
});

describe('immutability (CTX-7)', () => {
test('a promotion freezes a bundle that never passed through a factory', () => {
// The context flavors are interfaces, not classes, so 4b/4c can hand a promotion a
// literal-constructed context whose bundle was never frozen. Without this the promoted context is
// "immutable" in name only: the caller keeps a writable reference to its trace state.
const instrumentation = {
...noopInstrumentationBundle,
traceId: 'a'.repeat(32),
};
const forged: DispatchContext = {
kind: 'dispatch',
key: Symbol('forged'),
instrumentation,
};

const requestCtx = promoteToRequest(forged, aRequest());

expect(Object.isFrozen(requestCtx.instrumentation)).toBe(true);
expect(requestCtx.instrumentation).toBe(instrumentation); // frozen in place -- CTX-2 still holds
expect(
Object.isFrozen(
promoteToExchange(requestCtx, aResponse(requestCtx.request))
.instrumentation,
),
).toBe(true);
});

test('every context flavor is frozen', () => {
const dispatch = createDispatchContext();
expect(Object.isFrozen(dispatch)).toBe(true);
const requestCtx = promoteToRequest(dispatch, aRequest());
expect(Object.isFrozen(requestCtx)).toBe(true);
expect(
Object.isFrozen(
promoteToExchange(requestCtx, aResponse(requestCtx.request)),
),
).toBe(true);
});
});
Loading
Loading