Initial Commit
This commit is contained in:
49
openspec/archive/2026-05-26-api-openapi/proposal.md
Normal file
49
openspec/archive/2026-05-26-api-openapi/proposal.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# `@suaveplan/api-openapi` — OpenAPI 3.1 document emitter
|
||||
|
||||
**Package**: `@suaveplan/api-openapi`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
**Stage**: 8 — see umbrella `openspec/changes/api-plugin-foundation/`
|
||||
|
||||
## Why
|
||||
|
||||
Plugins author routes with Zod schemas (design doc §7). The API host needs to emit a single merged OpenAPI 3.1 document combining every plugin's routes, every manifest's metadata, the active auth adapter's `securitySchemes`, and host-level info. Without this, plugin authors would each write hand-maintained OpenAPI documents that drift from runtime behaviour. Auto-emitting from the Zod contract is the only sustainable answer.
|
||||
|
||||
## What changes
|
||||
|
||||
- ADDED: `createOpenapiEmitter({ manifests, routes, authAdapter, hostConfig })` — reads each plugin's manifest (`name`, `displayName`, `description`, `scopes`, route prefix, `publicRoutes`), every registered route's Zod schemas, the active auth adapter's `securitySchemes`, and `host.json` (server URLs, contact, license). Returns a `OpenapiEmitter` with `getJson()` and `getYaml()`.
|
||||
- ADDED: Zod-to-OpenAPI conversion via `@asteasolutions/zod-to-openapi` — established, OpenAPI 3.1, handles refs / discriminators / coercion. Plugins import `z` from `zod` directly (genesis convention).
|
||||
- ADDED: Namespacing rules — tags from plugin `displayName`; components/schemas prefixed (`billing.Invoice`); `operationId = <pluginName>.<method>.<routePath>`; security inherited from the active auth adapter except on `publicRoutes`.
|
||||
- ADDED: HTTP endpoints (mounted by the host): `GET /openapi.json`, `GET /openapi.yaml`, `GET /docs`, `GET /docs/{plugin}` (per-plugin filtered view).
|
||||
- ADDED: `openapi.generation` capability — optional. A plugin opts in by declaring `openapi.contributes = true`. Plugins NOT opting in still get their routes documented unless `openapi: false` is per-route.
|
||||
- ADDED: `host.openapi.strictness` (`"warn" | "refuse-install"`) — when a route is missing a request/response schema, the emitter either logs a warning or the host refuses to install the plugin. Defaults to `"warn"` in dev, `"refuse-install"` in production.
|
||||
- ADDED: Error codes (`OPENAPI_*`): `MISSING_SCHEMA`, `SCHEMA_CONVERSION_FAILED`, `OPERATION_ID_COLLISION`, `UNKNOWN_PLUGIN_IN_FILTER`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- The UI itself — Scalar / Swagger / ReDoc each ship in their own sibling package.
|
||||
- Authoring OpenAPI by hand — plugins MUST use Zod.
|
||||
- Generating client SDKs from the emitted document — separate change.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
|
||||
|
||||
| Requirement | Design doc section |
|
||||
|---|---|
|
||||
| Auto-emit OpenAPI 3.1 from Zod | §7 |
|
||||
| Endpoints `/openapi.json`, `/openapi.yaml`, `/docs`, `/docs/{plugin}` | §7 |
|
||||
| Namespacing rules | §7 |
|
||||
| `host.openapi.strictness` for missing schemas | §7 (this proposal locks the policy) |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/emitter/emitter.test.ts` — full emission against a fixture manifest + route set; assert tags, components, operationIds, security all correct.
|
||||
- `src/emitter/emitter.spec-conformance.test.ts` — strictness modes (`warn` logs but emits; `refuse-install` raises `OPENAPI_MISSING_SCHEMA`).
|
||||
- `src/endpoints/endpoints.test.ts` — every endpoint returns the right Content-Type and body shape (json vs yaml; filtered view excludes other plugins).
|
||||
- `src/namespacing/namespacing.test.ts` — operationId determinism + collision detection.
|
||||
- `tests/dep-graph.test.ts` — runtime deps (`api-types`, `error`, `logger`, `zod`, `@asteasolutions/zod-to-openapi`).
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only. The host mounts the OpenAPI endpoints when this package is installed.
|
||||
@@ -0,0 +1,93 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-openapi (api-openapi)
|
||||
|
||||
**Package**: `@suaveplan/api-openapi`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: OpenAPI 3.1 document emitted from Zod schemas
|
||||
|
||||
The package SHALL emit an OpenAPI 3.1 document from every plugin's manifest plus every registered route's Zod schemas (request body / params / query / response). Conversion uses `@asteasolutions/zod-to-openapi`.
|
||||
|
||||
#### Scenario: full document covers every plugin
|
||||
|
||||
- WHEN two plugins are registered each with two routes
|
||||
- THEN `emitter.getJson()` returns an OpenAPI 3.1 document with paths for all four routes
|
||||
|
||||
**Verified by:** `src/emitter/emitter.test.ts::"emits a 3.1 document covering every registered route"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Namespacing rules are deterministic
|
||||
|
||||
The package SHALL set tags to each plugin's `displayName`, prefix component schemas with `<pluginName>.<TypeName>`, and set `operationId` to `<pluginName>.<method>.<routePath>`. operationIds MUST be unique across the merged document; collisions raise `OPENAPI_OPERATION_ID_COLLISION`.
|
||||
|
||||
#### Scenario: operationId is deterministic
|
||||
|
||||
- WHEN a billing plugin declares `GET /invoices/:id`
|
||||
- THEN the emitted operationId is `billing.get./invoices/{id}`
|
||||
|
||||
#### Scenario: operationId collision raises error
|
||||
|
||||
- WHEN two plugins register routes that produce identical operationIds
|
||||
- THEN emission raises `OPENAPI_OPERATION_ID_COLLISION`
|
||||
|
||||
**Verified by:** `src/namespacing/namespacing.test.ts::"operationId is deterministic"` AND `src/namespacing/namespacing.test.ts::"collision raises OPENAPI_OPERATION_ID_COLLISION"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Security schemes inherit from the active auth adapter
|
||||
|
||||
The package SHALL read the active auth adapter's `securitySchemes` contribution and attach the default scheme to every route except those listed in any plugin's `publicRoutes`.
|
||||
|
||||
#### Scenario: public route has no security
|
||||
|
||||
- WHEN a route is listed in `publicRoutes`
|
||||
- THEN the emitted operation has `security: []`
|
||||
|
||||
#### Scenario: non-public route inherits default scheme
|
||||
|
||||
- WHEN a route is not on `publicRoutes`
|
||||
- THEN the emitted operation lists the auth adapter's default scheme
|
||||
|
||||
**Verified by:** `src/emitter/emitter.test.ts::"public route emits empty security"` AND `src/emitter/emitter.test.ts::"non-public route inherits adapter scheme"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Endpoints serve json, yaml, and per-plugin filtered views
|
||||
|
||||
The package SHALL expose `GET /openapi.json`, `GET /openapi.yaml`, `GET /docs`, and `GET /docs/{plugin}` handlers. The filtered view MUST return only operations owned by the named plugin; an unknown plugin name raises `OPENAPI_UNKNOWN_PLUGIN_IN_FILTER` (HTTP 404).
|
||||
|
||||
#### Scenario: yaml endpoint returns yaml content-type
|
||||
|
||||
- WHEN a client requests `GET /openapi.yaml`
|
||||
- THEN response Content-Type is `application/yaml` AND the body parses as YAML
|
||||
|
||||
#### Scenario: filtered view excludes other plugins
|
||||
|
||||
- WHEN `GET /docs/billing` is requested in a host with billing + users plugins
|
||||
- THEN the response document contains billing routes only AND no users routes
|
||||
|
||||
**Verified by:** `src/endpoints/endpoints.test.ts::"yaml endpoint returns application/yaml"` AND `src/endpoints/endpoints.test.ts::"filtered view excludes other plugins"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Missing schema is handled per strictness setting
|
||||
|
||||
The package SHALL read `host.openapi.strictness`. When `"warn"`, missing request/response schemas log a warning AND emit with `description: "schema-missing"`. When `"refuse-install"`, missing schemas raise `OPENAPI_MISSING_SCHEMA` BEFORE the plugin reaches `installed`.
|
||||
|
||||
#### Scenario: warn mode emits with placeholder
|
||||
|
||||
- WHEN a route has no response schema AND strictness is `"warn"`
|
||||
- THEN emission succeeds AND the warning counter `api.openapi.missing_schema_total{plugin, route}` increments
|
||||
|
||||
#### Scenario: refuse-install raises
|
||||
|
||||
- WHEN strictness is `"refuse-install"`
|
||||
- THEN emission raises `OPENAPI_MISSING_SCHEMA`
|
||||
|
||||
**Verified by:** `src/emitter/emitter.spec-conformance.test.ts::"warn mode emits placeholder and increments warning counter"` AND `src/emitter/emitter.spec-conformance.test.ts::"refuse-install raises OPENAPI_MISSING_SCHEMA"`
|
||||
68
openspec/archive/2026-05-26-api-openapi/tasks.md
Normal file
68
openspec/archive/2026-05-26-api-openapi/tasks.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Tasks — `@suaveplan/api-openapi`
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change.
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read design doc (§7)
|
||||
- [x] 1.2 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.3 Draft `proposal.md`
|
||||
- [x] 1.4 Draft `specs/api-openapi/spec.md`
|
||||
- [x] 1.5 Every requirement carries `**Verified by:**`
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [ ] 2.1 Scaffold `packages/api/api-openapi/`
|
||||
- [ ] 2.2 `src/emitter/` — `createOpenapiEmitter`, `OpenapiEmitter`
|
||||
- [ ] 2.3 `src/zod-conversion/` — wrapper over `@asteasolutions/zod-to-openapi`
|
||||
- [ ] 2.4 `src/namespacing/` — operationId / tag / component-prefix rules
|
||||
- [ ] 2.5 `src/endpoints/` — `/openapi.json`, `/openapi.yaml`, `/docs`, `/docs/{plugin}` handlers
|
||||
- [ ] 2.6 `src/strictness/` — `host.openapi.strictness` dev/prod resolution
|
||||
- [ ] 2.7 `src/error-codes.ts`
|
||||
- [ ] 2.8 `src/index.ts` — explicit named re-exports
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [ ] 3.1 `src/emitter/emitter.test.ts`
|
||||
- [ ] 3.2 `src/emitter/emitter.spec-conformance.test.ts`
|
||||
- [ ] 3.3 `src/endpoints/endpoints.test.ts`
|
||||
- [ ] 3.4 `src/namespacing/namespacing.test.ts`
|
||||
- [ ] 3.5 `tests/dep-graph.test.ts`
|
||||
- [ ] 3.6 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [ ] 4.1 Sibling `.md` per implementation `.ts`
|
||||
- [ ] 4.2 `README.md` `## Modules`
|
||||
- [ ] 4.3 Update `CLAUDE.md` package listing
|
||||
|
||||
## 5. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [ ] 5.1 `bun run check`
|
||||
- [ ] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-openapi`
|
||||
- [ ] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-openapi` — 100/100/100/100
|
||||
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-openapi`
|
||||
- [ ] 5.5 `bunx turbo build --filter=@suaveplan/api-openapi`
|
||||
|
||||
## 6. Pre-archive 7-row checklist
|
||||
|
||||
- [ ] 6.1 Generics threaded for typed schema rendering
|
||||
- [ ] 6.2 Zod schemas wired at every route ingestion boundary
|
||||
- [ ] 6.3 Every `OPENAPI_*` code thrown by a production path
|
||||
- [ ] 6.4 Strict-mode behaviours: `warn` vs `refuse-install` both tested
|
||||
- [ ] 6.5 OTel instruments (emission counter, conversion-error counter) asserted with `MeterProvider`
|
||||
- [ ] 6.6 Lifecycle dedup — repeated `getJson()` on unchanged input returns the same document
|
||||
- [ ] 6.7 No retroactive tick
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [ ] 7.1 Fold spec delta into `openspec/specs/api-openapi/spec.md`
|
||||
- [ ] 7.2 Move folder to archive
|
||||
- [ ] 7.3 Tick `8.6` in umbrella tasks
|
||||
- [ ] 7.4 Refresh `MANIFEST.md`
|
||||
Reference in New Issue
Block a user