ADR-070: Inter-context mechanism selection — simplest-fit ladder
Status: Accepted (2026-04-30) — partially supersedes ADR-065 D4 default-mechanism framing; refines ADR-063 §2 framing
Context
ADR-065 established the inter-context contract surface: producer-owned typed event payloads (D2), callee-owned OHS Protocols (D3), notification-flow handling (D4), and bridge tools (D5). ADR-063 §2 prescribed the callee-owned OHS Protocol + impl in callee context + bridge tool in apps/* shape as “the pattern” for call flow. Together, the two ADRs framed inter-context mechanism selection as a binary: notification → events; call → OHS Protocol.
The wave-5 mints during SPEC-252’s original refinement (EnshrinementGate, WorldModelPublisher, PendingCandidateReader, WorldModelHealthReader all minted as OHS Protocols at spectral.core.tools.protocols) demonstrated the binary framing tilts toward over-engineering. Each of the four had a single Python in-process consumer (an apps/api operator route module) — a use case handler in <context>.application.* consumed via validator rule 7 would have done the job. The OHS Protocol layer added DI indirection, contract-surface curation, and (under ADR-065’s substrate framing once relocated) bilateral contract-test scaffolding, all in service of conditions that did not hold.
The principle that needed explicit doctrine: simplest solution wins; escalate only when the simpler choice fails an explicit eval. ADR-070 makes that principle the inter-context selection rule, with concrete tiers and concrete eval criteria.
Decision
D1. Inter-context mechanism selection follows a simplest-fit ladder
Each tier is the default for its problem shape; escalation requires an explicit failure of the tier below.
| Tier | Mechanism | Where it lives | Default for |
|---|---|---|---|
| 1 | Use case handler — direct callable | <callee>.application.* | Single-Python-consumer call flow (e.g., apps/api route) |
| 2 | Callee-owned OHS Protocol + impl | Protocol at <callee>.contracts.protocols.*; impl at <callee>.application.* | Multi-framework-consumer call flow; LLM-tool wrapping; cross-process; polymorphism |
| 3 | Event substrate — producer-typed payload + consumer ACL | Payload at <producer>.contracts.events.*; substrate from spectral.core.events | Notification flow (producer doesn’t await) |
| 4 | Bridge tool — composes Tier 2 Protocol into a framework deliverable | apps/workers/tools/ or apps/mcp-<bridge>/ | Agent-to-agent delegation |
| 5 | Direct import between contexts | (forbidden — per ADR-065 D7) | ADR-mint per-exception only |
Tier 1 — Use case handler in <callee>.application.* (call-flow default).
The framework deliverable imports the use case directly per ADR-065 D7 + validator rule 7. Direct callable; no Protocol indirection. The application layer IS the context’s published surface in Clean Architecture; use case handlers ARE the context’s API surface for inter-context call flow. The default for apps/api request-scope routes invoking context use cases.
Tier 2 — Callee-owned OHS Protocol in <callee>.contracts.protocols.* (per ADR-065 D3).
Required when one or more is true:
- Multiple framework consumers need the surface (e.g.,
apps/workers/tools/+apps/api+apps/test-agents). - LLM tool wrapping with observability + error-mapping per ADR-060 D5 (the
apps/workers/tools/pattern). - Cross-process / cross-language boundary (e.g.,
apps/mcp-<bridge>/). - Genuine polymorphism — multiple impls need composition-time swap.
Tier 3 — Event substrate (notification flow) (per ADR-065 D2 + D4).
Producer broadcasts a typed payload; consumer parses with its own local ACL model; consumer projects into local state. Used when flow shape is notification (producer doesn’t await result).
Tier 4 — Bridge tool in apps/* (per ADR-065 D5).
When an agent in one context invokes a capability in another context through the framework layer (agent-to-agent delegation; e.g., Ops Agent → World Agent). The bridge composes a Tier 2 Protocol — Tier 4 implies Tier 2.
Tier 5 — Direct import between contexts (forbidden per ADR-065 D7).
ADR-mint per-exception is the only gate.
D2. Selection rule per inter-context need
Stage 1. Flow shape — notification or call?
- Notification (producer broadcasts; consumers don’t await) → Tier 3.
- Call (caller awaits result) → Stage 2.
Stage 2. Apply Tier 1’s two gates:
- (a) Functional gate — Can a use case in
<callee>.application.*satisfy the need? - (b) Shape gate — Is direct callable + import the idiomatic shape for the consumer’s behavior characteristics?
Both pass → Tier 1. Done.
Stage 3. If a gate fails, identify which Tier 2 condition triggers escalation; document on the consuming epic. → Tier 2.
Stage 4. If the call is one context’s agent invoking another context’s capability through the framework layer → Tier 4 (which composes a Tier 2 Protocol; Tier 2 is prerequisite).
D3. Eval-criteria precision
- “Multiple framework consumers” considers near-term realistic consumers, not strict alpha-state count. A Protocol with 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. Don’t down-tier on “exactly one consumer today” — design for system evolution, not alpha-state minima.
- 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 D1.
D4. Substrate Protocols are out of scope
OHS Protocols admitted under ADR-065 D1 at spectral.core.tools.protocols.* (substrate-level Protocols whose killer-test admission passes — e.g., OutboxReader, OutboxReplayer if their substrate-admission eval passes) are substrate-level, not inter-context-level. ADR-070 governs inter-context mechanism selection (between contexts, or context ↔ framework deliverable). Substrate Protocols follow ADR-065 D1’s killer-test admission discipline; the simplest-fit ladder doesn’t apply.
Alternatives considered
- Keep ADR-065 D4’s “defaults to notification flow” framing. Rejected — the wave-5 evidence shows it produces over-engineering for single-consumer call-flow cases. The default-to-notification claim makes sense when consumers are unknown and notification is the right flow shape; for known single-consumer call flow, it’s wrong.
- Per-PR judgment without explicit ladder. Rejected — relies on individual judgment about when OHS Protocols are warranted; produces inconsistent decisions and re-litigation per refinement. The ladder pins the criteria so the judgment doesn’t drift.
- Single doctrine statement: “use case handlers always; escalate on demand.” Rejected — leaves notification flow under-specified, conflates flow-shape selection with mechanism selection, and removes the structural relationship between Tiers (Tier 4 implies Tier 2; Tier 3 is selected by flow shape, not by escalation). The ladder makes those structural relationships explicit.
Consequences
- ADR-065 D4’s “defaults to notification flow” framing is partially superseded. The notification-flow mechanism description (typed payload, ACL, projection) stands. The “defaults to” claim is replaced with the simplest-fit ladder. Per the doctrine in
docs/decisions/overview.md, the superseded segment in D4 is replaced with a one-line pointer to this ADR. - ADR-063 §2’s “the pattern per ADR-065 D3 + D5” framing is refined by this ADR’s ladder. The mechanism description (callee-owned Protocol + impl + bridge tool) stands as Tier 2. The implicit claim that this IS the call-flow pattern is refined: this is the Tier 2 escalation; Tier 1 is the default. ADR-063 §2 carries a pointer to this ADR alongside its existing ADR-065 supersession blockquote.
- SPEC-252 wave-5 OHS Protocols collapse to Tier 1 use case handlers. Affected surfaces:
EnshrinementGate,WorldModelPublisher,PendingCandidateReader,WorldModelHealthReader. All four ship as use case handlers inworlds.application.*; SPEC-238 T2.3 reframe ships this shape (noworlds.contracts.protocols.*mints required for the four wave-5 surfaces). - SPEC-252 body simplifies. The DI seam composition-factory framing reframes around use-case-handler imports rather than Protocol DI. Import-boundary rules:
apps/apimay importworlds.application.*per validator rule 7; noworlds.contracts.protocols.*import needed for the four wave-5 surfaces. - Existing Tier 2 Protocols stay.
WorldAgentRunner,EvalSetProvider,EmbeddingProvider— all multi-consumer or apps/workers/tools wrapping; Tier 2 justified. (T3MemoryReaderwas on this list at mint time but retired by ADR-064 D3 broadened 2026-04-30 — T3 memory reads are notification-shaped, served by event-driven worlds-local projection rather than a Reader Protocol.) - Bridge tools (Tier 4) keep their Protocol prerequisites. Ops Agent / Spectral Agent / World Agent agent-tool surfaces continue using OHS Protocols at
<context>.contracts.protocols.*per ADR-065 D5; no change to apps/workers/tools wiring. - Codex extension lands alongside this ADR. A new section in
system-design/foundations/contract-surfaces.mdxdocuments the ladder with one worked example per Tier. - Broader review queued. Apply the ladder retroactively to all currently-planned OHS Protocols in the M10 re-refinement queue. Initial scan suggests SPEC-252’s wave-5 four are the only over-engineered set; broader review confirms or surfaces additional Tier-1 collapse candidates.
References
- ADR-001 — three-context topology
- ADR-031 — single-library + app leaves; framework-layer composition pattern
- ADR-044 — event substrate (Tier 3 mechanism)
- ADR-060 — agent tool invocation (Tier 4 worked example; LLM-tool wrapping criterion)
- ADR-063 — inter-context access pattern (no SQL grants; §2 framing refined here)
- ADR-065 — inter-context contract surface (D2 → Tier 3; D3 → Tier 2; D5 → Tier 4; D7 → Tier 5; D4 default-mechanism framing partially superseded here)
- ADR-066 — bilateral contract tests (Tier 3 drift detection)
feedback_design_for_system_evolution_not_alpha_state— design for evolution, not alpha-state constraints (D3 eval-criteria precision)project_cross_bc_axis_is_flow_shape— flow shape is the inter-context axis (Stage 1)- Codex
system-design/foundations/contract-surfaces.mdx— pattern documentation (worked-examples section landing alongside this ADR)