feat(core)!: reject {} at the last five config slots — Scalar|AtLeastOne (P20) - #507
Merged
Merged
Conversation
…stOne (P20)
The five `StitchConfig` slots R6 flagged as bare all-optional `*Options` bags —
`hooks`, `input`, `multipart`, `sse`, `stream` — now type their object form so
the opaque `{}` is a **compile error** (CONTRACT.md P20):
- `hooks?: AtLeastOne<Hooks>` and `input?: AtLeastOne<InputSchemas>` (no scalar)
- `multipart?: MultipartNesting | AtLeastOne<MultipartOptions>` and
`stream?: StreamDecode | AtLeastOne<StreamOptions>` (P12 dominant-field scalar)
- `sse?: boolean | AtLeastOne<SseOptions>` (P13 enable-with-defaults toggle)
The all-defaults case is now the scalar, not the opaque `{}`. `expandShorthand`
folds each scalar into its envelope at compose time — `multipart: 'dot'` →
`{ nesting }`, `stream: 'ndjson'` → `{ decode }`, `sse: true` →
`{ reconnect: true }`, and `sse: false` clears the slot — so the engine and
`__config` still only ever see the resolved object form (ResolvedStitchConfig now
omits + re-declares these five). compose writes the chained `hooks` / normalized
`input` through the resolved view, past the authoring-side `AtLeastOne` gate.
Clears the last of R6's P20 backlog: `scripts/contract-violations.baseline.json`
shrinks from 5 to **0**. Whole-entry bundle ticks 24→25 kB min+gzip (advertised
sizes updated across all sites); `import { stitch }` stays 20 kB.
BREAKING CHANGE: `StitchConfig.hooks`, `input`, `multipart`, `sse`, and `stream`
no longer accept the empty object `{}`. Enable-with-defaults is now the scalar —
`sse: true`, `stream: 'ndjson'`, `multipart: 'dot'` — and `hooks`/`input` require
at least one field set. Object forms with ≥1 field are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
rejifald
added a commit
that referenced
this pull request
Jul 29, 2026
…erivation (P16) (#525) Broken out of #470. Stacked on #501. **P16 — one implementation, five consumers.** `StitchQueryOptions` and the whole query-key derivation path were BYTE-COPIED into all five framework bindings: react, vue, svelte, solid and angular each carried their own private `nameOf`, `isSecretHeader`, `keyInputFor` and `stitchQueryOptions`. Five copies of a security-relevant redaction list is five chances to drift, and the copies had ALREADY drifted from each other. They all move into `@stitchapi/query-core`, the framework-agnostic keystone the bindings already depend on, and are exported for the bindings to re-export: nameOf(stitch) — the stable, human-meaningful first key segment keyInputFor(input) — the sanitised input copy (secret headers/params redacted) deriveQueryKey(s, input) — [nameOf(s), keyInputFor(input)] StitchQueryOptions<T> — the TanStack-shaped options type stitchQueryOptions(...) — the overloaded helper Net effect: about 715 lines deleted from the bindings against ~166 added in query-core. **The secret denylist is no longer a copy either.** `keyInputFor` now calls core's exported `isSecretKey` predicate instead of each binding maintaining its own hard-coded header list. A secret stem registered with `registerSecretKey` is now honoured by every binding's query key, which was never true of the copies — this is the drift that mattered most, since a missed stem means a credential lands in a TanStack cache key. **Fix — react and angular no longer accept-and-ignore a streaming flag.** `UseStitchOptions` and `InjectStitchOptions` extended the core options INCLUDING `streaming`, then never read it: setting it compiled and silently did nothing. Both now `Omit` it, so the mistake is a type error. **P9 — angular `InjectInput` → `MaybeSignal`.** The type describes "a value, a signal, or a thunk", which is not an input and not angular-specific; `MaybeSignal` says what it is. Hard break, no aliases — pre-GA, per P19 as amended in #470. NOTE on the contract ratchet: it reports "5 known, baselined" here rather than 0 because #501's branch predates #507, which cleared those five R6 entries on main. The baseline file is UNCHANGED by this commit — no new violations. It returns to 0 once #501 catches up with main. Verified: workspace typecheck clean, full workspace test suite green, check:exports green for every package + companion, contract ratchet introduces nothing new, docs-links 110 routes OK, prettier clean, apps/docs production build passes. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
rejifald
added a commit
that referenced
this pull request
Jul 30, 2026
…drop the bare queryOptions alias (P3/P9) (#501) * refactor(query)!: rename the query result type to StitchQueryResult; drop the bare queryOptions alias (P3/P9) Extracts the query-family slice from #470 (the contract-freeze sweep). The read-back query state is a produced shape, so `StitchQueryState`→`StitchQueryResult` (and `UseStitchReturn`→`UseStitchResult` in react/vue) — hoisted in query-core, re-exported by the five TanStack-family bindings. The bare `queryOptions` (a `@deprecated` alias of `stitchQueryOptions` that collided with TanStack's own `queryOptions`) is removed. `StitchQueryOptions` — the deliberate TanStack-mirror options type (P3 carve-out / P22) — keeps its name. Hard break (P19, pre-GA). BREAKING CHANGE: `StitchQueryState`→`StitchQueryResult`, `UseStitchReturn`→ `UseStitchResult`, and the bare `queryOptions` alias is removed (use `stitchQueryOptions`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(query-core)!: single-source StitchQueryOptions + query-key derivation (P16) (#525) Broken out of #470. Stacked on #501. **P16 — one implementation, five consumers.** `StitchQueryOptions` and the whole query-key derivation path were BYTE-COPIED into all five framework bindings: react, vue, svelte, solid and angular each carried their own private `nameOf`, `isSecretHeader`, `keyInputFor` and `stitchQueryOptions`. Five copies of a security-relevant redaction list is five chances to drift, and the copies had ALREADY drifted from each other. They all move into `@stitchapi/query-core`, the framework-agnostic keystone the bindings already depend on, and are exported for the bindings to re-export: nameOf(stitch) — the stable, human-meaningful first key segment keyInputFor(input) — the sanitised input copy (secret headers/params redacted) deriveQueryKey(s, input) — [nameOf(s), keyInputFor(input)] StitchQueryOptions<T> — the TanStack-shaped options type stitchQueryOptions(...) — the overloaded helper Net effect: about 715 lines deleted from the bindings against ~166 added in query-core. **The secret denylist is no longer a copy either.** `keyInputFor` now calls core's exported `isSecretKey` predicate instead of each binding maintaining its own hard-coded header list. A secret stem registered with `registerSecretKey` is now honoured by every binding's query key, which was never true of the copies — this is the drift that mattered most, since a missed stem means a credential lands in a TanStack cache key. **Fix — react and angular no longer accept-and-ignore a streaming flag.** `UseStitchOptions` and `InjectStitchOptions` extended the core options INCLUDING `streaming`, then never read it: setting it compiled and silently did nothing. Both now `Omit` it, so the mistake is a type error. **P9 — angular `InjectInput` → `MaybeSignal`.** The type describes "a value, a signal, or a thunk", which is not an input and not angular-specific; `MaybeSignal` says what it is. Hard break, no aliases — pre-GA, per P19 as amended in #470. NOTE on the contract ratchet: it reports "5 known, baselined" here rather than 0 because #501's branch predates #507, which cleared those five R6 entries on main. The baseline file is UNCHANGED by this commit — no new violations. It returns to 0 once #501 catches up with main. Verified: workspace typecheck clean, full workspace test suite green, check:exports green for every package + companion, contract ratchet introduces nothing new, docs-links 110 routes OK, prettier clean, apps/docs production build passes. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
rejifald
added a commit
that referenced
this pull request
Aug 4, 2026
`claude/download-consolidate` was cut 2026-07-06 with a 2026-07-23 merge base. Main has since landed the pre-GA breaking sweep, so replaying the seven commits was only half the work — the other half is what the ratchets then demanded. Core - `download`'s `interpret` composes `verdictOf(res, cfg)` (ADR 0022 Decision 4) and yields `data`, not `value`. The M1 complete-body rule sits AFTER the composed verdict, as the surface's own, and now reads `verdict.accept`: `classifyStatus` consults it only at `>= 400`, so without that read a caller who declared `206` NORMAL would still be rejected — the surface overriding an explicit declaration rather than ruling where none was made. Second test in download-stray-206.spec.ts pins it. - `rebuildError`'s cause-carry lands on top of main's body/url arm instead of replacing it. Test rig + specs - `throttle-host-pooling` keeps the deterministic manual-clock rewrite (the wall-clock flake fix) minus its `scope`-alias half — #490 removed the alias. ADR 0023 Decision 1 ratified exactly the 500ms in-process spacing it asserts. - Rig duration fields drop the `Ms` suffix (D3/P17), matching main's own `stream.chunkDelay`: `ttfbDelayMs` → `ttfbDelay`, `chunkDelayMs` → `chunkDelay`. The rig keeps main's `retryAfterSeconds` (the header's native unit) over the branch's `retryAfter`; four existing specs already depend on it. - Spec config: auth moved to `stitchapi/auth` (ADR 0021) and `apiKey` takes `secret` (#580); `retry.baseMs`/`baseDelay` fold into `retry.backoff.base` (#513); `respectRetryAfter` → `retry.respect` (#608); `AdapterProgress.phase` → `direction`. @stitchapi/download - `DownloadBatch.cancel`/`cancelAll` fold into `cancel(id?)` — R8 landed after this branch was cut and flags the shared prefix (P24). For a verb the optional parameter IS the collapse; an envelope would be the same pair one level deeper. - `hooks` is built unannotated so `onError` infers REQUIRED: the slot is `AtLeastOne<Hooks>` now (P20, #507), which an all-optional annotation cannot satisfy. - Version + vitest to the rc.7 lockstep; lockfile regenerated from main's. - classify.ts and the README no longer claim the engine drops the transport cause — this branch's own core fix carries it through. The hook seam stays: it is strictly wider (it also catches a thrown non-`Error`). Verify: 40/40 workspace projects green (core 1460 passed, download 15/15); check:lint / types / types-d / exports / exports:companions / contract / unknown-keys / release / docs-links / format all clean; core size 23.45 gzip (budget 23.50) and 20.87 for `import { stitch }` (20.90), @stitchapi/download 2.45/2.65 KB; `yakir check` both tiers 0 drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rejifald
added a commit
that referenced
this pull request
Aug 4, 2026
`claude/download-consolidate` was cut 2026-07-06 with a 2026-07-23 merge base. Main has since landed the pre-GA breaking sweep, so replaying the seven commits was only half the work — the other half is what the ratchets then demanded. Core - `download`'s `interpret` composes `verdictOf(res, cfg)` (ADR 0022 Decision 4) and yields `data`, not `value`. The M1 complete-body rule sits AFTER the composed verdict, as the surface's own, and now reads `verdict.accept`: `classifyStatus` consults it only at `>= 400`, so without that read a caller who declared `206` NORMAL would still be rejected — the surface overriding an explicit declaration rather than ruling where none was made. Second test in download-stray-206.spec.ts pins it. - `rebuildError`'s cause-carry lands on top of main's body/url arm instead of replacing it. Test rig + specs - `throttle-host-pooling` keeps the deterministic manual-clock rewrite (the wall-clock flake fix) minus its `scope`-alias half — #490 removed the alias. ADR 0023 Decision 1 ratified exactly the 500ms in-process spacing it asserts. - Rig duration fields drop the `Ms` suffix (D3/P17), matching main's own `stream.chunkDelay`: `ttfbDelayMs` → `ttfbDelay`, `chunkDelayMs` → `chunkDelay`. The rig keeps main's `retryAfterSeconds` (the header's native unit) over the branch's `retryAfter`; four existing specs already depend on it. - Spec config: auth moved to `stitchapi/auth` (ADR 0021) and `apiKey` takes `secret` (#580); `retry.baseMs`/`baseDelay` fold into `retry.backoff.base` (#513); `respectRetryAfter` → `retry.respect` (#608); `AdapterProgress.phase` → `direction`. @stitchapi/download - `DownloadBatch.cancel`/`cancelAll` fold into `cancel(id?)` — R8 landed after this branch was cut and flags the shared prefix (P24). For a verb the optional parameter IS the collapse; an envelope would be the same pair one level deeper. - `hooks` is built unannotated so `onError` infers REQUIRED: the slot is `AtLeastOne<Hooks>` now (P20, #507), which an all-optional annotation cannot satisfy. - Version + vitest to the rc.7 lockstep; lockfile regenerated from main's. - classify.ts and the README no longer claim the engine drops the transport cause — this branch's own core fix carries it through. The hook seam stays: it is strictly wider (it also catches a thrown non-`Error`). Verify: 40/40 workspace projects green (core 1460 passed, download 15/15); check:lint / types / types-d / exports / exports:companions / contract / unknown-keys / release / docs-links / format all clean; core size 23.45 gzip (budget 23.50) and 20.87 for `import { stitch }` (20.90), @stitchapi/download 2.45/2.65 KB; `yakir check` both tiers 0 drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
What
Carves P20 (No empty-object config) out of the monolithic contract-freeze sweep (#470) as a standalone hard-break against
main. Closes the last of R6's P20 backlog.The five
StitchConfigslots R6 flagged as bare all-optional*Optionsbags now type their object form so the opaque{}is a compile error:hooksHooksAtLeastOne<Hooks>inputInputSchemasAtLeastOne<InputSchemas>multipartMultipartOptionsMultipartNesting | AtLeastOne<MultipartOptions>streamStreamOptionsStreamDecode | AtLeastOne<StreamOptions>sseSseOptionsboolean | AtLeastOne<SseOptions>The all-defaults case is now the scalar, not the opaque
{}:sse: true,stream: 'ndjson',multipart: 'dot'(P12/P13).expandShorthandfolds each into its envelope at compose time (multipart: 'dot'→{ nesting },stream: 'ndjson'→{ decode },sse: true→{ reconnect: true },sse: falseclears the slot), so the engine and__configstill only ever see the resolved object form —ResolvedStitchConfigomits + re-declares these five, andcomposewrites chainedhooks/ normalizedinputthrough the resolved view.Why
docs/CONTRACT.mdP20: the empty object{}must not be a valid value at a config slot — where{}would mean "enable with defaults", that case must be a scalar. This is the enforcement teeth behind P13 and refines P15.Notes
scripts/contract-violations.baseline.jsonshrinks 5 → 0 (R6 fully cleared).import { stitch }stays 20 kB.composition.specasserts the new scalar folds resolve correctly; newconfig-at-least-one.specpins the{}-rejection at each slot with@ts-expect-error(enforced bycheck:types).Gate
build·check:types(full workspace + test tsconfig) ·test(1210 pass) ·check:contract(0 known violations) ·check:lint·prettier --check— all green. Full pre-push gate (yakir bundle-advertised-size + build-docs + exports) passed.Breaking
BREAKING CHANGE:
StitchConfig.hooks,input,multipart,sse, andstreamno longer accept the empty object{}. Enable-with-defaults is now the scalar —sse: true,stream: 'ndjson',multipart: 'dot'— andhooks/inputrequire at least one field set. Object forms with ≥1 field are unchanged.🤖 Generated with Claude Code