Skip to content

feat(core): recovery-chain primitives — product-spec §8.2 (RECOV-1..RECOV-16) - #37

Merged
Wahbeh-Mohammad merged 1 commit into
6-phase-4from
8-phase-4b-recovery-chain-primitives
Aug 26, 2026
Merged

feat(core): recovery-chain primitives — product-spec §8.2 (RECOV-1..RECOV-16)#37
Wahbeh-Mohammad merged 1 commit into
6-phase-4from
8-phase-4b-recovery-chain-primitives

Conversation

@Wahbeh-Mohammad

Copy link
Copy Markdown
Contributor

Closes #8. Phase 4b.

Note: this branch merged origin/3-phase-3 first. The diff against main also contains Phase 3.
The 4b work is the one commit after that merge.

What has been added

packages/core/src/recovery/ — six files. No folder barrel. Nothing enters the public API.
packages/core/etc/core.api.md does not change.

File Function Requirements
outcome.ts Outcome<T>, success, failure, fold RECOV-1
request-chain.ts Sequential fold. An empty chain returns its input. A step that throws stops the chain. RECOV-3, 14
response-chain.ts Response steps run on a Success only. Recovery steps always run. A throw closes the response one time and keeps the original error primary. RECOV-4..9, 12..14
cancellation.ts wrapCancellation. It never throws. RECOV-11
status-mapping.ts A response step over Phase 3b's toHttpError(). RECOV-15, 16
orchestrator.ts dispatchWithRecovery. One try/catch holds the request chain and the transport call. The unwrap rethrows the error unchanged. RECOV-2, 10

Two helpers at the package root:

  • assertNever in invariant.ts. It closes the first discriminated-union switch in this codebase.
  • suppress() in suppress.ts. It resolves blocker F1. SuppressedError came to Node in 24.0.0.
    The floor is >=20.3. A floor increase would remove support for Node 18, 20 and 22. The helper
    uses the native class when the runtime has one. If not, it builds a stand-in with the same shape.
    Callers test the shape. Callers do not use instanceof. Phases 5a, 6a, 6b and 6c can now call it.

This phase supplies primitives only. It assembles no default chain.

Review outcomes

Three review passes ran.

Pass 2 found a RECOV-8 defect. apply() could throw a TypeError when a step returned a value
that was not an outcome. RECOV-8 does not permit apply() to throw. toFailureClosingSuccess is
now total. Three tests hold this condition. The same pass found an unguarded String() in
assertNever. It threw on a null-prototype object. It is now guarded.

Pass 1 removed a dead statement that satisfies put into dist/. It replaced two tests that
deleted a global and could not run in parallel. It added the type-level tests for Outcome<T>. It
moved the RECOV-15 conformance test to the chain level.

