# Contracts Domain — Architecture

> Architectural overview of `@oshun/contracts` and its sibling contract
> packages: their role in the monorepo, the zero-upstream-dependency invariant,
> the event envelope and two-tier validation registry, the per-domain canonical
> surfaces, and the honest split between implemented and scaffold packages.

---

The Oshun platform is built from dozens of independent domain libraries — Isis,
Sophia, Hathor, Yemaya, Lilith, Nyx, Veritas, Psyche, Concordia and more. Each
domain owns its own business logic, database, and API. But domains constantly
exchange data: an `Asset` generated by Isis crosses into Yemaya's project
canvas; a `tara.ritual.completed` event fans out to Arete, Nisaba, and Nyx; a
`ConsentRecord` collected anywhere must satisfy the same lifecycle rules
everywhere. Without a shared vocabulary, every domain would invent its own
slightly-different `User`, its own event envelope, and integration would degrade
into a perpetual mapping exercise.

`@oshun/contracts` (`libs/contracts/`) is the **single canonical source of
truth** for those cross-domain shapes. It is the lowest software layer in the
TypeScript half of the stack — the mirror of what Neith is for the Rust half. It
depends on nothing inside the platform (only on `zod`), and nearly everything
above it depends on it. A new engineer can treat it as the platform's _type
system_: the agreement that makes one domain's output compatible with another
domain's expectations, validated at runtime as well as compile time.

The directory `libs/contracts/` is not one package but eleven. The primary
package `@oshun/contracts` lives in `src/`; alongside it sit ten **separate Nx
libraries** — each its own `project.json`, and (except the scaffold
`@maat/contracts`, which has only a `project.json`) its own `package.json` —
that own a specific domain's contract surface (`@iris/contracts`,
`@psyche/contracts`, `@concordia/contracts`, `@freya/contracts`,
`@contracts/brigid`, `@contracts/cybele`, `@contracts/saraswati`,
`@contracts/annapurna`, `@contracts/athena`, and the scaffold
`@maat/contracts`).

---

## Position in the Dependency Graph

`libs/contracts/project.json` tags the package `scope:shared`,
`layer:contracts`, `type:lib`, and `libs/contracts/package.json` declares
exactly one runtime dependency:

```json
"dependencies": { "zod": "catalog:" }
```

This **zero-upstream-dependency rule** is the keystone invariant. Adding any
`@oshun/*` import to contracts would risk a circular dependency that could
ripple through every consumer in the monorepo at once. The rule applies to all
eleven packages in the directory: each one depends only on `zod` (plus the test
toolchain). The arrow always points _toward_ contracts and never back out.

```mermaid
flowchart TB
  subgraph consumers["Consumers (domain services, BFF, admin console, event bus)"]
    isis["Isis"]
    yemaya["Yemaya"]
    veritas["Veritas"]
    bff["OSHUN BFF"]
    bus["event-bus (@oshun/shared)"]
  end

  subgraph contracts["@oshun/contracts (libs/contracts/src)"]
    common["common/ — ~160 entity & governance schemas"]
    events["events/ — envelope + 12 domain modules"]
    surfaces["per-domain surfaces — llm, aja, tara, nyx,\nmetis, nisaba, veritas, oya, v3, v6, v9, iris, agent"]
    bffmodels["BFF-canonical — atelier, identity,\nlibrary, messaging, studio"]
  end

  subgraph siblings["Sibling Nx packages (own package.json)"]
    iris["@iris/contracts"]
    concordia["@concordia/contracts"]
    psyche["@psyche/contracts"]
    industrial["@freya · @contracts/brigid · cybele ·\nsaraswati · annapurna · athena"]
    maat["@maat/contracts (scaffold)"]
  end

  isis --> common
  isis --> events
  yemaya --> common
  veritas --> surfaces
  bff --> bffmodels
  bus --> events
  consumers --> siblings

  common --> zod["zod (only runtime dep)"]
  events --> zod
  surfaces --> zod
  bffmodels --> zod
  siblings --> zod
```

