From 6af8ab0df201e245fac503aa8822750662ebf6b9 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 17 Apr 2026 16:20:57 -0400 Subject: [PATCH] docs(http-api): refresh bridge inventory and clarify Schema.Class vs Struct (#23164) --- packages/opencode/specs/effect/http-api.md | 77 ++++++++++++++++------ 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 71b50250ed..09cb86a000 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -189,10 +189,46 @@ Ordering for a route-group migration: SDK shape rule: -- every schema migration must preserve the generated SDK output byte-for-byte -- `Schema.Class` emits a named `$ref` in OpenAPI via its identifier — use it only for types that already had `.meta({ ref })` in the old Zod schema -- inner / nested types that were anonymous in the old Zod schema should stay as `Schema.Struct` (not `Schema.Class`) to avoid introducing new named components in the OpenAPI spec -- if a diff appears in `packages/sdk/js/src/v2/gen/types.gen.ts`, the migration introduced an unintended API surface change — fix it before merging +- every schema migration must preserve the generated SDK output byte-for-byte **unless the new ref is intentional** (see Schema.Class vs Schema.Struct below) +- if an unintended diff appears in `packages/sdk/js/src/v2/gen/types.gen.ts`, the migration introduced an unintended API surface change — fix it before merging + +### Schema.Class vs Schema.Struct + +The pattern choice determines whether a schema becomes a **named** export in the SDK or stays **anonymous inline**. + +**Schema.Class** emits a named `$ref` in OpenAPI via its identifier → produces a named `export type Foo = ...` in `types.gen.ts`: + +```ts +export class Info extends Schema.Class("FooConfig")({ ... }) { + static readonly zod = zod(this) +} +``` + +**Schema.Struct** stays anonymous and is inlined everywhere it is referenced: + +```ts +export const Info = Schema.Struct({ ... }).pipe( + withStatics((s) => ({ zod: zod(s) })), +) +export type Info = Schema.Schema.Type +``` + +When to use each: + +- Use **Schema.Class** when: + - the original Zod had `.meta({ ref: ... })` (preserve the existing named SDK type byte-for-byte) + - the schema is a top-level endpoint request or response (SDK consumers benefit from a stable importable name) +- Use **Schema.Struct** when: + - the type is only used as a nested field inside another named schema + - the original Zod was anonymous and promoting it would bloat SDK types with no import value + +Promoting a previously-anonymous schema to Schema.Class is acceptable when it is top-level or endpoint-facing, but call it out in the PR — it is an additive SDK change (`export type Foo = ...` newly appears) even if it preserves the JSON shape. + +Schemas that are **not** pure objects (enums, unions, records, tuples) cannot use Schema.Class. For those, add `.annotate({ identifier: "FooName" })` to get the same named-ref behavior: + +```ts +export const Action = Schema.Literals(["ask", "allow", "deny"]).annotate({ identifier: "PermissionActionConfig" }) +``` Temporary exception: @@ -365,17 +401,16 @@ Current instance route inventory: endpoints: `GET /question`, `POST /question/:requestID/reply`, `POST /question/:requestID/reject` - `permission` - `bridged` endpoints: `GET /permission`, `POST /permission/:requestID/reply` -- `provider` - `bridged` (partial) - bridged endpoint: `GET /provider/auth` - not yet ported: `GET /provider`, OAuth mutations -- `config` - `next` - best next endpoint: `GET /config/providers` +- `provider` - `bridged` + endpoints: `GET /provider`, `GET /provider/auth`, `POST /provider/:providerID/oauth/authorize`, `POST /provider/:providerID/oauth/callback` +- `config` - `bridged` (partial) + bridged endpoint: `GET /config/providers` later endpoint: `GET /config` defer `PATCH /config` for now -- `project` - `later` - best small reads: `GET /project`, `GET /project/current` +- `project` - `bridged` (partial) + bridged endpoints: `GET /project`, `GET /project/current` defer git-init mutation first -- `workspace` - `later` +- `workspace` - `next` best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status` defer create/remove mutations first - `file` - `later` @@ -393,12 +428,12 @@ Current instance route inventory: - `tui` - `defer` queue-style UI bridge, weak early `HttpApi` fit -Recommended near-term sequence after the first spike: +Recommended near-term sequence: -1. `provider` auth read endpoint -2. `config` providers read endpoint -3. `project` read endpoints -4. `workspace` read endpoints +1. `workspace` read endpoints (`GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`) +2. `config` full read endpoint (`GET /config`) +3. `file` JSON read endpoints +4. `mcp` JSON read endpoints ## Checklist @@ -411,8 +446,12 @@ Recommended near-term sequence after the first spike: - [x] gate behind `OPENCODE_EXPERIMENTAL_HTTPAPI` flag - [x] verify OTEL spans and HTTP logs flow to motel - [x] bridge question, permission, and provider auth routes -- [ ] port remaining provider endpoints (`GET /provider`, OAuth mutations) -- [ ] port `config` read endpoints +- [x] port remaining provider endpoints (`GET /provider`, OAuth mutations) +- [x] port `config` providers read endpoint +- [x] port `project` read endpoints (`GET /project`, `GET /project/current`) +- [ ] port `workspace` read endpoints +- [ ] port `GET /config` full read endpoint +- [ ] port `file` JSON read endpoints - [ ] decide when to remove the flag and make Effect routes the default ## Rule of thumb