Skip to content
GitHub
Contract Surfaces

Contract Surfaces

Strict context boundaries are what let the world model stay an authority customers cite — the two pillars (worlds and platform) cannot share state directly because shared state would erode the isolation between the standard and the system being measured against it. This page covers how the contexts cooperate without coupling: three categorical seam types, then a simplest-fit ladder for picking the right seam shape per integration.

This page serves three readers. Engineers picking a mechanism for a new integration get a ladder + worked examples + the validator rules that enforce the choice. Strategic readers understand why this discipline exists at all (the world model’s authority depends on it). Reviewers can audit any specific integration against the ladder. The mechanism choices are mechanical rather than aesthetic: get them right and the authority isolation holds; get them wrong and the contexts couple in ways that compromise the world model’s standing.

Spectral’s three contexts (spectral.core, spectral.worlds, spectral.platform) are code boundaries. Library code in worlds cannot import library code in platform, and vice versa. Integration happens through three categorical surfaces:

SurfaceOwnerLives inConsumed by
Substrate transport (envelope, retry, idempotency)core (kernel)spectral.core.eventsEvery context’s listener infrastructure
Producer-owned typed payloads + ProtocolsThe producing context<context>.contracts.events.*, <context>.contracts.protocols.*Bilateral contract tests + framework-layer (apps/*) consumers; never the other context’s library code
Framework-layer bridge toolsapps/* (e.g. apps/workers)apps/* framework deliverablesAgent runtimes wired in by the workers entrypoint

The categorical split is the durable doctrine; the shape an interaction takes within those surfaces follows from its flow shape (notification or call) and, within call flow, from a simplest-fit ladder. The decision lineage lives in ADR-065 (canonical contract surface + admission rules + payload pattern), ADR-063 (no SQL grants between contexts), and ADR-070 (mechanism selection ladder).


When a producer publishes a fact and consumers react without the producer awaiting a result.

  • The substrate envelope shape is declared in spectral.core.events (owned by core). Producer-owned typed event payloads live in <producer>.contracts.events.* per ADR-065 D2; the catalog at Events.
  • The producer writes a row to core.outbox inside its business transaction.
  • The event substrate (LISTEN/NOTIFY + transactional outbox) delivers the event to subscribed consumers.
  • Consumers parse the envelope payload via their own local <EventName>Event model per ADR-065 D4 (consumer ACL — anti-corruption layer — pattern); they never import the producer’s typed class outside tests/contracts/.
  • Consumer-side state is the consumer’s own concern, materialized into the consumer’s own schema.
  • Replay, additive-only payload evolution, DLQ, and observability are inherited from the substrate.

Call flow → simplest-fit ladder per ADR-070

Section titled “Call flow → simplest-fit ladder per ADR-070”

When a caller dispatches a request and needs the result back, the mechanism is selected by a simplest-fit ladder. The default is the smallest layer that fits the consumer’s behavior characteristics; escalate only when an explicit eval condition holds. See Mechanism selection — the simplest-fit ladder below for the full ladder + selection rule + worked examples per Tier.

The two tiers in active use today:

  • Tier 1 — Use case handler in <callee>.application.*, consumed by the framework deliverable (apps/api route, apps/workers handler, apps/test-agents harness) per validator rule 7. Default for single-Python-consumer call flow. The application layer IS the context’s published surface in Clean Architecture; use case handlers ARE its API surface.
  • Tier 2 — Callee-owned OHS Protocol in <callee>.contracts.protocols.* per ADR-065 D3 + ADR-070 D1. Required when one or more is true: multiple framework consumers need the surface; LLM-tool wrapping (the apps/workers/tools/ pattern per ADR-060 D5); cross-process / cross-language boundary; genuine polymorphism. The Protocol is owned by the context that implements the capability and is consumed by apps/* framework deliverables only.

Tier 2’s full mechanism shape:

  1. Callee-owned OHS Protocol in <callee>.contracts.protocols.*. (E.g. spectral.worlds.contracts.protocols.world_agent.WorldAgentRunner.)
  2. Implementation in the callee context’s application layer (e.g., spectral.worlds.application). The implementation never imports from the caller context.
  3. Bridge tool in apps/* per ADR-065 D5. The bridge imports <callee>.contracts.protocols.* (framework-to-context, allowed under validator rule 7) and is composed into the caller agent’s tool list via DI at workers startup. The caller agent (e.g. Ops Agent in spectral.platform.application) sees an opaque tool callable — never the Protocol or its implementation.
  4. Caller-context code never imports from another context. The seam is structural (validator rule 2 enforces); bridge tools live at the framework layer, not in caller-context code.

WorldAgentRunner (in spectral.worlds.contracts.protocols.world_agent) is the only currently-implemented Tier 2 + Tier 4 example; future Tier 2 protocols (e.g., the EvalSet provider seam landing with the Evaluate-phase consumer epic) follow the same shape. Auto-generated docs at Protocols. For shapes that only need eventual consistency on a fact already recorded — e.g., the platform view of accepted/rejected rule candidates in spectral.platform — notification flow with a richer event payload + a consumer-side projection (per ADR-065 D4) is the cleaner choice (see ADR-055 D3 Option A).


No SQL grants between contexts at any layer. The architecture validator’s invariant AST rule flags any direct SQL access (psycopg connection, ORM session, or raw SELECT / INSERT / UPDATE / DELETE text) targeting another context’s schema from outside the framework-layer composition entrypoints. There is no allowlist; violations block CI.

The core schema is shared substrate (outbox, retention registry, embedding profile, deployments, workers, users mirror) — access to core from worlds or platform is unaffected by the rule. Within a single context’s own schema, RLS and access controls are also unaffected.


Mechanism selection — the simplest-fit ladder

Section titled “Mechanism selection — the simplest-fit ladder”

Each tier is the default for its problem shape; escalation requires an explicit failure of the tier below (per ADR-070).

TierMechanismWhere it livesDefault for
1Use case handler — direct callable<callee>.application.* (consumed by apps/* per validator rule 7)Single-Python-consumer call flow (e.g., apps/api request-scope route)
2Callee-owned OHS Protocol + implProtocol at <callee>.contracts.protocols.*; impl at <callee>.application.*Multi-framework-consumer call flow; LLM-tool wrapping; cross-process; genuine polymorphism
3Event substrate — producer-typed payload + consumer ACL + projectionPayload at <producer>.contracts.events.*; ACL local to consumer; substrate from spectral.core.eventsNotification flow (producer broadcasts; consumers don’t await)
4Bridge tool — composes Tier 2 Protocol into a framework deliverableapps/workers/tools/ or apps/mcp-<bridge>/Agent-to-agent delegation (e.g., Ops Agent → World Agent)

Direct import between contexts is forbidden by default (per ADR-065 D7); the mermaid below flags it explicitly. ADR-mint per exception only.

TierCallerNeedMechanism
1apps/api operator route — health dashboard backendRead current world-model health snapshot for the dashboardDirect import of worlds.application.world_model_health.ReadWorldModelHealth use case handler per validator rule 7. Single Python in-process consumer; no Protocol indirection.
2Ops Agent ask_world_agent tool (in apps/workers/tools/)Question → answer string from World AgentDI-injected WorldAgentRunner Protocol (defined in spectral.worlds.contracts.protocols.world_agent). Tier 2 because: multi-framework-consumer (Ops Agent + Spectral Agent + apps/test-agents harness) per ADR-070 D3 and LLM-tool wrapping per ADR-060 D5.
3worlds T3 observation consumer (World Agent’s evolution loop)Body content of recently-written T3 memories for evidence accumulationSubscribes to platform.t3_memory.written; consumer-side ACL parses the payload with worlds-local T3MemoryWrittenEvent model; projection writes to a worlds-local replica table. Notification-shaped read; no Reader Protocol per ADR-064 D3 (broadened).
3spectral.platform’s SystemCard projection handlerMaterialize a SystemCardSnapshot row when a new world-model version publishesSubscribes to worlds.world_model_card.published event; consumer-side ACL parses the payload with a local WorldModelCardPublishedEvent model; projection writes to system_card_snapshots table keyed by evaluation_authority_ref.
3Operations outcome view in spectral.platformRead of accepted/rejected rule candidatesrule_candidate_outcome_recorded event + Operations-side local replica in platform.rule_candidate_outcomes_replica per ADR-055 D3 Option A.
3 (intra-platform)FailureClusterService → Operations Agent”A failure cluster was detected”platform.failure_cluster.detected event consumed by Operations to upsert platform.rule_candidates_pending.
4Ops Agent’s ask_world_agent tool (the bridge layer)Compose WorldAgentRunner Protocol (Tier 2) into the agent’s tool list at workers startupThe bridge tool lands at apps/workers/tools/ask_world_agent.py (per the Tier 4 framework-layer pattern); it imports the Protocol per validator rule 7, binds an impl via DI, and exposes an opaque tool callable to the Ops Agent’s LangGraph graph. Tier 4 implies Tier 2.

Intra-platform Tier 3 (producer + consumer both in spectral.platform) uses the same substrate mechanism as cross-context — the <context>.contracts.events.* placement reflects ownership, not transport.

The diagram encodes the full decision tree. Two operational notes the diagram cannot carry:

  • The two Tier 1 gates are named: a functional gate (does a use case in <callee>.application.* satisfy the need?) and a shape gate (is direct callable + import idiomatic for the consumer’s behavior characteristics?). Either failing escalates to Tier 2.
  • Tier 2 escalation requires documenting which of the four conditions (multi-consumer, LLM-tool wrap, cross-process, polymorphism) triggered it on the consuming epic.
  • “Multiple framework consumers” considers near-term realistic consumers, not strict current-state count. A Protocol with an obvious multi-consumer trajectory (apps/workers/tools shared across all three agents; multiple apps/api routes consuming the same surface) lands at Tier 2 now.
  • Test decoupling alone does not justify Tier 2 — Tier 1 use case handlers are mockable via DI of a callable or import-path patching. Tier 2 is reserved for the four conditions in the table above.

The workers entrypoint is the framework-layer composition seam for agent-resident code. It is the one place the import boundary between contexts is intentionally crossed — by code that is not context code, just runtime composition. Per ADR-031, apps are framework-layer leaves; per ADR-060 D-runtime, all three Spectral agents run in workers; per the context isolation rule, agent context code never imports another context.

Closed-over factories provide the wiring: the workers entrypoint instantiates the target-context implementation at startup and passes it as a positional argument to the caller-context’s tool factory or handler factory. The caller’s code receives a typed protocol; it never sees the concrete class or its imports.

Composition factory shape — registration list of reader Protocols

Section titled “Composition factory shape — registration list of reader Protocols”

The workers composition factory accepts an explicit registration list of reader Protocols at startup, not an open-ended container or a service-locator lookup. Each entry pairs a Protocol type (the caller’s consumption surface) with a concrete factory that returns the callee-context implementation. The factory is invoked once at workers boot; the resulting typed Protocol is closed-over by the agent-resident tool factories that need it. A new reader between contexts is added by extending the registration list; nothing else in agent context code changes. The list is also the authoritative inventory the architecture validator uses to confirm every import between contexts lives at the workers framework layer rather than inside context code.


Events earn their keep when:

  • The producer should not block on consumer behavior (one-way push).
  • Multiple consumers may react to the same fact independently.
  • Replay, audit, and observability of the fact are first-class.
  • Consumers want eventual consistency through their own materialized state, not real-time reads of the producer’s state.

When the caller needs a result back, events introduce correlation IDs, timeouts, suspend-resume, and lost-response handling for what is structurally an in-process function call. DI at the framework-layer seam is simpler and uses primitives that already exist.


tools/quality/validate_architecture.py carries the AST rule that flags direct SQL access between contexts from outside framework-layer composition entrypoints. The rule is invariant — there is no allowlist. The rule extends naturally to any future SQL pattern between contexts, including stored-procedure invocation, view access, or direct psycopg query construction targeting another context’s schema.