# OpenAPI Domain — Architecture

> Architectural overview of `@oshun/openapi`: the platform's single source of
> truth for REST API contracts — a typed spec registry, a Zod-driven spec/client
> generation pipeline, and a set of CI gates that keep specs, generated
> TypeScript types, and typed clients from ever drifting apart.

---

`@oshun/openapi` is the contract layer for every REST API in the Oshun platform.
It lives at `libs/openapi/` and exists to solve one structural problem: in a
monorepo with more than a dozen API-owning domains (Lilith, Yemaya, Isis,
Sophia, Hathor, Bellona, Nyx, Calliope, Concordia, Metis, the Oshun V1 domains,
and the V2/V3 surfaces), if each team hand-maintained its own OpenAPI YAML with
no shared tooling, the specs would drift silently from the handlers that
implement them, from the TypeScript types that consumers import, and from each
other. This library gives all of those contracts a single home, a typed catalog
keyed off a small metadata model, and a fully automated
generate-validate-and-gate pipeline.

The crucial architectural fact — and the one most often gotten wrong — is that
**`@oshun/openapi` is a build-time and CI tooling artifact, not a runtime
dependency.** Production services never load YAML spec files at request time.
They import the TypeScript types and the typed fetch clients that this library
_generates_ from those specs. `package.json` declares `"private": true`,
`"type": "module"`, version `0.1.0`; the only thing it ships into other packages
at runtime are the generated `src/generated/*.ts` type modules and the generated
`src/v3-clients/` fetch clients (exposed through the `./generated` and
`./v3-clients` subpath exports). The single runtime exception is the V2
Companion public API, which contracts against the registry to keep its published
spec and its live `/api/v2` endpoints pinned together (covered under
cross-domain boundaries below).

A new engineer should hold three mental layers:

1. **The library core** (`src/`) — the typed registry, the loader utilities, and
   the Oshun V1 spec builder. This is the importable, unit-tested surface.
2. **The tooling layer** (`scripts/`) — eleven `tsx` scripts that validate
   specs, detect breaking changes, detect spec↔type drift, generate specs and
   clients from canonical Zod sources, generate TypeScript types from specs, and
   build HTML docs. Each has a paired `*:check` mode that is the CI gate.
3. **The artifacts** (`src/specs/`, `src/generated/`, `src/v3-clients/`, `v2/`,
   `docs/`) — the YAML specs (hand-authored and generated), the generated
   TypeScript type modules, the generated V3 tenant clients, the V2 companion
   spec, and the static documentation site.

---

## Where It Sits in the Stack

`@oshun/openapi` sits _above_ the canonical contract sources and _below_ the
domain API servers. It draws raw material from four workspace libraries and
emits consumable artifacts for the rest of the platform.

Its four workspace dependencies are, exactly (`package.json`):

- `@oshun/persistence` — the V1 object/enum persistence contracts
  (`V1_OBJECT_PERSISTENCE_CONTRACTS`, `V1_ENUM_PERSISTENCE_CONTRACTS`) and the
  `ContractPersistenceDomain` union. These Zod contracts are the source for the
  six V1 domain specs and the BFF spec.
- `@oshun/contracts` (via the `/v3` subpath) — `buildV3OpenApiDocument`,
  `V3_CONTRACT_REGISTRY`, `getV3ContractsForTenant`, and the V3 fixtures. Source
  for `v3.yaml` and the four V3 tenant clients.
- `@calliope/core` — `CalliopeOpenApiComponentSchemas` (a
  `Record<string, z.ZodTypeAny>`). Source for `calliope-api.yaml`.
- `@oshun/domain-registry` — `DOMAIN_REGISTRY` (display names, shell narratives,
  BFF base paths), used to title and tag the generated V1 specs.

Its consumers are the domain API servers (which import
`@oshun/openapi/generated` types and the typed clients), the CI pipeline (which
runs the `*:check` gates), and the developer portal / docs site (which renders
the generated `docs/` output). The dependency arrow always points from consumers
toward this library; the library never imports a domain's business logic.

