Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View 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.

View File

@@ -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"`

View 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`