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
7 changes: 7 additions & 0 deletions .changeset/seam-foundations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@dexpace/core": minor
---

Add the seam foundations: the `Transport` contract with its `composeSignal`/`isTimeoutSignal` cancellation helpers and `CancellationError`, the operation-input projection (`OperationDescriptor`, `buildRequest`, `OperationAssemblyError`), and `DexpaceError` as the new root of the error taxonomy above `DomainModelError`.

`DomainModelError` now extends `DexpaceError` instead of `Error`. This is additive — every existing leaf keeps its parent, its behavior, and its `instanceof DomainModelError` narrowing.
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,26 @@ jobs:

- name: Dependency audit
run: bun run audit

node-floor-conformance:
needs: ci
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2
with:
bun-version-file: .bun-version

- name: Install (frozen lockfile)
run: bun install --frozen-lockfile

- name: Build
run: bun run build

- uses: actions/setup-node@v4
with:
node-version: 18.17.0

- name: Verify the built artifact against the declared minimum Node version (NFR-10/NFR-17)
run: node scripts/verify-node-floor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ phase — ⏳ Deferred (named reason) — N/A.
| ID | Level | Requirement gist | Status | Where |
|---|---|---|---|---|
| SEAM-26 | MUST | `OperationDescriptor`: method + path required, four projections default to empty | ✅ | Task 6, direct conformance test (parameterless GET) |
| SEAM-27 | MUST | `buildRequest()` encoding + base-URL composition rules | ✅ | Task 6 — worked example, trailing-slash normalization, empty-path no-op, base-query preservation, fragment/malformed rejection, missing-placeholder error, and the path-param `/`-encoding property test, one test per conformance note |
| SEAM-27 | MUST | `buildRequest()` encoding + base-URL composition rules | ✅ | Task 6 — worked example, trailing-slash normalization, empty-path no-op, base-query preservation, fragment/malformed rejection, missing-placeholder error, and the path-param `/`-encoding property test, one test per conformance note. **One deviation, stricter than the requirement's letter:** a `.`/`..` path-param value is rejected rather than encoded, because no encoding survives the WHATWG URL parser's dot-segment normalization — recorded in the design doc's `## Deviation Ledger (for Phase 10)` section, which Phase 10 folds into `docs/sdk-design-nodejs/10-deliberate-deviations-from-the-reference-contract.md` |

