Shared Platform · Contracts

Contracts (libs/contracts)

The contract surface is implemented and hard-tested, not a scaffold.

9sections12 minread2diagrams

On this page

libs/contracts/ is the Zod contract surface: the single, typed source of truth that every boundary in the platform validates against. It answers a narrow but load-bearing question — when the BFF, a domain orchestration library, and a Vitest spec all need to agree on what a "ritual check-in," a "grounded answer," or an "embodied-instruction demonstration request" is, where does that shape live so the three of them cannot drift apart? The answer is one package, @oshun/contracts, whose source tree groups its schemas into 23 first-level domain directories under libs/contracts/src/ (the platform overview rounds this to "two dozen"), each a barrel index.ts, most of them re-exported through the package root libs/contracts/src/index.ts. The flagship is Tara: its canonical contracts file libs/contracts/src/tara/index.ts is 2,688 lines of schemas, refinements, and shipped reference datasets, and it is wired into the root barrel by a single export * from './tara' (index.ts:25).

What makes this tree the contract layer rather than another library is a property the build enforces and a grep can confirm: it depends on nothing but Zod. Across all of libs/contracts/src/, production code imports zod (238 import sites) and nothing else domain-shaped — there is not one from '@oshun/...' import of a non-contracts package, no database driver, no HTTP client (package.json lists exactly one dependency: zod). That purity is the point: a schema can be imported by a UE-adjacent TypeScript client, by the Fastify BFF, and by a *.spec.ts test alike without dragging in a Postgres pool. The contracts surface is imported across 25+ areas of the monorepo — apps/isis and libs/isis lead, then apps/oshun (the BFF), apps/oya, libs/oshun, libs/metis, libs/nyx, libs/shared, and more — which is exactly the write-once/compose-everywhere discipline the rest of the platform follows. This page maps that surface; it is one slab of the stack described in the platform overview.

What ships, honestly#

The contract surface is implemented and hard-tested, not a scaffold. The canonical conformance spec, libs/contracts/src/contracts.spec.ts, is a single ~352 KB file that calls safeParse 263 times and parse 78 times, and — crucially — it asserts both the accepting and the rejecting case (every domain has expect(Schema.safeParse(good).success).toBe(true) paired with ...(bad).success).toBe(false)), so the tests would fail against a schema that accepted everything. That is the difference between a validator and a shape-shaped placeholder.

Maturity is uneven across the 23 domains, and this page labels the differences in the spirit of the repository's no-stub culture and the docs center's implemented / spec-only / provider-gated convention:

  • common is enormous and central161 source modules with 125 colocated test files (108 .spec.ts + 17 .test.ts). It is the shared vocabulary every other domain composes (primitives, consent, residency, evidence packs, grounded answers).
  • The V1 customer domains are deeptara (2,688 lines), arete, nisaba, nyx, metis, veritas each carry genuine domain logic (state-machine transition tables, superRefine invariants, shipped reference datasets), documented per-domain on the V1 feature pages.
  • v3, v6, v9 are product-version namespaces — sub-trees of many modules (v3 alone re-exports lilith, tara, saraswati, commons, consent, fixtures, registry, openapi), namespaced under V3Contracts / V6Contracts / V9Contracts to keep their symbols from colliding with the V1 flat exports.
  • A handful are single-file barrelsatelier, identity, library, messaging, studio are one src file each with no colocated spec, the smaller/younger surfaces of the tree. They are honest typed vocabularies, not fabricated depth, and this page does not imply otherwise.

One more honest seam: not every wire shape lives in contracts. The package is the canonical domain vocabulary, but individual BFF routes still define local request DTOs inline when a shape is route-specific — e.g. RecordTurnRequestSchema is a local z.object in apps/oshun/bff/src/routes/metis-tutor-memory.ts:40, and SubmitRunSchema is local to agentic-runs-lifecycle.ts:26. Contracts is the shared, cross-boundary vocabulary; it is not a registry of every payload the platform parses.

The shape of the tree — 23 domains#

