Files
suaveplan/openspec/archive/2026-05-25-api-tenant-provider/specs/api-tenant-provider/spec.md
2026-05-27 23:29:53 +01:00

5.3 KiB

Capability Spec Delta: api-tenant-provider (api-tenant-provider)

Package: @suaveplan/api-tenant-provider Status: PROPOSED

Every requirement carries a **Verified by:** line.

ADDED Requirements

Requirement: TenantProvider contract

The package SHALL export TenantProvider: list(opts?: ListOpts): AsyncIterable<TenantRecord>, count(): Promise<number>, lookup(tenantId: TenantId): Promise<TenantRecord | null>, dispose(): Promise<void>. lookup MUST return null (not throw) when the tenant is not found.

Scenario: every method present

  • WHEN expectTypeOf<TenantProvider>() is checked
  • THEN all four methods are declared

Scenario: lookup returns null for missing tenant

  • WHEN a stub provider with no tenants is given an arbitrary id
  • THEN lookup(id) resolves to null

Verified by: src/provider/provider.spec-conformance.test.ts::"TenantProvider declares list count lookup dispose" AND src/provider/provider.test.ts::"lookup returns null for missing tenant"


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 from "zod"
  • THEN zero matches

Verified by: tests/zod-import-form.test.ts::"every src file uses default-import z"


Requirement: TenantRecord schema

The package SHALL export tenantRecordSchema: z.ZodSchema<TenantRecord> describing { id: TenantId, displayName?: string, status: "active" | "suspended" | "archived", createdAt: string }. id MUST be branded as TenantId (not plain string). Unknown keys MUST be rejected.

Scenario: minimal record parses

  • WHEN { id: "t1", status: "active", createdAt: "2026-05-13T..." } is parsed
  • THEN it succeeds AND id is typed as TenantId

Scenario: unknown status rejected

  • WHEN { id: "t1", status: "weird", createdAt: "..." } is parsed
  • THEN Zod reports an issue at status

Verified by: src/tenant-record/tenant-record.test.ts::"minimal record parses" AND src/tenant-record/tenant-record.test.ts::"unknown status rejected"


Requirement: Status filter — active-by-default

list(opts) MUST default to enumerating only tenants whose status === "active". list({ includeSuspended: true }) MUST also enumerate suspended tenants. archived tenants are NEVER enumerated by list — they are only reachable via lookup.

Scenario: default excludes suspended and archived

  • WHEN the provider holds [active, suspended, archived] AND list() is consumed
  • THEN only the active tenant is yielded

Scenario: includeSuspended surfaces suspended

  • WHEN list({ includeSuspended: true }) is consumed
  • THEN both active and suspended tenants are yielded; archived is NOT yielded

Verified by: src/provider/status-filter.test.ts::"default list excludes suspended and archived" AND src/provider/status-filter.test.ts::"includeSuspended surfaces suspended but not archived"


Requirement: Pagination cursor round-trip

ListOpts.cursor MUST round-trip — consuming part of list({ pageSize: 10 }), capturing a cursor, and resuming with list({ pageSize: 10, cursor }) MUST yield the remainder without duplication or omission. Invalid cursors MUST throw TENANT_PROVIDER.INVALID_CURSOR.

Scenario: cursor resumes without gap or overlap

  • WHEN 25 tenants exist AND the caller pulls 10 then resumes with the cursor
  • THEN the resumption yields exactly 15 tenants AND no duplicates

Scenario: invalid cursor rejected

  • WHEN an arbitrary string is passed as cursor
  • THEN TENANT_PROVIDER.INVALID_CURSOR is thrown

Verified by: src/pagination/pagination.test.ts::"cursor resumes without gap or overlap" AND src/pagination/pagination.test.ts::"invalid cursor rejected"


Requirement: CapabilityId augmentation contributes "tenant.provider"

The package SHALL augment the CapabilityId union to include "tenant.provider".

Scenario: tenant.provider assignable to CapabilityId

  • WHEN expectTypeOf<"tenant.provider">().toMatchTypeOf<CapabilityId>() is checked
  • THEN the assertion passes

Verified by: src/capability-id/capability-id.spec-conformance.test.ts::"tenant.provider is a valid CapabilityId after augmentation"


Requirement: Error codes registry frozen

The package SHALL export TENANT_PROVIDER_CODES containing exactly: TENANT_NOT_FOUND, SOURCE_UNAVAILABLE, INVALID_CURSOR.

Scenario: every code reachable

  • WHEN the test enumerates TENANT_PROVIDER_CODES
  • THEN every code has a production throw site

Verified by: src/error-codes/error-codes.spec-conformance.test.ts::"every TENANT_PROVIDER_CODES entry thrown by production path"


Requirement: Conformance harness for providers

The package SHALL export runTenantProviderConformance({ factory, label }). Every concrete provider (in-memory in Stage 4, DB-backed downstream) MUST pass it.

Scenario: harness covers list, count, lookup, status filter, pagination, dispose

  • WHEN the harness runs against a stub provider
  • THEN test categories exist for each documented behavior

Verified by: src/testing/conformance.test.ts::"harness covers list count lookup status-filter pagination dispose"