---

## `@oshun/contracts` Internal Structure

`src/index.ts` is the root barrel; `libs/contracts/package.json` defines the
subpath `exports` map. The two surfaces are deliberately _not_ identical, which
is an important detail when importing:

- **Root barrel re-exports** (`import { … } from '@oshun/contracts'`): `common`,
  `events`, `llm`, `aja`, `arete`, `tara`, `nyx`, `living-scene`, `nisaba`,
  `metis`, `veritas`, `iris`, `agent`, `oya`, plus the namespaced `V3Contracts`,
  `V6Contracts`, and `V9Contracts`.
- **Subpath-only surfaces** (reachable _only_ via an explicit subpath, never the
  root barrel): `./atelier`, `./identity`, `./library`, `./messaging`,
  `./studio`, and `./tts` (with `./tts/providers`, `./tts/reference-client`,
  `./tts/ssml-emotion`). These are excluded from the root barrel to keep the
  default import surface — and its name collisions — manageable.
- **Asymmetries worth knowing:** `v9` is reachable through the root barrel but
  has **no** `./v9` subpath export; `./tts` has a subpath export but is **not**
  in the root barrel. Import from the most specific subpath you can.

Consumers should prefer the narrowest subpath (`@oshun/contracts/events` over
the root) so they pull in only the schemas they actually use.

### `common/` — platform entities and governance

`src/common/index.ts` re-exports roughly **160 source modules** (≈285 files
including co-located tests) — by far the largest area. `primitives.ts` is the
foundation every other module imports: `UUIDSchema`, `SlugSchema`,
`TimestampSchema`, the offset/cursor pagination schemas, `ErrorSchema` +
`ErrorCodes`, and `MetadataSchema`. Using these shared primitives — rather than
each module re-deriving `z.string().uuid()` — guarantees the platform represents
identity, time, paging, and errors identically everywhere.

On top of the primitives sit the canonical platform entities (`user.ts`,
`asset.ts`, `project.ts`, `persona.ts`, `voice-profile.ts`, `avatar-pack.ts`,
`model-card.ts`, `consent-record.ts`, `claim.ts`, `evidence-pack.ts`, and many
more) and the much larger body of admin-console, privacy/DSAR,
safety/moderation, and grounded-research contracts. The `specifications.md` page
documents these field-by-field; the architectural point is that consent, audit,
and provenance are treated as **first-class cross-domain concerns** — every
domain's consent flow produces a `ConsentRecord` of the same shape, whose Zod
`superRefine` validators encode the lifecycle rules (timestamp ordering,
verified verification for high-risk categories) so they are enforced at parse
time rather than scattered across service logic.

### Per-domain canonical surfaces

Some domains have contract requirements rich enough to warrant their own
organized module while still living inside the zero-upstream package. Each is an
independently-importable subpath:

- **`./llm`** — the canonical LLM gateway contract: the `IsisLLMClient`
  interface plus `primitives`/`messages`/`tools`/`request`/`response`/`stream`/
  `errors`/`client` modules and a `./llm/test-utils` gateway double. The header
  comment is explicit that gateway implementations must depend on
  `@oshun/contracts/llm` only — never on `@isis/llm-providers` internals — so
  the backend can be swapped behind the interface.
- **`./aja`** (motion-AI / embodied instruction), **`./arete`** (habits/goals),
  **`./tara`** (contemplative-practice taxonomies), **`./nyx`** (sky-event
  canonical contracts — distinct from `events/nyx.ts`), **`./metis`**
  (learning/tutoring), **`./nisaba`** (manuscript/scholarship), **`./veritas`**
  (fact-check canonical contracts — distinct from `events/veritas.ts`).
