Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
# Add @suaveplan/api-plugin-database (Path A — wraps SurrealAdapter)
**Package**: `@suaveplan/api-plugin-database`
**Status**: PROPOSED
**Type**: new-package
- **Spec delta**: ./specs/api-plugin-database/spec.md
## Why
`@suaveplan/api` plugin hosts need a one-shot way to bring up a SurrealDB-backed `DatabaseAdapter` with production-grade orthogonals: a connection pool, an advisory-locked migration runner, tenant provisioning, retry/backoff, RFC 7807 error rendering, and a full OTel surface. Genesis already ships `@suaveplan/surrealdb` (which exposes `SurrealAdapter` implementing `@suaveplan/db`'s `DatabaseAdapter`) and `@suaveplan/api-migrations` (which ships a real SurrealQL migration adapter and runner). Without this plugin, every consuming app must hand-wire those primitives, and the cross-cutting pool/tenancy/retry/problem-json/OTel pieces fall on application code.
The upstream `../suaveplan/packages/server/api-plugin-database/` re-implements its own `SurrealAdapter`, exposes an adapter surface (`query`/`select`/`create`/`update`/`delete`/`transaction`/`use`/`info`/`getClient`/`releaseClient`/`getPoolSize`) that is irreconcilable with genesis's `DatabaseAdapter` shape, and hangs tenant lifecycle off a descriptor hook (`onTenantCreate` / `onTenantDelete`) that genesis's `PluginDescriptor` does not carry. Path A is therefore: keep the orthogonal surface upstream contributed (pool, migrations, tenancy callable, retry, problem-json, OTel, error codes), but bind every adapter call to the canonical `SurrealAdapter` and expose it via `ServiceRegistry["db"]: DatabaseAdapter`.
## What changes
- ADDED: `createDatabasePlugin(options?)` — `PluginDescriptor<DatabasePluginConfig>` factory using `definePlugin`. `onInit` builds the pool, registers `db` in `ServiceRegistry`. `onReady` auto-runs migrations under advisory lock. `onClose` drains the pool.
- ADDED: `ConnectionPool` — bounded pool of `SurrealAdapter` instances with min/max sizing, idle reaping, `acquireTimeoutMs`, waiter queue, and `size()` introspection.
- ADDED: Migration ledger + advisory lock + runner integrating with `@suaveplan/api-migrations` (`createSurrealqlAdapter` + `createMigrationRunner`) plus a thin lock layer on top.
- ADDED: `provisionTenant(adapter, config, tenantId)` / `decommissionTenant(adapter, config, tenantId)` callable exports supporting `shared` | `namespace-per-tenant` | `database-per-tenant`.
- ADDED: `withRetry(fn, options)` — exponential backoff with optional jitter.
- ADDED: `respondProblemJson(c, err)` — RFC 7807 renderer that consults the registered `DATABASE_*` error metadata.
- ADDED: `DATABASE_CODES` registry (8 codes via `defineCodes`) + concrete `DatabaseError` extending `SuaveplanError`.
- ADDED: `DatabasePluginOptionsSchema` / `DatabasePluginConfigSchema` Zod boundary on every public option bundle.
- ADDED: OTel instrument bundle (`pool.checkouts_total`, `pool.checkins_total`, `pool.saturation`, `query.duration_ms` histogram keyed by `{ outcome }`, `migrations.applied_total`).
- ADDED: `ServiceRegistry.db` augmentation (`DatabaseAdapter` slot) consumed by every downstream plugin.
## Out of scope
- A re-implementation of `SurrealAdapter.query`/`select`/`create`/`update`/`delete`/`transaction`/`use`/`info`/`getClient`/`releaseClient`/`getPoolSize` — every consumer obtains the pooled `DatabaseAdapter` via `ctx.getService("db")` and calls the canonical surface defined by `@suaveplan/db`.
- Descriptor-level `onTenantCreate` / `onTenantDelete` hooks — the genesis `PluginDescriptor` has no tenant-lifecycle slots; tenant lifecycle ships as the callable exports `provisionTenant` / `decommissionTenant` plus a documented event-bus integration pattern (`api-plugin-tenant` invokes them on its own tenant-create/delete events).
## Source mapping
This change derives from `../suaveplan/enterprise/api-plugin-database.md` and the upstream source tree `../suaveplan/packages/server/api-plugin-database/`.
| Requirement | Source | Notes |
|---|---|---|
| Connection pool | `src/pool/connection-pool.ts` | Reworked to pool `SurrealAdapter` instances rather than raw `Surreal` clients so pool members implement `DatabaseAdapter`. |
| Migration runner / ledger / lock | `src/migrations/runner/runner.ts`, `src/migrations/ledger/ledger.ts`, `src/migrations/lock/lock.ts`, `src/migrations/discovery/discovery.ts` | Discovery delegated to `@suaveplan/api-migrations` `createMigrationRunner` + `createSurrealqlAdapter`; advisory lock layered on top. |
| Tenancy provisioning callables | `src/tenancy/tenant-lifecycle.ts` | Re-exported as callable functions instead of descriptor hooks (platform constraint). |
| Retry / backoff | `src/retry/retry.ts` | Verbatim API, Zod-validated options. |
| RFC 7807 problem-json | `src/routes/problem-json.ts` | Bound to `@suaveplan/error`'s formatter; uses local `DATABASE_*` metadata. |
| Plugin factory | `src/plugin/create-database-plugin.ts` | Rewritten on top of genesis `definePlugin` + `ServiceRegistry`; no `onTenantCreate/Delete` hooks. |
| `DATABASE_*` error codes | `src/errors/error-codes.ts` | Registered via genesis `defineCodes` + `SuaveplanError`. |
| OTel surface | `enterprise/api-plugin-database.md` §3 | Pool checkout/checkin, query duration histogram, migration counter (delegates to `api-migrations` for the migrations counter, mounts pool/query locally). |
| Zod boundary | `src/config/config-schema.ts` | Re-shaped under genesis Zod v4 import convention. |
### Excluded from this migration
- `SurrealAdapter.query(sql, vars: Record)` — 🚫 superseded by `@suaveplan/surrealdb` `SurrealAdapter`. Consumers call `ctx.getService("db").query(sql, params: readonly unknown[])` per the `@suaveplan/db` `DatabaseAdapter` contract.
- `SurrealAdapter.queryOne` — 🚫 superseded; consumers call `(await db.query(...))[0]` or use the standard adapter pattern.
- `SurrealAdapter.select(table, id)` / `.create(table, data)` / `.update(table, id, data)` / `.delete(table, id)` — 🚫 superseded by `SurrealAdapter`'s native CRUD methods exposed on `DatabaseAdapter`.
- `SurrealAdapter.transaction(): Transaction` (the inversion-of-control transaction handle pattern) — 🚫 superseded by `DatabaseAdapter.transaction(fn, opts)` callback-style transactions defined by `@suaveplan/db`.
- `SurrealAdapter.use(ns, db)` / `.info()` / `.ping()` — 🚫 superseded by `SurrealAdapter`'s lifecycle (`connect`/`disconnect`/`isConnected`); namespace/database are set at construction time.
- `SurrealAdapter.getClient()` / `.releaseClient(c)` / `.getPoolSize()` — 🚫 platform mismatch. Genesis pools `SurrealAdapter` instances (each owning its own client); callers acquire via `pool.acquire()` and release via `pool.release(adapter)`. Pool size is reported by `pool.size()`.
- Descriptor `onTenantCreate(ctx, tenantId)` / `onTenantDelete(ctx, tenantId)` hooks — 🚫 platform constraint: genesis `PluginDescriptor` has no tenant-lifecycle hook slots. Equivalent functionality ships as `provisionTenant(adapter, config, tenantId)` and `decommissionTenant(adapter, config, tenantId)` callable exports that `@suaveplan/api-plugin-tenant` (or any host) invokes explicitly on its own tenant-lifecycle events via the existing event bus.
- Back-compat `"surreal"` service alias — 🚫 not part of the genesis contract; the canonical slot is `db`.
- `databaseMiddleware` Hono middleware — 🚫 genesis pattern is `ctx.getService("db")` from the host; the plugin does not contribute middleware.
## Verification approach
- `src/error-codes/error-codes.test.ts` — every `DATABASE_*` code is thrown from a production path.
- `src/retry/retry.test.ts` — exponential backoff, jitter, abort-aware retry.
- `src/pool/pool.test.ts` — pool acquires/releases, max-size waiter queue, `acquireTimeoutMs`, idle reaper, close drain.
- `src/migrations/lock.test.ts` — advisory lock with lease expiry, takeover, release.
- `src/migrations/runner.test.ts` — `.surql` discovery, ledger apply, drift detection, advisory-lock integration.
- `src/tenancy/tenancy.test.ts` — all three strategies provision/decommission with tenant-id validation.
- `src/routes/problem-json.test.ts` — RFC 7807 shape for `DatabaseError`, `HttpError`, and unknown throwables.
- `src/config/config-schema.test.ts` — Zod boundary accepts canonical input and rejects out-of-range knobs.
- `src/otel/instruments.test.ts` — every counter/histogram emits against an in-memory `MeterProvider`.
- `src/plugin/plugin.test.ts` — full lifecycle: `onInit` builds pool + registers service, `onReady` migrates under lock, `onClose` drains pool.
- `tests/integration/full-boot.test.ts` — real SurrealDB testcontainer via `@suaveplan/testing-integration-surrealdb`, full pool + migration + tenancy roundtrip.
## Migration impact
None — additive new package.
## Impact
### Affected dependents
No existing dependents (verified by `bunx turbo ls --filter=...@suaveplan/api-plugin-database` — new package). Downstream packages that will consume `ServiceRegistry["db"]` (e.g. `api-plugin-user`, future `api-plugin-tenant`) augment the registry themselves; this plugin only seeds `db`.

View File

@@ -0,0 +1,233 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: api-plugin-database (add-api-plugin-database)
**Package**: `@suaveplan/api-plugin-database`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
## ADDED Requirements
### Requirement: Stable error-code registry
The package SHALL register a `DATABASE` namespace via `defineCodes` and expose a `DatabaseError` class extending `SuaveplanError<DatabaseErrorCode>`. Every code SHALL be thrown from at least one production call site.
#### Scenario: Code list is stable
- **WHEN** consumers import `DATABASE_CODES`
- **THEN** the export contains exactly: `CONNECTION_FAILED`, `POOL_EXHAUSTED`, `POOL_CLOSED`, `QUERY_FAILED`, `MIGRATION_FAILED`, `LOCK_CONTENDED`, `TENANT_INVALID`, `INVALID_CONFIG`
#### Scenario: Every code is reachable
- **WHEN** the package's source tree is scanned
- **THEN** every code is thrown from a non-test production path
**Verified by:** `src/error-codes/error-codes.test.ts::"defineCodes produces the expected DATABASE_* namespace"` AND `src/api-plugin-database.spec-conformance.test.ts::"every DATABASE_* code is thrown from a production path"`
---
### Requirement: Connection pool over SurrealAdapter
The package SHALL expose `ConnectionPool` that owns a bounded set of `SurrealAdapter` instances. `acquire()` SHALL return an idle adapter, lazily grow to `max`, or queue on a waiter when at capacity. `release(adapter)` SHALL hand the adapter to the next waiter or return it to the idle set. `acquireTimeoutMs` SHALL reject queued waiters with `DATABASE.POOL_EXHAUSTED`. `close()` SHALL drain idle adapters and reject every pending waiter with `DATABASE.POOL_CLOSED`.
#### Scenario: Acquire under capacity returns existing idle adapter
- **WHEN** the pool has idle adapters and a caller invokes `acquire()`
- **THEN** the call resolves with that adapter and `size().idle` decreases by 1
#### Scenario: Acquire above min lazily creates up to max
- **WHEN** the pool currently has `< max` adapters and no idle ones
- **THEN** `acquire()` constructs a new adapter, connects it, marks it in-use, and `size().total` increases by 1
#### Scenario: Acquire at max times out
- **WHEN** all `max` adapters are in use and a caller invokes `acquire()` without a free slot within `acquireTimeoutMs`
- **THEN** the call rejects with `DatabaseError(DATABASE.POOL_EXHAUSTED)`
#### Scenario: Release wakes a waiter
- **WHEN** there is a queued waiter and a caller invokes `release(adapter)`
- **THEN** the waiter's promise resolves with that adapter
#### Scenario: Close drains and rejects pending acquires
- **WHEN** waiters are queued and the pool is closed
- **THEN** every waiter rejects with `DatabaseError(DATABASE.POOL_CLOSED)` and `acquire()` afterwards rejects with the same code
**Verified by:** `src/pool/pool.test.ts::"acquire returns an idle adapter"` AND `src/pool/pool.test.ts::"acquire lazily grows up to max"` AND `src/pool/pool.test.ts::"acquire rejects with POOL_EXHAUSTED past acquireTimeoutMs"` AND `src/pool/pool.test.ts::"release hands the adapter to a queued waiter"` AND `src/pool/pool.test.ts::"close drains adapters and rejects pending waiters"`
---
### Requirement: Retry with exponential backoff
The package SHALL expose `withRetry(fn, options)` that re-invokes `fn` up to `maxAttempts` times, sleeping `min(baseBackoffMs * 2^(attempt-1), maxBackoffMs)` (optionally jittered) between attempts. The caller MAY provide `isRetryable(err, attempt)` to bail out early.
#### Scenario: First-attempt success returns immediately
- **WHEN** `fn` resolves on its first call
- **THEN** `withRetry` returns the value without sleeping
#### Scenario: Retries on transient failure
- **WHEN** `fn` rejects twice and then resolves
- **THEN** `withRetry` invokes `fn` three times and returns the eventual value
#### Scenario: Throws the last error when exhausted
- **WHEN** `fn` rejects every attempt up to `maxAttempts`
- **THEN** `withRetry` throws the last error
#### Scenario: Non-retryable predicate bails out
- **WHEN** `isRetryable` returns false
- **THEN** `withRetry` throws immediately without further attempts
**Verified by:** `src/retry/retry.test.ts::"first-attempt success returns the value"` AND `src/retry/retry.test.ts::"retries on transient failure"` AND `src/retry/retry.test.ts::"throws after maxAttempts exhausted"` AND `src/retry/retry.test.ts::"isRetryable false bails out early"`
---
### Requirement: Migration runner with advisory lock
The package SHALL run `.surql` migrations through `@suaveplan/api-migrations`' `createSurrealqlAdapter` + `createMigrationRunner` while holding an advisory lock backed by a SurrealDB row whose `expiresAt` acts as a lease. A second concurrent runner SHALL observe the lock and skip work. Lease expiry SHALL allow takeover by another holder.
#### Scenario: Advisory lock excludes a concurrent holder
- **WHEN** holder A holds an unexpired lock and holder B attempts `acquireLock`
- **THEN** B's `acquireLock` returns `false`
#### Scenario: Expired lock is taken over
- **WHEN** the recorded lease is in the past and a new holder attempts `acquireLock`
- **THEN** the new holder records its own lease and `acquireLock` returns `true`
#### Scenario: Runner applies pending migrations under the lock
- **WHEN** the `.surql` directory contains pending files and `runMigrations` is called
- **THEN** every pending file is applied in lexicographic order, the ledger records each, and `applied[].name` lists them in order
#### Scenario: Lock contention surfaces as DATABASE.LOCK_CONTENDED
- **WHEN** the lock is held by another holder and `runMigrations` cannot acquire
- **THEN** the result includes the discovered files under `skipped` and no migrations are applied
**Verified by:** `src/migrations/lock.test.ts::"acquireLock returns false when another holder is active"` AND `src/migrations/lock.test.ts::"expired lock can be taken over"` AND `src/migrations/runner.test.ts::"applies pending migrations in order and records ledger entries"` AND `src/migrations/runner.test.ts::"returns skipped list when lock cannot be acquired"`
---
### Requirement: Tenancy provisioning callables
The package SHALL expose `provisionTenant(adapter, config, tenantId)` and `decommissionTenant(adapter, config, tenantId)` callables that honour all three strategies (`shared`, `namespace-per-tenant`, `database-per-tenant`). Tenant IDs SHALL be validated against `^[A-Za-z0-9_-]{1,64}$`; invalid IDs SHALL throw `DatabaseError(DATABASE.TENANT_INVALID)`.
#### Scenario: Shared strategy adds the discriminator field
- **WHEN** the config strategy is `shared` and `provisionTenant` is called
- **THEN** the adapter receives a `DEFINE FIELD … tenant_id` query
#### Scenario: Namespace strategy provisions a tenant namespace
- **WHEN** the config strategy is `namespace-per-tenant`
- **THEN** the adapter receives a `DEFINE NAMESPACE` query with the encoded tenant id
#### Scenario: Database strategy provisions a tenant database
- **WHEN** the config strategy is `database-per-tenant`
- **THEN** the adapter receives a `DEFINE DATABASE` query with the encoded tenant id
#### Scenario: Invalid tenant id throws TENANT_INVALID
- **WHEN** the supplied tenant id violates the validation regex
- **THEN** `provisionTenant` throws `DatabaseError(DATABASE.TENANT_INVALID)` before any query is issued
**Verified by:** `src/tenancy/tenancy.test.ts::"shared strategy adds the tenant_id discriminator"` AND `src/tenancy/tenancy.test.ts::"namespace-per-tenant strategy issues DEFINE NAMESPACE"` AND `src/tenancy/tenancy.test.ts::"database-per-tenant strategy issues DEFINE DATABASE"` AND `src/tenancy/tenancy.test.ts::"invalid tenant id throws TENANT_INVALID"`
---
### Requirement: RFC 7807 problem-json renderer
The package SHALL expose `respondProblemJson(ctx, err)` that delegates to `@suaveplan/error`'s formatter and pins the response content-type to `application/problem+json`.
#### Scenario: DatabaseError is rendered as problem+json
- **WHEN** `respondProblemJson` is invoked with a `DatabaseError`
- **THEN** the response carries `application/problem+json` and a body whose `type` is the registered URI for that code
#### Scenario: Unknown throwables fall through to 500
- **WHEN** `respondProblemJson` is invoked with a plain `Error`
- **THEN** the response status is 500 and the body is RFC 7807-shaped
**Verified by:** `src/routes/problem-json.test.ts::"DatabaseError renders as problem+json with the registered status"` AND `src/routes/problem-json.test.ts::"unknown throwables fall through to a 500 problem+json"`
---
### Requirement: Zod-validated configuration boundary
The package SHALL validate every public options bundle through Zod schemas at the call boundary. Invalid input SHALL throw `DatabaseError(DATABASE.INVALID_CONFIG)` with the `ZodError` carried as `cause`.
#### Scenario: Canonical input parses
- **WHEN** `DatabasePluginOptionsSchema.parse({ url: "ws://x/rpc" })` is invoked
- **THEN** every default field is populated (pool, retry, tenancy, migrations, query)
#### Scenario: Out-of-range pool max is rejected
- **WHEN** `createDatabasePlugin({ pool: { max: 0 } as never })` is invoked
- **THEN** the factory throws `DatabaseError(DATABASE.INVALID_CONFIG)` with the underlying `ZodError`
**Verified by:** `src/config/config-schema.test.ts::"parses canonical input with defaults"` AND `src/config/config-schema.test.ts::"rejects out-of-range pool max"` AND `src/plugin/plugin.test.ts::"createDatabasePlugin throws INVALID_CONFIG for bad options"`
---
### Requirement: OTel instrumentation
The package SHALL emit OpenTelemetry instruments through `@opentelemetry/api`'s global meter. The bundle SHALL include: `db.pool.checkouts_total` counter, `db.pool.checkins_total` counter, `db.pool.saturation` up-down-counter, `db.query.duration_ms` histogram (labelled by `{ outcome: "success" | "error" }`), and a delegation to `@suaveplan/api-migrations`' `migrations.applied_total`. A `resetDatabaseInstruments()` helper SHALL clear the cached bundle for test reuse.
#### Scenario: Every instrument emits against an in-memory MeterProvider
- **WHEN** the test setup installs an in-memory `MeterProvider`, calls every recorder, and force-flushes
- **THEN** the exporter snapshot contains a metric with each declared name
#### Scenario: Bundle is cached across calls
- **WHEN** `getDatabaseInstruments()` is invoked twice
- **THEN** the second call returns the same object as the first (referential equality)
**Verified by:** `src/otel/instruments.test.ts::"every instrument emits against the in-memory exporter"` AND `src/otel/instruments.test.ts::"caches the bundle across calls"`
---
### Requirement: Plugin factory wires pool, migrations, and service registry
The package SHALL expose `createDatabasePlugin(options?)` returning a `PluginDescriptor<DatabasePluginConfig>`. `onInit` SHALL construct the pool, eagerly create `min` adapters, and register the pool's primary adapter under `ServiceRegistry["db"]`. `onReady` SHALL run migrations under advisory lock when `migrations.autoRun` is `true`. `onClose` SHALL drain the pool. Construction-time Zod validation MUST gate every option.
#### Scenario: Plugin descriptor exposes the canonical contract
- **WHEN** consumers call `createDatabasePlugin({ url: "ws://x/rpc" })`
- **THEN** the descriptor has `name === "database"`, `version === "0.1.0"`, four lifecycle hooks defined, and `defaults` is the merged Zod-validated config
#### Scenario: onInit registers the db service
- **WHEN** the plugin lifecycle runs through `onInit` against a stub adapter factory
- **THEN** `ctx.getService("db")` returns the pooled `DatabaseAdapter`
#### Scenario: onClose drains the pool
- **WHEN** the plugin's `onClose` runs
- **THEN** the pool's `close()` is invoked and the adapter is released
**Verified by:** `src/plugin/plugin.test.ts::"descriptor exposes the canonical name/version/hooks"` AND `src/plugin/plugin.test.ts::"onInit constructs the pool and registers the db service"` AND `src/plugin/plugin.test.ts::"onClose drains the pool"`
---
### Requirement: Real SurrealDB integration
The package SHALL ship at least one integration test against a real SurrealDB instance via `@suaveplan/testing-integration-surrealdb`. The test SHALL exercise the full lifecycle (pool acquire → run migrations → tenant provision → release → close).
#### Scenario: Full roundtrip against a real container
- **WHEN** the integration test boots a real SurrealDB container, drives `createDatabasePlugin` through its lifecycle, and provisions a tenant
- **THEN** every step succeeds and the pool reports zero leaks at shutdown
**Verified by:** `tests/integration/full-boot.test.ts::"full pool + migration + tenancy roundtrip against real SurrealDB"`

View File

@@ -0,0 +1,49 @@
# Tasks — Add @suaveplan/api-plugin-database
## 1. Spec authoring
- [x] 1.1 Read `../suaveplan/enterprise/api-plugin-database.md`
- [x] 1.2 Read `openspec/AGENTS.md` §§ 0a, 2a, 6a, 7, 12, 13, 14
- [x] 1.3 Draft `proposal.md` with `## Source mapping` (in-scope) AND `### Excluded from this migration`
- [x] 1.4 Draft `specs/api-plugin-database/spec.md` with `## ADDED Requirements` + `**Verified by:**`
## 2. Implementation
- [x] 2.1 `src/error-codes/error-codes.ts` — `DATABASE_CODES` via `defineCodes`, `DatabaseError` extending `SuaveplanError`
- [x] 2.2 `src/types/types.ts` — internal types (pool entry, waiter, plugin config)
- [x] 2.3 `src/config/config-schema.ts` — Zod boundary (`DatabasePluginOptionsSchema`, `DatabasePluginConfigSchema`)
- [x] 2.4 `src/otel/instruments.ts` — counters + histogram bundle
- [x] 2.5 `src/retry/retry.ts` — exponential backoff with optional jitter + abort
- [x] 2.6 `src/pool/pool.ts` — bounded pool of `SurrealAdapter` instances
- [x] 2.7 `src/migrations/lock.ts` — advisory lock with lease expiry + takeover
- [x] 2.8 `src/migrations/runner.ts` — wraps `@suaveplan/api-migrations` runner under the advisory lock
- [x] 2.9 `src/tenancy/tenancy.ts` — `provisionTenant` / `decommissionTenant` callables, all 3 strategies
- [x] 2.10 `src/routes/problem-json.ts` — RFC 7807 renderer
- [x] 2.11 `src/services/registry-augmentation.ts` — `ServiceRegistry.db` slot
- [x] 2.12 `src/plugin/plugin.ts` — `createDatabasePlugin` factory
- [x] 2.13 `src/index.ts` — explicit named re-exports + side-effect imports
## 3. Tests
- [x] 3.1 Unit tests for each module (`*.test.ts`)
- [x] 3.2 `src/api-plugin-database.spec-conformance.test.ts` — every error code thrown from a production path
- [x] 3.3 Real SurrealDB testcontainer integration test under `tests/integration/`
- [x] 3.4 OTel emission asserted against real in-memory `MeterProvider`
## 4. Documentation
- [x] 4.1 Sibling `.md` for every `.ts` implementation file (≥200 words, basic + advanced)
- [x] 4.2 `README.md` with `## Modules` linking every co-located `.md`
## 5. Pre-archive verification (the 5 gates)
- [x] 5.1 `bun run check` — Biome 0 errors
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-plugin-database` green
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-plugin-database` — 100/100/100/100
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-plugin-database` — 0 errors
- [x] 5.5 `bunx turbo build --filter=@suaveplan/api-plugin-database` green
## 6. Archive
- [x] 6.1 Fold `specs/api-plugin-database/spec.md` into `openspec/specs/api-plugin-database/spec.md`
- [x] 6.2 Move `openspec/changes/add-api-plugin-database/` to `openspec/archive/2026-05-18-add-api-plugin-database/`