Skip to content

Commit 6af8ab0

Browse files
authored
docs(http-api): refresh bridge inventory and clarify Schema.Class vs Struct (#23164)
1 parent 984f5ed commit 6af8ab0

1 file changed

Lines changed: 58 additions & 19 deletions

File tree

packages/opencode/specs/effect/http-api.md

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,46 @@ Ordering for a route-group migration:
189189

190190
SDK shape rule:
191191

192-
- every schema migration must preserve the generated SDK output byte-for-byte
193-
- `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
194-
- 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
195-
- 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
192+
- 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)
193+
- 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
194+
195+
### Schema.Class vs Schema.Struct
196+
197+
The pattern choice determines whether a schema becomes a **named** export in the SDK or stays **anonymous inline**.
198+
199+
**Schema.Class** emits a named `$ref` in OpenAPI via its identifier → produces a named `export type Foo = ...` in `types.gen.ts`:
200+
201+
```ts
202+
export class Info extends Schema.Class<Info>("FooConfig")({ ... }) {
203+
static readonly zod = zod(this)
204+
}
205+
```
206+
207+
**Schema.Struct** stays anonymous and is inlined everywhere it is referenced:
208+
209+
```ts
210+
export const Info = Schema.Struct({ ... }).pipe(
211+
withStatics((s) => ({ zod: zod(s) })),
212+
)
213+
export type Info = Schema.Schema.Type<typeof Info>
214+
```
215+
216+
When to use each:
217+
218+
- Use **Schema.Class** when:
219+
- the original Zod had `.meta({ ref: ... })` (preserve the existing named SDK type byte-for-byte)
220+
- the schema is a top-level endpoint request or response (SDK consumers benefit from a stable importable name)
221+
- Use **Schema.Struct** when:
222+
- the type is only used as a nested field inside another named schema
223+
- the original Zod was anonymous and promoting it would bloat SDK types with no import value
224+
225+
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.
226+
227+
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:
228+
229+
```ts
230+
export const Action = Schema.Literals(["ask", "allow", "deny"]).annotate({ identifier: "PermissionActionConfig" })
231+
```
196232

197233
Temporary exception:
198234

@@ -365,17 +401,16 @@ Current instance route inventory:
365401
endpoints: `GET /question`, `POST /question/:requestID/reply`, `POST /question/:requestID/reject`
366402
- `permission` - `bridged`
367403
endpoints: `GET /permission`, `POST /permission/:requestID/reply`
368-
- `provider` - `bridged` (partial)
369-
bridged endpoint: `GET /provider/auth`
370-
not yet ported: `GET /provider`, OAuth mutations
371-
- `config` - `next`
372-
best next endpoint: `GET /config/providers`
404+
- `provider` - `bridged`
405+
endpoints: `GET /provider`, `GET /provider/auth`, `POST /provider/:providerID/oauth/authorize`, `POST /provider/:providerID/oauth/callback`
406+
- `config` - `bridged` (partial)
407+
bridged endpoint: `GET /config/providers`
373408
later endpoint: `GET /config`
374409
defer `PATCH /config` for now
375-
- `project` - `later`
376-
best small reads: `GET /project`, `GET /project/current`
410+
- `project` - `bridged` (partial)
411+
bridged endpoints: `GET /project`, `GET /project/current`
377412
defer git-init mutation first
378-
- `workspace` - `later`
413+
- `workspace` - `next`
379414
best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`
380415
defer create/remove mutations first
381416
- `file` - `later`
@@ -393,12 +428,12 @@ Current instance route inventory:
393428
- `tui` - `defer`
394429
queue-style UI bridge, weak early `HttpApi` fit
395430

396-
Recommended near-term sequence after the first spike:
431+
Recommended near-term sequence:
397432

398-
1. `provider` auth read endpoint
399-
2. `config` providers read endpoint
400-
3. `project` read endpoints
401-
4. `workspace` read endpoints
433+
1. `workspace` read endpoints (`GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`)
434+
2. `config` full read endpoint (`GET /config`)
435+
3. `file` JSON read endpoints
436+
4. `mcp` JSON read endpoints
402437

403438
## Checklist
404439

@@ -411,8 +446,12 @@ Recommended near-term sequence after the first spike:
411446
- [x] gate behind `OPENCODE_EXPERIMENTAL_HTTPAPI` flag
412447
- [x] verify OTEL spans and HTTP logs flow to motel
413448
- [x] bridge question, permission, and provider auth routes
414-
- [ ] port remaining provider endpoints (`GET /provider`, OAuth mutations)
415-
- [ ] port `config` read endpoints
449+
- [x] port remaining provider endpoints (`GET /provider`, OAuth mutations)
450+
- [x] port `config` providers read endpoint
451+
- [x] port `project` read endpoints (`GET /project`, `GET /project/current`)
452+
- [ ] port `workspace` read endpoints
453+
- [ ] port `GET /config` full read endpoint
454+
- [ ] port `file` JSON read endpoints
416455
- [ ] decide when to remove the flag and make Effect routes the default
417456

418457
## Rule of thumb

0 commit comments

Comments
 (0)