- **`./oya`** — the §3.2 embodied-hive surface (drones/ground robots sharing one
  world model and safety envelope). Its `index.ts` states the contracts are kept
  coherent with the Rust engine types in `libs/oya/engine` (`oya-types`,
  `oya-fleet`, `oya-safety`, …) so payloads validate identically on both sides.
  `oya/fleet.ts:121` encodes the _hard capability gate_ in code —
  `bidIsCapabilityValid(agent, bid)` returns
  `agent.capabilities.includes(bid.requiredCapability)` — the contract-layer
  mirror of the engine's safety invariant.
- **Versioned product lines** — `./v3` (Lilith metaverse / Tara Studio /
  Saraswati Stage), `./v6` (the Egbe/Ori identity contracts:
  `ori-identity-core`, `ori-passport`, `personality-model`, `relationship-edge`,
  plus a generated `openapi.ts` and a `registry.ts`/`fixtures.ts`), and `v9`
  (Metis: the Atlas `concept-graph`, the Hephaestus `explorable`, and the
  `lesson` artifact).
- **Root re-exports** — `living-scene` (score + cinematographic-technique
  catalog), `iris` (memory-entry + continuation contracts, also a sibling
  package — see below), and `agent` (the §18.11/§18.12 tool-catalog and grants
  contracts).

### BFF-canonical models

Five subpath-only surfaces — `./atelier`, `./identity`, `./library`,
`./messaging`, `./studio` — are a newer category: canonical models for tables
the OSHUN BFF currently writes to under `goal3_stub_*` names. Each promotes an
ad-hoc BFF table into a typed, versioned contract with an explicit state machine
and `superRefine` invariants. For example, `studio/index.ts` models a
`StudioScene` whose `drafting → review → policy-cleared → published` machine
requires the lilith-persona-policy adapter to have stamped a decision before
publish; `library/index.ts` models a polymorphic `LibraryCollection` whose items
are kind-prefixed slugs so the canonical table needs no cross-domain foreign
keys; `identity/index.ts` models a SCIM-provisioned `ScimIdentity` with a replay
`changeLog`; `messaging/index.ts` and `atelier/index.ts` model channel links and
one-to-one shareable scenes with a cue-privacy hash. These exist so cross-tenant
analytics, the erasure pipeline, and routing all share one lookup.

---

## The Event System

The event subsystem (`src/events/`) is the most architecturally load-bearing
part of the library, because it is the contract that lets twelve domains publish
and consume each other's events over the bus without sharing code.

### Envelope and typed-schema factory (`events/envelope.ts`)

Every event on the bus is wrapped in `EventEnvelopeSchema`. On the base schema
`payload` is `z.unknown()`, so infrastructure code can deserialize and route an
event using only envelope fields (`id`, `type`, `source`, `timestamp`,
`version`, `priority`, `metadata`, `aggregate`) without knowing the payload
type. The `type` is a dotted `domain.action` string validated by
`/^[a-z0-9]+(\.[a-z0-9_]+)+$/`; `source` is the 13-value `EventSourceSchema`
(`tara`, `isis`, `sophia`, `hathor`, `bellona`, `yemaya`, `lilith`, `aphrodite`,
`nyx`, `psyche`, `veritas`, `concordia`, `system`). Correlation and causation
IDs live inside `metadata`, not on the envelope root, and `id` is a UUID v4 —
not a ULID.

Typed per-event schemas come from the
`createEventSchema(eventType, source, payloadSchema)` factory, which
`.extend()`s the envelope with a literal `type`/`source` and a concrete
`payload`. Every domain module builds its events this way. The same file also
defines the publish/consume infrastructure contracts —
`EventPublishOptionsSchema` (routing, idempotency, retry),
`EventPublishResultSchema`, `EventConsumerConfigSchema` (batch/retry policy),
and `DeadLetterEventSchema` for failed-processing capture.

### Two-tier registry (`events/index.ts` + `events/validation.ts`)

There are deliberately **two** registries, and the difference between them is a
real failure mode to understand:

1. **`AllEventTypes`** (in `events/index.ts`) is a `const` map of _every_
   event-type string — **181 strings across 12 domains** (isis 10, sophia 9,
   hathor 7, bellona 8, yemaya 12, lilith 10, tara 1, aphrodite 24, nyx 24,
   psyche 37, veritas 27, concordia 12). It is compile-time documentation and
   the typo-proof way to reference an event by name. Adding a string here does
   _not_ make the event runtime-validatable.
2. **`EventSchemaRegistry`** (a `Map<string, EventSchemaEntry>` in
   `events/validation.ts`) maps a type string to its full Zod schema _and_
   payload schema, so `validateEvent` can check envelope and payload in one
   call. This registry is **partial by design**: it currently registers only the
   Isis, Tara, Sophia, Hathor, Bellona, Yemaya, and Lilith schemas — **57 of the
   181 types**. Aphrodite, Nyx, Psyche, Veritas, and Concordia events are
   _defined_ (their `*EventSchema` exists in the domain module) but are **not
   wired into the runtime registry**.

The consequence: `validateEvent` validates the envelope of _any_ event, but only
runs payload validation for the 57 registered types; for the rest it returns
`{ success: true }` after envelope-only checks
(`if (!schemaEntry) return { success: true, data: envelopeResult.data }`). When
publishing, `createPublishValidator` warns rather than throws on an unregistered
type. This is an honest fail-open seam, not a bug, but it means **registering a
new event in the `Map` is the step that actually turns runtime validation on** —
and is recommended for any event crossing a trust boundary.

### Validation pipeline and middleware

`events/validation.ts` exposes the runtime API: `validateEvent`,
`validatePayload`, their `*OrThrow` variants (which raise the structured
`EventValidationError` carrying simplified `ValidationIssue[]`),
`isEventTypeRegistered`, `getEventSchema`/`getPayloadSchema`,
`getRegisteredEventTypes`, and `getEventTypesByDomain`. Two higher-level helpers
wrap these for the bus: `createValidationMiddleware` wraps consumers (strict =
throw, non-strict = warn-and-continue) and `createPublishValidator` wraps
producers. A typical flow is: producer builds a payload →
`createPublishValidator` checks it against the registry → envelope is published
→ consumer's `createValidationMiddleware` re-validates on receipt before the
handler runs.

The Concordia module is a partial exception that points at the future direction:
it ships its own `ConcordiaEventSchemaRegistry` (every Concordia event keyed by
type) inside the domain module, demonstrating per-domain registries that the
central `EventSchemaRegistry` does not yet aggregate.

---

## Sibling Nx Packages

Ten further packages share the directory but are independent libraries — each
(except the `@maat/contracts` scaffold, which carries only a `project.json`)
versioned on its own `package.json` cadence, each still bound by the
zero-upstream rule.

- **`@iris/contracts`** (`iris/`) — Iris AI-assistant contracts: conversation
  (message content blocks, streaming events), a four-tier memory model (core /
  working / archival / episodic) with operations and `MemoryState`, and agent
  contracts (tool definitions, agent config/execution, `MEMORY_TOOLS`). Subpaths
  `./common`, `./conversation`, `./memory`, `./agent`.
- **`@concordia/contracts`** (`concordia/`) — the largest sibling: ~100 feature
  directories (case model, parties, issues, agreements + agreement DSL,
  preference/utility models, settlement lifecycle, access/authority/consent,
  escalation, search kernels, oversight, and cross-domain integration modules).
  Its `index.ts` barrel is **honest that it is partial** — it currently exports
  the use-case-classification surface (Phase 179.1.2.1), with the case/party/
  issue/agreement/preference schemas slated for follow-on Phase 179.2.\* tasks.
  The full source tree exists; the barrel is the gate.
- **`@psyche/contracts`** (`psyche/`) — Psyche video-conferencing AI contracts.
  Currently exports only its `common/` module; avatar, voice, behavior,
  perception, conferencing, knowledge, and persona modules are commented as _to
  be added as services are migrated_ — a real, declared in-progress state.