```mermaid
flowchart TD
  subgraph Sources["Canonical Zod sources"]
    PERS["@oshun/persistence\nV1 object/enum contracts"]
    CONTR["@oshun/contracts/v3\nbuildV3OpenApiDocument"]
    CALL["@calliope/core\nComponentSchemas"]
    DREG["@oshun/domain-registry\nDOMAIN_REGISTRY"]
  end

  subgraph Gen["Generators (scripts/)"]
    V1SPEC["generate-oshun-v1-specs"]
    V1CLI["generate-oshun-v1-api-clients"]
    CALSPEC["generate-calliope-spec"]
    V3SPEC["generate-v3-spec"]
    V3CLI["generate-v3-clients"]
    TSGEN["generate (openapi-typescript)"]
  end

  subgraph Artifacts["Committed artifacts"]
    YAML["src/specs/*.yaml\n+ v2/companion.yaml"]
    HAND["hand-authored\n<domain>-api.yaml, main.yaml"]
    GENTS["src/generated/*.ts"]
    V3C["src/v3-clients/*"]
    DOCS["docs/ (Redoc)"]
  end

  subgraph Core["Library core (src/)"]
    REG["utils/registry.ts\nSPEC_PATHS, SPEC_REGISTRY"]
    LOAD["utils/loader.ts\nloadSpec / mergeSpecs"]
    BUILD["oshun-v1/spec-builder.ts"]
  end

  subgraph Gates["CI gates (*:check)"]
    VAL["validate.ts"]
    DIFF["diff.ts"]
    DRIFT["drift-check.ts"]
    VV3["validate-v3-spec.ts"]
  end

  PERS --> BUILD --> V1SPEC --> YAML
  PERS --> V1CLI
  DREG --> BUILD
  CONTR --> V3SPEC --> YAML
  CONTR --> V3CLI --> V3C
  CALL --> CALSPEC --> YAML
  HAND --> YAML
  YAML --> TSGEN --> GENTS
  YAML --> LOAD
  YAML --> REG
  YAML --> DOCS
  YAML --> VAL
  YAML --> DIFF
  YAML --> DRIFT
  GENTS --> DRIFT
  YAML --> VV3
  GENTS --> SERVERS["Domain API servers\n(@oshun/openapi/generated + clients)"]
  V3C --> SERVERS
```

---

## Library Core (`src/`)

The importable surface is a small barrel (`src/index.ts`) re-exporting three
groups: loader utilities, the registry, and the V1 spec builder. There is no
deep machinery here — the core is deliberately thin so that the heavy logic
lives in the scripts and the canonical Zod sources.

### Registry — `src/utils/registry.ts`

The registry is the typed catalog that answers "what APIs exist, who owns them,
and where are their spec files?" without anyone grepping the tree. It has two
`const` sources of truth and a handful of helpers.

- **`ApiDomain`** is a string-literal union (not a runtime enum) of every Oshun
  API domain: `tara`, `arete`, `veritas`, `lilith`, `yemaya`, `isis`, `sophia`,
  `hathor`, `bellona`, `calliope`, `nyx`, `nisaba`, `metis`, `v2`, `v3`,
  `oshun-bff`, `shared`. Each carries an inline source comment describing its
  scope. A union means the compiler checks domain strings with zero runtime
  cost.
- **`SPEC_PATHS`** is declared `as const`, so keys and string values are typed
  literals — this prevents path typos in loaders, validators, and generators. It
  mirrors the domain hierarchy: top-level keys per domain, nested objects for
  per-contract/per-feature keys.
- **`SPEC_REGISTRY`** is a `Record<string, SpecMetadata>` registering 20 specs.
  Each `SpecMetadata` carries `name`, `version`, `description`, `specPath` (a
  `SPEC_PATHS` value), `domain`, `tags`, `basePath`, and an optional
  `deprecated`. No entry currently sets `deprecated`.
- **Helpers**: `getSpecsByDomain(domain)` filters registry values by domain;
  `getSpecsByTag(tag)` filters by tag membership (enabling cross-cutting views
  like "every `astronomy` spec"); `getDomainPaths(key)` returns all spec paths
  under a `SPEC_PATHS` key. `extractEndpoints(spec)` walks `paths` across the
  seven HTTP methods into flat `{ method, path, operationId, tags, summary }`
  descriptors; `extractSchemas(spec)` flattens `components.schemas` into
  `{ name, type, description }`, joining OpenAPI-3.1 array `type`s with `|`.

### Loader — `src/utils/loader.ts`

The loader is the only code that touches spec files on disk, so callers never
construct paths. `resolveSpecFilePath` applies three rules: absolute paths pass
through; a `v2`/`v2/...` path resolves against the package root
(`libs/openapi/`); everything else resolves against `SPECS_DIR` (`src/specs/`).
`loadSpec`/`loadSpecSync` read and `YAML.parse` into an `OpenAPIV3_1.Document`;
`listSpecs` recursively scans `src/specs/` and `v2/` (both guarded by
`fs.existsSync`); `mergeSpecs` shallow-merges `paths` and the five `components`
sub-maps (`schemas`, `responses`, `parameters`, `requestBodies`,
`securitySchemes`) and de-duplicates `tags` by name — the mechanism for building
a single browsable document for the portal.

### V1 Spec Builder — `src/oshun-v1/spec-builder.ts`

This module is the heart of the generation story. It deterministically derives
OpenAPI 3.1 documents _and_ typed-client resource descriptors directly from the
`@oshun/persistence` Zod contracts plus `@oshun/domain-registry` metadata —
keeping the builder logic separate from the scripts that invoke it so it is
independently unit-testable.

For each of the six V1 domains (`OSHUN_V1_API_DOMAINS` =
`['tara', 'arete', 'veritas', 'nyx', 'nisaba', 'metis']`), `buildContractPaths`
turns each object contract into a five-operation REST resource —
`list`/`create`/`get`/`upsert`/`tombstone` (GET/POST/GET/PUT/DELETE) — with
operation-ID stems built from an optional `Bff` prefix, the PascalCase domain,
and the contract name. `buildComponents` emits the shared envelope every spec
carries: a `bearerAuth` HTTP-bearer-JWT scheme; four reusable parameters
(`SourceRecordIdParam`, `LimitParam` 1–250/default 50, `CursorParam`,
`IncludeTombstonesParam`); six reusable error responses (`BadRequest`,
`Unauthorized`, `Forbidden`, `NotFound`, `Conflict`, `InternalError`); and three
fixed envelope schemas (`ErrorResponse`, `PageInfo`, `TombstoneResponse`) plus
the per-contract schemas. Each Zod schema is converted with
`z.toJSONSchema(schema, { io: 'input', unrepresentable: 'any' })` and then
recursively stripped of `$schema` keys so it embeds cleanly into `components`.
`buildBffSpec()` combines all six domains under their `bff-base-path` plus a
`/api/oshun/shared` group from the `cross-cutting` contracts, using
domain-prefixed schema names (`TaraRitualTemplate`, `SharedEvidencePack`) to
keep the combined namespace unambiguous.

---

## Tooling Layer (`scripts/`)

Eleven `tsx` scripts implement the validation and generation pipeline. They fall
into three roles: validators, change-detectors, and generators. Each of the five
canonical-source generators (`generate-calliope-spec`,
`generate-oshun-v1-specs`, `generate-oshun-v1-api-clients`, `generate-v3-spec`,
`generate-v3-clients`) has a `--check`/`:check` variant that rebuilds the
artifact in memory, compares it byte-for-byte to what is committed, and exits
non-zero on any difference — that is the mechanism that makes "single source of
truth" enforceable. (The umbrella `generate` and `generate-docs` orchestrators
run the generators but carry no `:check` gate of their own.)

**Validators.** `validate.ts` is a self-contained OpenAPI 3.1 structural
validator with zero external schema-library dependency — it field-checks
`openapi`/`info`, paths and parameters, response codes, schemas, security
schemes, and broken `#/components/schemas` refs, treating a duplicate
`operationId` as a hard error. (`validate` runs `--domain-only`; `validate:all`
covers every file.) `validate-v3-spec.ts` is a _semantic_ validator: it loads
`v3.yaml` and, for every contract in `V3_CONTRACT_REGISTRY`, asserts the
contract's fixture validates both as a `POST` request body and as a `200` `GET`
response body via the `oas3-validator` package. A separate Nx target,
`openapi:validate`, runs the external `@redocly/cli lint` over `src/specs/`.

**Change detectors.** `diff.ts` compares seven tracked domain specs against a
git base ref (`git show <base>:...`) and flags five breaking-change categories
(`path-removed`, `operation-removed`, `parameter-required`, `response-removed`,
and a reserved `schema-breaking`); path matching is template-aware, so renaming
a path parameter is _not_ breaking, and removing a non-`2xx` response is _not_
breaking. `drift-check.ts` exports a testable `detectDrift` that parses each
YAML into a `SpecModel` and the matching generated `.ts` (by regex) into the
same model, then reports any path / operationId / schema present in one but not
the other, plus a header check that the generated file still points at its
source YAML — across eleven `DOMAIN_MAPS` pairs.

**Generators.** `generate-oshun-v1-specs.ts` and
`generate-oshun-v1-api-clients.ts` drive the V1 spec builder;
`generate-calliope-spec.ts` builds `calliope-api.yaml` from `@calliope/core`;
`generate-v3-spec.ts` and `generate-v3-clients.ts` build `v3.yaml` and the four
V3 tenant clients from `@oshun/contracts/v3`; `generate.ts` is the umbrella
_type_ generator; `generate-docs.ts` builds the Redoc site.

