This document supersedes Arbiter's local draft at `~/Business/workspace/Arbiter/docs/APOPHIS_OUTBOUND_MOCK_PROPOSAL.md` and its interim adapter at `~/Business/workspace/Arbiter/src/server/server/services/StripeFetchAdapter.js`.
The direction in that proposal is correct: routes should be able to declare the contracts and expectations of their outbound dependencies, and APOPHIS should use those declarations to generate mocks, inject dependency-layer chaos, and support both contract testing and imperative E2E testing.
This spec keeps that idea small and consistent with the runtime paths APOPHIS already has.
## Goals
1. Let routes declare outbound dependency contracts once and reuse them anywhere.
2. Generate contract-conformant outbound mock responses from JSON Schema.
3. Apply chaos at the dependency layer, before application code receives the response.
4. Record outbound calls so tests and contracts can inspect them.
5. Work in both APOPHIS contract tests and imperative E2E tests.
6. Reuse existing chaos, fast-check, and flake-detection infrastructure.
7. Avoid service-specific adapters and avoid a second testing engine.
## Non-Goals
1. No Stripe-specific or service-specific code in APOPHIS.
2. No second DSL for outbound expectations.
3. No new backward-compatibility layer for old chaos config.
4. No static JS analysis in this cut. The design must enable it later, not implement it now.
## Parsimony Rules
1. One schema annotation: `x-outbound`.
2. One shared registry: `outboundContracts`.
3. One runtime owner: `OutboundMockRuntime`.
4. One fetch interception path: reuse `wrapFetch()` and `createOutboundInterceptor()` instead of inventing another chaos stack.
5. One property-generation engine: reuse `convertSchema()` for dependency responses instead of creating a second generator pipeline.
6. One seeded randomness model: derive outbound mock randomness from the same test seed via sub-seeds, never `Date.now()`.
7. No service adapters in core. If an application wants an adapter, it should be a thin local wrapper over fetch, not the core abstraction.
## Core Design
### 1. Shared outbound contracts are registered once
Line numbers below are current as of 2026-04-27 and should be rechecked before editing.
### 1. `src/types.ts`
Current anchors:
1.`RouteContract`: lines 28-42
2.`TestConfig`: lines 256-264
3.`ChaosConfig`: lines 308-355
4.`ApophisOptions`: lines 498-519
5.`ApophisDecorations`: lines 612-627
Modify:
1. Add `outbound?: readonly OutboundBinding[]` to `RouteContract` at lines 28-42.
2. Add `OutboundContractSpec`, `OutboundBinding`, `ResolvedOutboundContract`, and `OutboundCallRecord` near existing outbound chaos types at lines 266-356.
3. Add `outboundMocks?: false | { ... }` to `TestConfig` at lines 256-264.
4. Add `outboundContracts?: Record<string, OutboundContractSpec>` to `ApophisOptions` at lines 498-519.
5. Add `registerOutboundContracts`, `enableOutboundMocks`, `disableOutboundMocks`, and `getOutboundCalls` to `ApophisDecorations` at lines 612-627.
Keep it parsimonious:
1. Do not add a second chaos config type.
2. Do not add separate inline-vs-reference types if a discriminated union on `x-outbound` handles both.
3. Keep `OutboundContractSpec` JSON-Schema-centric so `convertSchema()` can reuse it directly.
### 2. `src/domain/contract.ts`
Current anchor: lines 35-95 extract `x-category`, `x-requires`, `x-ensures`, and `x-timeout`.
Modify:
1. Parse `schema['x-outbound']`.
2. Normalize string refs and inline objects into `RouteContract.outbound`.
3. Preserve current caching behavior in `contractCache`.
Keep it parsimonious:
1. Normalize shape here once.
2. Do not resolve references here because this module does not own plugin-level registries.
3. Do not add outbound-specific execution logic here.
### 3. `src/plugin/index.ts`
Current anchor: lines 29-102 own plugin setup, registries, route capture, and decorations.
Modify:
1. Instantiate an `OutboundContractRegistry` and `OutboundMockRuntime` next to existing registries.
2. Register `opts.outboundContracts` during plugin setup.
3. Add imperative outbound mock decorations.
4. Register a built-in outbound extension that exposes `outbound_calls(this)` and `outbound_last(this)`.
Keep it parsimonious:
1. Do not create a separate plugin or extension package for outbound support.
2. Reuse existing plugin lifecycle instead of bolting on a second orchestrator.
### 4. `src/plugin/contract-builder.ts`
Current anchor: lines 14-29 build the config passed into `runPetitTests()`.
Modify:
1. Pass through `opts.outboundMocks`.
Keep it parsimonious:
1. No logic here beyond forwarding config.
### 5. `src/plugin/stateful-builder.ts`
Current anchor: lines 13-29 build the config passed into `runStatefulTests()`.
Modify:
1. Pass through `opts.outboundMocks`.
2. Keep parity with `contract-builder.ts`.
### 6. `src/test/petit-runner.ts`
Current anchors:
1. lines 237-243 function signature
2. line 322 constructs `EnhancedChaosEngine`
3. lines 342-430 build request and execute the route
4. lines 497-512 attach chaos diagnostics
Modify:
1. Resolve `route.outbound` against the shared registry before execution.
2. Install `OutboundMockRuntime` around the single route execution.
3. If `chaosEngine.executeWithChaos()` returns `outboundInterceptor`, compose it into the runtime instead of inventing a second path.
4. Attach outbound call trace into diagnostics and eval context.
5. In property mode, expand outbound response scenarios using `convertSchema(..., { context: 'response' })` and deterministic seeds.
Keep it parsimonious:
1. Do not create a second runner.
2. Do not fork request generation logic; augment the existing execution loop.
3. Reuse the runner seed and `SeededRng` instead of introducing local randomness.
### 7. `src/test/stateful-runner.ts`
Current anchors:
1. line 25 imports legacy `ChaosEngine` from `../quality/chaos.js`
2. line 62 stores `chaosEngine?: ChaosEngine`
3. lines 272-279 execute with chaos
4. line 394 constructs `new ChaosEngine(config.chaos, config.seed)`
Modify:
1. Migrate stateful testing to `EnhancedChaosEngine` from `src/quality/chaos-v2.ts`.
2. Install the same outbound mock runtime used by `petit-runner.ts`.
3. Use the same outbound scenario generation rules so stateful and contract runners do not diverge.
Keep it parsimonious:
1. Do not maintain two chaos stacks.
2. Do not implement outbound mocking twice.
### 8. `src/quality/chaos-v2.ts`
Current anchors:
1. lines 52-125 `wrapFetch()`
2. lines 148-188 seed management and outbound interceptor construction
3. lines 214-274 route execution and outbound interceptor attachment
Modify:
1. Keep `wrapFetch()` as the only fetch interception primitive.
2. Add a small composition helper if needed so `OutboundMockRuntime` can run `mock response -> outbound chaos overlay -> Response`.
3. Keep per-route chaos resolution in `buildOutboundInterceptor()`.
Keep it parsimonious:
1. Do not move mock generation into chaos-v2.
2. Chaos owns chaos, not contract generation.
### 9. `src/quality/chaos-outbound.ts`
Current anchor: lines 36-105 create the pure outbound interceptor.
Modify:
1. No structural redesign required.
2. Ensure the interceptor remains transport-agnostic and can wrap both real fetch and mock responders.
Keep it parsimonious:
1. This file should stay pure.
2. Do not add runtime registry logic here.
### 10. `src/domain/schema-to-arbitrary.ts`
Current anchor: lines 214-217 export `convertSchema()`.
2. Add a tiny helper for weighted status-code sampling if needed, but do not fork the schema conversion logic.
Keep it parsimonious:
1. No second schema generator.
2. No outbound-specific arbitrary builder unless it is only a thin composition over `convertSchema()`.
### 11. `src/quality/flake.ts`
Current anchor: lines 56-97 derive reruns from a seed.
Modify:
1. Public API can stay unchanged if outbound runtime derives every sub-seed from the rerun seed.
2. If diagnostics are added, include outbound scenario seed in rerun metadata, but do not add a separate flake engine.
Keep it parsimonious:
1. Flake support should come from determinism, not from more feature flags.
### 12. New files
Add only these new files:
1.`src/domain/outbound-contracts.ts`
- normalize, resolve, and validate `x-outbound` bindings against the shared registry
2.`src/infrastructure/outbound-mock-runtime.ts`
- install and restore fetch patch
- record calls
- resolve overrides
- return generated or overridden responses
3.`src/extensions/outbound.ts`
- built-in extension exposing outbound call facts to APOSTL
Do not add service-specific adapters, provider-specific modules, or a separate outbound runner.
## Interaction With Existing Chaos
The order must be:
1. route declares outbound contract
2. runtime resolves contract
3. runtime generates or overrides mock response
4. existing outbound chaos interceptor applies delay/error/dropout if configured
5. application code receives the final dependency response
This keeps chaos at the correct layer and reuses the current outbound chaos implementation.
Do not invert the order by making chaos choose a response before the contract mock runtime runs. Outbound mocking must generate the dependency response first; chaos then mutates or delays that response.
## Interaction With flake detection
This feature must be deterministic under a single seed.
Rules:
1. route command generation already depends on `config.seed`
2. outbound response generation must derive from that same seed
3. per-contract sampling must use stable sub-seeds, e.g. `hashCombine(seed, stableHash(contractName))`
4. route-local chaos and outbound mock generation must not perturb each other beyond their dedicated sub-streams
If these rules hold, `FlakeDetector` needs no public redesign.
## Interaction With property-based testing
Phase 1:
1.`mode: 'example'` uses one generated success response per dependency plus explicit override cases.
2. This is enough to support contract tests and imperative E2E immediately.
Phase 2:
1.`mode: 'property'` samples across every documented outbound status code.
2. For each sampled dependency response, APOPHIS executes the route and checks route postconditions.
3. This gives property-based testing on both sides of the integration boundary.
We should not block phase 1 on phase 2, but the types and runtime must be designed so phase 2 is an additive change, not a rewrite.
5.`src/test/stateful-runner.test.ts` or new stateful integration tests
- stateful runner uses the same outbound runtime and chaos path
6.`src/test/flake.test.ts` new
- same seed gives same outbound responses and same call trace
- different seeds explore different dependency outputs without nondeterministic drift
## Migration Guidance
For Arbiter:
1. move the Stripe contract definitions out of `src/server/server/services/StripeFetchAdapter.js`
2. register them once via `outboundContracts`
3. change route schemas to use `x-outbound`
4. delete the local adapter after APOPHIS fetch instrumentation is in place
The long-term target is that applications declare outbound behavior through `outboundContracts` and `x-outbound`; provider-specific fetch wrappers remain application-local.
## Deferred, But Enabled By This Design
1. static analysis of whether a route contract can be satisfied for all permitted dependency responses
2. detection of impossible route postconditions before running property tests
3. contract coverage reports across inbound and outbound boundaries
Those features should be built later on top of the normalized outbound contract registry, not by expanding the runtime surface prematurely.