Pass 3 ran both CI jobs, step by step. All 15 steps pass.

  • 588 tests in 50 files. 98.68% functions, 99.73% lines. The floor is 80%.
  • test:node: 36 cases. The matrix covers both branches of the suppress() guard. lts/* uses the
    native class. The pinned 20.3.0 uses the stand-in.
  • No node: import. No enum. No internal barrel. SPDX on line 1 of all 15 new files. No import
    cycle.

This PR also repairs bunfig.toml. The Phase 3 merge left a duplicate [test] root key. TOML does
not permit this. bun test did not start on this branch.

Open and deferred items

docs/open-items.md section F holds nine entries. No entry blocks this PR.

Item Status Note
F1 WATCH apply() does not guard its seed outcome. Only dispatchWithRecovery supplies it.
F2 WATCH A bad step return can pass through when nothing reads it.
F3 Phase 10 No invariant() calls in recovery/. Phase 10 sets the rule for all phases. At the fold sites, invariant() is the wrong tool. It throws, and RECOV-8 does not permit this.
F4 Phase 10 The chains are classes. The style guide asks for free functions.
F5 Phase 10 #private fields have no per-use note. This applies to the whole package.
F6 Phase 10 RECOV-11 does no work in this port. AbortSignal stays aborted.
F7 WATCH The CI matrix supplies the coverage of both suppress() branches.
F8 ACT Phase 4a is not built. The 4b design said it was. 4b does not need it. 4c does. Run 4a before 4c.
F9 WATCH The Phase 4 checklist marks for 4a and 4c are plan-level only.

Older entries were checked again. B1 is closed by the node-conformance matrix. B2 is now one file,
eslint.config.js. The four Phase 3b deferrals are complete.

@Wahbeh-Mohammad Wahbeh-Mohammad self-assigned this Aug 26, 2026
…ECOV-16)

Phase 4b. Ships `packages/core/src/recovery/` — six files, no folder barrel,
nothing on the published API surface (`packages/core/etc/core.api.md` is
byte-identical):

- `outcome.ts`      Outcome<T>, success/failure/fold                (RECOV-1)
- `request-chain.ts` sequential fold, empty = identity, throw
                     propagates for the orchestrator to convert     (RECOV-3, 14)
- `response-chain.ts` response phase on Success only, recovery phase
                     always, close-on-throw exactly once with the
                     original throwable primary, no auto-close on a
                     deliberately returned substitute        (RECOV-4..9, 12..14)
- `cancellation.ts`  wrapCancellation — never throws, which is what
                     keeps RECOV-2 absolute                         (RECOV-11)
- `status-mapping.ts` a thin response step over 3b's unchanged
                     toHttpError()                                  (RECOV-15, 16)
- `orchestrator.ts`  dispatchWithRecovery — one try/catch over the
                     request chain and the transport hop; the final
                     unwrap rethrows by identity                    (RECOV-2, 10)

Plus two package-root helpers: `assertNever` in `invariant.ts` (the codebase's
first discriminated-union `default`) and `suppress()` in `suppress.ts`.

F1, the cross-phase blocker, resolved to branch (b)
---------------------------------------------------
RECOV-12 pairs a step's throwable with a close failure, which is what
`SuppressedError` is for — and `SuppressedError` reached Node only in 24.0.0,
against this package's `engines.node >=20.3` floor (set by `AbortSignal.any()`),
and is absent from the `lib` it compiles against, so the direct form neither
type-checks nor runs there. Raising the floor would drop Node 18, 20 and 22 for
one error class. `suppress()` uses the native class where the runtime has one
and a shape-compatible stand-in where it does not, reading the global per call.
Callers assert the shape, never `instanceof SuppressedError` — that form would
silently assert nothing on the floor. Phases 5a, 6a, 6b and 6c reached for the
native class on the same premise; their docs now point at the helper.

F2 resolved as a Deviation Ledger row: the zero-vs-fifteen `invariant()` split
with 4c is project-wide, so Phase 10 settles the density rule once.

Three review passes
-------------------
Pass 1, against the knowledge corpus: a dead `statusMappingStep;` statement
reaching the published dist/ (`satisfies` erases to its operand, not to
nothing); two test files that could not survive parallel execution because they
deleted a global; no type-level test for the exported generic `Outcome<T>`; the
RECOV-15 conformance clause tested on the step in isolation rather than through
the chain; two step-down-rule violations.

Pass 2, against the normative text: **a RECOV-8 violation** — `apply()` could
raise `TypeError: undefined is not an object` when a step returned a
non-outcome, against "MUST NOT throw under any input". `toFailureClosingSuccess`
is now total: the discriminant read and the `close()` call share one `try`, so a
misbehaving step becomes a Failure with its own throwable still primary. Also an
unguarded `String()` in `assertNever`'s default message, which throws on a
null-prototype object.

Pass 3: re-ran every step of both CI jobs, swept the structure, and wrote what
survives into `docs/open-items.md` section F.

Also fixes a merge residue: the phase-3 merge left `bunfig.toml` with a
duplicated `[test] root` key, which TOML rejects, so `bun test` failed to load
bunfig at all on this branch.

Verification (all exit 0)
-------------------------
`bun install --frozen-lockfile`, typecheck, lint, build, `bun test --coverage`
(588 tests / 50 files, 98.68% funcs / 99.73% lines against the 80% floor), api,
lint:publish, verify:dual-consumption, verify:consumer-types, verify:seam-1,
verify:runtime-floor, audit, test:node (36 cases, +1 file covering the
SuppressedError guard and RECOV-12 over Node's own Web Streams), test:knowledge.
No `node:` import, no `enum`, no internal barrel, SPDX on line 1 of all 15 new
files, no import cycle under `packages/core/src`.

Refs: #8
@Wahbeh-Mohammad
Wahbeh-Mohammad force-pushed the 8-phase-4b-recovery-chain-primitives branch from c25ad5e to e778816 Compare August 26, 2026 17:54
@Wahbeh-Mohammad
Wahbeh-Mohammad merged commit 893f0c1 into 6-phase-4 Aug 26, 2026
3 checks passed
@Wahbeh-Mohammad
Wahbeh-Mohammad deleted the 8-phase-4b-recovery-chain-primitives branch August 26, 2026 18:09
Wahbeh-Mohammad added a commit that referenced this pull request Aug 26, 2026
…eline (#39)

* feat(core): add the execution context model — Phase 4a (CTX-1..CTX-20, XCUT-14) (#36)

Ships the per-call correlation state the pipeline is built on, per
product-spec/07-execution-context-model.md and
docs/superpowers/specs/2026-07-25-phase4a-execution-context-design.md.

New `packages/core/src/context/`, layered instrumentation → errors → context →
store, with no `index.ts` barrel (docs/knowledge/module-organization.md:18 bans
internal barrels; 4c imports the files directly):

- `instrumentation.ts` — the `InstrumentationBundle` shape (CTX-14) and its
  frozen no-op default (CTX-15, CTX-20). `activeSpan`/`tracerFactory` stay typed
  `unknown`: a real tracing adapter owns their shape, deferred to Phase 7.
- `errors.ts` — `DuplicateContextKeyError extends DexpaceError`, carrying the
  offending `key` as a readonly field (CTX-8).
- `context.ts` — `DispatchContext`/`RequestContext`/`ExchangeContext` as a
  frozen discriminated union over plain data, with `create*` factories and the
  two one-way promotions (CTX-1, CTX-2, CTX-3, CTX-5, CTX-6, CTX-7, CTX-16).
  No classes: nothing here owns a lifecycle. Call keys are `Symbol()`, never a
  trace-derived string. The factories and both promotions freeze the
  instrumentation bundle in place — `Object.freeze` is shallow, so freezing only
  the context would leave a caller-supplied bundle writable behind the
  `instrumentation` slot, and the flavors are interfaces, so a literal-built
  context can reach a promotion without passing a factory.
- `store.ts` — `ContextStore`, a bounded `Map` with a post-insert drain loop,
  plus the process-wide `contextStore` singleton (CTX-7..13, CTX-18, CTX-19).
  Also the subject of XCUT-14, which names "context registries" first among the
  caller-keyed process-lived maps that must be capped — and is the only
  appendix-B conformance row this code satisfies, since appendix B has no CTX
  section at all.

Nothing enters the public barrel: `context/` is SDK-internal correlation
plumbing, and `packages/core/etc/core.api.md` is byte-identical.

Tests: 45 across four colocated files, each header citing the IDs it exercises.
A 23-mutant sweep over the module kills 21; one survivor was an equivalent
mutant, and the other — collapsing `#drain`'s loop into a single
check-then-evict — is unkillable by construction, since both callers set one key
before draining so the map never exceeds cap + 1. The loop is kept because
CTX-12 and XCUT-14 mandate the shape for runtimes with real concurrency; both
`#drain` and its describe block say so, and it is registered as open item A6.

Verified against the CI-pinned toolchain rather than the local one: every `ci`
job step under bun 1.3.14 (`.bun-version`), and node-conformance on both matrix
legs — the 20.3.0 floor and lts/* (v24.20.0) — 31 pass each. The context module
itself was additionally exercised against the built artifact on both Node
versions, confirming the plan's claim that nothing here is runtime-divergent.

Also fixes bunfig.toml, where merging the Phase 3 and knowledge-CLI branches
left `root = "packages"` twice under `[test]`; Bun refuses a config with a
redefined key, so `bun test` failed to start at all.

Deliberate deferrals, all registered in docs/open-items.md rather than left
silent: CTX-17's positive half (install-on-first-promotion) belongs to 4c, which
owns the store handle; real W3C Trace Context generation to Phase 7;
`contextsEqual()` unscheduled; and CTX-8's message clause (a Symbol's
description names the flavor, not the instance) awaiting a decision as A5.

* feat(core): recovery-chain primitives — product-spec §8.2 (RECOV-1..RECOV-16) (#37)

Phase 4b. Ships `packages/core/src/recovery/` — six files, no folder barrel,
nothing on the published API surface (`packages/core/etc/core.api.md` is
byte-identical):

- `outcome.ts`      Outcome<T>, success/failure/fold                (RECOV-1)
- `request-chain.ts` sequential fold, empty = identity, throw
                     propagates for the orchestrator to convert     (RECOV-3, 14)
- `response-chain.ts` response phase on Success only, recovery phase
                     always, close-on-throw exactly once with the
                     original throwable primary, no auto-close on a
                     deliberately returned substitute        (RECOV-4..9, 12..14)
- `cancellation.ts`  wrapCancellation — never throws, which is what
                     keeps RECOV-2 absolute                         (RECOV-11)
- `status-mapping.ts` a thin response step over 3b's unchanged
                     toHttpError()                                  (RECOV-15, 16)
- `orchestrator.ts`  dispatchWithRecovery — one try/catch over the
                     request chain and the transport hop; the final
                     unwrap rethrows by identity                    (RECOV-2, 10)

Plus two package-root helpers: `assertNever` in `invariant.ts` (the codebase's
first discriminated-union `default`) and `suppress()` in `suppress.ts`.

F1, the cross-phase blocker, resolved to branch (b)
---------------------------------------------------
RECOV-12 pairs a step's throwable with a close failure, which is what
`SuppressedError` is for — and `SuppressedError` reached Node only in 24.0.0,
against this package's `engines.node >=20.3` floor (set by `AbortSignal.any()`),
and is absent from the `lib` it compiles against, so the direct form neither
type-checks nor runs there. Raising the floor would drop Node 18, 20 and 22 for
one error class. `suppress()` uses the native class where the runtime has one
and a shape-compatible stand-in where it does not, reading the global per call.
Callers assert the shape, never `instanceof SuppressedError` — that form would
silently assert nothing on the floor. Phases 5a, 6a, 6b and 6c reached for the
native class on the same premise; their docs now point at the helper.

F2 resolved as a Deviation Ledger row: the zero-vs-fifteen `invariant()` split
with 4c is project-wide, so Phase 10 settles the density rule once.

Three review passes
-------------------
Pass 1, against the knowledge corpus: a dead `statusMappingStep;` statement
reaching the published dist/ (`satisfies` erases to its operand, not to
nothing); two test files that could not survive parallel execution because they
deleted a global; no type-level test for the exported generic `Outcome<T>`; the
RECOV-15 conformance clause tested on the step in isolation rather than through
the chain; two step-down-rule violations.

Pass 2, against the normative text: **a RECOV-8 violation** — `apply()` could
raise `TypeError: undefined is not an object` when a step returned a
non-outcome, against "MUST NOT throw under any input". `toFailureClosingSuccess`
is now total: the discriminant read and the `close()` call share one `try`, so a
misbehaving step becomes a Failure with its own throwable still primary. Also an
unguarded `String()` in `assertNever`'s default message, which throws on a
null-prototype object.

Pass 3: re-ran every step of both CI jobs, swept the structure, and wrote what
survives into `docs/open-items.md` section F.

Also fixes a merge residue: the phase-3 merge left `bunfig.toml` with a
duplicated `[test] root` key, which TOML rejects, so `bun test` failed to load
bunfig at all on this branch.

Verification (all exit 0)
-------------------------
`bun install --frozen-lockfile`, typecheck, lint, build, `bun test --coverage`
(588 tests / 50 files, 98.68% funcs / 99.73% lines against the 80% floor), api,
lint:publish, verify:dual-consumption, verify:consumer-types, verify:seam-1,
verify:runtime-floor, audit, test:node (36 cases, +1 file covering the
SuppressedError guard and RECOV-12 over Node's own Web Streams), test:knowledge.
No `node:` import, no `enum`, no internal barrel, SPDX on line 1 of all 15 new
files, no import cycle under `packages/core/src`.

Refs: #8

* feat(core): stage-based pipeline — product-spec §8.1 (PIPE-1..PIPE-40) (#38)

* feat(core): add the execution context model — Phase 4a (CTX-1..CTX-20, XCUT-14)

Ships the per-call correlation state the pipeline is built on, per
product-spec/07-execution-context-model.md and
docs/superpowers/specs/2026-07-25-phase4a-execution-context-design.md.

New `packages/core/src/context/`, layered instrumentation → errors → context →
store, with no `index.ts` barrel (docs/knowledge/module-organization.md:18 bans
internal barrels; 4c imports the files directly):

- `instrumentation.ts` — the `InstrumentationBundle` shape (CTX-14) and its
  frozen no-op default (CTX-15, CTX-20). `activeSpan`/`tracerFactory` stay typed
  `unknown`: a real tracing adapter owns their shape, deferred to Phase 7.
- `errors.ts` — `DuplicateContextKeyError extends DexpaceError`, carrying the
  offending `key` as a readonly field (CTX-8).
- `context.ts` — `DispatchContext`/`RequestContext`/`ExchangeContext` as a
  frozen discriminated union over plain data, with `create*` factories and the
  two one-way promotions (CTX-1, CTX-2, CTX-3, CTX-5, CTX-6, CTX-7, CTX-16).
  No classes: nothing here owns a lifecycle. Call keys are `Symbol()`, never a
  trace-derived string. The factories and both promotions freeze the
  instrumentation bundle in place — `Object.freeze` is shallow, so freezing only
  the context would leave a caller-supplied bundle writable behind the
  `instrumentation` slot, and the flavors are interfaces, so a literal-built
  context can reach a promotion without passing a factory.
- `store.ts` — `ContextStore`, a bounded `Map` with a post-insert drain loop,
  plus the process-wide `contextStore` singleton (CTX-7..13, CTX-18, CTX-19).
  Also the subject of XCUT-14, which names "context registries" first among the
  caller-keyed process-lived maps that must be capped — and is the only
  appendix-B conformance row this code satisfies, since appendix B has no CTX
  section at all.

Nothing enters the public barrel: `context/` is SDK-internal correlation
plumbing, and `packages/core/etc/core.api.md` is byte-identical.

Tests: 45 across four colocated files, each header citing the IDs it exercises.
A 23-mutant sweep over the module kills 21; one survivor was an equivalent
mutant, and the other — collapsing `#drain`'s loop into a single
check-then-evict — is unkillable by construction, since both callers set one key
before draining so the map never exceeds cap + 1. The loop is kept because
CTX-12 and XCUT-14 mandate the shape for runtimes with real concurrency; both
`#drain` and its describe block say so, and it is registered as open item A6.

Verified against the CI-pinned toolchain rather than the local one: every `ci`
job step under bun 1.3.14 (`.bun-version`), and node-conformance on both matrix
legs — the 20.3.0 floor and lts/* (v24.20.0) — 31 pass each. The context module
itself was additionally exercised against the built artifact on both Node
versions, confirming the plan's claim that nothing here is runtime-divergent.

Also fixes bunfig.toml, where merging the Phase 3 and knowledge-CLI branches
left `root = "packages"` twice under `[test]`; Bun refuses a config with a
redefined key, so `bun test` failed to start at all.

Deliberate deferrals, all registered in docs/open-items.md rather than left
silent: CTX-17's positive half (install-on-first-promotion) belongs to 4c, which
owns the store handle; real W3C Trace Context generation to Phase 7;
`contextsEqual()` unscheduled; and CTX-8's message clause (a Symbol's
description names the flavor, not the instance) awaiting a decision as A5.

* feat(core): recovery-chain primitives — product-spec §8.2 (RECOV-1..RECOV-16)

Phase 4b. Ships `packages/core/src/recovery/` — six files, no folder barrel,
nothing on the published API surface (`packages/core/etc/core.api.md` is
byte-identical):

- `outcome.ts`      Outcome<T>, success/failure/fold                (RECOV-1)
- `request-chain.ts` sequential fold, empty = identity, throw
                     propagates for the orchestrator to convert     (RECOV-3, 14)
- `response-chain.ts` response phase on Success only, recovery phase
                     always, close-on-throw exactly once with the
                     original throwable primary, no auto-close on a
                     deliberately returned substitute        (RECOV-4..9, 12..14)
- `cancellation.ts`  wrapCancellation — never throws, which is what
                     keeps RECOV-2 absolute                         (RECOV-11)
- `status-mapping.ts` a thin response step over 3b's unchanged
                     toHttpError()                                  (RECOV-15, 16)
- `orchestrator.ts`  dispatchWithRecovery — one try/catch over the
                     request chain and the transport hop; the final
                     unwrap rethrows by identity                    (RECOV-2, 10)

Plus two package-root helpers: `assertNever` in `invariant.ts` (the codebase's
first discriminated-union `default`) and `suppress()` in `suppress.ts`.

F1, the cross-phase blocker, resolved to branch (b)
---------------------------------------------------
RECOV-12 pairs a step's throwable with a close failure, which is what
`SuppressedError` is for — and `SuppressedError` reached Node only in 24.0.0,
against this package's `engines.node >=20.3` floor (set by `AbortSignal.any()`),
and is absent from the `lib` it compiles against, so the direct form neither
type-checks nor runs there. Raising the floor would drop Node 18, 20 and 22 for
one error class. `suppress()` uses the native class where the runtime has one
and a shape-compatible stand-in where it does not, reading the global per call.
Callers assert the shape, never `instanceof SuppressedError` — that form would
silently assert nothing on the floor. Phases 5a, 6a, 6b and 6c reached for the
native class on the same premise; their docs now point at the helper.

F2 resolved as a Deviation Ledger row: the zero-vs-fifteen `invariant()` split
with 4c is project-wide, so Phase 10 settles the density rule once.

Three review passes
-------------------
Pass 1, against the knowledge corpus: a dead `statusMappingStep;` statement
reaching the published dist/ (`satisfies` erases to its operand, not to
nothing); two test files that could not survive parallel execution because they
deleted a global; no type-level test for the exported generic `Outcome<T>`; the
RECOV-15 conformance clause tested on the step in isolation rather than through
the chain; two step-down-rule violations.

Pass 2, against the normative text: **a RECOV-8 violation** — `apply()` could
raise `TypeError: undefined is not an object` when a step returned a
non-outcome, against "MUST NOT throw under any input". `toFailureClosingSuccess`
is now total: the discriminant read and the `close()` call share one `try`, so a
misbehaving step becomes a Failure with its own throwable still primary. Also an
unguarded `String()` in `assertNever`'s default message, which throws on a
null-prototype object.

Pass 3: re-ran every step of both CI jobs, swept the structure, and wrote what
survives into `docs/open-items.md` section F.

Also fixes a merge residue: the phase-3 merge left `bunfig.toml` with a
duplicated `[test] root` key, which TOML rejects, so `bun test` failed to load
bunfig at all on this branch.

Verification (all exit 0)
-------------------------
`bun install --frozen-lockfile`, typecheck, lint, build, `bun test --coverage`
(588 tests / 50 files, 98.68% funcs / 99.73% lines against the 80% floor), api,
lint:publish, verify:dual-consumption, verify:consumer-types, verify:seam-1,
verify:runtime-floor, audit, test:node (36 cases, +1 file covering the
SuppressedError guard and RECOV-12 over Node's own Web Streams), test:knowledge.
No `node:` import, no `enum`, no internal barrel, SPDX on line 1 of all 15 new
files, no import cycle under `packages/core/src`.

Refs: #8

* feat(core): stage-based pipeline — product-spec §8.1 (PIPE-1..PIPE-40)

Phase 4c. Ships `packages/core/src/pipeline/` — the fixed-stage step composition
runtime, its builder, the per-call cursor/fork mechanism, and the
execution-context-store wiring 4a deferred here. Plumbing only: no pillar step
bodies, no standard-resilience preset, nothing added to the public barrel.

- `stage.ts` — `Stage` as a string-literal union plus `STAGE_ORDER` and
  `PILLAR_STAGES`. No TS `enum` (`erasableSyntaxOnly`); inserting a stage later
  is one splice and touches no existing stage identity (PIPE-1..4, PIPE-8).
- `step.ts` — `Step`/`StepContext`/`Next`/`StepDescriptor`. A step is a function
  wrapped in a descriptor carrying a `type` symbol, which is what PIPE-6's
  reference identity and PIPE-18/19's anchor matching key off.
- `cursor.ts` — one recursive dispatcher per call. `ctx.next` and every
  `ctx.fork()` are one-shot closures over it, pinned to the same target position,
  sharing a single mutable in-flight request so a substitution sticks for the
  whole call (PIPE-9..PIPE-17).
- `runtime.ts` — `Runtime implements Transport`: empty-pipeline fast path,
  context install/promote/evict-in-`finally` on both paths, and `exchangeSource`
  so the exchange context describes the request that was actually sent
  (PIPE-9, PIPE-10, PIPE-25..27, CTX-17).
- `builder.ts` — stage-bucketed surgical edits with fail-fast validation at the
  mutating call, flattened once at `build()` (PIPE-7, PIPE-18..PIPE-25, PIPE-38).
- `errors.ts` — five flat `DexpaceError` leaves, each rendering its identifying
  symbols into its own message.

Tests are colocated and cite their PIPE IDs, including fast-check properties for
the builder's ordering laws (PIPE-22, PIPE-38) and the driven probe test for
PIPE-1/PIPE-2's stage ordering. Deliberately deferred, each named in the design
doc or the roadmap's Deferred Items Log: PIPE-17's "readable by any step" clause
and `StepContext.signal` (Phase 5a Task 1), PIPE-24/PIPE-35/PIPE-39 (Phase 5+),
PIPE-2's redirect/retry half and PIPE-40's 2-hop clause (Phase 5b/5c). Open
finding F9 — the cursor does not observe the caller's `AbortSignal` between
steps — stays undecided in the roadmap and must be settled before 5a Task 1.

Full CI sequence green locally: typecheck, lint, build, test --coverage (690
tests, pipeline files at 100%), api (report byte-identical), lint:publish,
verify:dual-consumption, verify:consumer-types, verify:seam-1,
verify:runtime-floor, audit, test:node.

* docs: add changesets for phase 4a & phase 4c.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant