# OpenAPI Domain — Technical Specifications

> Technical specification for `@oshun/openapi`: the centralized OpenAPI 3.1
> specification registry, spec loaders, validators, breaking-change and drift
> detectors, code generators, typed API clients, and the per-domain spec-file
> inventory. Status: **IMPLEMENTED** — this document is grounded in the source
> under `libs/openapi/`.

---

## Scope and Status

The openapi domain is the `@oshun/openapi` library at `libs/openapi/`. It is a
build-time / CI tooling library (`package.json` declares `"private": true`,
version `0.1.0`, `"type": "module"`). It is not loaded by production services at
runtime — domain API servers consume the generated TypeScript types and the
generated typed clients, not the YAML spec files.

The domain has three concerns:

1. **Spec storage** — OpenAPI 3.1 YAML spec files under `src/specs/`, one per
   domain, plus the V2 companion spec under `v2/`.
2. **A typed registry** — `SPEC_PATHS`, `SPEC_REGISTRY`, and the `ApiDomain`
   union in `src/utils/registry.ts` catalog every spec with metadata.
3. **Tooling** — fourteen `tsx` scripts under `scripts/` that validate specs,
   detect breaking changes, detect spec-to-generated-type drift, generate
   TypeScript types, generate three families of specs from canonical Zod
   contracts, generate typed API clients, and build HTML documentation.

A second, unrelated directory exists at the repository root: `openapi/`. It
holds two legacy standalone spec files (`openapi/isis/openapi.yaml`,
`openapi/lilith/openapi.yaml`) plus two `.backup-*` files for the Lilith spec.
These root-level files are not referenced by `@oshun/openapi`'s registry,
loaders, or scripts; the canonical specs all live under
`libs/openapi/src/specs/`.

---

## Package Structure

The annotated tree below shows every file of significance. The `scripts/`
directory contains fourteen `tsx` scripts that can each be run directly via
`pnpm run <script-name>`; the `src/` directory holds the library code that
scripts and consumers import.

```
libs/openapi/
├── package.json                 # @oshun/openapi, v0.1.0, private, ESM
├── project.json                 # Nx project: build + lint + test + 9 nx:run-commands targets
├── tsconfig.json                # base config, references lib + spec tsconfigs
├── tsconfig.lib.json             # build config, excludes specs/tests/generated
├── tsconfig.spec.json            # test config
├── vitest.config.ts              # vitest: node env, src/** + scripts/** spec globs, v8 coverage
├── nyx/
│   └── README.md                 # pointer doc for the Phase 21 Nyx API contract
├── v2/
│   └── companion.yaml            # V2 Companion Public API spec (served outside src/specs)
├── docs/                         # generated HTML docs output (9 domain subdirs + index.html)
├── scripts/                      # 14 tsx tooling scripts
│   ├── validate.ts               # OpenAPI 3.1 structural validator
│   ├── diff.ts                   # breaking-change detector vs a git base ref
│   ├── drift-check.ts            # YAML ↔ generated-TS drift detector (exports detectDrift)
│   ├── drift-check.spec.ts       # vitest suite for detectDrift
│   ├── generate.ts               # orchestrating type generator (runs all sub-generators)
│   ├── generate-docs.ts          # Redoc HTML documentation generator
│   ├── generate-calliope-spec.ts # Calliope spec generator from @calliope/core Zod schemas
│   ├── generate-oshun-v1-specs.ts    # Oshun V1 domain + BFF spec generator from Zod contracts
│   ├── generate-oshun-v1-api-clients.ts # Oshun V1 typed client package generator
│   ├── generate-v3-spec.ts       # V3 spec generator from @oshun/contracts/v3
│   ├── generate-v3-clients.ts    # V3 typed tenant client generator
│   └── validate-v3-spec.ts       # V3 spec validation via oas3-validator
└── src/
    ├── index.ts                  # public API barrel
    ├── openapi.spec.ts           # vitest suite for registry + loader + generators
    ├── utils/
    │   ├── loader.ts             # load / merge / resolve spec files
    │   └── registry.ts           # SpecMetadata, ApiDomain, SPEC_PATHS, SPEC_REGISTRY, helpers
    ├── oshun-v1/
    │   └── spec-builder.ts       # builds Oshun V1 specs + client resources from Zod contracts
    ├── v3-clients/               # generated V3 typed clients (do not edit by hand)
    │   ├── index.ts
    │   ├── base.ts               # V3 fetch transport + V3ApiError
    │   ├── lilith-platform.ts
    │   ├── tara-studio.ts
    │   ├── saraswati-stage.ts
    │   ├── lilith-commons.ts
    │   └── __tests__/client.spec.ts
    ├── generated/                # generated TypeScript types (do not edit by hand)
    │   ├── index.ts              # re-exports all domain type modules
    │   ├── lilith.ts   yemaya.ts   isis.ts     sophia.ts   hathor.ts
    │   ├── bellona.ts  nyx.ts      calliope.ts concordia.ts metis.ts
    │   ├── oshun-bff.ts  main.ts
    └── specs/                    # OpenAPI 3.1 YAML spec files
        ├── main.yaml             # legacy consolidated "AI Wisdom Platform API" spec
        ├── v3.yaml               # generated V3 contract spec
        ├── lilith/lilith-api.yaml
        ├── yemaya/yemaya-api.yaml
        ├── isis/isis-api.yaml
        ├── sophia/sophia-api.yaml
        ├── hathor/hathor-api.yaml
        ├── bellona/bellona-api.yaml
        ├── nyx/nyx-api.yaml
        ├── nyx/nyx-v1-contracts.yaml
        ├── calliope/calliope-api.yaml
        ├── concordia/concordia-api.yaml
        ├── tara/tara-v1-contracts.yaml
        ├── arete/arete-v1-contracts.yaml
        ├── veritas/veritas-v1-contracts.yaml
        ├── nisaba/nisaba-v1-contracts.yaml
        ├── metis/metis-v1-contracts.yaml
        └── oshun-bff/oshun-bff-v1-contracts.yaml
```

Note: the `src/specs/` directory does **not** contain per-feature spec files
like `lilith/chat.yaml`, `yemaya/projects.yaml`, `isis/generation.yaml`, or
`sophia/documents.yaml`. Each implemented domain has a single
`<domain>-api.yaml` spec; the Oshun V1 domains additionally have a generated
`<domain>-v1-contracts.yaml`. The `SPEC_PATHS` constant declares several
fine-grained and `shared/*` keys (see below) that have no backing file on disk
yet — those are forward declarations, not present specs.

---

## Public API

`src/index.ts` exports three groups of symbols. Everything a downstream consumer
or script needs comes through this barrel — direct imports into `src/utils/` or
`src/oshun-v1/` are possible but not required.

### Loader utilities (from `./utils/loader`)

These five functions handle all file I/O for spec documents. Consumers never
need to read raw YAML themselves.

| Export         | Kind     | Purpose                                                          |
| -------------- | -------- | ---------------------------------------------------------------- |
| `loadSpec`     | function | Async-read a YAML spec and parse it to an `OpenAPIV3_1.Document` |
| `loadSpecSync` | function | Synchronous variant for build scripts and generators             |
| `getSpecPath`  | function | Resolve a relative spec path to an absolute filesystem path      |
| `listSpecs`    | function | Recursively list all `.yaml`/`.yml` spec files                   |
| `mergeSpecs`   | function | Merge a base document with additional documents into one         |

### Registry and metadata (from `./utils/registry`)

These exports give programmatic access to the spec catalog. The two `const`
objects are the primary sources of truth; the functions are convenience wrappers
over them.

| Export             | Kind          | Purpose                                                    |
| ------------------ | ------------- | ---------------------------------------------------------- |
| `SPEC_PATHS`       | const object  | Typed map (`as const`) of every spec file's relative path  |
| `SPEC_REGISTRY`    | const object  | `Record<string, SpecMetadata>` of every registered spec    |
| `getSpecsByDomain` | function      | Filter `SPEC_REGISTRY` values by `ApiDomain`               |
| `getSpecsByTag`    | function      | Filter `SPEC_REGISTRY` values by membership in `tags`      |
| `getDomainPaths`   | function      | Return all relative spec paths for a `SPEC_PATHS` key      |
| `extractEndpoints` | function      | Flatten a document's `paths` into endpoint descriptors     |
| `extractSchemas`   | function      | Flatten a document's `components.schemas` into descriptors |
| `SpecMetadata`     | type (export) | Per-spec metadata interface                                |
| `ApiDomain`        | type (export) | Union of all Oshun API domain identifiers                  |

### Oshun V1 spec builder (from `./oshun-v1/spec-builder`)

These exports are used exclusively by the generation scripts. Application code
does not call them at runtime.

| Export                      | Kind          | Purpose                                                  |
| --------------------------- | ------------- | -------------------------------------------------------- |
| `buildOshunV1SpecArtifacts` | function      | Build all V1 domain-service specs plus the BFF spec      |
| `getOshunV1ApiDomains`      | function      | Return the frozen list of the six V1 API domains         |
| `getOshunV1ClientResources` | function      | Derive typed-client resource descriptors for a V1 domain |
| `getOshunV1DomainSpecPath`  | function      | Return the relative spec path for a V1 domain            |
| `getOshunV1SpecFilePaths`   | function      | Return the relative spec paths of every V1 artifact      |
| `JsonObject`                | type (export) | Recursive plain-JSON object type used for spec documents |
| `OshunV1ApiDomain`          | type (export) | Union of the six V1 API domains                          |
| `OshunV1ClientResource`     | type (export) | A single REST resource a generated V1 client exposes     |
| `OshunV1SpecArtifact`       | type (export) | A generated spec: `id`, `filePath`, `title`, `document`  |

### Re-exported third-party types

`OpenAPIV3_1`, `OpenAPIV3`, `OpenAPI`, and `IJsonSchema` are re-exported from
the `openapi-types` package for consumer convenience.

Beyond the barrel, `package.json` declares additional subpath exports:
`./generated` and `./generated/*` (the generated type modules), `./v3-clients`
and `./v3-clients/*` (the generated V3 clients), `./v2/*` (the raw V2 YAML), and
`./specs/*` (the raw domain YAML files).

---

## `SpecMetadata` Type

Every entry in `SPEC_REGISTRY` conforms to this interface, defined in
`src/utils/registry.ts`. The fields together provide everything needed to
describe an API in a developer portal or to locate its spec file on disk.

| Field         | Type        | Required | Meaning                                                 |
| ------------- | ----------- | -------- | ------------------------------------------------------- |
| `name`        | `string`    | yes      | Human-readable display name of the API                  |
| `version`     | `string`    | yes      | Spec version string (e.g. `0.1.0`, `1.0.0`, `3.0.0`)    |
| `description` | `string`    | yes      | One-line summary of the API surface                     |
| `specPath`    | `string`    | yes      | Relative path to the spec file (a `SPEC_PATHS` value)   |
| `domain`      | `ApiDomain` | yes      | Owning domain identifier                                |
| `tags`        | `string[]`  | yes      | Free-form classification tags for cross-cutting queries |
| `basePath`    | `string`    | yes      | URL base path the API is mounted under                  |
| `deprecated`  | `boolean`   | no       | Marks a spec as deprecated; omitted when not deprecated |

---

## `ApiDomain` Union

`ApiDomain` is a TypeScript string-literal union — not a runtime enum. It
enumerates every Oshun API domain that can own a spec. Using a union (rather
than an enum) means TypeScript checks domain strings at compile time without any
runtime overhead. Each member carries an inline comment in the source explaining
its scope:

| Value       | Scope                                                               |
| ----------- | ------------------------------------------------------------------- |
| `tara`      | Meditation, ritual, and guided-session APIs                         |
| `arete`     | Habits, goals, routines, and coaching APIs                          |
| `veritas`   | Stories, claims, sources, and evidence APIs                         |
| `lilith`    | Consciousness Experience — end-user APIs                            |
| `yemaya`    | Creative Studio — production APIs                                   |
| `isis`      | Generative Factory — generation APIs                                |
| `sophia`    | Knowledge Engine — search / research APIs                           |
| `hathor`    | World Builder — worldbuilding APIs                                  |
| `bellona`   | Engine Bridge — engine integration APIs                             |
| `calliope`  | AI artist lifecycle and creative management                         |
| `nyx`       | Astronomy — celestial objects, ephemeris, events, satellites        |
| `nisaba`    | Passages, manuscripts, annotations, notebooks, and study APIs       |
| `metis`     | Educational platform — learner, admin, tutoring, assessment, build  |
| `v2`        | V2 fighting-game companion and public API surface                   |
| `v3`        | V3 Lilith metaverse, Tara Studio, Saraswati Stage, and Commons APIs |
| `oshun-bff` | Oshun backend-for-frontend APIs                                     |
| `shared`    | Cross-domain shared APIs                                            |

---

## `SPEC_PATHS` Reference

`SPEC_PATHS` is declared `as const`, so its keys and string values are typed
literals — this prevents path typos in loaders, validators, and generators.
Paths are relative to `src/specs/` (except `v2/companion.yaml`, which the loader
resolves against `libs/openapi/v2/`, and `metis.api`, which points at an
external repository path). The object is intentionally structured to mirror the
domain hierarchy: top-level keys are domain names, nested objects hold
per-feature or per-contract keys.

```typescript
const SPEC_PATHS = {
  main: 'main.yaml',
  lilith: { chat, lectures, tts, consciousness, mythology, webrtc },
  yemaya: { projects, assets, collaboration, members, reviews },
  isis: { generation, workflows, models, jobs },
  sophia: { documents, search, knowledge, citations },
  hathor: { worlds, elements, narratives, simulations },
  bellona: { sessions, builds, exports },
  calliope: { api: 'calliope/calliope-api.yaml' },
  tara: { v1Contracts: 'tara/tara-v1-contracts.yaml' },
  arete: { v1Contracts: 'arete/arete-v1-contracts.yaml' },
  veritas: { v1Contracts: 'veritas/veritas-v1-contracts.yaml' },
  nyx: {
    api: 'nyx/nyx-api.yaml',
    v1Contracts: 'nyx/nyx-v1-contracts.yaml',
    objects,
    ephemeris,
    events,
    satellites,
  },
  nisaba: { v1Contracts: 'nisaba/nisaba-v1-contracts.yaml' },
  metis: {
    api: 'services/metis/openapi/metis.openapi.json',
    v1Contracts: 'metis/metis-v1-contracts.yaml',
  },
  v2: { companion: 'v2/companion.yaml' },
  v3: { contracts: 'v3.yaml' },
  oshunBff: { v1Contracts: 'oshun-bff/oshun-bff-v1-contracts.yaml' },
  shared: { auth, users, health, notifications },
} as const;
```

Two important facts about `SPEC_PATHS`:

- **Forward-declared keys.** The fine-grained `lilith.*`, `yemaya.*`, `isis.*`,
  `sophia.*` (including `citations`), `hathor.*`, `bellona.*`,
  `nyx.objects/ephemeris/events/satellites`, and the entire `shared.*` group
  name files that are not present on disk. The `registry.ts` source comments
  this region "to be created during domain migrations." They keep the typed key
  set stable for tools that will consume those specs once the files exist.
- **`metis.api` is an external path.** It points at
  `services/metis/openapi/metis.openapi.json` — a ~1 MB JSON spec owned by the
  Metis service, not a YAML file under `src/specs/`. The type generator reads it
  as an external spec (see Code Generation).

---

## `SPEC_REGISTRY` Reference

`SPEC_REGISTRY` is a `Record<string, SpecMetadata>`. It currently registers 20
specs, each identified by a short string key. The table below lists every
registry key with its core metadata fields; the tags section below the table
explains how `getSpecsByTag` groups them.

| Key                | `name`                    | `domain`    | `version` | `basePath`           |
| ------------------ | ------------------------- | ----------- | --------- | -------------------- |
| `main`             | Oshun Platform API        | `lilith`    | `0.1.0`   | `/v1`                |
| `auth`             | Authentication API        | `shared`    | `0.1.0`   | `/v1/auth`           |
| `users`            | Users API                 | `shared`    | `0.1.0`   | `/v1/users`          |
| `health`           | Health API                | `shared`    | `0.1.0`   | `/v1/health`         |
| `v3-contracts`     | Oshun V3 Contract API     | `v3`        | `3.0.0`   | `/api/v3`            |
| `calliope`         | Calliope API              | `calliope`  | `1.0.0`   | `/v1`                |
| `oshun-v1-tara`    | Tara V1 Contract API      | `tara`      | `1.0.0`   | `/api/v1/tara`       |
| `oshun-v1-arete`   | Arete V1 Contract API     | `arete`     | `1.0.0`   | `/api/v1/arete`      |
| `oshun-v1-veritas` | Veritas V1 Contract API   | `veritas`   | `1.0.0`   | `/api/v1/veritas`    |
| `nyx`              | Nyx Astronomy API         | `nyx`       | `1.0.0`   | `/api/v1`            |
| `oshun-v1-nyx`     | Nyx V1 Contract API       | `nyx`       | `1.0.0`   | `/api/v1/nyx`        |
| `nyx-objects`      | Celestial Objects API     | `nyx`       | `1.0.0`   | `/api/v1/objects`    |
| `nyx-ephemeris`    | Ephemeris API             | `nyx`       | `1.0.0`   | `/api/v1/ephemeris`  |
| `nyx-events`       | Astronomical Events API   | `nyx`       | `1.0.0`   | `/api/v1/events`     |
| `nyx-satellites`   | Satellite Tracking API    | `nyx`       | `1.0.0`   | `/api/v1/satellites` |
| `oshun-v1-nisaba`  | Nisaba V1 Contract API    | `nisaba`    | `1.0.0`   | `/api/v1/nisaba`     |
| `metis`            | Metis API                 | `metis`     | `0.1.0`   | `/api`               |
| `oshun-v1-metis`   | Metis V1 Contract API     | `metis`     | `1.0.0`   | `/api/v1/metis`      |
| `v2-companion`     | V2 Companion Public API   | `v2`        | `0.1.0`   | `/api/v2`            |
| `oshun-v1-bff`     | Oshun BFF V1 Contract API | `oshun-bff` | `1.0.0`   | `/api/oshun`         |

Tags drive cross-cutting queries via `getSpecsByTag`. Each spec can belong to
multiple tag groups; the tag values are free-form strings:

- `main`: `core`, `platform`
- `auth`: `auth`, `security`; `users`: `users`, `profiles`; `health`: `health`,
  `observability`
- `v3-contracts`: `v3`, `contracts`, `lilith`, `tara`, `saraswati`, `commons`
- `calliope`: `artist`, `music`, `social`, `concerts`, `analytics`
- The `oshun-v1-*` specs all carry `oshun-v1` and `contracts` plus a domain tag
  and a theme tag (`tara`+`rituals`, `arete`+`coaching`, `veritas`+`evidence`,
  `nyx`+`astronomy`, `nisaba`+`study`, `metis`+`learning`, `bff`).
- `nyx`: `astronomy`, `ephemeris`, `satellites`, `celestial`; `nyx-objects`:
  `astronomy`, `objects`, `catalog`; `nyx-ephemeris`: `astronomy`, `ephemeris`,
  `calculations`; `nyx-events`: `astronomy`, `events`, `calendar`;
  `nyx-satellites`: `satellites`, `tracking`, `iss`
- `metis`: `education`, `tutoring`, `assessment`, `admin`, `analytics`, `build`
- `v2-companion`: `v2`, `companion`, `public-api`, `replays`, `notifications`

No registry entry currently sets `deprecated`.

---

## Registry Helper Functions

All four helpers live in `src/utils/registry.ts` and provide typed access to the
registry without callers needing to iterate `SPEC_REGISTRY` or `SPEC_PATHS`
directly.

### `getSpecsByDomain(domain: ApiDomain): SpecMetadata[]`

Returns every `SPEC_REGISTRY` value whose `domain` equals the argument. Used by
the developer portal to build per-domain documentation sections. Domains with no
registered spec (for example `isis` — its `isis-api.yaml` exists on disk but is
not registered) correctly return an empty array.

### `getSpecsByTag(tag: string): SpecMetadata[]`

Returns every `SPEC_REGISTRY` value whose `tags` array contains the argument.
Enables cross-cutting views such as "every `contracts` spec" or "every spec
tagged `astronomy`."

### `getDomainPaths(domain: keyof typeof SPEC_PATHS): string[]`

Returns all relative spec paths under a `SPEC_PATHS` key. If the key maps to a
plain string (e.g. `main`), it returns a single-element array; if it maps to a
nested object, it returns `Object.values()` of that object.

### `extractEndpoints(spec: OpenAPIV3_1.Document)`

Walks `spec.paths`. For each path item and each of the seven HTTP methods
(`get`, `post`, `put`, `patch`, `delete`, `head`, `options`), it emits one
descriptor. Path items that are null/undefined are skipped. Used by the
breaking-change detector and by portal endpoint indexes.

```typescript
{ method: string;        // uppercased, e.g. "GET"
  path: string;          // the path template
  operationId?: string;
  tags?: string[];
  summary?: string; }
```

### `extractSchemas(spec: OpenAPIV3_1.Document)`

Walks `spec.components.schemas`. For each schema it emits a descriptor. The
`type` handling explicitly accounts for OpenAPI 3.1 allowing `type` to be a
string array (e.g. `["string", "null"]`), joining array members with `|`.

```typescript
{ name: string;
  type: string;          // OpenAPI 3.1 type, joined with "|" if an array;
                         // defaults to "object" when type is absent
  description?: string; }
```

---

## Spec Loader (`src/utils/loader.ts`)

The loader resolves spec paths against two roots, keeping the calling code free
of path-construction logic:

- `SPECS_DIR` — `libs/openapi/src/specs/` (the default root)
- `V2_SPECS_DIR` — `libs/openapi/v2/` (for `v2`-prefixed paths)

### Path resolution — `resolveSpecFilePath`

The internal resolver applies three rules: absolute paths pass through
unchanged; a path equal to `v2` or starting with `v2/` resolves against the
package root (`libs/openapi/`); anything else resolves against `SPECS_DIR`.

### `loadSpec(specPath): Promise<OpenAPIV3_1.Document>`

Asynchronously reads the resolved file UTF-8 and parses it with the `yaml`
package's `YAML.parse`, returning the result cast to `OpenAPIV3_1.Document`.

### `loadSpecSync(specPath): OpenAPIV3_1.Document`

Same as `loadSpec` using `fs.readFileSync` — for build scripts and generators
that need blocking I/O.

### `getSpecPath(relativePath): string`

Public wrapper over `resolveSpecFilePath`; returns the absolute filesystem path
for a relative spec path.

### `listSpecs(): Promise<string[]>`

Recursively scans `SPECS_DIR` for `.yaml`/`.yml` files, returning each path
relative to `SPECS_DIR`. It then separately scans `V2_SPECS_DIR` and appends
those files with a `v2/`-prefixed relative path. Both roots are guarded with
`fs.existsSync`.

### `mergeSpecs(base, ...specs): OpenAPIV3_1.Document`

Merges a base document with any number of additional documents into a single
unified document. This is the mechanism for producing a combined API reference
when the developer portal needs all domains browsable in one document. Merge
semantics:

- **`paths`** — shallow-merged; a later spec's path entry overrides an earlier
  one of the same key.
- **`components`** — five sub-maps are individually shallow-merged: `schemas`,
  `responses`, `parameters`, `requestBodies`, `securitySchemes`.
- **`tags`** — deduplicated by `tag.name`; a tag whose name is already present
  is not re-added.

---

## Domain Spec-File Inventory

The `src/specs/` directory holds the OpenAPI 3.1 YAML files below. Endpoint and
schema counts are taken from the actual files; all are `openapi: 3.1.0`. The
counts for generated specs show "(Zod-generated)" in the schema column because
those schemas are derived programmatically from Zod contracts and therefore do
not have a meaningful fixed count to cite here — they grow as contracts are
added.

| Spec file                               | `info.title`                                 | Version | Endpoints (path entries) | Component schemas |
| --------------------------------------- | -------------------------------------------- | ------- | ------------------------ | ----------------- |
| `main.yaml`                             | AI Wisdom Platform API                       | `0.1.0` | ~694                     | ~993              |
| `v3.yaml`                               | Oshun V3 Contract API                        | `3.0.0` | ~60                      | ~31               |
| `lilith/lilith-api.yaml`                | Lilith API — Consciousness Experience        | `1.0.0` | ~12                      | ~25               |
| `yemaya/yemaya-api.yaml`                | Yemaya API — Creative Studio                 | `1.0.0` | ~10                      | ~24               |
| `isis/isis-api.yaml`                    | Isis API — Generative Factory                | `1.0.0` | ~17                      | ~34               |
| `sophia/sophia-api.yaml`                | Sophia API — Knowledge Engine                | `1.0.0` | ~45                      | ~61               |
| `hathor/hathor-api.yaml`                | Hathor API — Worldbuilding & Narrative       | `1.0.0` | ~126                     | ~65               |
| `bellona/bellona-api.yaml`              | Bellona API — Engine Bridge                  | `1.0.0` | ~23                      | ~32               |
| `nyx/nyx-api.yaml`                      | Nyx Astronomy API                            | `1.0.0` | ~15                      | ~23               |
| `calliope/calliope-api.yaml`            | Calliope API — Autonomous AI Artist Platform | `1.0.0` | ~13                      | (Zod-generated)   |
| `concordia/concordia-api.yaml`          | Concordia API — Cooperative Mediation        | `0.1.0` | ~11                      | ~52               |
| `tara/tara-v1-contracts.yaml`           | Tara V1 Contract API                         | `1.0.0` | ~12                      | (Zod-generated)   |
| `arete/arete-v1-contracts.yaml`         | Arete V1 Contract API                        | `1.0.0` | ~20                      | (Zod-generated)   |
| `veritas/veritas-v1-contracts.yaml`     | Veritas V1 Contract API                      | `1.0.0` | ~20                      | (Zod-generated)   |
| `nyx/nyx-v1-contracts.yaml`             | Nyx V1 Contract API                          | `1.0.0` | ~8                       | (Zod-generated)   |
| `nisaba/nisaba-v1-contracts.yaml`       | Nisaba V1 Contract API                       | `1.0.0` | ~26                      | (Zod-generated)   |
| `metis/metis-v1-contracts.yaml`         | Metis V1 Contract API                        | `1.0.0` | ~22                      | (Zod-generated)   |
| `oshun-bff/oshun-bff-v1-contracts.yaml` | Oshun BFF V1 Contract API                    | `1.0.0` | ~148                     | (Zod-generated)   |
| `v2/companion.yaml`                     | V2 Companion Public API                      | `0.1.0` | ~12                      | (hand-authored)   |

The specs fall into three authoring categories:

1. **Hand-authored YAML** — `main.yaml`, the seven `<domain>-api.yaml` files for
   lilith/yemaya/isis/sophia/hathor/bellona/nyx, `concordia-api.yaml`, and
   `v2/companion.yaml`. These are edited directly.
2. **Generated from Zod via the V1 spec builder** — the six
   `<domain>-v1-contracts.yaml` files plus `oshun-bff-v1-contracts.yaml`. Each
   carries the header
   `# This file is generated by libs/openapi/scripts/generate-oshun-v1-specs.ts.`
3. **Generated from Zod via dedicated generators** — `calliope-api.yaml` (header
   `# This file is auto-generated by libs/openapi/scripts/generate-calliope-spec.ts`)
   and `v3.yaml` (header
   `# Generated by libs/openapi/scripts/generate-v3-spec.ts. Do not edit by hand.`).

### Concordia spec endpoints

`concordia/concordia-api.yaml` (Phase 179) is hand-authored at version `0.1.0`.
Its base path is `/v1/concordia` and it defines 11 path entries / 11 operations
covering the full cooperative mediation lifecycle from case creation to audit
export:

| Path                                              | Operation               |
| ------------------------------------------------- | ----------------------- |
| `/v1/concordia/cases`                             | `createConcordiaCase`   |
| `/v1/concordia/cases/{caseId}/parties`            | `addConcordiaParty`     |
| `/v1/concordia/cases/{caseId}/intake`             | `submitConcordiaIntake` |
| `/v1/concordia/cases/{caseId}/issues`             | `createConcordiaIssue`  |
| `/v1/concordia/cases/{caseId}/preference-queries` | `createPreferenceQuery` |
| `/v1/concordia/cases/{caseId}/search-runs`        | `startSearchRun`        |
| `/v1/concordia/cases/{caseId}/pareto-frontier`    | `getParetoFrontier`     |
| `/v1/concordia/cases/{caseId}/drafts`             | `createSettlementDraft` |
| `/v1/concordia/cases/{caseId}/reviews`            | `createReviewDecision`  |
| `/v1/concordia/cases/{caseId}/execute`            | `executeSettlement`     |
| `/v1/concordia/audit/{caseId}`                    | `getCaseAudit`          |

Its tag set is `Cases`, `Intake`, `Search`, `Drafts`, `Review`, `Execute`,
`Audit`, covering case intake, party-isolated preference inference, Pareto
search, settlement drafting, human review, settlement execution, and audit
export.

---

## Oshun V1 Spec Builder (`src/oshun-v1/spec-builder.ts`)

This module deterministically generates OpenAPI 3.1 documents and typed-client
descriptors directly from the canonical Zod contracts in `@oshun/persistence`,
combined with domain metadata from `@oshun/domain-registry`. It is the single
source of the V1 contract specs and clients. The separation between this module
and the scripts that call it means the generation logic is independently
testable.

### Inputs

The builder draws from three workspace libraries:

- `V1_OBJECT_PERSISTENCE_CONTRACTS` and `V1_ENUM_PERSISTENCE_CONTRACTS` from
  `@oshun/persistence` — arrays of `ObjectPersistenceContract` /
  `EnumPersistenceContract`, each carrying `kind`, `domain`, `contractName`,
  `schemaName`, `modelName`, `tableName`, and a Zod `schema`.
- `DOMAIN_REGISTRY` from `@oshun/domain-registry` — supplies each domain's
  `displayName`, `shellNarrative.summary`, and `bff-base-path`.
- `ContractPersistenceDomain` — the union
  `tara | arete | veritas | nyx | nisaba | metis | cross-cutting`.

### Exported types

The builder exports four types that the generation scripts and the public barrel
re-export:

- **`OshunV1ApiDomain`** = `Exclude<ContractPersistenceDomain, 'cross-cutting'>`
  — the six domains that get their own service spec.
- **`OshunV1SpecArtifact`** —
  `{ id: string; filePath: string; title: string; document: JsonObject }`.
- **`OshunV1ClientResource`** —
  `{ domain: OshunV1ApiDomain; contractName: string; propertyName: string; schemaName: string; collectionPath: string; itemPath: string }`.
- **`JsonObject`** — a recursive plain-JSON object type.

### Constants

Three module-level constants control the shape of every generated document:

- `OSHUN_V1_API_DOMAINS` — the frozen tuple
  `['tara', 'arete', 'veritas', 'nyx', 'nisaba', 'metis']`.
- `CONTRACT_VERSION` — `'1.0.0'`, the `info.version` for every generated spec.
- `JSON_SCHEMA_DIALECT` — `'https://json-schema.org/draft/2020-12/schema'`,
  emitted as the document's `jsonSchemaDialect`.

### Exported functions

| Function                       | Returns                                                        |
| ------------------------------ | -------------------------------------------------------------- |
| `getOshunV1ApiDomains()`       | the six-element `OSHUN_V1_API_DOMAINS` tuple                   |
| `buildOshunV1SpecArtifacts()`  | one `OshunV1SpecArtifact` per domain plus one BFF artifact (7) |
| `getOshunV1SpecFilePaths()`    | the `filePath` of every artifact                               |
| `getOshunV1DomainSpecPath(d)`  | `` `${d}/${d}-v1-contracts.yaml` ``                            |
| `getOshunV1ClientResources(d)` | one `OshunV1ClientResource` per object contract in domain `d`  |

### Generated document shape

`buildDomainServiceSpec(domain)` produces one document per V1 domain.
`buildBffSpec()` produces the combined BFF document. Both documents share the
same component structure described in "Generated components" below.

Each **per-domain service spec** contains:

- `openapi: '3.1.0'`, `jsonSchemaDialect`, an `info` block titled
  `<DisplayName> V1 Contract API`.
- A single server whose `url` is `/api/v1/<domain>`.
- One tag named after the domain, described by the registry's
  `shellNarrative.summary`.
- Document-level security `[{ bearerAuth: [] }]`.
- `paths` and `components` generated by `buildContractPaths` and
  `buildComponents`.

The **BFF spec** combines all six domains into one document. For each domain it
generates paths under that domain's `bff-base-path` (e.g.
`/api/oshun/domains/tara`), plus a `/api/oshun/shared` group derived from the
`cross-cutting` contracts. The server `url` is `/api/oshun`. BFF schema names
are domain-prefixed (see component naming below).

### Generated paths per object contract — `buildContractPaths`

Each object contract yields a five-operation REST resource. The collection path
is `<basePath>/<pluralized-kebab contractName>`, and the item path appends
`/{sourceRecordId}`. Operation IDs are stems built from an optional `Bff`
prefix, the PascalCase domain, and the contract name (singular or pluralized):

| HTTP              | Operation ID stem | Summary                   | Success | Error responses (`$ref`)     |
| ----------------- | ----------------- | ------------------------- | ------- | ---------------------------- |
| `GET` collection  | `list<Plural>`    | List `<Contract>` records | `200`   | 401, 403, 500                |
| `POST` collection | `create<Stem>`    | Create `<Contract>`       | `201`   | 400, 401, 403, 409, 500      |
| `GET` item        | `get<Stem>`       | Get `<Contract>`          | `200`   | 401, 403, 404, 500           |
| `PUT` item        | `upsert<Stem>`    | Upsert `<Contract>`       | `200`   | 400, 401, 403, 404, 409, 500 |
| `DELETE` item     | `tombstone<Stem>` | Tombstone `<Contract>`    | `200`   | 401, 403, 404, 500           |

The collection `GET` accepts `LimitParam`, `CursorParam`, and
`IncludeTombstonesParam`; the item path declares `SourceRecordIdParam` at the
path level. `POST`/`PUT` request bodies reference the contract schema; the
`DELETE` response references `TombstoneResponse`.

### Generated components — `buildComponents`

Every generated document's `components` block is built from the same set of
reusable pieces, ensuring that every V1 domain spec and the BFF spec share the
same wire-format conventions for authentication, pagination, error handling, and
tombstoning:

- **`securitySchemes.bearerAuth`** — `type: http`, `scheme: bearer`,
  `bearerFormat: JWT`.
- **`parameters`** — four reusable parameters:
  - `SourceRecordIdParam` — required path string, `minLength: 1`.
  - `LimitParam` — optional query integer, `minimum 1`, `maximum 250`,
    `default 50`.
  - `CursorParam` — optional query string.
  - `IncludeTombstonesParam` — optional query boolean, `default false`.
- **`responses`** — six reusable error responses, each `application/json` with
  the `ErrorResponse` schema: `BadRequest`, `Unauthorized`, `Forbidden`,
  `NotFound`, `Conflict`, `InternalError`.
- **`schemas`** — three fixed envelope schemas plus per-contract schemas:
  - `ErrorResponse` — `object`, `additionalProperties: false`, required
    `error` + `message`, optional `requestId` and free-form `details`.
  - `PageInfo` — `object`, required `limit` (integer 1–250) + `hasMore`
    (boolean), optional `cursor` and `nextCursor`.
  - `TombstoneResponse` — `object`, required `sourceRecordId` + `tombstonedAt`
    (date-time), optional `tombstoneReason`.
  - For each object contract: `<SchemaName>` (the Zod schema converted to JSON
    Schema), `<SchemaName>ListResponse` (`{ data: [...], page: PageInfo }`), and
    `<SchemaName>MutationResponse` (`{ data: <SchemaName> }`).
  - For each enum contract: `<SchemaName>` (the Zod enum converted).

### Component naming — `componentSchemaName`

Schema name generation uses two modes to keep per-domain and BFF schemas
unambiguous:

- **`plain`** (used by per-domain service specs) — keeps `contractName`
  unchanged, e.g. `RitualTemplate`.
- **`prefixed`** (used by the BFF spec) — prepends the PascalCase domain name,
  e.g. `TaraRitualTemplate`, or uses `Shared` for `cross-cutting` contracts,
  e.g. `SharedEvidencePack`.

### Zod-to-JSON-Schema conversion — `zodToJsonSchema`

Each Zod schema is converted with
`z.toJSONSchema(schema, { io: 'input', unrepresentable: 'any' })`. The result is
post-processed by `stripSchemaDialect`, which recursively removes every
`$schema` key so the JSON Schema embeds cleanly into the OpenAPI `components`
block.

### Pluralization / casing helpers

The builder includes several string-transformation helpers to produce consistent
resource paths, operation IDs, and client property names:

`toPascalCase`, `toKebabCase`, `pluralizeKebab`, `pluralizePascal`,
`lowerFirst`, and `operationPrefix`. Pluralization handles the `y → ies` rule
(when not preceded by a vowel) and the `s/x/ch/sh → +es` rule.

---

## Validation — `scripts/validate.ts`

`validate.ts` is a self-contained OpenAPI 3.1 structural validator. It does not
depend on an external schema library; it implements field-level checks directly.
This means it can be fast and have zero external runtime dependencies beyond the
`yaml` package already used by the loaders. CLI flag: `--domain-only` restricts
scanning to the recognized domain directories.

### Files scanned

The validator recursively collects `.yaml`/`.yml` files from `src/specs/` and
from `v2/`. With `--domain-only`, at the root level it only descends into the
directories in `DOMAIN_DIRS` (`isis`, `sophia`, `hathor`, `bellona`, `yemaya`,
`lilith`, `calliope`, `nyx`, `concordia`, `tara`, `arete`, `veritas`, `nisaba`,
`metis`, `v2`, `oshun-bff`) and skips root-level spec files such as `main.yaml`
and `v3.yaml`.

### Checks performed

Each file is YAML-parsed (a parse failure is a hard error) and run through five
check groups, each accumulating `errors` (which make a spec invalid) and
`warnings` (advisory only):

**`validateRequiredFields`**

- `openapi` present and starting with `3.` (else error).
- `info` present, with `info.title` and `info.version` (errors); missing
  `info.description` is a warning.
- At least one of `paths` or `webhooks` is non-empty (OpenAPI 3.1 allows either)
  — else error.

**`validatePaths`**

- Each path key must start with `/`.
- Path-template parameters (`{name}`) must be unique within a path; duplicates
  are errors. Non-identifier parameter names are warnings.
- Per operation (the eight HTTP methods): missing `operationId` is a warning;
  non-identifier `operationId` is a warning; **duplicate `operationId` across
  the document is an error**. Missing `summary`/`description` and missing `tags`
  are warnings.
- `responses` must be present (error) with at least one `2xx` (warning if not).
  Response codes must be `default` or match `[1-5]\d\d` (else error).
- Every `{param}` in the path must be defined in path-level or operation-level
  `parameters`, including resolution of `$ref` parameters via
  `resolveParameterRef` — an undefined path parameter is an error.

**`validateSchemas`**

- A schema with no `type`, `allOf`, `oneOf`, `anyOf`, `$ref`, or `enum` is a
  warning.
- An `object` schema with neither `properties` nor `additionalProperties` is a
  warning.
- A `required` field name that is not present in `properties` is an error.

**`validateSecurity`**

- Missing `securitySchemes` is a warning.
- Any operation-level `security` requirement naming an undefined scheme is an
  error.

**`validateRefs`**

- Recursively walks the document; any `$ref` of the form
  `#/components/schemas/<Name>` whose `<Name>` is not a defined schema is an
  error (broken reference detection).

### Output and exit code

The validator prints a per-file report and a summary (total specs, valid,
invalid, total errors, total warnings). It exits `1` if any spec is invalid and
`0` otherwise. Empty path lists fail with "No paths or webhooks defined."

`npm` scripts: `validate` runs `validate.ts --domain-only`; `validate:all` runs
`validate.ts` over every file. The Nx target `openapi:validate` instead runs the
external `@redocly/cli lint` over `src/specs/`.

---

## Breaking-Change Detection — `scripts/diff.ts`

`diff.ts` compares the current specs against a base git ref to detect changes
that would break API consumers. It is the mechanism by which the platform
enforces backward compatibility on every PR.

### CLI

- `--base=<ref>` — the git reference to compare against; default `origin/main`.
- `--check` — fail (exit `1`) when breaking changes are found.

`npm` scripts: `diff` (report only) and `diff:check` (`--check`).

### Specs compared

A fixed list `DOMAIN_SPECS` of seven domain specs is compared:
`lilith/lilith-api.yaml`, `yemaya/yemaya-api.yaml`, `isis/isis-api.yaml`,
`sophia/sophia-api.yaml`, `hathor/hathor-api.yaml`, `bellona/bellona-api.yaml`,
`concordia/concordia-api.yaml`. For each, the base version is retrieved via
`git show <base>:libs/openapi/src/specs/<file>` and the current version from
disk. A spec that exists only on disk is reported as a new spec (no comparison);
a spec that exists only in the base is reported as an entirely removed spec (a
breaking change).

### Breaking changes detected

The `BreakingChange.type` union has five members. `detectBreakingChanges` flags
the following conditions — note that path comparison is template-aware (renaming
a path parameter does not count as a breaking change):

| `type`               | Condition                                                                      |
| -------------------- | ------------------------------------------------------------------------------ |
| `path-removed`       | a base path has no current match                                               |
| `operation-removed`  | a base operation (`get/post/put/patch/delete`) is gone                         |
| `parameter-required` | an optional parameter became required, **or** a required parameter was removed |
| `response-removed`   | a `2xx` success response code was removed from an operation                    |
| `schema-breaking`    | (declared in the union; reserved for schema-level breaks)                      |

Removal of non-`2xx` (error) response codes is explicitly _not_ treated as
breaking. `normalizePathTemplate` rewrites `{anything}` to `{}` and
`findPathByWireTemplate` matches paths whose only difference is parameter naming
— so renaming a path parameter is not flagged.

### Non-breaking changes

`detectNonBreakingChanges` reports newly added paths and newly added operations
on existing paths. These are surfaced in the report to give reviewers a complete
picture of what changed.

### Output and exit code

A per-spec report plus a summary (specs compared, new specs, removed specs,
total breaking and non-breaking changes). With `--check`, the script exits `1`
when any spec has breaking changes, and prints guidance to bump the major
version or run a deprecation period.

---

## Drift Detection — `scripts/drift-check.ts`

`drift-check.ts` detects the most common contract-drift symptom: a spec YAML was
edited but its generated TypeScript was not regenerated (or vice versa).
Catching this in CI prevents a scenario where the TypeScript types engineers
import no longer match the spec the API actually implements. The module exports
`detectDrift` and supporting types so it can be unit-tested;
`drift-check.spec.ts` is its vitest suite.

### CLI

- `--check` — exit `1` when drift is detected.
- `--json` — emit a machine-readable `DriftReport` instead of the human report.

`npm` scripts: `drift` (report) and `drift:check` (`--check`).

### Domain map

`DOMAIN_MAPS` pairs each generated TypeScript module with its source YAML (11
pairs): `lilith`, `yemaya`, `isis`, `sophia`, `hathor`, `bellona`, `concordia`,
`nyx` all map to `<domain>/<domain>-api.yaml`; `calliope` maps to
`calliope/calliope-api.yaml`; `metis` maps to `metis/metis-v1-contracts.yaml`;
`oshun-bff` maps to `oshun-bff/oshun-bff-v1-contracts.yaml`.

### Checks performed per domain

For each pair, the YAML is parsed into a `SpecModel` (`paths`, `operationId`s,
`components.schemas`) and the generated `.ts` is parsed by regex into the same
model. `detectDrift` then emits a `DriftEntry` for any of four categories:

1. **`paths`** — every YAML path must appear in the generated `paths` type. YAML
   `{param}` placeholders are normalized to `{string}` to correlate against what
   `openapi-typescript` emits as `` `${string}` ``. Both missing and extra paths
   are reported.
2. **`operations`** — every YAML `operationId` must appear as an
   `operations['<id>']` reference in the generated TS.
3. **`schemas`** — every YAML component schema must appear in the generated TS,
   either as `components['schemas']['<Name>']` or as a direct declaration inside
   the `schemas:` block.
4. **`header`** — the generated file must carry an `Auto-generated from: <yaml>`
   comment still pointing at the matching YAML; a missing or mismatched header
   is rename drift.

If a YAML or generated file is entirely absent, that itself is reported as a
drift entry.

### `DriftReport` / API surface

- `detectDrift(domains?, options?)` — `options.specsDir` and
  `options.generatedDir` override the roots (used by the test suite to point at
  temp fixture trees).
- `DriftReport` —
  `{ domains: number; driftCount: number; entries: DriftEntry[] }`.
- The human formatter prints up to five missing/extra items per category and an
  "... and N more" tail.

---

## Code Generation

The generation pipeline is the bridge between spec sources (hand-authored YAML
and canonical Zod contracts) and the TypeScript artifacts that application code
actually imports. There are two directions: specs generated _from_ Zod
contracts, and TypeScript types generated _from_ specs.

### `scripts/generate.ts` — type generation orchestrator

The `generate` script is the umbrella generator. It runs in order:

1. Invoke `generate-oshun-v1-specs.ts` to regenerate the V1 + BFF YAML specs.
2. Invoke `generate-oshun-v1-api-clients.ts` to regenerate the V1 typed client
   packages.
3. Invoke `generate-calliope-spec.ts` to regenerate `calliope-api.yaml`.
4. For every `.yaml`/`.yml` under `src/specs/` and `v2/`, run
   `openapi-typescript` to emit a `.ts` module into `src/generated/`.
5. For each external spec in `EXTERNAL_SPECS`, do the same.

Output naming: a `v2/` spec becomes `v2-<name>.ts`; a `<name>-api.yaml` becomes
`<dirname>.ts` (so `lilith/lilith-api.yaml` → `lilith.ts`); any other file keeps
its base name.

`EXTERNAL_SPECS` are two specs owned by other packages, generated with an
explicit output name:

- `services/metis/openapi/metis.openapi.json` → `metis.ts`
- `apps/oshun/bff/openapi/oshun-bff.openapi.yaml` → `oshun-bff.ts`

A missing external spec is skipped with a warning rather than failing the run.

The Nx target `openapi:gen` runs `generate.ts` directly.

### `src/generated/` — generated type modules

`src/generated/index.ts` is a barrel that `export * as <domain>` for: `lilith`,
`yemaya`, `isis`, `sophia`, `hathor`, `bellona`, `nyx`, `calliope`, `concordia`,
`oshunBff`, `metis`. It also re-exports each module's `paths` type under a
`<Domain>Paths` alias (`LilithPaths`, `YemayaPaths`, …, `MetisPaths`). The
directory additionally contains `main.ts`. Every generated file carries a "DO
NOT EDIT" header. Domain service code imports these types instead of
hand-writing request/response shapes.

### `scripts/generate-calliope-spec.ts`

Generates `src/specs/calliope/calliope-api.yaml` from the
`CalliopeOpenApiComponentSchemas` record exported by `@calliope/core` (a
`Record<string, z.ZodTypeAny>`). Each Zod schema is converted with
`z.toJSONSchema(..., { io: 'input', unrepresentable: 'any' })` and stripped of
`$schema` keys. The script then assembles a complete document by hand:

- `info` titled "Calliope API - Autonomous AI Artist Platform", version `1.0.0`,
  with `contact` "Oshun Platform Team" and `license` "MIT".
- Three servers: production `https://api.oshun.io/calliope`, staging
  `https://staging-api.oshun.io/calliope`, local `http://localhost:3010`.
- Six tags: `Artists`, `Songs`, `Eras`, `Social`, `Concerts`, `Analytics`.
- Document-level `bearerAuth` security.
- 13 path entries covering artist CRUD, nested song / era / social-post /
  concert CRUD, and three analytics read endpoints (`overview`, `streaming`,
  `engagement`).
- `components` with a `bearerAuth` HTTP-bearer-JWT scheme; reusable path
  parameters (`ArtistIdParam`, `SongIdParam`, `EraIdParam`, `PostIdParam`,
  `ConcertIdParam`, all UUID strings) and query parameters (`PageParam`,
  `LimitParam` with `maximum 100` / `default 20`, `WindowFromParam`,
  `WindowToParam` date-times); four reusable responses (`BadRequest`,
  `Unauthorized`, `NotFound`, `InternalError`); and the Zod-derived schemas.

The serialized output is prefixed with the header
`# This file is auto-generated by libs/openapi/scripts/generate-calliope-spec.ts`
and written with `yaml`'s `stringify({ lineWidth: 0 })`.

**Check mode** (`--check`, the `generate:calliope:check` script): instead of
writing, the script compares the freshly built content to the file on disk and
exits `1` if they differ — enforcing that the committed spec is current.

### `scripts/generate-oshun-v1-specs.ts`

Calls `buildOshunV1SpecArtifacts()` and, for each artifact, serializes the
document with `yaml`'s `stringify({ lineWidth: 110 })`, prefixes the header
`# This file is generated by libs/openapi/scripts/generate-oshun-v1-specs.ts.`,
then pipes it through `prettier` (`--stdin-filepath oshun-v1.yaml`) before
writing under `src/specs/`. In `--check` mode it reports any spec that is
missing or out of date and exits `1`. `npm` scripts: `generate:oshun-v1-specs`
and `generate:oshun-v1-specs:check`.

### `scripts/generate-oshun-v1-api-clients.ts`

Generates one typed API-client package per V1 domain. The package list
(`CLIENT_PACKAGES`) is derived from `getOshunV1ApiDomains()`; each entry yields
package name `@<domain>/api-client`, project name `<domain>-api-client`, root
`libs/<domain>/api-client`, and scope tag `scope:<domain>`. The `metis` package
has `preserveExistingScaffold: true`, so for Metis only `src/client.ts`,
`src/client.test.ts`, and `src/generated/openapi.ts` are (re)written — its
`package.json`, `project.json`, tsconfigs, `vitest.config.ts`, and `index.ts`
are left untouched; all other domains get the full scaffold regenerated.

Per package the generator writes:

- `package.json`, `project.json`, three tsconfigs, `vitest.config.ts`,
  `src/index.ts` (full scaffold; skipped for Metis).
- `src/client.ts` — a typed client with a
  `ResourceClient<Record, List, Mutation>` per object contract, exposing `list`,
  `create`, `get`, `upsert`, `tombstone`. Includes an injectable `FetchLike`, a
  configurable base URL and headers, query-string assembly (`limit`, `cursor`,
  `includeTombstones`), and an `OshunApiClientError` thrown on non-OK responses.
- `src/client.test.ts` — a vitest spec asserting the five operations issue the
  expected `GET/POST/GET/PUT/DELETE` calls to the contract routes.
- `src/generated/openapi.ts` — the domain spec run through `openapi-typescript`
  into a temp directory and copied in.

All files are formatted via `prettier --stdin-filepath`. `--check` mode reports
out-of-date or missing files and exits `1`. `npm` scripts:
`generate:oshun-v1-clients` and `generate:oshun-v1-clients:check`.

### `scripts/generate-v3-spec.ts`

Generates `src/specs/v3.yaml` by calling `buildV3OpenApiDocument()` from
`@oshun/contracts/v3`, serializing with `yaml`'s
`stringify({ aliasDuplicateObjects: false, lineWidth: 100 })`, and prefixing the
header
`# Generated by libs/openapi/scripts/generate-v3-spec.ts. Do not edit by hand.`
It accepts an optional positional target which must be `v3` (any other value
throws). `--check` mode compares to disk and throws if stale. The generated V3
spec is titled "Oshun V3 Contract API", version `3.0.0`, and carries four tags:
`lilith-platform`, `tara-studio`, `saraswati-stage`, `lilith-commons`. `npm`
scripts: `generate:v3-spec` and `generate:v3-spec:check`.

### `scripts/generate-v3-clients.ts`

Generates the four typed V3 tenant clients plus `base.ts` and `index.ts` into
`src/v3-clients/`. Each client is scoped to one V3 tenant — the tenant list
(`TENANT_FILES`) maps each `V3ClientTenant` to a file name, factory function,
and interface name:

| Tenant            | File                 | Factory                      | Interface              |
| ----------------- | -------------------- | ---------------------------- | ---------------------- |
| `lilith-platform` | `lilith-platform.ts` | `createLilithPlatformClient` | `LilithPlatformClient` |
| `tara-studio`     | `tara-studio.ts`     | `createTaraStudioClient`     | `TaraStudioClient`     |
| `saraswati-stage` | `saraswati-stage.ts` | `createSaraswatiStageClient` | `SaraswatiStageClient` |
| `lilith-commons`  | `lilith-commons.ts`  | `createLilithCommonsClient`  | `LilithCommonsClient`  |

For each tenant, `getV3ContractsForTenant(tenant)` from `@oshun/contracts/v3`
supplies contract descriptors; the generator emits a `get<Contract>(id)` and
`create<Contract>(input)` method per contract, each issuing a `v3JsonRequest` to
`/api/v3/<tenant>/<routeSegment>`. `--check` mode reports any out-of-date file.
`npm` scripts: `generate:v3-clients` and `generate:v3-clients:check`.
`generate:v3` / `generate:v3:check` run the V3 spec and client generators
together; the matching Nx targets are `generate:v3` and `generate:v3:check`.

### `src/v3-clients/` — generated V3 clients

This directory is "do not edit by hand" — it is fully regenerated by
`generate-v3-clients.ts`. The transport layer and all four tenant clients live
here.

- `base.ts` exports the transport: the `V3FetchResponse` and `V3Fetch`
  interfaces, the `V3ClientOptions` config (`baseUrl`, optional `fetch`,
  optional `headers`), the `V3ApiError` class (carrying `status`, `code`,
  `requestId`, `body`), and `v3JsonRequest<T>(options, path, init)`.
  `v3JsonRequest` falls back to `globalThis.fetch`, sets
  `accept: application/json` (and `content-type` when a body is present), reads
  the body according to content type, and throws a `V3ApiError` — including the
  `x-request-id` response header — on a non-OK response.
- `index.ts` re-exports `base` and the four tenant client modules.
- Each tenant file (e.g. `lilith-platform.ts`) exports an interface and a
  factory. `LilithPlatformClient` covers nine V3 contracts: `LilithSession`,
  `AvatarBinding`, `Presence`, `Room`, `Venue`, `SpatialTranscript`, `Report3D`,
  `ProvenanceBundle3D`, `EmbodiedConsent`.

### `scripts/validate-v3-spec.ts`

Validates `src/specs/v3.yaml` against every contract in `V3_CONTRACT_REGISTRY`
(from `@oshun/contracts/v3`) using the `oas3-validator` package. For each
contract descriptor it constructs the collection path
`/api/v3/<clientTenant>/<routeSegment>` and the item path `<collection>/{id}`,
then asserts that the descriptor's `fixture` validates both as a `POST` request
body to the collection and as a `200` `GET` response body from the item path.
Any validator error array is thrown. `npm` script: `validate:v3`; Nx target:
`validate:v3`.

---

## Documentation Generation — `scripts/generate-docs.ts`

`generate-docs.ts` builds a static HTML documentation site from the domain specs
using Redoc (loaded from `cdn.redoc.ly` at page runtime). The output is a
deployable static site that gives external and internal developers a searchable,
schema-aware view of every documented API.

### Documented domains

A fixed `DOMAINS` record configures nine domains with a title and description:
`isis`, `sophia`, `hathor`, `bellona`, `yemaya`, `lilith`, `calliope`, `nyx`,
`concordia`. For each, the script reads the first `.yaml`/`.yml` file in
`src/specs/<domain>/`, extracts `info.title`/`version`/`description`, and:

- writes `docs/<domain>/index.html` — a Redoc page embedding the spec JSON, with
  a shared dark navbar linking all nine domains and a custom Redoc theme (indigo
  `#6366f1` primary, Inter / JetBrains Mono typography);
- copies the spec to `docs/<domain>/openapi.yaml` for download.

It also writes `docs/index.html` — a landing page with one card per domain
(title, version badge, description) and a "Getting Started" section
(authentication via JWT bearer tokens, production/staging base URLs, the default
1000-requests-per-minute rate limit, JSON response format).

### Output location

All output goes to `libs/openapi/docs/`. The committed `docs/` tree currently
holds `index.html` plus per-domain `index.html` + `openapi.yaml` for all nine
configured domains.

`npm` scripts: `generate:docs` runs the generator; `docs` runs it and then
`npx serve docs` for local preview.

---

## Dependencies

The `package.json` dependency list below is the exact source of truth for what
this library imports. Runtime dependencies are loaded at generation time (during
builds and CI); dev dependencies are invoked by scripts.

```json
"dependencies": {
  "@calliope/core": "workspace:*",
  "@oshun/contracts": "workspace:*",
  "@oshun/domain-registry": "workspace:*",
  "@oshun/persistence": "workspace:*",
  "openapi-types": "^12.1.3",
  "yaml": "^2.4.1",
  "zod": "catalog:"
},
"devDependencies": {
  "oas3-validator": "1.0.0",
  "openapi3-ts": "0.11.0",
  "openapi-typescript": "^7.10.1",
  "tsx": "^4.7.0",
  "vite-tsconfig-paths": "catalog:"
},
"peerDependencies": { "typescript": "catalog:" }
```

Each dependency serves a specific role in the generation and validation
pipeline:

- `@calliope/core` — source of `CalliopeOpenApiComponentSchemas` for the
  Calliope spec generator.
- `@oshun/contracts` — source of `buildV3OpenApiDocument`,
  `V3_CONTRACT_REGISTRY`, `getV3ContractsForTenant`, and the V3 fixtures used by
  the V3 spec/client generators and validator (via the `/v3` subpath).
- `@oshun/domain-registry` — source of `DOMAIN_REGISTRY` (display names, shell
  narratives, BFF base paths) for the Oshun V1 spec builder.
- `@oshun/persistence` — source of the V1 object/enum persistence contracts and
  the `ContractPersistenceDomain` type.
- `openapi-types` — TypeScript types for OpenAPI documents; `OpenAPIV3_1` is
  re-exported.
- `yaml` — YAML parse/stringify across loaders and every generator.
- `zod` — used by the spec builder and Calliope generator for `z.toJSONSchema`.
- `oas3-validator` — runtime validation engine for `validate-v3-spec.ts`.
- `openapi-typescript` — converts specs to TypeScript types in `generate.ts` and
  the V1 client generator.
- `tsx` — runs every script.

---

## Nx Project Targets

`project.json` defines the project (`@oshun/openapi`, tags `scope:oshun`,
`type:lib`) with ten targets. The `build` target compiles the library and copies
spec YAML files as assets; the remaining targets all invoke `tsx` scripts via
`nx:run-commands`.

| Target                              | Executor          | Action                                                                      |
| ----------------------------------- | ----------------- | --------------------------------------------------------------------------- |
| `build`                             | `@nx/js:tsc`      | Compile to `dist/libs/openapi`, copying `src/specs` and `v2` YAML as assets |
| `lint`                              | `@nx/eslint:lint` | Lint the project                                                            |
| `test`                              | `@nx/vite:test`   | Run vitest (`passWithNoTests: true`)                                        |
| `openapi:validate`                  | `nx:run-commands` | `npx @redocly/cli lint libs/openapi/src/specs/`                             |
| `openapi:gen`                       | `nx:run-commands` | `tsx libs/openapi/scripts/generate.ts`                                      |
| `generate:oshun-v1-specs[:check]`   | `nx:run-commands` | Delegates to the matching pnpm script                                       |
| `generate:oshun-v1-clients[:check]` | `nx:run-commands` | Delegates to the matching pnpm script                                       |
| `generate:v3[:check]`               | `nx:run-commands` | Delegates to the matching pnpm script                                       |
| `validate:v3`                       | `nx:run-commands` | Delegates to the `validate:v3` pnpm script                                  |

The `build` target's TS config is `tsconfig.lib.json`, which includes
`src/**/*.ts` and `scripts/**/*.ts` but excludes test files and
`src/generated/**/*.ts`.

---

## npm Script Reference

All scripts are invokable via `pnpm run <script>` from the `libs/openapi/`
directory. The `*:check` variants are the ones CI invokes — they never write
files, only compare and exit with an error code.

