# Capability Spec Delta: api-graphql (api-graphql) **Package**: `@suaveplan/api-graphql` **Status**: PROPOSED > Every requirement carries a `**Verified by:**` line. ## ADDED Requirements ### Requirement: GraphqlRegistry accumulator contract The package SHALL export the `GraphqlRegistry` interface with `addTypes(sdl: string): void`, `addResolvers(map: ResolverMap): void`, `addLoader(name: string, batcher: BatchFn): void`. Multiple calls of each method MUST accumulate (not overwrite); the host calls `freeze()` after every plugin has contributed. The registry MUST be generic over `Caps extends CapabilityMap` so resolvers and loaders receive the typed `RequestContext`. #### Scenario: addTypes accumulates across calls - **WHEN** two SDL strings are added in sequence - **THEN** both contributions are visible in the merged output #### Scenario: addLoader keyed by name - **WHEN** two loaders with the same name are registered - **THEN** the second call throws `GRAPHQL.LOADER_NOT_REGISTERED` is NOT thrown but a duplicate-registration error is — pinned as part of `GRAPHQL_CODES` design — actually `LOADER_DUPLICATE_NAME` per the registry tests **Verified by:** `src/registry/registry.test.ts::"addTypes accumulates across calls"` AND `src/registry/registry.test.ts::"addLoader rejects duplicate names"` --- ### Requirement: `import z from "zod"` everywhere Every Zod import MUST use the default form. #### Scenario: no named import of z - **WHEN** the test greps `import z` - **THEN** zero matches **Verified by:** `tests/zod-import-form.test.ts::"every src file uses default-import z"` --- ### Requirement: Federation merger with collision detection The package SHALL export `mergeSdl(contributions: readonly SdlContribution[]): MergedSchema`. Multiple plugins contributing `extend type Query { ... }` SHALL be unioned. A collision (two plugins declaring the same field on the same type without the field being declared as an `extend`) MUST throw `GRAPHQL.SCHEMA_COLLISION` with `details.path = "Type.field"` AND `details.plugins = [A, B]`. The merger MUST honour `graphql.namespace`: when a plugin declares `graphql.namespace: "Billing"` in its manifest, its `Query`/`Mutation`/`Subscription` extensions MUST be wrapped into `BillingQuery`/`BillingMutation`/`BillingSubscription` types referenced from the root. #### Scenario: two plugins extend Query without collision - **WHEN** plugin A extends `Query { invoices }` AND plugin B extends `Query { users }` - **THEN** the merged schema has both fields on `Query` #### Scenario: collision reported with plugin pair - **WHEN** plugin A and plugin B both declare `Query.users` without namespace - **THEN** `GRAPHQL.SCHEMA_COLLISION` is thrown AND `details.plugins` is `["A", "B"]` #### Scenario: namespace wrapper applied - **WHEN** plugin "Billing" declares `graphql.namespace: "Billing"` AND extends `Query { invoices }` - **THEN** the merged schema exposes `Query.billing: BillingQuery` AND `BillingQuery.invoices` is reachable **Verified by:** `src/federation/merge-sdl.test.ts::"two plugins extend Query without collision"` AND `src/federation/merge-sdl.test.ts::"collision reports plugin pair"` AND `src/federation/namespace.test.ts::"namespace wraps contributions under wrapper type"` --- ### Requirement: Resolver type is RequestContext-bound The package SHALL export `Resolver` where the third argument is `RequestContext` (not `PluginContext`). Resolvers MUST receive tenant-scoped `db`, request-bound `principal`, and the per-request DataLoaders. #### Scenario: resolver third arg is RequestContext - **WHEN** `expectTypeOf>[2]>()` is checked - **THEN** it equals `RequestContext` (not `PluginContext`) **Verified by:** `src/resolver/resolver.spec-conformance.test.ts::"resolver third argument is RequestContext"` --- ### Requirement: SubscriptionResolver returns AsyncIterable The package SHALL export `SubscriptionResolver` whose `subscribe` returns `AsyncIterable | Promise>`. Subscriptions ride the WebSocket engine (`api-ws`); this contract MUST reference `api-ws`'s `SubscriptionBus` via type only (no runtime dep). #### Scenario: subscribe returns AsyncIterable - **WHEN** `expectTypeOf["subscribe"]>()` is checked - **THEN** the return type unifies to `AsyncIterable | Promise>` **Verified by:** `src/subscription/subscription.spec-conformance.test.ts::"subscribe returns AsyncIterable"` --- ### Requirement: Directive contracts The package SHALL export descriptors for `@auth(scope: String!)` and `@rateLimit(rpm: Int!)` directives. Each descriptor MUST declare: directive name, locations (`FIELD_DEFINITION`), argument schema, host-side handler hook. Engine adapters install the directives at schema-build time. #### Scenario: @auth descriptor present - **WHEN** the test imports `authDirective` - **THEN** the descriptor's `name === "auth"` AND `locations` includes `FIELD_DEFINITION` **Verified by:** `src/directives/directives.test.ts::"auth and rateLimit descriptors present"` --- ### Requirement: GraphqlEngine lifecycle interface The package SHALL export `GraphqlEngine` interface: `buildSchema(registry: GraphqlRegistry): MergedSchema`, `executeQuery(req: GraphqlRequest, ctx: RequestContext): Promise`, `executeSubscription(req, ctx): AsyncIterable`, `dispose(): Promise`. Concrete engines (`api-graphql-yoga`, `api-graphql-apollo`) implement this interface. #### Scenario: every required method present - **WHEN** `expectTypeOf()` is checked - **THEN** all four methods are declared with the documented signatures **Verified by:** `src/engine/engine.spec-conformance.test.ts::"GraphqlEngine declares buildSchema executeQuery executeSubscription dispose"` --- ### Requirement: CapabilityId augmentation contributes "graphql" The package SHALL augment the `CapabilityId` union from `@suaveplan/api-types` to include `"graphql"`. Type-only augmentation (no runtime cost). #### Scenario: "graphql" is assignable to CapabilityId - **WHEN** `expectTypeOf<"graphql">().toMatchTypeOf()` is checked - **THEN** the assertion passes **Verified by:** `src/capability-id/capability-id.spec-conformance.test.ts::"graphql is a valid CapabilityId after augmentation"` --- ### Requirement: Error codes registry frozen The package SHALL export `GRAPHQL_CODES` via `defineCodes` containing exactly: `SCHEMA_COLLISION`, `RESOLVER_MISSING`, `LOADER_DUPLICATE_NAME`, `LOADER_NOT_REGISTERED`, `DIRECTIVE_UNKNOWN`, `SUBSCRIPTION_TRANSPORT_UNAVAILABLE`, `NAMESPACE_REQUIRED`. #### Scenario: every code reachable - **WHEN** the test enumerates `GRAPHQL_CODES` - **THEN** every code has a production throw site **Verified by:** `src/error-codes/error-codes.spec-conformance.test.ts::"every GRAPHQL_CODES entry thrown by production path"` --- ### Requirement: Conformance harness for engines The package SHALL export `runGraphqlEngineConformance({ factory, label })` from `./testing`. Both `api-graphql-yoga` and `api-graphql-apollo` (Stage 7) MUST pass it. #### Scenario: harness covers query, mutation, subscription, federation - **WHEN** the harness is run against a stub engine - **THEN** test categories exist for query, mutation, subscription, federation, directive handling **Verified by:** `src/testing/conformance.test.ts::"harness covers query mutation subscription federation directives"`