From d12e782ffbe60ad70c005641e51f181cc02e82b8 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Tue, 16 Jun 2026 16:59:31 +0300 Subject: [PATCH 1/2] docs: add an SDK roadmap for deferred features, open decisions, and codegen --- docs/roadmap.md | 205 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 docs/roadmap.md diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 00000000..66f2bd74 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,205 @@ +# SDK Roadmap + +This document tracks confirmed-but-deferred enhancements, open design decisions, and the +code-generation effort. Each item links to its tracking issue and records a recommended +direction and its dependencies, so the work can be sequenced rather than picked up ad hoc. + +Bug fixes, documentation, CI, and the smaller self-contained enhancements are handled directly +as pull requests and are not listed here. The items below are larger features, decisions that +should be made before the relevant code is written, or design work for a generator that does not +yet exist. + +## Status legend + +- **Planned** — confirmed worth doing; deferred only for scope/sequencing. +- **Decision needed** — a real fork with material trade-offs; pick a direction before writing code. +- **Design** — forward-looking design for the (not-yet-started) code generator. + +--- + +## Track A — Async parity + +The synchronous pipeline ships retry, bearer auth, and pagination; the asynchronous side does +not yet have equivalents. These bring the async path up to parity. + +### Async retry step at the RETRY stage — [#31] (Planned, large) +No `AsyncHttpStep` occupies `Stage.RETRY`, so async pipelines get no retry. Add a public +`AsyncRetryStep : AsyncHttpStep` with `final override val stage = Stage.RETRY` mirroring the sync +`RetryStep`, plus a concrete `DefaultAsyncRetryStep` that reuses `HttpRetryOptions` and a +`ScheduledExecutorService` for non-blocking backoff (no thread parked during the delay). Reuse the +existing classification/`Retry-After` logic. Should land after the retry-defaults reconciliation +(#38) so both async and sync share one backoff source of truth. + +### Async bearer-auth with background token refresh — [#32] (Planned, medium) +Add an async bearer-auth `AsyncHttpStep` and a non-blocking refresh path on the token provider +(`fetchAsync(...) : CompletableFuture`, defaulting to wrapping the blocking `fetch`), +so a valid-but-near-expiry token is returned immediately while the refresh runs off-thread. +Coordinate with the bearer-token eviction work (#33) so the sync and async steps share eviction +and refresh semantics. + +### Async pagination — [#34] (Planned, large) +Both pagination surfaces are blocking today. After the pagination unification decision (#30), +add an async sibling that drives an iterative `executeAsync` re-arm, closing each page on +completion, with `Flow`/`Flux` bridges living in the coroutines/reactor adapter modules (not in +`sdk-core`). Blocked on #30. + +--- + +## Track B — Core capabilities + +Confirmed features deferred for scope. None is a defect; each adds a genuinely missing capability. + +### Shared instrumentation emitter — [#26] (Planned, medium) +The sync and async instrumentation steps duplicate the emit/redact/preview/metrics logic, with a +standing extraction marker. The two copies have already diverged once. Extract the stateless logic +into an internal `InstrumentationEmitters` constructed from the values both steps need +(`HttpInstrumentationOptions`, `ClientLogger`, `Clock`, the lazy metric instruments). Pure internal +refactor — high value for preventing future drift between the two steps. + +### Per-call options channel on the request context — [#27] (Planned, medium) +`RequestContext` carries no per-call override channel. Add an immutable `RequestOptions` +(per-phase timeout overlay, response-validation toggle, ad-hoc credential override) with +`applyDefaults` merge semantics, keeping the transport SPIs single-method. Depends on the +`Timeout` value type from #41. + +### Per-phase `Timeout` value type — [#41] (Planned, medium) +No core timeout type exists; per-phase timeouts are configured ad hoc per transport. Add an +immutable `Timeout` (connect / read / write / request `Duration`s, read/write defaulting to +request) that adapters translate to native settings, kept distinct from the retry total-timeout. +Prerequisite for #27. + +### AutoCloseable SSE stream — [#35] (Planned, medium) +The SSE surface is a bare `Sequence`, so a partially-consumed stream can leak the response. Add an +`SseStream : AutoCloseable, Iterable` that owns the `Response` and closes it on +stream close or partial consumption — mirroring the close-on-partial-consume invariant pagination +already enforces. + +### Multipart request body — [#61] (Planned, large) +No multipart support today. Add a public `MultipartRequestBody : RequestBody` (immutable + Builder) +with one shared frame-size function driving both `writeTo` and `contentLength`, file parts +streaming zero-copy via `FileRequestBody` and non-file parts encoded through the `Serde` SPI (no +Jackson in `sdk-core`). + +### Opt-in resource leak detector — [#45] (Planned, medium) +No leak detection exists. Add an internal, opt-in, log-only `LeakDetector` that WARNs when a +caller-owned closeable becomes phantom-reachable unclosed, using a reflectively-obtained +`java.lang.ref.Cleaner` so Java-8 bytecode no-ops on JDK 8 and the whole thing is gated behind a +system property. Never auto-closes. + +--- + +## Track C — Decisions to make + +These need a recorded decision (and sometimes a small spike) before any code is written. + +### URL model: resolved `java.net.URL` vs deconstructed — [#29] (Decision needed) +**Recommended:** keep a single opaque URL field on `Request` (do not explode into +scheme/host/port/segments/query on the immutable model), but migrate the stored type from +`java.net.URL` to `java.net.URI` — `URI` is parse-only, DNS-free, and already what the JDK +transport needs. Preserve the existing textual, DNS-free equality. Record the decision in +`docs/architecture.md`. Gates #28 and the typed-page generation in #56. + +### First-class `QueryParams` multimap — [#28] (Planned, medium; gated on #29) +`QueryParam` is a `TODO()` stub and `RequestRebuilder` does query-string surgery by splitting on +`&` with single-value semantics. Add a public `QueryParams` multimap modeled on `Headers` +(insertion-ordered, multi-value, explicit encoding rules). Lands after the URL-model decision so it +projects into the chosen type. + +### Unify the two pagination stacks — [#30] (Decision needed, large) +Two parallel pagination surfaces exist and are both public API: `http.paging` +(`PagedIterable`/`PagedResponse`, BYO fetcher) and `pagination` (`Paginator`/`Page` + strategies, +self-driving). **Recommended:** make the strategy-driven `Paginator` + `Page` the canonical +surface (it owns the client, matches peer SDKs, and is the model the typed-page generation in #56 +builds on) and deprecate the other. A public-API redesign — decide before coding. Gates #34. + +### Deep-array value-equality utility — [#49] (Decision needed, small) +A `contentEquals`/`contentHashCode` helper has no consumer in the current tree; it is tied to the +future generated DTOs (with `ByteArray` fields). **Recommended:** defer until codegen needs it, to +avoid adding public surface that immediately churns the API snapshot and the coverage floor with no +caller. Fold into the codegen runtime when that lands. + +### VirtualThreads close-event log level — [#10] (Decision needed, trivial) +The `executor.closed` event is emitted at DEBUG (not INFO). **Recommended:** keep DEBUG (it matches +every other async-adapter event and keeps clean-shutdown noise off INFO) and close the issue, +optionally noting the intent in the `close()` KDoc. No code change beyond the optional doc line. + +### Release automation — [#75] (Decision needed, gated on CI) +Versioning, changelog, and publishing are manual, and the version string is duplicated across ~10 +build scripts. **Recommended (once CI lands):** record a decision on adopting release-please; if +adopted, collapse the duplicated version to a single anchor and add a release workflow + Sonatype +publishing. Gated on CI (#70) and complements the publishing-convention-plugin work (#71). + +--- + +## Track D — Code generation + +The SDK is deliberately a hand-written HTTP-client toolkit; **there is no generator in the tree +yet.** Issues #50–#69 are design for a future KotlinPoet-based generator that would emit typed +clients over this toolkit. None is a defect, and most cannot become code until the generator and +its keystone runtime type (#50) exist. They are captured here (and several warrant short design-doc +sections now) so the effort can start coherently. + +### Keystone: dependency-free four-state JSON field model — [#50] (Design, large) +The foundation the rest of the model generation builds on: a dep-free sealed `JsonField` +(`Known` / `Missing` / `Null` / `Raw`) plus a `RawJson` tree in `sdk-core`, with all +Jackson↔`RawJson` conversion confined to the `sdk-serde-jackson` adapter — mirroring how `Tristate` +is split today. Almost every other codegen item depends on this. **Land a design doc first** +(`docs/codegen-json-field.md`), then the runtime type, then the generator template. + +### Generated model shape (all depend on #50) +- **Thin models over a hand-written runtime — [#51]** (Design): generated models stay <100 lines + (fields + accessors) while the runtime owns the forward-compat machinery. +- **`additionalProperties` pass-through — [#52]** (Design): capture unknown fields into an immutable + `Map` so read-modify-write round-trips don't drop server-added fields. +- **Unions as private-ctor + per-variant accessors + visitor — [#53]** (Design): Java-8-safe + `oneOf` emission with a retained raw node and an `unknown(raw)` fallback. +- **Forward-compatible enums — [#54]** (Design): open value + known/value pair so deserialization + never throws on an unrecognized server value. +- **Discriminator/const fields as defaulted raw values with dual accessors — [#65]** (Design): + fold into the #50 design effort. +- **Optional `validate()`/`isValid()`/`validity()` triad — [#64]** (Design): opt-in, memoized, + off the deserialize path; used only as last-resort union disambiguation behind a discriminator. + +### Generated service/client shape +- **Two-tier raw/cooked service methods — [#55]** (Design): a "cooked" method returning the parsed + body and a "raw" method returning a lazy `ParsedResponse`. Builds on the `ResponseHandler` + seam (#36, now in review) and the operation-params SPI (#57). +- **Minimal `OperationParams` SPI — [#57]** (Design): projects an operation's inputs into + headers/query/path/body and feeds the context chain. Gated on the `QueryParams` multimap (#28). +- **Curated operation overload set — [#58]** (Design): one canonical method per operation plus a + small curated overload set leaning on Kotlin default arguments, rather than the full overload + cross-product. +- **Lazy sub-service accessor tree — [#59]** (Design): `by lazy` sub-service accessors on a + generated root client, reusing a nested raw-response impl. +- **`withOptions(Consumer)` returning a new immutable client — [#60]** (Design): gated on + first deciding whether to introduce a single cloneable client-config with `toBuilder()`. +- **Typed page classes that rebuild typed params — [#56]** (Design, xlarge): `nextPage()` + re-invokes the operation with a typed param object, not a spliced URL string. Gated on #57, #28, + and the pagination unification (#30). +- **Per-endpoint SSE adapter — [#62]** (Design): maps an `SseStream` to a lazily-decoded + `Iterable`. Gated on #35 and #36. +- **Per-operation auth descriptors with a precedence ladder — [#63]** (Design): the generator emits + an `AuthMetadata` per operation; `sdk-core`'s auth step consumes it scheme-agnostically. The + scheme-agnostic primitives partly exist already. + +### Generator plumbing & outputs +- **Strict structured-output JSON-schema encoding rules — [#66]** (Design, small): capture the + encoding contract (all-required + `additionalProperties:false` + optional-as-nullable-union) as a + design-doc section now; it is adapter-only, never `sdk-core`. +- **Reusable fail-soft recursive validator skeleton — [#67]** (Design, small): a small generic + recursion-guarded, path-prefixed validator idiom for the generator's own IR; build in codegen + week 1–2, once the IR exists. +- **Provenance file stamped into generated SDKs — [#68]** (Design, small): generator version + + input-contract hash, emitted into generated output only, never into the hand-written toolkit. +- **Spring Boot starter per generated API — [#69]** (Design, medium): an optional sibling + `-spring-boot-starter` with `@ConfigurationProperties`, a customizer `fun interface`, and an + `@AutoConfiguration` assembling {IoProvider + transport + HttpPipeline}, keeping Spring out of + `sdk-core` and the generated client. + +### Suggested codegen sequencing + +1. Design docs: #50 (keystone), #66, #67, #68 — these can be written now, before any generator code. +2. Land #50's runtime type in `sdk-core`; decide #29 (URL model) and #28 (`QueryParams`). +3. Stand up the generator IR + the fail-soft validator (#67), then model emission (#51–#54, #64, #65). +4. Service/client emission (#55, #57, #58, #59, #60), then pagination/SSE/auth generation (#56, #62, #63). +5. Packaging outputs (#68 provenance, #69 Spring starter). From 67c6fcbedaf24e3519ac14e95ff6eb161f029856 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Tue, 16 Jun 2026 22:24:03 +0300 Subject: [PATCH 2/2] docs: make roadmap issue refs auto-link and align log-level wording --- docs/roadmap.md | 95 ++++++++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 66f2bd74..249c057e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -12,7 +12,8 @@ yet exist. ## Status legend - **Planned** — confirmed worth doing; deferred only for scope/sequencing. -- **Decision needed** — a real fork with material trade-offs; pick a direction before writing code. +- **Decision needed** — a real fork with material trade-offs; pick a direction before writing + code. - **Design** — forward-looking design for the (not-yet-started) code generator. --- @@ -22,7 +23,7 @@ yet exist. The synchronous pipeline ships retry, bearer auth, and pagination; the asynchronous side does not yet have equivalents. These bring the async path up to parity. -### Async retry step at the RETRY stage — [#31] (Planned, large) +### Async retry step at the RETRY stage — #31 (Planned, large) No `AsyncHttpStep` occupies `Stage.RETRY`, so async pipelines get no retry. Add a public `AsyncRetryStep : AsyncHttpStep` with `final override val stage = Stage.RETRY` mirroring the sync `RetryStep`, plus a concrete `DefaultAsyncRetryStep` that reuses `HttpRetryOptions` and a @@ -30,14 +31,14 @@ No `AsyncHttpStep` occupies `Stage.RETRY`, so async pipelines get no retry. Add existing classification/`Retry-After` logic. Should land after the retry-defaults reconciliation (#38) so both async and sync share one backoff source of truth. -### Async bearer-auth with background token refresh — [#32] (Planned, medium) +### Async bearer-auth with background token refresh — #32 (Planned, medium) Add an async bearer-auth `AsyncHttpStep` and a non-blocking refresh path on the token provider (`fetchAsync(...) : CompletableFuture`, defaulting to wrapping the blocking `fetch`), so a valid-but-near-expiry token is returned immediately while the refresh runs off-thread. Coordinate with the bearer-token eviction work (#33) so the sync and async steps share eviction and refresh semantics. -### Async pagination — [#34] (Planned, large) +### Async pagination — #34 (Planned, large) Both pagination surfaces are blocking today. After the pagination unification decision (#30), add an async sibling that drives an iterative `executeAsync` re-arm, closing each page on completion, with `Flow`/`Flux` bridges living in the coroutines/reactor adapter modules (not in @@ -49,38 +50,38 @@ completion, with `Flow`/`Flux` bridges living in the coroutines/reactor adapter Confirmed features deferred for scope. None is a defect; each adds a genuinely missing capability. -### Shared instrumentation emitter — [#26] (Planned, medium) +### Shared instrumentation emitter — #26 (Planned, medium) The sync and async instrumentation steps duplicate the emit/redact/preview/metrics logic, with a standing extraction marker. The two copies have already diverged once. Extract the stateless logic into an internal `InstrumentationEmitters` constructed from the values both steps need (`HttpInstrumentationOptions`, `ClientLogger`, `Clock`, the lazy metric instruments). Pure internal refactor — high value for preventing future drift between the two steps. -### Per-call options channel on the request context — [#27] (Planned, medium) +### Per-call options channel on the request context — #27 (Planned, medium) `RequestContext` carries no per-call override channel. Add an immutable `RequestOptions` (per-phase timeout overlay, response-validation toggle, ad-hoc credential override) with `applyDefaults` merge semantics, keeping the transport SPIs single-method. Depends on the `Timeout` value type from #41. -### Per-phase `Timeout` value type — [#41] (Planned, medium) +### Per-phase `Timeout` value type — #41 (Planned, medium) No core timeout type exists; per-phase timeouts are configured ad hoc per transport. Add an immutable `Timeout` (connect / read / write / request `Duration`s, read/write defaulting to request) that adapters translate to native settings, kept distinct from the retry total-timeout. Prerequisite for #27. -### AutoCloseable SSE stream — [#35] (Planned, medium) +### AutoCloseable SSE stream — #35 (Planned, medium) The SSE surface is a bare `Sequence`, so a partially-consumed stream can leak the response. Add an `SseStream : AutoCloseable, Iterable` that owns the `Response` and closes it on stream close or partial consumption — mirroring the close-on-partial-consume invariant pagination already enforces. -### Multipart request body — [#61] (Planned, large) +### Multipart request body — #61 (Planned, large) No multipart support today. Add a public `MultipartRequestBody : RequestBody` (immutable + Builder) with one shared frame-size function driving both `writeTo` and `contentLength`, file parts streaming zero-copy via `FileRequestBody` and non-file parts encoded through the `Serde` SPI (no Jackson in `sdk-core`). -### Opt-in resource leak detector — [#45] (Planned, medium) +### Opt-in resource leak detector — #45 (Planned, medium) No leak detection exists. Add an internal, opt-in, log-only `LeakDetector` that WARNs when a caller-owned closeable becomes phantom-reachable unclosed, using a reflectively-obtained `java.lang.ref.Cleaner` so Java-8 bytecode no-ops on JDK 8 and the whole thing is gated behind a @@ -92,38 +93,39 @@ system property. Never auto-closes. These need a recorded decision (and sometimes a small spike) before any code is written. -### URL model: resolved `java.net.URL` vs deconstructed — [#29] (Decision needed) +### URL model: resolved `java.net.URL` vs deconstructed — #29 (Decision needed) **Recommended:** keep a single opaque URL field on `Request` (do not explode into scheme/host/port/segments/query on the immutable model), but migrate the stored type from `java.net.URL` to `java.net.URI` — `URI` is parse-only, DNS-free, and already what the JDK transport needs. Preserve the existing textual, DNS-free equality. Record the decision in `docs/architecture.md`. Gates #28 and the typed-page generation in #56. -### First-class `QueryParams` multimap — [#28] (Planned, medium; gated on #29) +### First-class `QueryParams` multimap — #28 (Planned, medium; gated on #29) `QueryParam` is a `TODO()` stub and `RequestRebuilder` does query-string surgery by splitting on `&` with single-value semantics. Add a public `QueryParams` multimap modeled on `Headers` (insertion-ordered, multi-value, explicit encoding rules). Lands after the URL-model decision so it projects into the chosen type. -### Unify the two pagination stacks — [#30] (Decision needed, large) +### Unify the two pagination stacks — #30 (Decision needed, large) Two parallel pagination surfaces exist and are both public API: `http.paging` (`PagedIterable`/`PagedResponse`, BYO fetcher) and `pagination` (`Paginator`/`Page` + strategies, self-driving). **Recommended:** make the strategy-driven `Paginator` + `Page` the canonical surface (it owns the client, matches peer SDKs, and is the model the typed-page generation in #56 builds on) and deprecate the other. A public-API redesign — decide before coding. Gates #34. -### Deep-array value-equality utility — [#49] (Decision needed, small) +### Deep-array value-equality utility — #49 (Decision needed, small) A `contentEquals`/`contentHashCode` helper has no consumer in the current tree; it is tied to the future generated DTOs (with `ByteArray` fields). **Recommended:** defer until codegen needs it, to avoid adding public surface that immediately churns the API snapshot and the coverage floor with no caller. Fold into the codegen runtime when that lands. -### VirtualThreads close-event log level — [#10] (Decision needed, trivial) -The `executor.closed` event is emitted at DEBUG (not INFO). **Recommended:** keep DEBUG (it matches -every other async-adapter event and keeps clean-shutdown noise off INFO) and close the issue, -optionally noting the intent in the `close()` KDoc. No code change beyond the optional doc line. +### VirtualThreads close-event log level — #10 (Decision needed, trivial) +The `executor.closed` event is emitted at the SDK's VERBOSE level (which maps to SLF4J DEBUG), not +INFO. **Recommended:** keep VERBOSE (it matches every other async-adapter event and keeps +clean-shutdown noise off INFO) and close the issue, optionally noting the intent in the `close()` +KDoc. No code change beyond the optional doc line. -### Release automation — [#75] (Decision needed, gated on CI) +### Release automation — #75 (Decision needed, gated on CI) Versioning, changelog, and publishing are manual, and the version string is duplicated across ~10 build scripts. **Recommended (once CI lands):** record a decision on adopting release-please; if adopted, collapse the duplicated version to a single anchor and add a release workflow + Sonatype @@ -139,67 +141,70 @@ clients over this toolkit. None is a defect, and most cannot become code until t its keystone runtime type (#50) exist. They are captured here (and several warrant short design-doc sections now) so the effort can start coherently. -### Keystone: dependency-free four-state JSON field model — [#50] (Design, large) +### Keystone: dependency-free four-state JSON field model — #50 (Design, large) The foundation the rest of the model generation builds on: a dep-free sealed `JsonField` (`Known` / `Missing` / `Null` / `Raw`) plus a `RawJson` tree in `sdk-core`, with all -Jackson↔`RawJson` conversion confined to the `sdk-serde-jackson` adapter — mirroring how `Tristate` -is split today. Almost every other codegen item depends on this. **Land a design doc first** -(`docs/codegen-json-field.md`), then the runtime type, then the generator template. +Jackson↔`RawJson` conversion confined to the `sdk-serde-jackson` adapter — mirroring how +`Tristate` is split today. Almost every other codegen item depends on this. **Land a design doc +first** (`docs/codegen-json-field.md`), then the runtime type, then the generator template. ### Generated model shape (all depend on #50) -- **Thin models over a hand-written runtime — [#51]** (Design): generated models stay <100 lines +- **Thin models over a hand-written runtime — #51** (Design): generated models stay <100 lines (fields + accessors) while the runtime owns the forward-compat machinery. -- **`additionalProperties` pass-through — [#52]** (Design): capture unknown fields into an immutable +- **`additionalProperties` pass-through — #52** (Design): capture unknown fields into an immutable `Map` so read-modify-write round-trips don't drop server-added fields. -- **Unions as private-ctor + per-variant accessors + visitor — [#53]** (Design): Java-8-safe +- **Unions as private-ctor + per-variant accessors + visitor — #53** (Design): Java-8-safe `oneOf` emission with a retained raw node and an `unknown(raw)` fallback. -- **Forward-compatible enums — [#54]** (Design): open value + known/value pair so deserialization +- **Forward-compatible enums — #54** (Design): open value + known/value pair so deserialization never throws on an unrecognized server value. -- **Discriminator/const fields as defaulted raw values with dual accessors — [#65]** (Design): +- **Discriminator/const fields as defaulted raw values with dual accessors — #65** (Design): fold into the #50 design effort. -- **Optional `validate()`/`isValid()`/`validity()` triad — [#64]** (Design): opt-in, memoized, +- **Optional `validate()`/`isValid()`/`validity()` triad — #64** (Design): opt-in, memoized, off the deserialize path; used only as last-resort union disambiguation behind a discriminator. ### Generated service/client shape -- **Two-tier raw/cooked service methods — [#55]** (Design): a "cooked" method returning the parsed +- **Two-tier raw/cooked service methods — #55** (Design): a "cooked" method returning the parsed body and a "raw" method returning a lazy `ParsedResponse`. Builds on the `ResponseHandler` - seam (#36, now in review) and the operation-params SPI (#57). -- **Minimal `OperationParams` SPI — [#57]** (Design): projects an operation's inputs into + seam (#36) and the operation-params SPI (#57). +- **Minimal `OperationParams` SPI — #57** (Design): projects an operation's inputs into headers/query/path/body and feeds the context chain. Gated on the `QueryParams` multimap (#28). -- **Curated operation overload set — [#58]** (Design): one canonical method per operation plus a +- **Curated operation overload set — #58** (Design): one canonical method per operation plus a small curated overload set leaning on Kotlin default arguments, rather than the full overload cross-product. -- **Lazy sub-service accessor tree — [#59]** (Design): `by lazy` sub-service accessors on a +- **Lazy sub-service accessor tree — #59** (Design): `by lazy` sub-service accessors on a generated root client, reusing a nested raw-response impl. -- **`withOptions(Consumer)` returning a new immutable client — [#60]** (Design): gated on +- **`withOptions(Consumer)` returning a new immutable client — #60** (Design): gated on first deciding whether to introduce a single cloneable client-config with `toBuilder()`. -- **Typed page classes that rebuild typed params — [#56]** (Design, xlarge): `nextPage()` +- **Typed page classes that rebuild typed params — #56** (Design, xlarge): `nextPage()` re-invokes the operation with a typed param object, not a spliced URL string. Gated on #57, #28, and the pagination unification (#30). -- **Per-endpoint SSE adapter — [#62]** (Design): maps an `SseStream` to a lazily-decoded +- **Per-endpoint SSE adapter — #62** (Design): maps an `SseStream` to a lazily-decoded `Iterable`. Gated on #35 and #36. -- **Per-operation auth descriptors with a precedence ladder — [#63]** (Design): the generator emits +- **Per-operation auth descriptors with a precedence ladder — #63** (Design): the generator emits an `AuthMetadata` per operation; `sdk-core`'s auth step consumes it scheme-agnostically. The scheme-agnostic primitives partly exist already. ### Generator plumbing & outputs -- **Strict structured-output JSON-schema encoding rules — [#66]** (Design, small): capture the +- **Strict structured-output JSON-schema encoding rules — #66** (Design, small): capture the encoding contract (all-required + `additionalProperties:false` + optional-as-nullable-union) as a design-doc section now; it is adapter-only, never `sdk-core`. -- **Reusable fail-soft recursive validator skeleton — [#67]** (Design, small): a small generic +- **Reusable fail-soft recursive validator skeleton — #67** (Design, small): a small generic recursion-guarded, path-prefixed validator idiom for the generator's own IR; build in codegen week 1–2, once the IR exists. -- **Provenance file stamped into generated SDKs — [#68]** (Design, small): generator version + +- **Provenance file stamped into generated SDKs — #68** (Design, small): generator version + input-contract hash, emitted into generated output only, never into the hand-written toolkit. -- **Spring Boot starter per generated API — [#69]** (Design, medium): an optional sibling +- **Spring Boot starter per generated API — #69** (Design, medium): an optional sibling `-spring-boot-starter` with `@ConfigurationProperties`, a customizer `fun interface`, and an `@AutoConfiguration` assembling {IoProvider + transport + HttpPipeline}, keeping Spring out of `sdk-core` and the generated client. ### Suggested codegen sequencing -1. Design docs: #50 (keystone), #66, #67, #68 — these can be written now, before any generator code. +1. Design docs: #50 (keystone), #66, #67, #68 — these can be written now, before any generator + code. 2. Land #50's runtime type in `sdk-core`; decide #29 (URL model) and #28 (`QueryParams`). -3. Stand up the generator IR + the fail-soft validator (#67), then model emission (#51–#54, #64, #65). -4. Service/client emission (#55, #57, #58, #59, #60), then pagination/SSE/auth generation (#56, #62, #63). +3. Stand up the generator IR + the fail-soft validator (#67), then model emission + (#51–#54, #64, #65). +4. Service/client emission (#55, #57, #58, #59, #60), then pagination/SSE/auth generation + (#56, #62, #63). 5. Packaging outputs (#68 provenance, #69 Spring starter).