## Cross-cutting

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ landed intact, plus a manual completeness cross-check against every phase's own
**Prerequisite:** Phases 0 through 8b's specs and plans exist exactly as committed on `main` as of this plan's
writing (`2026-07-28`). This plan does not require any phase's *code* to exist — Phase 10 audits documents, not a
running SDK (see the design doc's "Why This Phase Doesn't Need Phase 9 to Run First" section). It reads, but does
not modify, every Phase 3a-8b spec and the 5c/6a/6b/6c plans' Deviation Ledger sections.
not modify, every Phase 2 and 3a-8b spec and the 5c/6a/6b/6c plans' Deviation Ledger sections.

## Global Constraints

Expand Down Expand Up @@ -56,9 +56,9 @@ No new files. Two existing files modified.
- (staging only — no file written this task; Task 2 applies this text)

**Interfaces:**
- Consumes: every Phase 3a-8b spec's `## Deviation Ledger (for Phase 10)` section; 5c/6a/6b/6c plans' `##
- Consumes: every Phase 2 and 3a-8b spec's `## Deviation Ledger (for Phase 10)` section; 5c/6a/6b/6c plans' `##
Deviation Ledger Additions (for Phase 10)` sections; Phase 1's plan (`RequestConditions.applyTo` ETag note);
Phase 2's design (`SEAM-5`-`10`/`SEAM-18` naming); the current 12-item text of
the current 12-item text of
`docs/sdk-design-nodejs/10-deliberate-deviations-from-the-reference-contract.md`.
- Produces: the exact Markdown body Task 2 writes into that file.

Expand Down Expand Up @@ -400,16 +400,24 @@ silently skips them and under-verifies.
- (read-only verification pass, no modifications)

**Interfaces:**
- Consumes: every Phase 3a-8b spec's Deviation Ledger section, the 5c/6a/6b/6c plans' Deviation Ledger Additions,
Task 2's rewritten §10.
- Consumes: every Phase 2 and 3a-8b spec's Deviation Ledger section, the 5c/6a/6b/6c plans' Deviation Ledger
Additions, Task 2's rewritten §10.
- Produces: a pass/fail judgment — if this fails, return to Task 1 and add the missing material before considering
the phase done.

- [ ] **Step 1: Re-open every one of these 15 files and confirm each phase named below has at least one
corresponding sentence in the rewritten §10** (this list is exhaustive — every phase from 3a through 8b that has
a Deviation Ledger section):
- [ ] **Step 1: Re-open every one of these 17 files and confirm every *row* of each one's ledger table has a
corresponding sentence in the rewritten §10** (this list is exhaustive — every phase from 2 through 9 that has
a Deviation Ledger section; regenerate it with
`grep -rln '^## Deviation Ledger (for Phase 10)' docs/superpowers/specs/` rather than trusting this transcription):

Check rows, not phases. A phase-label grep is not sufficient evidence here: several phases are cited by many
items, so `Phase 2` (or `Phase 5a`, or `Phase 8a`) appearing in §10 proves only that *something* from that
phase survived — it cannot detect one dropped row from a phase that is already cited elsewhere. That is exactly
how Phase 2's dot-segment `SEAM-27` rejection went unrecorded until it was caught by hand during Phase 2's own
validation. Walk each table row by row.

```
2 docs/superpowers/specs/2026-07-23-phase2-seam-foundations-design.md
3a docs/superpowers/specs/2026-07-24-phase3a-io-contracts-design.md
3b docs/superpowers/specs/2026-07-25-phase3b-body-lifecycle-design.md
4a docs/superpowers/specs/2026-07-25-phase4a-execution-context-design.md
Expand All @@ -425,17 +433,24 @@ silently skips them and under-verifies.
7b docs/superpowers/specs/2026-07-28-phase7b-observability-design.md
8a docs/superpowers/specs/2026-07-28-phase8a-transport-design.md
8b docs/superpowers/specs/2026-07-28-phase8b-async-runtime-design.md
9 docs/superpowers/specs/2026-07-28-phase9-cross-cutting-conformance-design.md
```

Expected: every phase's citation `(Phase Xy)` appears at least once in
Expected, primary: every ledger table row across those 16 files is either represented by a sentence in the
rewritten §10 or explicitly listed as legitimately excluded (see the note below on 4a's and 7b's
implementation-detail entries). A row that is neither is a dropped deviation — return to Task 1.

Expected, secondary (necessary, not sufficient): every phase's citation `(Phase Xy)` appears at least once in
`docs/sdk-design-nodejs/10-deliberate-deviations-from-the-reference-contract.md` — spot-check with
`grep -o 'Phase [0-9][a-z]\?' docs/sdk-design-nodejs/10-deliberate-deviations-from-the-reference-contract.md | sort -u`
and confirm the output set is `{Phase 0, Phase 1, Phase 2, Phase 3a, Phase 3b, Phase 4a, Phase 4b, Phase 4c,
Phase 5a, Phase 5b, Phase 5c, Phase 6a, Phase 6b, Phase 6c, Phase 7a, Phase 7b, Phase 8a, Phase 8b, Phase 9}` —
every phase from 3a through 8b now cited at least once, including 4a (execution-context store/key collapse,
every phase from 2 through 8b now cited at least once, including 4a (execution-context store/key collapse,
folded into Item 1) and 7b (`AsyncLocalStorage` propagation, also folded into Item 1), which an earlier draft
of this plan wrongly left uncited. `Phase 9` appears only as the unblock trigger for Item 12's two open
questions and Item 10's shrink-test origin note, not as a phase with its own deviation entry. Note: 4a's and
of this plan wrongly left uncited. `Phase 9` is cited both as the unblock trigger for Item 12's two open
questions and Item 10's shrink-test origin note **and** as a phase with its own two-row ledger (`XCUT-23`
satisfied vacuously; `NFR-8` shipping nothing) — an earlier draft of this plan asserted it had no deviation
entry of its own, which its design doc contradicts. Note: 4a's and
7b's *other* ledger entries (Symbol() call-key ergonomics, `ContextInit` options-object shape, `contextsEqual()`
omission, the retry/redirect logging vocabulary gap, the `Tracer`/`Span` structural-subset choice) are
legitimately excluded from §10 — they're implementation-detail choices made where the spec was silent, not
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,18 @@ Since `DomainModelError` lives in `http/errors.ts` (Phase 1) and is already expo
`DexpaceError` goes in the same file and joins the barrel alongside it — an additive API change, so it needs a
changeset but not a major bump.

## Deviation Ledger (for Phase 10)

Phase 2 predates the per-phase ledger convention that specs 3a-8b follow; this section is the retrofit, so Phase
10's reconciliation reads Phase 2 the same way it reads every other phase instead of pulling two items out of the
prose above by name.

| Deviation | Against | Reason |
|---|---|---|
| A `.`/`..` path-parameter value is rejected (`OperationAssemblyError`), not encoded | `SEAM-27` ("percent-encoded as single path segments") | Both are RFC 3986 *unreserved*, so encoding leaves them untouched, and the WHATWG URL parser folds `%2E` back to `.` during dot-segment normalization — no encoding keeps them as one literal segment. Forwarding `..` lets a path value rewrite the path (`/things/..` resolves to `/`), the injection class the requirement's own parenthetical exists to stop. Stricter than the requirement's letter, in service of its intent; no other value is affected |
| Discovery / registration / conflict-resolution machinery is never built | `SEAM-5`-`SEAM-10` | Nothing in this port is pluggable enough to need discovering: one `Transport` shape, one async primitive, and Web Streams as the platform's own byte-stream answer rather than a third-party library to keep out of core (`sdk-design/03` §3.1, §3.5) |
| The sync↔async bridge is never built; only its options-threading clause survives | `SEAM-18` | A bridge connects two transport seams; `SEAM-11`/`SEAM-16` collapse to one here, so there is nothing on either bank. Every bridge-specific obligation presupposes a blocking transport Node cannot idiomatically have. The one non-bridge clause — per-call options threaded through, never dropped — survives as an ordinary `Transport.send()` obligation, not a deviation |

## Testing

Phase 2 ships interfaces plus three pure functions. Only the pure functions are behaviorally testable; every
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ roadmap's Deferred Items Log already targets at Phase 10 by name, and settles on
tension a Phase 1 plan explicitly deferred here.

**Scope is documents, not code.** Phase 10 ships no package — the roadmap table lists it as `— (review only)`.
Every phase spec from 3a through 8b already carries its own `## Deviation Ledger (for Phase 10)` section (Phases
1 and 2 predate that convention but each has an equivalent flagged item, see below); those sections are complete,
already-reasoned raw material. Phase 10's job is consolidation and cross-referencing, not new investigation —
Every phase spec from 3a through 8b already carries its own `## Deviation Ledger (for Phase 10)` section, and
Phase 2's design carries a retrofitted one (Phase 1 still predates the convention but has an equivalent flagged
item, see below); those sections are complete, already-reasoned raw material. Phase 10's job is consolidation and cross-referencing, not new investigation —
with two exceptions. First, two items (`NFR-12`, `NFR-16`) are soft gaps that need a real build/publish artifact
to verify, which doesn't exist in this docs-only repository state yet; Phase 10 cannot close those, and records
them as open with an explicit unblock trigger rather than papering over them. Second, four items (two redirect/
Expand All @@ -26,9 +26,9 @@ conformance only, and will never produce that evidence. Phase 10 decides those f
nothing in this phase's own scope is left waiting on Phase 9.

**Governing documents:** `docs/sdk-design-nodejs/10-deliberate-deviations-from-the-reference-contract.md` (the
document this phase rewrites), the roadmap's own Deferred Items Log, and every Phase 3a–8b spec's Deviation
Ledger section plus the handful of plan-level "Deviation Ledger Additions" sections (5c, 6a, 6b, 6c) that amend
their design doc's ledger with plan-time corrections.
document this phase rewrites), the roadmap's own Deferred Items Log, and every Phase 2, 3a–8b, and 9 spec's
Deviation Ledger section plus the handful of plan-level "Deviation Ledger Additions" sections (5c, 6a, 6b, 6c)
that amend their design doc's ledger with plan-time corrections.