| Script                            | Command                                               | Purpose                                         |
| --------------------------------- | ----------------------------------------------------- | ----------------------------------------------- |
| `validate`                        | `tsx scripts/validate.ts --domain-only`               | Validate domain specs only                      |
| `validate:all`                    | `tsx scripts/validate.ts`                             | Validate every spec including root-level        |
| `diff`                            | `tsx scripts/diff.ts`                                 | Report breaking changes vs `origin/main`        |
| `diff:check`                      | `tsx scripts/diff.ts --check`                         | Fail CI on breaking changes                     |
| `drift`                           | `tsx scripts/drift-check.ts`                          | Report spec↔generated-type drift                |
| `drift:check`                     | `tsx scripts/drift-check.ts --check`                  | Fail CI on drift                                |
| `generate:calliope`               | `tsx scripts/generate-calliope-spec.ts`               | Generate the Calliope spec from Zod             |
| `generate:calliope:check`         | `tsx scripts/generate-calliope-spec.ts --check`       | Verify the Calliope spec is current             |
| `generate:oshun-v1-specs`         | `tsx scripts/generate-oshun-v1-specs.ts`              | Generate the V1 + BFF specs from Zod            |
| `generate:oshun-v1-specs:check`   | `… --check`                                           | Verify the V1 + BFF specs are current           |
| `generate:oshun-v1-clients`       | `tsx scripts/generate-oshun-v1-api-clients.ts`        | Generate the V1 typed client packages           |
| `generate:oshun-v1-clients:check` | `… --check`                                           | Verify the V1 client packages are current       |
| `generate:v3-spec`                | `tsx scripts/generate-v3-spec.ts`                     | Generate `v3.yaml` from `@oshun/contracts/v3`   |
| `generate:v3-spec:check`          | `… --check`                                           | Verify `v3.yaml` is current                     |
| `generate:v3-clients`             | `tsx scripts/generate-v3-clients.ts`                  | Generate the four V3 tenant clients             |
| `generate:v3-clients:check`       | `… --check`                                           | Verify the V3 clients are current               |
| `generate:v3`                     | `generate:v3-spec && generate:v3-clients`             | Generate V3 spec + clients together             |
| `generate:v3:check`               | `generate:v3-spec:check && generate:v3-clients:check` | Verify V3 spec + clients                        |
| `validate:v3`                     | `tsx scripts/validate-v3-spec.ts`                     | Validate `v3.yaml` against V3 contract fixtures |
| `generate`                        | `tsx scripts/generate.ts`                             | Orchestrate all generators + type generation    |
| `generate:docs`                   | `tsx scripts/generate-docs.ts`                        | Build the Redoc HTML documentation site         |
| `docs`                            | `generate:docs && npx serve docs`                     | Build and serve the docs locally                |
| `build`                           | `tsc`                                                 | Compile the library                             |
| `clean`                           | `rm -rf dist docs`                                    | Remove build and docs output                    |

---

## CI Integration

The `*:check` scripts are the CI gates. They all share the same contract: build
the expected artifact, compare it to what is committed, and exit `1` on any
difference. A consistent CI configuration runs these five steps in order:

1. `validate:all` — every spec must be a structurally valid OpenAPI 3.1
   document.
2. `diff:check` — no breaking changes versus the base branch in the seven
   tracked domain specs.
3. `drift:check` — no spec↔generated-type drift in the eleven mapped domains.
4. `generate:calliope:check`, `generate:oshun-v1-specs:check`,
   `generate:oshun-v1-clients:check`, `generate:v3:check` — every generated spec
   and client is in sync with its canonical Zod source.
5. `validate:v3` — the V3 spec accepts every V3 contract fixture.

This enforces the single-source-of-truth rule: generated specs and clients must
be regenerated and committed alongside any change to their Zod source.

---

## Tests

Two vitest suites run under the `test` target (`vitest.config.ts` includes
`src/**` and `scripts/**` spec globs, node environment, v8 coverage). Together
they cover the registry, loader, spec builder, drift detector, and the generated
V3 clients.

- **`src/openapi.spec.ts`** — exercises `SPEC_PATHS` values, `SPEC_REGISTRY`
  entries (main, auth, calliope, metis, V2, V3, and every generated `oshun-v1-*`
  entry), `getSpecsByDomain` / `getSpecsByTag` / `getDomainPaths`,
  `extractEndpoints` / `extractSchemas`, the V2 companion spec loaded via
  `loadSpecSync` / `listSpecs`, and `buildOshunV1SpecArtifacts` /
  `getOshunV1ClientResources` (asserting concrete contract paths and schema
  names such as `RitualTemplate` and `CourseBuild`).
- **`scripts/drift-check.spec.ts`** — exercises `detectDrift` against temporary
  fixture trees: a zero-drift case and cases covering missing paths, missing
  operations, missing schemas, header drift, a missing YAML, and a missing
  generated file.
- **`src/v3-clients/__tests__/client.spec.ts`** — exercises the generated V3
  clients via `createTaraStudioClient`: a typed happy-path `POST`, and
  `V3ApiError` propagation for `4xx` and `5xx` responses including
  `code`/`requestId`/`body` metadata.

---

## Integration Points

The table below shows every external consumer of this library and how they use
it. The boundary is clean: this library provides contracts and tooling only; it
has no knowledge of business logic.

| Consumer                   | Usage                                                                                           |
| -------------------------- | ----------------------------------------------------------------------------------------------- |
| Domain API servers         | Import generated types from `@oshun/openapi/generated` and the generated V1/V3 typed clients    |
| CI pipeline                | Runs `validate:all`, `diff:check`, `drift:check`, and the `*:check` generation gates            |
| Developer portal           | `generate:docs` output (`docs/`) deployed as the documentation site                             |
| `@calliope/core`           | Supplies `CalliopeOpenApiComponentSchemas` consumed by the Calliope spec generator              |
| `@oshun/contracts` (`/v3`) | Supplies `buildV3OpenApiDocument`, `V3_CONTRACT_REGISTRY`, V3 fixtures for V3 generation        |
| `@oshun/persistence`       | Supplies the V1 object/enum persistence contracts the V1 spec builder generates from            |
| `@oshun/domain-registry`   | Supplies `DOMAIN_REGISTRY` metadata for V1 spec titles, tags, and BFF base paths                |
| Metis service              | Owns `services/metis/openapi/metis.openapi.json`, consumed as an external spec by `generate.ts` |
| Oshun BFF app              | Owns `apps/oshun/bff/openapi/oshun-bff.openapi.yaml`, consumed as an external spec              |
| V2 public API              | `apps/oshun/web` V2 handlers and route adapters consume `libs/openapi/v2/companion.yaml`        |

### V2 Companion public API binding

The V2 Companion Public API is the one external HTTP surface that contracts
against this library at runtime. It is split across two directories in the
`apps/oshun/web` Next.js app:

- `apps/oshun/web/api/v2/` holds the framework-agnostic handler layer —
  `public-api.ts` defines `V2_PUBLIC_API_BASE_PATH = '/api/v2'`, the
  `V2_PUBLIC_API_ENDPOINTS` map, and the typed handlers (`getV2PlayerStats`,
  `getV2PlayerHistory`, `getV2Replay`, `createV2CoachStream`,
  `subscribeV2Notifications`), all read-only with `gameplayAuthority: false`.
- `apps/oshun/web/src/app/api/v2/` holds the thin Next.js App Router adapters
  (`player/[playerId]/stats/route.ts`, `replay/[replayId]/route.ts`,
  `match/[matchId]/coach-stream/route.ts`, `notification/subscribe/route.ts`,
  and `openapi/route.ts`), which delegate to those handlers and re-serve the
  spec.

Both layers are pinned to the same contract: `@oshun/openapi` resolves the
`v2-companion` spec from `libs/openapi/v2/companion.yaml` (registered with
`basePath: '/api/v2'`), and the `openapi/route.ts` adapter serves that exact
document via `SPEC_PATHS.v2.companion`, so the published OpenAPI description and
the live `/api/v2` endpoints can never drift apart.

---

## Acceptance Criteria

The openapi domain enforces these properties through its `*:check` scripts and
test suites. These criteria are the machine-verifiable definition of "the domain
is healthy."

1. Every spec under `src/specs/` and `v2/` is a structurally valid OpenAPI 3.1
   document (`validate:all` — `openapi` version, required `info` fields, paths
   or webhooks, unique operation IDs, no broken `#/components/schemas` refs,
   defined path parameters, defined security schemes).
2. No tracked domain spec introduces a removed path/operation/`2xx` response or
   a parameter that became required or was removed, versus the base branch
   (`diff:check`).
3. Each generated TypeScript module's paths, operation IDs, schemas, and
   `Auto-generated from:` header stay in sync with its source YAML
   (`drift:check`).
4. `calliope-api.yaml`, the six V1 contract specs,
   `oshun-bff-v1-contracts.yaml`, `v3.yaml`, and the V1 and V3 typed clients are
   byte-identical to a fresh regeneration from their canonical Zod sources
   (`generate:*:check`).
5. The V3 spec validates every V3 contract fixture as both request and response
   bodies (`validate:v3`).
6. `SPEC_PATHS`, `SPEC_REGISTRY`, the registry helpers, and the V1 spec builder
   produce the documented values (`src/openapi.spec.ts`).