- **Industrial / commerce packages** — `@freya/contracts` (luxury goods),
  `@contracts/brigid` (industrial), `@contracts/cybele` (construction),
  `@contracts/saraswati` (cross-domain integration adapters to
  Brigid/Asase/Freya/ Cybele/Maat), and `@contracts/annapurna` +
  `@contracts/athena` (commerce / food-service domains). The latter group
  follows a uniform six-module shape — `api-schemas.ts`, `events.ts`,
  `integration.ts`, `graphql.ts`, `grpc.ts`, `openapi.ts` — providing real Zod
  request/response schemas (e.g. Annapurna's `AnnapurnaOrderCreateSchema`,
  `AnnapurnaMoneySchema` with a currency enum), though these are smaller
  surfaces than the platform core.
- **`@maat/contracts`** (`maat/`) — a **scaffold only**. `src/index.ts` exports
  just the `MaatContractEnvelope<TPayload>` interface; the subdirectories
  (`agents/`, `compliance/`, `finance/`, `supply-chain/`, …) hold only
  `.gitkeep` files. The Maat contract surface is not yet implemented.

> Note: `libs/contracts/veritas/` is an empty placeholder (only `.gitkeep`). The
> implemented Veritas canonical contracts are the `@oshun/contracts/veritas`
> _subpath_ (`libs/contracts/src/veritas/`), not this sibling directory.

---

## Invariants, Failure Modes, and Extension Points

**Zero-upstream dependency.** The hard invariant. Any PR that adds an `@oshun/*`
import to any package under `libs/contracts/` must be rejected — primitives both
sides need (like a `UserId`) are expressed as plain Zod string schemas rather
than imported.

**Compile-time and runtime in lockstep.** Every type is `z.infer`-ed from its
schema, so a schema change propagates to TypeScript types and runtime validators
together. Zod was chosen over JSON Schema or bare interfaces precisely because
events arrive from the bus and HTTP bodies as raw JSON where types are erased.

**Adding an event (the four-step lifecycle).** (1) Define a `*PayloadSchema` and
`*EventSchema` (via `createEventSchema`) in the domain's `events/*.ts`. (2) It
surfaces automatically through `export *` in `events/index.ts`; add the type
string to `AllEventTypes`. (3) _To enable runtime validation_, register the
schema in `EventSchemaRegistry` in `events/validation.ts` — the step most easily
forgotten, and the difference between a discoverable type and a validated one.
(4) Bump the envelope `version` (semver, default `1.0.0`) on a breaking payload
change, keeping the old schema for in-flight consumers during rolling deploys.

**The registry-coverage gap is the main subtle failure mode.** Because
`EventSchemaRegistry` covers only 57 of 181 types, a malformed Aphrodite/Nyx/
Psyche/Veritas/Concordia _payload_ will pass `validateEvent` (envelope-only) and
only fail deeper in the consumer. Treat envelope-passing as necessary, not
sufficient, until the type is registered.

**Contract ↔ Prisma alignment.** `common/contract-prisma-alignment.ts` defines
the alignment-checking schema, exercised by
`common/contract-prisma-alignment.test.ts` and
`contract-prisma-migrations.test.ts`. These guard against the class of bug where
a migration adds a non-nullable column but the contract still treats the field
as optional, which would surface as a runtime deserialization failure on
contract-validated payloads. `contracts.spec.ts` provides self-consistency tests
over the common and event schemas.

**Cross-domain integration boundaries.** Contracts owns _shape_, never behavior.
Product logic stays in each domain's own libraries; the `oya` surface stays
byte-coherent with the Rust `libs/oya/engine` types; the LLM surface stays
implementation-agnostic behind `IsisLLMClient`; the BFF-canonical surfaces
promote ad-hoc tables into typed contracts. Where a real integration is absent —
`@maat/contracts`, the partial `@psyche/contracts`, the gated
`@concordia/contracts` barrel — the package says so explicitly rather than
faking coverage. That honesty _is_ the contract: a fail-loud scaffold beats a
fabricated surface.