## Why This Phase Doesn't Need Phase 9 to Run First

Expand Down Expand Up @@ -63,10 +63,13 @@ rather keep the original alongside the reconciled version.

## Reconciliation Process

1. **Collect.** Pull every phase's `## Deviation Ledger (for Phase 10)` section (specs 3a-8b) plus every plan's
`## Deviation Ledger Additions (for Phase 10)` section (5c, 6a, 6b, 6c) verbatim. Phases 1 and 2 predate the
convention: Phase 1's plan flags the HTTP-18/HTTP-50 ETag tension inline (not in a named section); Phase 2's
design names `SEAM-5`-`SEAM-10` and `SEAM-18` directly. Both are pulled in by name.
1. **Collect.** Pull every phase's `## Deviation Ledger (for Phase 10)` section (specs 2, 3a-8b, and 9) plus every
plan's `## Deviation Ledger Additions (for Phase 10)` section (5c, 6a, 6b, 6c) verbatim. Phase 2 originally
predated the convention and was pulled in by name for `SEAM-5`-`SEAM-10` and `SEAM-18` only; its design now
carries a retrofitted ledger section covering those two plus the dot-segment path-parameter rejection
(`SEAM-27`) that the by-name pull would have missed, so Phase 2 is collected like any other phase and needs no
special case. **Phase 1 still does:** its plan flags the HTTP-18/HTTP-50 ETag tension inline, not in a named
section, and is pulled in by name.
2. **Cross-reference against the original 12.** Each of the 12 items in the current §10 either: (a) is confirmed
as-built with no change, (b) is confirmed but needs expansion because later phases added detail the
prediction didn't anticipate, or (c) needs correction because a later phase's actual mechanism diverged from
Expand Down Expand Up @@ -219,12 +222,14 @@ passing it through.
## Verification / Completeness Check

