Architectural overview of
@oshun/contractsand 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:
"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.
@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 namespacedV3Contracts,V6Contracts, andV9Contracts. - 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:
v9is reachable through the root barrel but has no./v9subpath export;./ttshas 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: theIsisLLMClientinterface plusprimitives/messages/tools/request/response/stream/errors/clientmodules and a./llm/test-utilsgateway double. The header comment is explicit that gateway implementations must depend on@oshun/contracts/llmonly — never on@isis/llm-providersinternals — 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 fromevents/nyx.ts),./metis(learning/tutoring),./nisaba(manuscript/scholarship),./veritas(fact-check canonical contracts — distinct fromevents/veritas.ts)../oya— the §3.2 embodied-hive surface (drones/ground robots sharing one world model and safety envelope). Itsindex.tsstates the contracts are kept coherent with the Rust engine types inlibs/oya/engine(oya-types,oya-fleet,oya-safety, …) so payloads validate identically on both sides.oya/fleet.ts:121encodes the hard capability gate in code —bidIsCapabilityValid(agent, bid)returnsagent.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 generatedopenapi.tsand aregistry.ts/fixtures.ts), andv9(Metis: the Atlasconcept-graph, the Hephaestusexplorable, and thelessonartifact). - Root re-exports —
living-scene(score + cinematographic-technique catalog),iris(memory-entry + continuation contracts, also a sibling package — see below), andagent(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:
AllEventTypes(inevents/index.ts) is aconstmap 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.EventSchemaRegistry(aMap<string, EventSchemaEntry>inevents/validation.ts) maps a type string to its full Zod schema and payload schema, sovalidateEventcan 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*EventSchemaexists 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 andMemoryState, 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). Itsindex.tsbarrel 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 itscommon/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'sAnnapurnaOrderCreateSchema,AnnapurnaMoneySchemawith a currency enum), though these are smaller surfaces than the platform core. @maat/contracts(maat/) — a scaffold only.src/index.tsexports just theMaatContractEnvelope<TPayload>interface; the subdirectories (agents/,compliance/,finance/,supply-chain/, …) hold only.gitkeepfiles. 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/veritassubpath (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.