# APOPHIS Plugin Contract System Specification ## Status: Partially implemented - Registry, types, and registration API: **implemented** - Runner integration (merging plugin contracts into route execution): **pending** - Built-in contracts for `@fastify/auth`, `@fastify/compress`, `@fastify/cors`, `@fastify/rate-limit`: **registered but not yet applied** **Note**: Plugin contracts are complementary to Protocol Extensions (see `docs/protocol-extensions-spec.md`). Protocol extensions add domain-specific predicates (JWT, X.509, SPIFFE); plugin contracts add hook-phase behavioral contracts for Fastify plugins. ## 1. Overview The Plugin Contract System enables Fastify plugins to declare APOPHIS contracts that are automatically merged into route contracts at test time. Plugins specify which hooks they participate in and what behavioral contracts they enforce at each phase of the request lifecycle. **Key invariant**: Route contracts are the **composition** of route-level contracts plus all plugin contracts whose `appliesTo` pattern matches the route path. ## 2. Terminology - **MUST**: Absolute requirement. Violation prevents system operation. - **SHOULD**: Strong recommendation. Violation produces warnings. - **MAY**: Optional capability. No impact if absent. - **MUST NOT**: Prohibited behavior. Violation is a bug. - **Plugin Contract**: A set of APOSTL expressions declared by a plugin, scoped to specific hook phases and route prefixes. - **Phase Contract**: Contracts that apply at a specific point in the Fastify hook pipeline. - **Contract Composition**: The merging of route-level and plugin-level contracts into a single testable set. ## 3. Architecture ### 3.1 Plugin Contract Declaration Plugins declare contracts via the APOPHIS registry during registration: ```typescript fastify.apophis.registerPluginContracts(name: string, spec: PluginContractSpec) ``` **File**: `src/plugin/index.ts` (NEW METHOD, line 130+) ### 3.2 Plugin Contract Specification ```typescript interface PluginContractSpec { /** Route path prefix pattern. Plugin contracts apply to routes matching this prefix. * MUST support wildcards: '/api/**' matches '/api/users', '/api/users/:id' * MUST default to '**' (all routes) if omitted */ appliesTo: string /** Contracts organized by hook phase. * MUST support: onRequest, preParsing, preValidation, preHandler, preSerialization, onSend, onResponse * MAY support additional phases as Fastify evolves */ hooks: { [phase: string]: { /** Preconditions that MUST hold before this phase executes */ requires?: string[] /** Postconditions that MUST hold after this phase executes */ ensures?: string[] } } /** Plugin metadata for diagnostics */ meta?: { name?: string version?: string description?: string } } ``` **File**: `src/types.ts` (NEW SECTION after line 223) ### 3.3 Contract Registry APOPHIS maintains an in-memory registry of plugin contracts: ```typescript class PluginContractRegistry { private contracts: Map = new Map() /** Register a plugin's contract specification. * MUST validate that appliesTo is a valid pattern. * MUST reject duplicate registrations unless the new spec is byte-for-byte equivalent. * SHOULD warn if a plugin declares contracts for phases it doesn't actually hook into. */ register(name: string, spec: PluginContractSpec): void /** Find all plugin contracts that apply to a given route. * MUST match appliesTo pattern against route.path. * MUST return contracts from all matching plugins. * MUST preserve plugin registration order in results. */ findContractsForRoute(route: RouteContract): Array<{ plugin: string; spec: PluginContractSpec }> /** Merge route contracts with applicable plugin contracts. * MUST deduplicate identical formulas. * MUST preserve source attribution for diagnostics. * MUST NOT mutate original route contracts. */ composeContracts(route: RouteContract): ComposedContract } ``` **File**: `src/domain/plugin-contracts.ts` (NEW FILE) ### 3.4 Contract Composition When APOPHIS tests a route, it composes contracts from all applicable sources: ``` ComposedContract = { route: RouteContract, phases: { [phase: string]: { requires: Array<{ formula: string; source: 'route' | 'plugin:name' }> ensures: Array<{ formula: string; source: 'route' | 'plugin:name' }> } } } ``` **Composition rules**: - Route-level `x-requires` → `route` phase (handler execution) - Route-level `x-ensures` → `route` phase (handler execution) - Plugin `hooks[phase].requires` → respective phase - Plugin `hooks[phase].ensures` → respective phase - Phase `onRequest` contracts run before route handler - Phase `onSend` contracts run after route handler but before response sent - Phase `onResponse` contracts run after response fully sent **File**: `src/domain/plugin-contracts.ts:80-120` (NEW) ### 3.5 Route Schema Integration Routes MAY declare which plugins they expect via `x-plugins`: ```typescript schema: { 'x-plugins': ['auth', 'rate-limit'], 'x-ensures': ['status:200'], } ``` **Behavior**: - If `x-plugins` is present, APOPHIS MUST warn if any listed plugin has no registered contracts. - If `x-plugins` is present, APOPHIS MUST warn if a plugin's `appliesTo` doesn't match this route. - If `x-plugins` is absent, APOPHIS MUST still apply all matching plugin contracts silently. - `x-plugins` is for documentation and validation, not contract scoping. **File**: `src/domain/contract.ts` (MODIFY `extractContract`, line 45+) ### 3.6 Built-in Plugin Contracts APOPHIS MUST provide contract specifications for common Fastify plugins: **File**: `src/plugins/built-in-contracts.ts` (NEW FILE) ```typescript export const BUILTIN_PLUGIN_CONTRACTS: Record = { '@fastify/auth': { appliesTo: '**', hooks: { onRequest: { requires: ['request_headers(this).authorization != null'], }, }, }, '@fastify/compress': { appliesTo: '**', hooks: { onSend: { ensures: ['response_headers(this).content-encoding != null'], }, }, }, '@fastify/cors': { appliesTo: '**', hooks: { onRequest: { ensures: ['response_headers(this).access-control-allow-origin != null'], }, }, }, '@fastify/rate-limit': { appliesTo: '**', hooks: { onRequest: { ensures: [ 'response_headers(this).x-ratelimit-limit != null', 'response_headers(this).x-ratelimit-remaining != null', ], }, }, }, } ``` **Registration**: - Built-in contracts MUST be registered automatically when APOPHIS plugin initializes. - Built-in contracts MAY be overridden by explicit plugin registrations. - Built-in contracts SHOULD be documented in `docs/PLUGIN_CONTRACTS_SPEC.md`. **File**: `src/plugin/index.ts:48-69` (MODIFY initialization) ### 3.7 Test Runner Integration The PETIT runner MUST compose contracts before executing each route: **File**: `src/test/petit-runner.ts:180-190` (MODIFY) ```typescript // Before generating commands, compose contracts for each route const composedRoutes = routes.map(route => { const composed = pluginContractRegistry.composeContracts(route) // Warn if route declares x-plugins but plugin contracts don't match const declaredPlugins = route.schema?.['x-plugins'] as string[] | undefined if (declaredPlugins) { for (const pluginName of declaredPlugins) { const pluginContracts = pluginContractRegistry.findContractsForRoute(route) .filter(c => c.plugin === pluginName) if (pluginContracts.length === 0) { console.warn(`Route ${route.method} ${route.path} declares plugin '${pluginName}' but no contracts match`) } } } return { ...route, composed } }) ``` ### 3.8 Phase-Aware Contract Testing APOPHIS MUST label each plugin contract with its phase. Exact phase execution is required only where hook interception is implemented: **File**: `src/test/petit-runner.ts:250-300` (MODIFY execute loop) ```typescript // For each command: // 1. Test onRequest phase contracts (plugin only) // 2. Execute request // 3. Test route-level contracts (handler) // 4. Test onSend/onResponse phase contracts (plugin only) // Phase 1: onRequest contracts if (composed.hooks?.onRequest?.requires) { const preCtx = buildPreRequestContext(request) validatePhaseContracts(composed.hooks.onRequest.requires, preCtx, route) } // Phase 2: Execute request (existing code) ctx = await executeHttp(...) // Phase 3: Route-level contracts (existing code) validatePostconditions(composed.route.ensures, ctx, route) // Phase 4: onSend/onResponse contracts if (composed.hooks?.onSend?.ensures) { validatePhaseContracts(composed.hooks.onSend.ensures, ctx, route) } ``` **Note**: Phase-aware testing requires hook interception. Since Fastify doesn't expose hook execution points, APOPHIS MAY approximate by testing all plugin contracts against the final response context, with phase noted in diagnostics. ### 3.9 Diagnostics and Reporting Contract violations MUST include source attribution: ```typescript interface ContractViolation { // ... existing fields ... readonly source: 'route' | 'plugin:name' readonly phase?: string // 'onRequest', 'onSend', etc. } ``` **File**: `src/types.ts:170-192` (EXTEND ContractViolation) Test results MUST show plugin contract coverage: ```typescript interface TestSummary { // ... existing fields ... readonly pluginContractsApplied: number readonly pluginContractsFailed: number } ``` **File**: `src/types.ts:277-285` (EXTEND TestSummary) ## 4. API Surface ### 4.1 Plugin Registration ```typescript // In plugin registration fastify.apophis.registerPluginContracts('my-auth', { appliesTo: '/api/**', hooks: { onRequest: { requires: ['request_headers(this).authorization != null'], }, }, }) ``` **File**: `src/plugin/index.ts` (NEW METHOD) ### 4.2 Route Declaration ```typescript fastify.get('/api/users', { schema: { 'x-plugins': ['my-auth', '@fastify/rate-limit'], 'x-ensures': ['status:200', 'response_body(this).id != null'], } }, handler) ``` ### 4.3 Test Execution ```typescript const result = await fastify.apophis.contract({ depth: 'standard', // Plugin contracts are applied automatically }) // Results include plugin contract attribution console.log(result.summary.pluginContractsApplied) ``` ## 5. Implementation Plan ### Phase 1: Core Registry (2 hours) **Files**: - `src/types.ts:223+` — Add `PluginContractSpec`, `ComposedContract`, extend `ContractViolation` - `src/domain/plugin-contracts.ts` — `PluginContractRegistry` class - `src/plugin/index.ts:130+` — Add `registerPluginContracts()` method - `src/plugin/index.ts:48-69` — Auto-register built-in contracts **Tests**: - `src/test/plugin-contracts.test.ts` — Registry operations, pattern matching, composition ### Phase 2: Route Integration (2 hours) **Files**: - `src/domain/contract.ts:45+` — Extract `x-plugins` from schema - `src/test/petit-runner.ts:180-190` — Compose contracts before test generation - `src/test/petit-runner.ts:250-300` — Apply composed contracts during execution **Tests**: - `src/test/plugin-contracts-integration.test.ts` — End-to-end plugin contract testing ### Phase 3: Built-in Contracts (1 hour) **Files**: - `src/plugins/built-in-contracts.ts` — Common plugin contracts - `docs/PLUGIN_CONTRACTS_SPEC.md` — Documentation **Tests**: - `src/test/built-in-contracts.test.ts` — Verify built-in contracts load correctly ### Phase 4: Diagnostics (1 hour) **Files**: - `src/types.ts:277-285` — Extend `TestSummary` with plugin metrics - `src/domain/contract-validation.ts` — Add source attribution to violations - `src/test/error-renderer.ts` — Show plugin source in failure output ## 6. Invariants ### 6.1 Registry Invariants - **I1**: Plugin contract registration MUST be idempotent. Registering the same plugin twice with identical spec MUST NOT throw. - **I2**: Plugin contract registration order MUST be deterministic and preserved in diagnostics. - **I3**: The registry MUST NOT mutate plugin specs after registration. All returned specs MUST be deep copies. ### 6.2 Composition Invariants - **I4**: Contract composition MUST be deterministic. Same route + same plugins = same composed contract. - **I5**: Contract composition MUST be idempotent. Composing an already-composed route MUST produce identical results. - **I6**: Plugin contracts MUST NOT override route contracts. If route and plugin declare the same formula, route's version takes precedence. ### 6.3 Execution Invariants - **I7**: Plugin contracts MUST be tested even if route has no `x-plugins` annotation. `x-plugins` is for validation, not scoping. - **I8**: Plugin contract failures MUST include plugin name in diagnostics. Users MUST know which plugin's contract failed. - **I9**: Plugin contract warnings (missing plugins, pattern mismatches) MUST NOT fail the test suite. They are informational only. ## 7. Backward Compatibility - Routes without `x-plugins` still receive matching plugin contracts; this is additive validation behavior and must be called out in migration notes. - Plugins without `registerPluginContracts()` MUST NOT cause errors. - Existing `TestSuite` and `TestResult` types MUST remain compatible. New fields are optional. ## 8. Open Questions 1. **Phase interception**: Can we actually test onRequest/onSend contracts separately without monkey-patching Fastify? If not, we test all plugin contracts against final context with phase noted. 2. **Plugin versioning**: Should contracts include version constraints? e.g., `@fastify/auth@^4.0.0` contracts. 3. **Conditional contracts**: Should plugins declare contracts conditionally based on configuration? e.g., auth plugin with `optional: true` mode. 4. **Performance**: Composing contracts for 10K routes with 20 plugins. O(n*m) is acceptable but should be cached. ## 9. References ### Codebase Citations - **Route discovery**: `src/domain/discovery.ts:26-32` - **Hook validator**: `src/infrastructure/hook-validator.ts` - **Contract extraction**: `src/domain/contract.ts:45+` - **PETIT runner**: `src/test/petit-runner.ts:166-428` - **Plugin entry**: `src/plugin/index.ts:48-69` - **Types**: `src/types.ts:170-292` ### External References - Fastify Hooks: https://www.fastify.io/docs/latest/Reference/Hooks/ - Fastify Plugin: https://github.com/fastify/fastify-plugin - Fastify Encapsulation: https://www.fastify.io/docs/latest/Reference/Encapsulation/ --- *Document Version: 1.0* *Author: APOPHIS Architecture Team* *Date: 2026-04-25*