Enumerated so the count is verifiable, grouped by role:

Group Domains
Shared vocabulary common (161 modules)
V1 customer domains tara, arete, nisaba, nyx, metis, veritas
Cross-cutting substrate events, llm, iris, aja, tts, living-scene
Agentic · studio · admin agent, studio, atelier, identity, library, messaging, oya
Product-version namespaces v3, v6, v9

That is 1 + 6 + 6 + 7 + 3 = 23 directories, each with its own index.ts. The groupings are descriptive, not a build boundary — the build boundary is the dependency direction (contracts depends only on Zod; everything else depends on contracts).

The re-export pattern — one barrel, several resolution styles#

libs/contracts/src/index.ts is the package root barrel, and it does not treat all 23 domains identically. There are five resolution styles, and the choice between them is driven by symbol-collision risk, because many domains define the same common names (a Vector3, a Capability, a Safety* enum).

  • Flat export * (7 domains: common, events, llm, aja, arete, tara, nyx). Their symbols land directly on the package root, so import { CheckInSchema } from '@oshun/contracts' works.
  • Flat and namespaced (4: v3, v6, v9, iris). Re-exported both with export * and as export * as V3Contracts (index.ts:28-37), so a caller can reach a V3 symbol flat or disambiguate it under V3Contracts.*.
  • Namespaced with selective prefixed names (4: living-scene, nisaba, metis, veritas). These get an export * as XContracts plus a curated list of individually re-exported, prefix-renamed symbols — e.g. ClaimSchema as VeritasClaimSchema (index.ts:135), PassageSchema as NisabaPassageSchema (index.ts:81) — so the common nouns ("Claim," "Passage," "Score") don't collide at the root.
  • Namespace-only (2: agent, oya). Reachable only as AgentContracts.* / OyaContracts.*. The Oya comment spells out the reason in code (index.ts:176-179): it is namespaced "to avoid top-level collisions with other domains (e.g. aja's Quaternion/Vector3, the many Safety*/Privacy*/Capability* names)." That collision is real — QuaternionSchema is defined in libs/contracts/src/aja/primitives.ts:17.
  • Subpath-only — absent from the root barrel (6: atelier, identity, library, messaging, studio, tts). These are not re-exported from index.ts at all; you reach them only through their per-domain entry point, @oshun/contracts/tts, @oshun/contracts/identity, and so on.

Those per-domain entry points are declared twice, and both must agree. The package.json exports map pins "./tara", "./common", "./oya", the nested "./tts/providers", "./tara/playback-rate", etc. to their source files, and tsconfig.base.json carries the matching path mappings — "@oshun/contracts": ["libs/contracts/src/index.ts"] (:1421), "@oshun/contracts/tara": ["libs/contracts/src/tara/index.ts"] (:1443) — with a catch-all "@oshun/contracts/*": ["libs/contracts/src/*"] (:1451) backstopping any subpath the explicit list misses. (The two lists are not perfectly in sync — v9, agent, and living-scene have no dedicated subpath export and ride the catch-all and/or the root namespace instead — a real, minor drift worth knowing when you import them.)

flowchart LR subgraph src["libs/contracts/src"] direction TB IDX["index.ts (root barrel)"] FLAT["flat export * · common, events, llm,\naja, arete, tara, nyx"] NS["export * as XContracts · v3/v6/v9, iris,\nliving-scene, nisaba, metis, veritas, agent, oya"] SUB["subpath-only · atelier, identity, library,\nmessaging, studio, tts"] IDX --- FLAT IDX --- NS end ROOT["@oshun/contracts"] PATH["@oshun/contracts/<domain>"] IDX --> ROOT FLAT -. flat names .-> ROOT NS -. namespaced .-> ROOT SUB --> PATH ROOT --> CONSUMERS["BFF · domain libs · specs · UE-adjacent TS"] PATH --> CONSUMERS

A schema, end to end#