Before the rewritten §10 is considered done, confirm every source is accounted for:
- Every phase spec 3a-8b's `## Deviation Ledger (for Phase 10)` section has at least one entry represented in
the reconciled ledger (cross-check against the extraction performed for this design — 15 of 15 sections
present and non-empty).
- Every phase spec 2, 3a-8b, and 9's `## Deviation Ledger (for Phase 10)` section has **every row** represented in
the reconciled ledger, or explicitly listed as legitimately excluded (cross-check against the extraction
performed for this design — 17 of 17 sections present and non-empty). Row-level, not section-level: a phase
cited by several items can still have one row silently dropped, which is how Phase 2's dot-segment `SEAM-27`
rejection was missed until Phase 2's own validation caught it by hand.
- Every plan-level `## Deviation Ledger Additions (for Phase 10)` section (5c, 6a, 6b, 6c) is folded in.
- Phase 1's ETag flag and Phase 2's `SEAM-5`-`10`/`SEAM-18` naming (pre-dating the ledger-section convention) are
represented.
- Phase 1's ETag flag (still pre-dating the ledger-section convention, still pulled in by name) is represented.
Phase 2 no longer needs a by-name pull — its design carries a retrofitted ledger section.
- Every roadmap Deferred Items Log row naming "Phase 10" (`NFR-8`, `NFR-12`, `NFR-16`, the `SEAM-5`-`10` and
`SEAM-18` rows, plus the four rows the 2026-07-28 update retargeted here from a Phase 9 sweep that turned out
never to run them — redirect-predicate scope, Basic/Digest preemptive-stamp reading,
Expand Down
30 changes: 30 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,36 @@ export default tseslint.config(
'@typescript-eslint/explicit-module-boundary-types': 'error',
// Every `eslint-disable` must carry a `-- reason` (NFR-7's documented-exception clause).
'@eslint-community/eslint-comments/require-description': 'error',
// lib.dom (added for the seam surface's AbortSignal/AbortController/DOMException) declares
// global Request/Response/Headers/window/document. A core file that forgets its own import no
// longer fails to compile — it silently type-checks against the DOM global instead. An
// imported binding shadows the global, so correctly-importing files are unaffected.
'no-restricted-globals': [
'error',
{
name: 'Request',
message:
'lib.dom global — import Request from src/http/request.js instead.',
},
{
name: 'Response',
message:
'lib.dom global — import Response from src/http/response.js instead.',
},
{
name: 'Headers',
message:
'lib.dom global — import Headers from src/http/headers.js instead.',
},
{
name: 'window',
message: 'Browser-only global; @dexpace/core is runtime-agnostic.',
},
{
name: 'document',
message: 'Browser-only global; @dexpace/core is runtime-agnostic.',
},
],
},
},
);
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"audit": "bun audit --audit-level=high --prod",
"verify:dual-consumption": "node scripts/verify-dual-consumption.mjs",
"verify:seam-1": "node scripts/verify-seam-1.mjs",
"verify:runtime-floor": "node scripts/verify-runtime-floor.mjs"
"verify:runtime-floor": "node scripts/verify-runtime-floor.mjs",
"verify:node-floor": "node scripts/verify-node-floor.mjs"
}
}
42 changes: 41 additions & 1 deletion packages/core/etc/core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,25 @@ export interface Builder<T> {
}

