ADR-099: Core infrastructure zone — in-kernel infrastructure implementations of core contracts
Context
spectral.core defines contracts — Protocols — over shared infrastructure that more than one context needs a concrete implementation of: core.llm.protocols.LLMProvider, core.embeddings.protocol.EmbeddingProvider, core.events.protocols.EventListener, and their peers across the eight functional areas (the eighth, storage, per D5). The contract is correctly placed in the kernel: it is context-agnostic substrate, expressed as a Protocol, and consumed by both worlds and platform.
The concrete implementation of such a contract had nowhere correct to live. A FastEmbed-backed EmbeddingProvider, an outbox-draining EventListener, a traced LLMProvider — each is the single shared concretion of a kernel contract, used by multiple contexts. But the kernel may import only stdlib + pydantic (ADR-097 D2), so it could not host an implementation that imports fastembed, psycopg, or litellm. The implementation was therefore forced into one of two wrong shapes:
- Mislocation into one context’s
infrastructure. A shared concretion placed underworlds.infrastructureorplatform.infrastructuremakes one context the de-facto owner of substrate the other also depends on — and the other context cannot reach it without an inter-context import the validator (correctly) forbids. - Accidental DI-wiring at a shared host. The implementation gets instantiated at
apps/workersorapps/apistartup and injected, which works mechanically but locates shared substrate in a composition leaf by accident of the apps/workers consolidation — there is no home for the substrate itself, only a place it happens to be wired.
Trace the deflation. This is one category — non-domain, non-context kernel code that needs an infrastructure SDK — sitting on one axis: does the code import infrastructure, yes or no? The pure kernel is the “no” side; these shared concretions are the “yes” side. The fix is therefore a path boundary within the kernel, not a new context, not a new top-level package, and not a relaxation of the kernel’s purity. The kernel already partitions by functional area (core/<area>/); this adds a sub-partition within each area for the area’s concrete implementations.
Decision
D1 — A path-globbed infrastructure zone may hold concrete implementations of core contracts
A path-globbed zone, src/spectral/core/<area>/infrastructure/, may hold concrete infrastructure implementations of that functional area’s core-defined contracts. Files in this zone may import:
- the standard library and
pydantic, - infrastructure SDKs (
fastembed,psycopg,httpx,litellm, and peers), and spectral.core(the contracts and value types they implement and exchange).
Files in this zone may import no bounded context (worlds, platform) and no domain types from any context. The zone holds the area’s concretions; it does not reach into any context’s domain model or tables.
D2 — The pure zone keeps kernel purity and may not import the infra zone
The pure zone is everything under core/ that is not under an infrastructure/ sub-directory. It keeps the existing kernel purity unchanged: stdlib + pydantic + spectral.core only, no infrastructure SDKs. In addition, the pure zone may not import the infra zone.
This second clause is the load-bearing one. Because a consumer of a core contract imports it from the pure zone (core.embeddings.protocol, not core.embeddings.infrastructure), and the pure zone cannot reach the infra zone, importing a core contract never transitively drags an infrastructure SDK into the consumer. The Protocol stays free of its implementations; the dependency rule’s innermost-ring guarantee is preserved for everyone who depends on the kernel by its contracts.
D3 — Validator: split core-no-infra-import into a two-zone discipline
ADR-097 D2’s core-no-infra-import rule is amended from a flat “all of core may import only stdlib + pydantic” to a two-zone discipline:
core-no-infra-importnow binds the pure zone: stdlib +pydantic+spectral.coreonly, and no import of the infra zone (D2).- A new slug,
core-infra-zone-no-context, binds the infra zone: infrastructure SDKs + stdlib +pydantic+spectral.coreare allowed; no bounded context and no domain types (D1).
core-no-context-import (ADR-097 D1, rule 1) continues to bind the whole of core/ — both zones — since neither zone may import a context.
The validator implementation (extending tools/quality/validate_architecture.py and the ADR-097 D1 registry table to seat the new slug) is deferred to a later session. This ADR specifies the rule; it does not write the checker. When the registry is updated, core-infra-zone-no-context is added as a new numbered row and core-no-infra-import’s “Enforces” cell narrows to the pure zone.
D4 — ADR-065 D1 carve-out: the type-shape constraint binds the pure zone only
ADR-065 D1’s seven-functional-area type-shape constraint — admitting only frozen Pydantic models, Protocols, enums, Literals, and Final constants — binds the pure zone only. The infra zone, by its purpose, holds concrete classes (a FastEmbedEmbeddingProvider, an OutboxListener): mutable, behavior-bearing, SDK-coupled. Those are categorically the shapes ADR-065 D1 excludes from the kernel’s expression substrate, and excluding them from the pure zone stays correct.
The infra zone is therefore not unconstrained — it is constrained by a different rule. Its admission control is core-infra-zone-no-context (D3): the discipline that keeps it context-free and domain-free, rather than the type-shape discipline that keeps the pure zone expression-only. core-subdir-shape (rule 8) and core-placement (rule 10) likewise apply to the pure zone’s shapes; the infra zone’s infrastructure/ sub-directory is the sanctioned exception to “no code below the area level,” scoped to concrete implementations of that area’s contracts.
D5 — Scope guard: only concrete implementations of the functional-area contracts
The infra zone admits only concrete implementations of the functional-area contracts (auth, events, db, retention, llm, embeddings, tools, storage). It is not a general-purpose escape hatch for anything that wants an SDK in the kernel.
storage is the eighth core area. core.storage is context-agnostic object/blob persistence: a durable, write-once, key→bytes object store (ObjectStore Protocol in the pure zone; the filesystem concretion in the infra zone). It is distinct from db — db is relational/RLS plumbing (session vars, request_scope), whereas storage is opaque-keyed artifact persistence. The store is agnostic of what is stored and of how keys are chosen: content-addressing (computing a sha256 and using it as the key) is a consumer policy layered on top, never a property of the storage contract. See D6 for the first inhabitant.
serialization (a future ninth area) is recorded but not yet seated. Turning values into their canonical wire/storage representation and back — text↔bytes encoding and structured (canonical-JSON / envelope) serialization — is also context-agnostic, multi-consumer substrate that today is owned by nobody (scattered inline .decode() / json.dumps calls). By the same ownership logic that seats storage, it has a home in core, as its own area core.serialization. It is named and chartered here but deliberately not built: its only immediate need is stdlib UTF-8 encode/decode (which has one right implementation and no policy worth a Protocol), so seating it now would be a one-function stub. Build-trigger: the first genuinely-shared serialization decision — a second inhabitant, or swappable serialization policy. Until then consumers call the stdlib inline. When the trigger fires, serialization is seated as a peer area the consumers compose (read bytes from storage, hand them to the codec) — it is never injected into storage, which stays agnostic. Charter it in the ADR body, not only on an issue: D6’s prior “the revisit is noted on the issue” promise was not kept for the module store, which is how the storage relocation was nearly missed — recording the charter here is the correction.
ADR-097 D5’s dissolution of agent-runtime substrate to apps/workers stands. The LangGraph supervisor, checkpointer, tool-factory, and interrupt() orchestration are framework-layer composition, not a core functional area — agent runtime is not among the areas. The infra zone does not readmit them; core/agents remains dissolved.
Killer test for admission to the infra zone: a context-agnostic concrete implementation of a core contract, used by more than one context or composition root. If the candidate reads a single context’s tables, or encodes a single context’s domain logic, it is that context’s infrastructure and stays there — it fails the test. If it is the shared concretion of a kernel Protocol with no domain coupling, it belongs in the zone.
D6 — Canonical first inhabitant and the confirmed/deferred placement map
The canonical first inhabitant is the FastEmbed embedding provider: the concrete EmbeddingProvider implementation relocates to core.embeddings.infrastructure.fastembed. It is context-agnostic (it embeds text; it knows nothing of worlds tables) and is needed wherever embeddings are produced.
Confirmed relocations (worked examples):
- The outbox listener — the
EventListenerconcretion that drains the outbox — relocates tocore.events.infrastructure. It is the shared event-substrate outbox listener, not any one context’s machinery. - The LLM control plane —
TracedProvider, the model-profile registry / router, and the budget / rate-limit enforcement — relocates tocore.llm.infrastructure, implementing thecore.llmcontracts. (This relocation is settled in concert with the LLM-control-plane reconciliation; the budget/usage state itself lives in thecoreschema per ADR-035 D6.)
Stays in context infrastructure (fails the killer test):
- The DB-coupled embeddings profile-resolver and hybrid-retriever stay in
worlds.infrastructure. They readworldstables and encode worlds retrieval policy — they are context infrastructure, not a context-agnostic concretion. Only the SDK-levelEmbeddingProvidermoves; the worlds-coupled machinery that uses it does not.
Resolved (2026-05-31, SPEC-540) — was a deferred candidate:
- The content-addressed module store was deferred here (“stays in
worldsfor now … revisited when the decision-server reader is built”). That reader materialized in SPEC-533 (the load-time hash-reverification loader that reads modules from the platform context), so the store now has two cross-context consumers — worlds deposit + platform load — and is context-agnostic. It passes the killer test and relocates: the agnostic substrate becomescore.storage.infrastructure.FilesystemObjectStoreimplementing thecore.storage.ObjectStorecontract (a durable, write-once, key→bytes object store), consumed directly by both contexts. The content-addressing policy stays inworlds:ContentHashand the sha256-as-key derivation are a worlds-side wrapper over the agnostic store, and the platform loader keeps its own local sha256 recomputation (ADR-080 D2).core.storageitself holds no content-addressing, no hashing, no module naming. This retires theModuleByteSourceOHS-Protocol seam SPEC-533 shipped to work around the store’s mislocation (the worlds-published Protocol, the platform-local twin, and the apps-seam binding are deleted; both contexts importcore.storagedirectly). Note: D6’s “the revisit is also noted on the issue that builds the reader” safety net was not honored — the note was never placed on SPEC-533 — which is why the relocation was missed. The charter-in-ADR-body record added to D5 (forserialization) is the corrective so this failure mode does not recur.
Alternatives considered
Relax the kernel to allow infrastructure SDKs everywhere in core/. Rejected. It would let any kernel module import a framework, re-opening exactly the dependency-direction gap ADR-097 D2 closed and through which agent-runtime substrate slipped into core/agents. The two-zone split keeps the contract surface pure (D2) while giving concretions a sanctioned home (D1); a flat relaxation gives up the former to get the latter.
Promote shared concretions to a new top-level package (e.g. spectral.shared_infra) outside core. Rejected. The concretions implement kernel contracts and belong with the area they serve; a parallel top-level package would split each functional area’s contract from its canonical implementation across two trees and invent a new dependency edge for consumers to learn. The category is “non-domain, non-context kernel code” — it is kernel code, and it belongs under core/, partitioned by the same functional areas.
Make one context the owner of each shared concretion. Rejected. It is the mislocation the Context section names: a shared concretion under worlds.infrastructure makes platform reach across an inter-context boundary the validator forbids, and inverts ownership of substrate both contexts depend on.
Leave the concretions DI-wired at apps/workers / apps/api with no home of their own. Rejected. That locates shared substrate in a composition leaf by accident of consolidation — the substrate has a wiring point but no home, and a second composition root (the decision server) would have to re-wire it independently. The zone gives the substrate a home; composition roots still wire it via DI, but they wire from a canonical location.
Consequences
- One new validator slug,
core-infra-zone-no-context(D3), and a narrowing ofcore-no-infra-importto the pure zone. Both are enforcement additions, not relaxations: the infra zone is newly permitted to import SDKs but newly forbidden to import any context or domain type, and the pure zone is newly forbidden to import the infra zone. Validator implementation is deferred; this ADR specifies the rules. - The kernel’s two axes stay fully enforced, per zone. The pure zone keeps both the dependency-direction discipline (
core-no-infra-import,core-no-context-import) and the type-shape discipline (ADR-065 D1 /core-subdir-shape/core-placement/core-method-purity). The infra zone keeps the context-free discipline (core-infra-zone-no-context+core-no-context-import) and is exempt from type-shape by D4. - Relocations. FastEmbed
EmbeddingProvider→core.embeddings.infrastructure.fastembed(D6); the outboxEventListener→core.events.infrastructure; the LLM control plane →core.llm.infrastructure. The DB-coupled worlds embeddings resolver/retriever stays inworlds(D6). The content-addressed module-store substrate →core.storage.infrastructure(D6 resolution, SPEC-540); the content-addressing wrapper (ContentHash) stays inworlds. - Placement-clause cascade. The placement clauses that located shared substrate by default cascade from this ADR and are amended in lockstep when the relocations land: ADR-038 D5 (embedding-provider placement →
core.embeddings.infrastructure), ADR-044 D7 (event-substrate / outbox-listener placement →core.events.infrastructure), ADR-035 D9 (LLM control-plane placement →core.llm.infrastructure) + D6 (budget/usage state in thecoreschema), and ADR-085 D1 (module-store substrate placement →core.storage.infrastructure; the object-storage substrate is the agnosticcore.storageobject store, content-addressed by the worlds-side consumer — SPEC-540). - Codex. The architecture page states the two-zone kernel structure (pure contracts + an
infrastructure/zone per area for shared concretions) where it previously describedcore/as a single pure layer. - The core-infrastructure-zone boundary is settled by this ADR; the validator implementation (the
core-infra-zone-no-contextchecker, D3) is later-session work. The deferred module-store revisit (D6) is resolved (SPEC-540): the substrate relocated tocore.storage.infrastructure.
References
- ADR-097 — architecture-validator rule registry; D2 (
core-no-infra-import) is split into the two-zone discipline here, and D5 (agent-runtime dissolution) stands (D3, D5) - ADR-065 — contract-surface admission rules; D1’s type-shape constraint is bound to the pure zone here (D4)
- ADR-031 — single-library package + clean-architecture layering; the kernel is the innermost ring and
core/partitions by functional area - ADR-038 — embedding model; D5 embedding-provider placement cascades to
core.embeddings.infrastructure(the FastEmbed first inhabitant, D6) - ADR-044 — event substrate; D7 outbox-listener placement cascades to
core.events.infrastructure(D6) - ADR-035 — LLM stack / in-process control plane; D9 LLM control-plane placement cascades to
core.llm.infrastructure, with budget/usage state in thecoreschema (D6) - ADR-085 — module-store consistency; D1’s “module store substrate is shared between worlds (writer) and platform (reader)” placement cascades to
core.storage.infrastructure(the eighth area, D5; the D6 resolution, SPEC-540). Content-addressing + routing-state projection are unchanged. - ADR-080 — decision-module integrity; content hashing (D1) + the load-time hash reverification (D2) stay consumer concerns over the agnostic
core.storagesubstrate (unchanged by the D6 relocation) - Codex — Architecture — kernel structure (two-zone) statement