The clearest way to see "the same shape in three places" is to follow one schema. Take CheckInSchema from libs/contracts/src/arete/index.ts:460. It is not a CRUD record — its superRefine (:480-530) encodes Arete's humane-habit logic as machine-checked invariants:

  • a done check-in must report engagementPercent === 100 (:481);
  • a partial check-in must be strictly between 0 and 100 (:489-498);
  • a skip, decline, or miss must be 0% (:500-509);
  • a skip or decline must carry a user-visible statusReason (:511-520);
  • and streakTreatment must equal getCheckInStreakTreatment(status) (:522-529) — so a decline cannot silently break a streak the way a miss does. The humane-streak promise is enforced by the type, not by a downstream service remembering to be kind.

That one schema is consumed, unchanged, at three boundaries:

1 · At the BFF boundary — the gateway parses the request body and fails loud on a bad shape. The canonical pattern, from a contracts-sourced schema:

ts
// apps/oshun/bff/src/routes/data-export.ts
import { CustomerDataExportCreateRequestSchema } from '@oshun/contracts'; // :4-7
const parsed = CustomerDataExportCreateRequestSchema.safeParse(
  request.body ?? {}
); // :84

and the fail-loud branch, from the agentic-run route (agentic-runs-lifecycle.ts:72-80):

ts
const parsed = SubmitRunSchema.safeParse(request.body);
if (!parsed.success) {
  reply.code(400).send({
    message: 'Agent run submission failed validation',
    reason: 'invalid_agent_run_submission',
    issues: parsed.error.issues, // the Zod issues, surfaced to the caller
  });
  return;
}

The route's own header comment states the contract-layer ethos plainly: it "never fabricates a completion" (:16). A malformed payload becomes a 400 with structured issues, not a half-built record three hops downstream.

2 · In the domain library — the same schemas validate at the service seam, and here a failure throws rather than returning a 400. The Aja embodiment adapter parses both sides of its boundary against the contracts surface, importing from the per-domain subpath @oshun/contracts/aja/index.js (libs/oshun/embodiment-aja/src/canonical-adapter.ts:9):

ts
const request  = EmbodiedInstructionDemonstrationRequestSchema.parse(input);          // :45
return           EmbodiedInstructionDemonstrationResponseSchema.parse(await adapter…); // :46

The request and the response are held to the same contract, so a misbehaving backend is caught at the seam, not passed through. The pattern recurs across the domain tree: SourceSetSchema.parse(input) in libs/oshun/evidence-sophia/src/source-set.ts:24, and ContinuationTokenSchema.parse(token) in libs/oshun/memory-iris/src/continuity/protocol.ts:52.

3 · In the Vitest speccontracts.spec.ts imports those exact symbols from the package root barrel (} from './index';, contracts.spec.ts:208) and the subpath where needed (MemoryScopeKeySchema from './iris/index', :209), then exercises each one against known-good and known-bad fixtures. Because the spec imports the same schema object the BFF and the domain lib import, a change that would break a consumer breaks the test first.

sequenceDiagram participant Client participant BFF as BFF route participant Schema as @oshun/contracts schema participant Domain as domain lib Client->>BFF: POST body BFF->>Schema: Schema.safeParse(body) alt invalid Schema-->>BFF: { success:false, error.issues } BFF-->>Client: 400 + issues (fail loud) else valid Schema-->>BFF: { success:true, data } BFF->>Domain: typed data Domain->>Schema: Schema.parse(input) / parse(response) Note over Schema,Domain: throws on drift — same shape, both sides end

"Validate at the boundary, fail loud" — what it means here#

The contracts layer is types, refinements, and canonical datasets — it holds no behavior. Its enforcement power comes entirely from a caller invoking parse/safeParse at a boundary. That gives the platform two complementary fail-loud modes:

  • safeParse at an untrusted edge (the BFF parsing a client body) returns a discriminated { success, data | error }, so the route can answer 400 with error.issues and never construct a partial entity.
  • parse at an internal seam (a domain adapter validating a backend response) throws a ZodError, surfacing drift as an exception rather than a silently-wrong object.

