# ADR-006: Operation specs as capabilities - **Status**: Superseded (see update below) - **Date**: 2026-04-19 - **Deciders**: alkdev - **Superseded by**: D3 in storage-spec-phase1-resolutions.md (2026-04-22) ## Context A spoke's capabilities were previously modeled as an opaque JSONB blob. Operations are the universal abstraction; they have names, namespaces, types, and typed schemas. ## Original Decision A spoke's capabilities are its registered operation specs. The spokes table stores minimal metadata. The operation_specs table stores full definitions. The relationship: spoke registers → hub creates operation_specs rows linked to that spoke. Queries for "what can spoke X do?" go through operation_specs filtered by spoke_id, not through a capabilities blob. The spokes table has no capabilities column. Instead, operation_specs has a spoke_id FK (nullable — hub-native operations have spoke_id = null). ## Revised Decision (D3, 2026-04-22) The original unified `operation_specs` table conflated two concepts: "what an operation IS" (a definition) and "who provides it right now" (a registration). These are now split into two tables: 1. **`operations`** (definitions): Stores the operation's identity — namespace, name, type, input/output schemas, access control, description, tags. Unique by `(namespace, name)`. No spoke FK — definitions are provider-independent. These persist even when all providers disconnect. 2. **`operation_registrations`** (provider bindings): Links a provider (spoke or client) to an operation definition. Has `operationId → operations.id` (CASCADE), `providerType` (spoke|client), `providerId`, `status` (active|inactive), and pre-remap identifiers. On spoke disconnect, registrations are set to `inactive`. On admin spoke-row deletion, registrations CASCADE. This supersedes the original unified model. The core principle from the original decision — that a spoke's capabilities are its registered operations, not a capabilities blob — remains unchanged. The query pattern shifts from `operation_specs filtered by spoke_id` to `operation_registrations filtered by providerId and status = 'active'`. ## Consequences Positive: capabilities are fully typed and queryable, consistent with the operations system, no duplicated capability data. Negative: requires a join to get spoke capabilities (acceptable since operation_registrations are indexed by providerId). The split adds a second table but cleanly separates definition persistence from runtime provider state, enabling multi-instance providers and operation survival across disconnects.