### The umbrella `generate.ts` — and what it does _not_ do

`generate.ts` (the `generate` script / `openapi:gen` Nx target) runs, in order:
(1) `generate-oshun-v1-specs.ts`, (2) `generate-oshun-v1-api-clients.ts`, (3)
`generate-calliope-spec.ts`, then (4) `openapi-typescript` over every
`.yaml`/`.yml` in `src/specs/` and `v2/`, plus (5) the two `EXTERNAL_SPECS`
owned by other packages (`services/metis/openapi/metis.openapi.json` →
`metis.ts` and `apps/oshun/bff/openapi/oshun-bff.openapi.yaml` → `oshun-bff.ts`;
a missing external spec is skipped with a warning). Output naming: a `v2/` spec
becomes `v2-<name>.ts`; a `<name>-api.yaml` becomes `<dirname>.ts`; otherwise
the base name is kept.

The non-obvious gotcha: **`generate.ts` does not run the V3 generators.** V3
spec and client generation is a separate `generate:v3`
(`generate-v3-spec.ts && generate-v3-clients.ts`). So `generate.ts` runs
`openapi-typescript` over whatever `v3.yaml` is currently on disk — a full
regeneration from scratch requires running `generate:v3` _and_ `generate`. This
ordering matters when changing V3 contracts.

---

## Artifacts and Their Authoring Provenance

The `src/specs/` tree holds one spec per implemented domain (there are **no**
per-feature files like `lilith/chat.yaml` — those keys in `SPEC_PATHS` are
forward declarations, see below). Specs fall into three authoring categories,
and the distinction is load-bearing because the `*:check` gates only police the
generated ones:

1. **Hand-authored YAML** — `main.yaml` (the legacy consolidated "AI Wisdom
   Platform API"), the seven `<domain>-api.yaml` files
   (lilith/yemaya/isis/sophia/hathor/bellona/nyx),
   `concordia/concordia-api.yaml` (Phase 179, 11 operations over the
   cooperative-mediation lifecycle), and `v2/companion.yaml`. Edited directly;
   policed only by the structural validator.
2. **Generated by the V1 spec builder** — the six `<domain>-v1-contracts.yaml`
   files plus `oshun-bff-v1-contracts.yaml`, each carrying the
   `# This file is generated by …generate-oshun-v1-specs.ts.` header.
3. **Generated by dedicated generators** — `calliope-api.yaml` and `v3.yaml`,
   each with its own "do not edit" header.

The generated TypeScript lives in `src/generated/` (one module per spec, plus a
barrel `index.ts` that `export * as <domain>` and aliases each `paths` type as
`<Domain>Paths`). The generated V3 clients live in `src/v3-clients/`: a shared
`base.ts` (the `V3Fetch` transport, `V3ClientOptions`, the `V3ApiError` class
carrying `status`/`code`/`requestId`/`body`, and `v3JsonRequest<T>` which falls
back to `globalThis.fetch`, sets the JSON headers, and throws `V3ApiError`
including the `x-request-id` header on non-OK), plus four tenant modules
(`lilith-platform`, `tara-studio`, `saraswati-stage`, `lilith-commons`), each
exposing a `get<Contract>(id)` / `create<Contract>(input)` pair per V3 contract.

---

## Invariants, Failure Modes, and Extension Points

**The single-source-of-truth invariant.** A committed generated artifact must be
byte-identical to a fresh regeneration from its canonical Zod source. This is
the whole point of the `*:check` mode: `generate-v3-spec.ts --check`, for
example, re-renders the document and `throw`s "V3 OpenAPI spec is out of date"
if disk differs. The gate scripts intended to enforce the domain's health are
`validate:all`, `diff:check`, `drift:check`, the four `generate:*:check` gates
(`calliope`, `oshun-v1-specs`, `oshun-v1-clients`, `v3` — the last being
`v3-spec:check && v3-clients:check`), and `validate:v3`. Each
rebuilds-and-diffs; none writes files. (These are the gate _scripts_; wiring
them into a specific CI workflow is by convention — treat the scripts, not a
particular pipeline file, as the contract.)

**Common failure modes.**

- _Drift_ — editing a YAML spec without regenerating its `.ts` (or vice versa):
  caught by `drift:check`. The most common cause is hand-editing a generated
  spec, which both `drift:check` and the relevant `generate:*:check` reject.
- _Silent breakage_ — removing a path, operation, or `2xx` response, or making a
  parameter required: caught by `diff:check` against the base ref.
- _Broken refs / duplicate operation IDs_ — caught by the structural validator.
- _Forgetting the two-step V3 regeneration_ — because `generate.ts` consumes the
  on-disk `v3.yaml`, a stale V3 spec silently produces stale `v3` types unless
  `generate:v3` is run first.

**Forward-declared keys (planned, not implemented).** `SPEC_PATHS` declares many
fine-grained keys whose files do not exist on disk — the entire `lilith.*`,
`yemaya.*`, `isis.*` (fine-grained), `sophia.*`, `hathor.*`, `bellona.*`,
`nyx.objects/ephemeris/events/satellites`, and the whole `shared.*` group. The
`registry.ts` source comments this region "to be created during domain
migrations." Consequently, **seven `SPEC_REGISTRY` entries point at files that
do not yet exist**: `auth`/`users`/`health` (the `shared/*` specs) and
`nyx-objects`/`nyx-ephemeris`/`nyx-events`/`nyx-satellites`. These are
deliberate forward declarations that keep the typed key set and portal catalog
stable ahead of the files; the loader will resolve them once the YAML lands.
Conversely, `isis-api.yaml` exists on disk but is **not** registered, so
`getSpecsByDomain('isis')` correctly returns an empty array. Treat the registry
as a roadmap-aware catalog, not a guarantee that every entry has a backing file.

**`metis.api` is an external path.** `SPEC_PATHS.metis.api` points at
`services/metis/openapi/metis.openapi.json` — a JSON spec owned by the Metis
service, not a YAML under `src/specs/`. `generate.ts` reads it (and the BFF
external spec) via `EXTERNAL_SPECS`, skipping with a warning if absent.

**Extension points.** Adding a new V1 domain is data-driven: extend the
`@oshun/persistence` contracts and the `OSHUN_V1_API_DOMAINS` tuple, and the
builder produces the spec, the BFF paths, and the typed client automatically.
Adding a new V3 tenant means extending `@oshun/contracts/v3` and the
`TENANT_FILES` table in `generate-v3-clients.ts`. Adding a hand-authored domain
spec means dropping a `<domain>-api.yaml` under `src/specs/<domain>/`, adding a
`SPEC_REGISTRY` entry, a `drift-check.ts` `DOMAIN_MAPS` pair, and (if it should
be browsable) a `generate-docs.ts` `DOMAINS` entry.

---

## Cross-Domain Boundaries

The boundary is intentionally clean: this library governs the _shape and
contract_ of every API; the owning domain libraries govern the _behavior_. The
generated types flow outward to every domain API server, which implements its
routes to match — typically deriving route schemas from the same Zod schemas via
`@hono/zod-openapi` so the implementation and the contract share one source.

The one runtime contract is the **V2 Companion public API**. The `v2-companion`
spec is registered with `basePath: '/api/v2'` and resolved from
`libs/openapi/v2/companion.yaml`. In `apps/oshun/web`, the framework-agnostic
handler layer (`api/v2/public-api.ts`, defining `V2_PUBLIC_API_BASE_PATH` and
the read-only handlers) and the thin Next.js App Router adapters
(`src/app/api/v2/.../route.ts`) both pin to that document — the
`openapi/route.ts` adapter re-serves the exact spec via
`SPEC_PATHS.v2.companion` — so the published OpenAPI description and the live
endpoints cannot drift apart.

---

## Status Summary

Fully implemented and tested: the registry, loader, and V1 spec builder
(`src/`); all eleven tooling scripts and the five canonical-source `*:check`
gates (`scripts/`); the generated type modules (`src/generated/`); the generated
V3 tenant clients (`src/v3-clients/`); the hand-authored and generated spec
inventory (`src/specs/`, `v2/`); the committed Redoc docs (`docs/`, nine
domains); and three vitest suites (`src/openapi.spec.ts`,
`scripts/drift-check.spec.ts`, `src/v3-clients/__tests__/client.spec.ts`).

Planned / roadmap-only: the fine-grained per-feature spec files named by the
forward-declared `SPEC_PATHS` keys and the entire `shared.*` group, including
the seven `SPEC_REGISTRY` entries (`auth`, `users`, `health`, and the four
`nyx-*` catalog/ephemeris/events/satellites specs) whose YAML files are not yet
on disk. These are honest forward declarations carried to stabilize the typed
catalog ahead of the domain migrations that will land the files — not
implemented endpoints.
