Initial Commit
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# `@suaveplan/api-tenant-provider-memory` — In-memory TenantProvider
|
||||
|
||||
**Package**: `@suaveplan/api-tenant-provider-memory`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
**Stage**: 4 (Base adapters) — see umbrella `openspec/changes/api-plugin-foundation/`
|
||||
|
||||
## Why
|
||||
|
||||
Per design doc §5.4, `perTenant: true` cron jobs require the host to enumerate tenants. `tenant.provider` is the capability that exposes that enumeration. Shipping a v1 in-memory reference implementation matters for two reasons:
|
||||
|
||||
1. It pins the contract — every later DB-backed adapter (Stage 8 or later) implements the same `TenantProvider` interface, so retrofitting tenant enumeration into a stable contract doesn't have to happen later (§5.4 last sentence).
|
||||
2. It's the default for the testing kit (`createTestHost` Stage 5) and for any deployment whose tenant list is small + static.
|
||||
|
||||
The provider returns an iterable/async-iterable of `TenantContext`. Designed to be both production-grade for static deployments AND a reference for the more elaborate Stage-8 DB-backed adapter.
|
||||
|
||||
Stage 4 invariant: ships direct AND worker-proxy surfaces; both pass `runTenantProviderConformance` (§5.2a, §13.4). (Conformance suite is owned by `api-conformance-suites` this stage.)
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package depending on `@suaveplan/api-tenant-provider` (contract), `@suaveplan/api-types`, `@suaveplan/api-worker-rpc`, `@suaveplan/error`, `zod`.
|
||||
|
||||
- ADDED: `createMemoryTenantProvider(opts)` — direct-surface factory returning a `TenantProvider`.
|
||||
- ADDED: `createMemoryTenantProviderWorkerProxy(port, opts)` — proxy-surface factory.
|
||||
- ADDED: `MemoryTenantProviderOptions` — Zod schema with `tenants: TenantContext[]` (initial set), `mutable: boolean` (default true — allow runtime `add`/`remove`/`update`).
|
||||
- ADDED: Mutation API (when `mutable: true`): `add(tenant)`, `remove(tenantId)`, `update(tenantId, patch)`.
|
||||
- ADDED: `list()` returns an array snapshot; `listAsync()` returns an `AsyncIterable<TenantContext>` for symmetry with future DB-backed adapter.
|
||||
- ADDED: `get(tenantId)` returns `TenantContext | undefined`.
|
||||
- ADDED: `TENANT_PROVIDER_MEMORY_*` error codes (`TENANT_PROVIDER_MEMORY_DUPLICATE_ID`, `TENANT_PROVIDER_MEMORY_NOT_FOUND`, `TENANT_PROVIDER_MEMORY_IMMUTABLE`, `TENANT_PROVIDER_MEMORY_INVALID_OPTIONS`).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- DB-backed enumeration — Stage 8 (or later) `api-tenant-provider-postgres` / `api-tenant-provider-surrealdb`.
|
||||
- Hierarchical tenants — flat model only at this stage.
|
||||
- Tenant-level feature flags — `@suaveplan/feature-flags` consumes the provider, not the other way round.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
|
||||
|
||||
| Requirement | Design doc section |
|
||||
|---|---|
|
||||
| `tenant.provider` ships in v1 | §5.4 |
|
||||
| In-memory reference impl | §5.4 |
|
||||
| Worker-proxy variant | §5.2a |
|
||||
| Conformance both surfaces | §13.4 |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/factory/create-memory-tenant-provider.test.ts` — list, get, mutation, duplicate-id rejection, immutability when `mutable: false`.
|
||||
- `src/factory/create-memory-tenant-provider.spec-conformance.test.ts` — `runTenantProviderConformance("memory-direct", ...)`.
|
||||
- `src/proxy/create-memory-tenant-provider-worker-proxy.test.ts` — proxy unit tests; async iteration across MessagePort.
|
||||
- `src/proxy/create-memory-tenant-provider-worker-proxy.spec-conformance.test.ts` — `runTenantProviderConformance("memory-worker-proxy", ...)`.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only.
|
||||
@@ -0,0 +1,72 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-tenant-provider-memory (api-tenant-provider-memory)
|
||||
|
||||
**Package**: `@suaveplan/api-tenant-provider-memory`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Enumerate tenants from in-memory state
|
||||
|
||||
The package SHALL implement `TenantProvider` with `list()` (sync snapshot), `listAsync()` (async iterable), and `get(tenantId)`. Initial state comes from `options.tenants`; runtime mutations are gated by `options.mutable`.
|
||||
|
||||
#### Scenario: list returns the initial set
|
||||
|
||||
- **WHEN** the provider is constructed with `tenants: [A, B, C]`
|
||||
- **THEN** `list()` returns an array of length 3 containing A, B, C in insertion order
|
||||
|
||||
#### Scenario: async iterator yields every tenant exactly once
|
||||
|
||||
- **WHEN** `for await (const t of provider.listAsync())` runs against an initial set of 3 tenants
|
||||
- **THEN** exactly 3 tenants are yielded AND each ID appears exactly once
|
||||
|
||||
#### Scenario: get returns undefined for unknown tenant
|
||||
|
||||
- **WHEN** `get("unknown")` is called
|
||||
- **THEN** the result is `undefined` (no throw)
|
||||
|
||||
**Verified by:** `src/factory/create-memory-tenant-provider.test.ts::"list returns initial set in insertion order"` AND `src/factory/create-memory-tenant-provider.test.ts::"listAsync yields every tenant exactly once"` AND `src/factory/create-memory-tenant-provider.test.ts::"get returns undefined for unknown tenant id"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Mutation API gated by mutable flag
|
||||
|
||||
When `options.mutable: true`, the provider SHALL expose `add(tenant)`, `remove(tenantId)`, `update(tenantId, patch)`. When `mutable: false`, those methods MUST throw `TENANT_PROVIDER_MEMORY_IMMUTABLE`.
|
||||
|
||||
#### Scenario: add rejects duplicate id
|
||||
|
||||
- **WHEN** `add({ id: "acme", ... })` is called twice with the same id
|
||||
- **THEN** the second call throws `TENANT_PROVIDER_MEMORY_DUPLICATE_ID`
|
||||
|
||||
#### Scenario: remove of unknown tenant throws
|
||||
|
||||
- **WHEN** `remove("not-there")` is called
|
||||
- **THEN** the call throws `TENANT_PROVIDER_MEMORY_NOT_FOUND`
|
||||
|
||||
#### Scenario: mutation blocked when immutable
|
||||
|
||||
- **WHEN** the provider is constructed with `mutable: false` AND `add(...)` is called
|
||||
- **THEN** the call throws `TENANT_PROVIDER_MEMORY_IMMUTABLE` AND state remains unchanged
|
||||
|
||||
**Verified by:** `src/factory/create-memory-tenant-provider.test.ts::"add rejects duplicate id"` AND `src/factory/create-memory-tenant-provider.test.ts::"remove of unknown tenant throws"` AND `src/factory/create-memory-tenant-provider.test.ts::"mutation blocked when immutable"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Direct and worker-proxy surfaces both pass conformance
|
||||
|
||||
The proxy surface SHALL support `listAsync()` across the `MessagePort` (chunked yield).
|
||||
|
||||
#### Scenario: direct conforms
|
||||
|
||||
- **WHEN** `runTenantProviderConformance("memory-direct", () => createMemoryTenantProvider(...))` runs
|
||||
- **THEN** every invariant passes
|
||||
|
||||
#### Scenario: worker-proxy conforms with async iteration
|
||||
|
||||
- **WHEN** `runTenantProviderConformance("memory-worker-proxy", ...)` runs over a real `MessageChannel` AND `listAsync()` is iterated to completion against a 1_000-tenant fixture
|
||||
- **THEN** every invariant passes AND every tenant id is yielded exactly once on the proxy side
|
||||
|
||||
**Verified by:** `src/factory/create-memory-tenant-provider.spec-conformance.test.ts::"memory-direct conforms to TenantProvider contract"` AND `src/proxy/create-memory-tenant-provider-worker-proxy.spec-conformance.test.ts::"memory-worker-proxy conforms with async iteration across MessagePort"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,66 @@
|
||||
# Tasks — `@suaveplan/api-tenant-provider-memory`
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change.
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read design doc (§5.4, §5.2a, §13.4)
|
||||
- [x] 1.2 Read `openspec/changes/api-tenant-provider/` for upstream contract
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.4 Draft `proposal.md`
|
||||
- [x] 1.5 Draft `specs/api-tenant-provider-memory/spec.md`
|
||||
- [x] 1.6 Every requirement carries `**Verified by:**`
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [ ] 2.1 Scaffold `packages/api/api-tenant-provider-memory/` (deps: api-tenant-provider, api-types, api-worker-rpc, error, zod)
|
||||
- [ ] 2.2 `src/options/memory-tenant-provider-options.ts` — Zod schema
|
||||
- [ ] 2.3 `src/factory/create-memory-tenant-provider.ts` — direct surface (list/get/add/remove/update + async iterator)
|
||||
- [ ] 2.4 `src/proxy/create-memory-tenant-provider-worker-proxy.ts` — proxy surface
|
||||
- [ ] 2.5 `src/error-codes.ts` — `TENANT_PROVIDER_MEMORY_*` codes via `defineCodes`
|
||||
- [ ] 2.6 `src/index.ts` — explicit named re-exports
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [ ] 3.1 `src/factory/create-memory-tenant-provider.test.ts`
|
||||
- [ ] 3.2 `src/factory/create-memory-tenant-provider.spec-conformance.test.ts` — direct conformance
|
||||
- [ ] 3.3 `src/proxy/create-memory-tenant-provider-worker-proxy.test.ts`
|
||||
- [ ] 3.4 `src/proxy/create-memory-tenant-provider-worker-proxy.spec-conformance.test.ts` — proxy conformance
|
||||
- [ ] 3.5 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [ ] 4.1 Sibling `.md` per implementation `.ts`
|
||||
- [ ] 4.2 README labels this as both production-grade (for static-tenant deployments) and reference-grade (for future DB adapters)
|
||||
- [ ] 4.3 Note in README that `createTestHost` uses this provider by default (forward-reference to Stage 5)
|
||||
|
||||
## 5. Pre-archive verification (5-gate triple)
|
||||
|
||||
- [ ] 5.1 `bun run check` — clean
|
||||
- [ ] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-tenant-provider-memory` — green
|
||||
- [ ] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-tenant-provider-memory` — 100/100/100/100
|
||||
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-tenant-provider-memory` — exit 0
|
||||
- [ ] 5.5 `bunx turbo build --filter=@suaveplan/api-tenant-provider-memory` — green
|
||||
|
||||
## 6. Pre-archive 7-row checklist
|
||||
|
||||
- [ ] 6.1 `expectTypeOf` asserts `list()` returns `TenantContext[]` and `listAsync()` returns `AsyncIterable<TenantContext>`
|
||||
- [ ] 6.2 Zod schema at options boundary
|
||||
- [ ] 6.3 Every `TENANT_PROVIDER_MEMORY_*` code thrown by at least one production path
|
||||
- [ ] 6.4 Strict-mode (`mutable: true` vs `false`) tested both ways
|
||||
- [ ] 6.5 OTel instruments (`api.tenant_provider.size`, `api.tenant_provider.mutations_total{op}`) asserted with in-memory `MeterProvider`
|
||||
- [ ] 6.6 Add-duplicate dedup tested (idempotent re-add of identical record OR duplicate error — pick one and pin it)
|
||||
- [ ] 6.7 No retroactive tick
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [ ] 7.1 Fold into `openspec/specs/api-tenant-provider-memory/spec.md`
|
||||
- [ ] 7.2 Move to `openspec/archive/<yyyy-mm-dd>-api-tenant-provider-memory/`
|
||||
- [ ] 7.3 Tick Stage 4 box in umbrella tasks
|
||||
- [ ] 7.4 Refresh `MANIFEST.md`
|
||||
Reference in New Issue
Block a user