// @public
export class DomainModelError extends Error {
export function buildRequest(baseUrl: string | URL, operation: OperationDescriptor): Request_2;

// @public
export class CancellationError extends DexpaceError {
constructor(message: string, options?: ErrorOptions);
}

// @public
export function composeSignal(userSignal?: AbortSignal, timeoutMs?: number): AbortSignal | undefined;

// @public
export class DexpaceError extends Error {
constructor(message: string, options?: ErrorOptions);
}

// @public
export class DomainModelError extends DexpaceError {
}

// @public
export class ETag {
static readonly ANY: ETag;
Expand Down Expand Up @@ -82,6 +97,9 @@ export class HttpRange {
export class HttpRangeValidationError extends DomainModelError {
}

// @public
export function isTimeoutSignal(signal: AbortSignal): boolean;

// @public
export class MediaType {
get charset(): string | undefined;
Expand All @@ -102,6 +120,22 @@ export class MediaTypeParseError extends DomainModelError {
// @public
export type Method = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH';

// @public
export class OperationAssemblyError extends DexpaceError {
constructor(message: string, parameterName: string);
readonly parameterName: string;
}

// @public
export interface OperationDescriptor {
readonly body?: unknown;
readonly headers?: Headers_2 | undefined;
readonly method: Method;
readonly pathParams?: Readonly<Record<string, string>> | undefined;
readonly pathTemplate: string;
readonly query?: QueryParams | undefined;
}

// @public
export class Protocol {
equals(other: Protocol): boolean;
Expand Down Expand Up @@ -250,6 +284,12 @@ export class Status {
static recognized(code: number): Status | undefined;
}

// @public
export interface Transport {
close(): Promise<void>;
send(request: Request_2, options?: RequestOptions, signal?: AbortSignal): Promise<Response_2>;
}

// @public
export class UrlConstructionError extends DomainModelError {
}
Expand Down
Loading
Loading