Underneath both, the superRefine guards make whole classes of illegal state unrepresentable: Arete's check-in invariants above; Tara's mood taxonomy, where a distressLevel: 'high' mood must set crisisHandoff.required: true (so you cannot ship a high-distress mood without a crisis route); and Tara's RitualSession, whose validateSessionTimeline refinement rejects a session whose declared state contradicts its own event log. These are documented in depth on the Tara feature page; the point for the platform is that the rule lives in the shared schema, so every consumer inherits it at once.

The honest limit: contracts cannot force a boundary to call it. A route that forgets to safeParse is unguarded — the discipline is convention plus the conformance spec, not a runtime that intercepts every payload. The platform's answer is that the schema is the cheapest possible thing to reach for (zod is the only dependency) and the spec makes its absence visible.

common: the shared vocabulary the whole platform reads#

common is the largest domain because it is the one every other domain — and even parts of the shared infrastructure — composes. At the bottom sit the primitives (common/primitives.ts): UUIDSchema (z.string().uuid()), SlugSchema (a URL-safe regex), TimestampSchema (z.string().datetime()), PaginationRequestSchema, and the Timestamps / SoftDelete mixins that every entity spreads. Above those it carries the cross-domain models — consent, evidence packs, grounded answers, citations, customer data export/deletion, and the canonical-audit-event — that no single product should own.

common is also the one place the dependency direction is allowed to reverse: two libs/shared/ privacy packages read its rule tables. @oshun/data-residency imports OSHUN_DATA_RESIDENCY_RULES (common/data-residency-rules.ts:402) over the residency-zone vocabulary OshunResidencyZone = eu | uk | us | ca | latam | apac | global (:86), and evaluates a proposed cross-zone transfer against it. That is the single sanctioned reach from infrastructure into the contract vocabulary — a residency zone is data, not a domain — and it is documented from the other side in Shared Libraries and Persistence & Data.

events: two envelopes, one vocabulary#

The events domain is worth separating from the runtime event bus it serves. events/envelope.ts defines the Zod validation envelopeEventSourceSchema enumerates 13 origins (tara, isis, sophia, hathor, bellona, yemaya, lilith, aphrodite, nyx, psyche, veritas, concordia, system), EventPrioritySchema the four levels — and events/index.ts re-exports a typed payload schema per domain (isis, sophia, hathor, … concordia). This is the shape an event must satisfy. The transport — the EventEnvelope interface, Redis pub/sub delivery, retry/dead-letter, signed outbound webhooks — lives in @oshun/event-bus under libs/shared/ and is documented in Shared Libraries. Contracts says what a isis.asset.generated payload is; the event bus carries it. Keeping the two separate is what lets a producer validate a payload with zero dependency on the bus implementation.

How this foundation connects to the others#

The contracts tree sits one layer above the shared infrastructure and one below domain orchestration, and its edges are deliberate:

  • Down into shared infrastructure — almost nothing; contracts depends only on Zod. The only reach is inbound: @oshun/data-residency and @oshun/deletion-fanout read common's rule/zone tables. See Shared Libraries.
  • Up into domain orchestrationlibs/oshun/ parses at its seams against these schemas (evidence-sophia, memory-iris, embodiment-aja above). The domain libraries turn a validated shape into product behavior. See Domain Orchestration.
  • Sideways into the BFF — the gateway safeParses request bodies against contracts request schemas (and its own route-local DTOs) before composing domain calls. See BFF & Gateway.
  • Into persistencecommon's contract-prisma-alignment module exists to keep the Zod shapes and the stored schema in step; the residency and deletion vocabularies the data layer enforces are contract tables. See Persistence & Data and Auth & Identity for the identity domain's consent and session shapes.

Because the contract is written once and imported everywhere, a change to what a ritual, a claim, or a residency rule is becomes a single, type-checked edit that the BFF, the domain library, and the test all see at the same moment — which is the entire reason the contract surface is a shared foundation rather than 23 private copies.