Initial Commit
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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"`
|
||||
Reference in New Issue
Block a user