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 underlibs/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:
- Spec storage — OpenAPI 3.1 YAML spec files under
src/specs/, one per domain, plus the V2 companion spec underv2/. - A typed registry —
SPEC_PATHS,SPEC_REGISTRY, and theApiDomainunion insrc/utils/registry.tscatalog every spec with metadata. - Tooling — fourteen
tsxscripts underscripts/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.
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.*(includingcitations),hathor.*,bellona.*,nyx.objects/ephemeris/events/satellites, and the entireshared.*group name files that are not present on disk. Theregistry.tssource 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.apiis an external path. It points atservices/metis/openapi/metis.openapi.json— a ~1 MB JSON spec owned by the Metis service, not a YAML file undersrc/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,platformauth:auth,security;users:users,profiles;health:health,observabilityv3-contracts:v3,contracts,lilith,tara,saraswati,commonscalliope:artist,music,social,concerts,analytics- The
oshun-v1-*specs all carryoshun-v1andcontractsplus 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,issmetis:education,tutoring,assessment,admin,analytics,buildv2-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.
{ 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 |.
{ 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/(forv2-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 bytag.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:
- Hand-authored YAML —
main.yaml, the seven<domain>-api.yamlfiles for lilith/yemaya/isis/sophia/hathor/bellona/nyx,concordia-api.yaml, andv2/companion.yaml. These are edited directly. - Generated from Zod via the V1 spec builder — the six
<domain>-v1-contracts.yamlfiles plusoshun-bff-v1-contracts.yaml. Each carries the header# This file is generated by libs/openapi/scripts/generate-oshun-v1-specs.ts. - Generated from Zod via dedicated generators —
calliope-api.yaml(header# This file is auto-generated by libs/openapi/scripts/generate-calliope-spec.ts) andv3.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_CONTRACTSandV1_ENUM_PERSISTENCE_CONTRACTSfrom@oshun/persistence— arrays ofObjectPersistenceContract/EnumPersistenceContract, each carryingkind,domain,contractName,schemaName,modelName,tableName, and a Zodschema.DOMAIN_REGISTRYfrom@oshun/domain-registry— supplies each domain'sdisplayName,shellNarrative.summary, andbff-base-path.ContractPersistenceDomain— the uniontara | 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', theinfo.versionfor every generated spec.JSON_SCHEMA_DIALECT—'https://json-schema.org/draft/2020-12/schema', emitted as the document'sjsonSchemaDialect.
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, aninfoblock titled<DisplayName> V1 Contract API.- A single server whose
urlis/api/v1/<domain>. - One tag named after the domain, described by the registry's
shellNarrative.summary. - Document-level security
[{ bearerAuth: [] }]. pathsandcomponentsgenerated bybuildContractPathsandbuildComponents.
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, eachapplication/jsonwith theErrorResponseschema:BadRequest,Unauthorized,Forbidden,NotFound,Conflict,InternalError.schemas— three fixed envelope schemas plus per-contract schemas:ErrorResponse—object,additionalProperties: false, requirederror+message, optionalrequestIdand free-formdetails.PageInfo—object, requiredlimit(integer 1–250) +hasMore(boolean), optionalcursorandnextCursor.TombstoneResponse—object, requiredsourceRecordId+tombstonedAt(date-time), optionaltombstoneReason.- 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) — keepscontractNameunchanged, e.g.RitualTemplate.prefixed(used by the BFF spec) — prepends the PascalCase domain name, e.g.TaraRitualTemplate, or usesSharedforcross-cuttingcontracts, 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
openapipresent and starting with3.(else error).infopresent, withinfo.titleandinfo.version(errors); missinginfo.descriptionis a warning.- At least one of
pathsorwebhooksis 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
operationIdis a warning; non-identifieroperationIdis a warning; duplicateoperationIdacross the document is an error. Missingsummary/descriptionand missingtagsare warnings. responsesmust be present (error) with at least one2xx(warning if not). Response codes must bedefaultor match[1-5]\d\d(else error).- Every
{param}in the path must be defined in path-level or operation-levelparameters, including resolution of$refparameters viaresolveParameterRef— an undefined path parameter is an error.
validateSchemas
- A schema with no
type,allOf,oneOf,anyOf,$ref, orenumis a warning. - An
objectschema with neitherpropertiesnoradditionalPropertiesis a warning. - A
requiredfield name that is not present inpropertiesis an error.
validateSecurity
- Missing
securitySchemesis a warning. - Any operation-level
securityrequirement naming an undefined scheme is an error.
validateRefs
- Recursively walks the document; any
$refof 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; defaultorigin/main.--check— fail (exit1) 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— exit1when drift is detected.--json— emit a machine-readableDriftReportinstead 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, operationIds,
components.schemas) and the generated .ts is parsed by regex into the same
model. detectDrift then emits a DriftEntry for any of four categories:
paths— every YAML path must appear in the generatedpathstype. YAML{param}placeholders are normalized to{string}to correlate against whatopenapi-typescriptemits as`${string}`. Both missing and extra paths are reported.operations— every YAMLoperationIdmust appear as anoperations['<id>']reference in the generated TS.schemas— every YAML component schema must appear in the generated TS, either ascomponents['schemas']['<Name>']or as a direct declaration inside theschemas:block.header— the generated file must carry anAuto-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.specsDirandoptions.generatedDiroverride 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:
- Invoke
generate-oshun-v1-specs.tsto regenerate the V1 + BFF YAML specs. - Invoke
generate-oshun-v1-api-clients.tsto regenerate the V1 typed client packages. - Invoke
generate-calliope-spec.tsto regeneratecalliope-api.yaml. - For every
.yaml/.ymlundersrc/specs/andv2/, runopenapi-typescriptto emit a.tsmodule intosrc/generated/. - 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.tsapps/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:
infotitled "Calliope API - Autonomous AI Artist Platform", version1.0.0, withcontact"Oshun Platform Team" andlicense"MIT".- Three servers: production
https://api.oshun.io/calliope, staginghttps://staging-api.oshun.io/calliope, localhttp://localhost:3010. - Six tags:
Artists,Songs,Eras,Social,Concerts,Analytics. - Document-level
bearerAuthsecurity. - 13 path entries covering artist CRUD, nested song / era / social-post /
concert CRUD, and three analytics read endpoints (
overview,streaming,engagement). componentswith abearerAuthHTTP-bearer-JWT scheme; reusable path parameters (ArtistIdParam,SongIdParam,EraIdParam,PostIdParam,ConcertIdParam, all UUID strings) and query parameters (PageParam,LimitParamwithmaximum 100/default 20,WindowFromParam,WindowToParamdate-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 aResourceClient<Record, List, Mutation>per object contract, exposinglist,create,get,upsert,tombstone. Includes an injectableFetchLike, a configurable base URL and headers, query-string assembly (limit,cursor,includeTombstones), and anOshunApiClientErrorthrown on non-OK responses.src/client.test.ts— a vitest spec asserting the five operations issue the expectedGET/POST/GET/PUT/DELETEcalls to the contract routes.src/generated/openapi.ts— the domain spec run throughopenapi-typescriptinto 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.tsexports the transport: theV3FetchResponseandV3Fetchinterfaces, theV3ClientOptionsconfig (baseUrl, optionalfetch, optionalheaders), theV3ApiErrorclass (carryingstatus,code,requestId,body), andv3JsonRequest<T>(options, path, init).v3JsonRequestfalls back toglobalThis.fetch, setsaccept: application/json(andcontent-typewhen a body is present), reads the body according to content type, and throws aV3ApiError— including thex-request-idresponse header — on a non-OK response.index.tsre-exportsbaseand the four tenant client modules.- Each tenant file (e.g.
lilith-platform.ts) exports an interface and a factory.LilithPlatformClientcovers 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#6366f1primary, Inter / JetBrains Mono typography); - copies the spec to
docs/<domain>/openapi.yamlfor 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.
"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 ofCalliopeOpenApiComponentSchemasfor the Calliope spec generator.@oshun/contracts— source ofbuildV3OpenApiDocument,V3_CONTRACT_REGISTRY,getV3ContractsForTenant, and the V3 fixtures used by the V3 spec/client generators and validator (via the/v3subpath).@oshun/domain-registry— source ofDOMAIN_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 theContractPersistenceDomaintype.openapi-types— TypeScript types for OpenAPI documents;OpenAPIV3_1is re-exported.yaml— YAML parse/stringify across loaders and every generator.zod— used by the spec builder and Calliope generator forz.toJSONSchema.oas3-validator— runtime validation engine forvalidate-v3-spec.ts.openapi-typescript— converts specs to TypeScript types ingenerate.tsand 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:
validate:all— every spec must be a structurally valid OpenAPI 3.1 document.diff:check— no breaking changes versus the base branch in the seven tracked domain specs.drift:check— no spec↔generated-type drift in the eleven mapped domains.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.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— exercisesSPEC_PATHSvalues,SPEC_REGISTRYentries (main, auth, calliope, metis, V2, V3, and every generatedoshun-v1-*entry),getSpecsByDomain/getSpecsByTag/getDomainPaths,extractEndpoints/extractSchemas, the V2 companion spec loaded vialoadSpecSync/listSpecs, andbuildOshunV1SpecArtifacts/getOshunV1ClientResources(asserting concrete contract paths and schema names such asRitualTemplateandCourseBuild).scripts/drift-check.spec.ts— exercisesdetectDriftagainst 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 viacreateTaraStudioClient: a typed happy-pathPOST, andV3ApiErrorpropagation for4xxand5xxresponses includingcode/requestId/bodymetadata.
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.tsdefinesV2_PUBLIC_API_BASE_PATH = '/api/v2', theV2_PUBLIC_API_ENDPOINTSmap, and the typed handlers (getV2PlayerStats,getV2PlayerHistory,getV2Replay,createV2CoachStream,subscribeV2Notifications), all read-only withgameplayAuthority: 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, andopenapi/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."
- Every spec under
src/specs/andv2/is a structurally valid OpenAPI 3.1 document (validate:all—openapiversion, requiredinfofields, paths or webhooks, unique operation IDs, no broken#/components/schemasrefs, defined path parameters, defined security schemes). - No tracked domain spec introduces a removed path/operation/
2xxresponse or a parameter that became required or was removed, versus the base branch (diff:check). - Each generated TypeScript module's paths, operation IDs, schemas, and
Auto-generated from:header stay in sync with its source YAML (drift:check). 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).- The V3 spec validates every V3 contract fixture as both request and response
bodies (
validate:v3). SPEC_PATHS,SPEC_REGISTRY, the registry helpers, and the V1 spec builder produce the documented values (src/openapi.spec.ts).