Files
2026-05-27 23:29:53 +01:00

92 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Add @suaveplan/seed — declarative database seeding & fixture framework
**Package**: `@suaveplan/seed`
**Status**: PROPOSED
**Type**: migration (upstream `../suaveplan/packages/server/seed`)
- **Design**: ./design.md
- **Spec delta**: ./specs/seed/spec.md
## Why
`STATUS.md` line 200 lists `@suaveplan/seed` as Pending. The upstream package is a small, runtime-pure orchestration core (3 features, 1 utility, fixtures, ~520 LOC of `src/`) that solves a problem genesis has no other answer for: declaratively seeding databases with dependency-ordered, idempotent, environment-filtered, batched, dry-run-capable runs against any storage adapter the caller injects. No genesis package covers this surface today, and several pending downstream packages — anything in the database or fixtures-oriented testing space — will want to depend on it. The upstream is well-shaped (98.85% test coverage), tier-pure (zero `node:`/`bun:` imports — only `performance.now()`), and dependency-light (single runtime dep on `@suaveplan/error`).
The migration is straightforward — preserve the public surface, drop the redundant `core/` wrapper + `seed-` filename prefix per the no-redundant-prefixes rule, upgrade plain `throw new Error(...)` to `SuaveplanError` with a `SEED.*` code registry, rewrite the buggy first-pass in `topoSort` cleanly, and ship with injectable `LoggerLike`/tracer slots (no-op defaults) consistent with every other genesis package. The enterprise audit (`../suaveplan/enterprise/seed.md`) lists ambitious additions (SurrealDB schema migrations, JWT tenant scoping, RBAC, audit logging, rate limiting, webhooks) — these are explicitly out of scope for this change and belong in a follow-up `add-api-plugin-seed` proposal that wraps the core behind a plugin tier.
## What changes
- ADDED: `SeedRunner` class — orchestrates `SeedRunConfig` runs against an injected `SeedStore`, with dependency-ordered execution (topological sort), `only`/`exclude` filtering, `dryRun`, `continueOnError`, per-batch processing (default `DEFAULT_BATCH_SIZE = 100`, configurable), three upsert strategies (`skip`/`update`/`replace`), per-seed `truncate`, and `onProgress` callback emission per completed seed.
- ADDED: `SeedRegistry` class — `register` / `registerMany` / `get` / `getAll` / `getForEnvironment(env)` / `has` / `remove` / `clear` / `size` / `names`; in-memory `Map<name, SeedDefinition>` with defensive-copy on read and `SEED.DUPLICATE_NAME` raised on re-registration.
- ADDED: `SeedBuilder` fluent class + `defineSeed(name)` convenience — chainable methods `table` / `environments(...envs)` / `dependsOn(...names)` / `data(records)` / `dataFactory(fn)` / `upsert(strategy, keys)` / `description(desc)` / `priority(n)` / `truncateFirst(enabled?)` / `build()`.
- ADDED: `FixtureFactory<T>` + `createFixtureFactory(defaults)` — auto-incrementing counter, `create(overrides?)`, `createMany(count, overrides?)`, `reset()`, `counter` getter.
- ADDED: `InMemorySeedStore` + `createInMemoryStore()` — full `SeedStore` shape (`insert`/`update`/`findExisting`/`truncate`/`tableExists`) plus test-affordance `getRecords(table)` and `clear()`.
- ADDED: `topoSort(seeds)` utility — Kahn's algorithm with priority tiebreak; raises `SEED.UNREGISTERED_DEPENDENCY` for dangling names, `SEED.CIRCULAR_DEPENDENCY` for cycles, `SEED.DEPENDENCY_DEPTH_EXCEEDED` when iterations exceed `MAX_DEPENDENCY_DEPTH × seedCount`.
- ADDED: `detectCycles(seeds)` utility — DFS-based cycle reporter returning `string[]` of `a -> b -> a` traces; empty array on clean graph.
- ADDED: `SeedDefinition` / `SeedRecord` / `SeedDataFactory` / `UpsertStrategy` / `SeedEnvironment` / `SeedStatus` / `SeedResult` / `SeedRunResult` / `SeedRunConfig` / `SeedStore` types in `types/common.ts`; `ISeedBuilder` / `ISeedRegistry` / `ISeedRunner` interface contracts co-located with each implementation.
- ADDED: Constants — `DEFAULT_ENVIRONMENTS` (`["dev","staging","test","production"]` as const), `MAX_DEPENDENCY_DEPTH = 50`, `DEFAULT_BATCH_SIZE = 100`, `SEED_STATUS` enum object.
- ADDED: `SEED_CODES` registry via `defineCodes("SEED", [...])` — 7 codes: `DUPLICATE_NAME`, `UNREGISTERED_DEPENDENCY`, `CIRCULAR_DEPENDENCY`, `DEPENDENCY_DEPTH_EXCEEDED`, `INVALID_CONFIG`, `STORE_ERROR`, `ABORTED`.
- ADDED: `SeedError extends SuaveplanError<SeedErrorCode>` with `seedName?` / `dependency?` / `path?` discriminants for diagnostics.
- ADDED: Optional injectable `LoggerLike` (`@suaveplan/types`) on `SeedRunner` constructor — no-op default. Logs `seed.run.start` / `seed.run.complete` / `seed.skipped` / `seed.failed` structured records when wired.
- ADDED: Optional `AbortSignal` plumbing on `SeedRunConfig.signal` — checked at every seed boundary; aborted runs return a `SeedRunResult` with `success: false` and remaining seeds marked `status: "skipped"` with `error: "Aborted"`. (Upstream had no signal support.)
- ADDED: Subpath exports `./runner/runner`, `./registry/registry`, `./builder/builder`, `./fixtures/fixtures`, `./utils/topo-sort/topo-sort`, `./types/common`, `./constants` — tree-shakeable.
## Out of scope
- **SurrealDB plugin wiring, `definePlugin()` lifecycle, `./surql` schema directory, `migrationsPath`.** The enterprise audit calls these out as required for the package to flip to ✅ Enterprise, but they belong to a higher-tier `api-plugin-seed` package that consumes `@suaveplan/seed` and provides the plugin shell. Keeping the core universal-tier is what makes it portable across server, browser-test, and worker contexts. Reason: same separation `drive` ↔ `drive-template`, `network` ↔ `network-plugin-*`.
- **JWT tenant scoping, RBAC/ABAC, rate limiting, idempotency headers, audit logging, webhook delivery.** All API-tier concerns; none belong in a pure orchestration core. Reason: orthogonal to seeding mechanics; the seed runner takes data and a store and writes records — auth lives at the HTTP boundary that wraps it.
- **Real SurrealDB integration test suite via `bun run test:with-db:filter`.** The core ships with an `InMemorySeedStore` for testing, and the real-database integration belongs to whichever adapter package provides the production `SeedStore` (e.g. a future `seed-store-surrealdb`). Reason: the core is store-agnostic by design; coupling tests to one DB violates that contract.
- **Benchmarks & CI regression gate.** Useful but not blocking for a pure orchestration core whose hot path is bounded by the injected store's throughput. Defer to enterprise wave. Reason: meaningful benchmarks require a real store; the in-memory store benchmarks are testing JavaScript loops.
- **i18n message catalog for error messages.** The error codes are stable; the human-readable messages are developer-facing English strings. When `i18n` lands on the genesis enterprise path for errors broadly, `SEED.*` codes inherit the same treatment. Reason: not seed-specific.
- **`SeedDefinition` Zod validation at the boundary.** The fluent builder + TypeScript types are the public contract; the developer-facing API is constructed in-process, not over a wire. Adding runtime Zod would be defensive coding without a real attack surface. Reason: orthogonal to the API surface this package exposes.
## Source mapping
Upstream source: `../suaveplan/packages/server/seed/src/` (23 files, 98.85% upstream coverage). Every public symbol in upstream `src/index.ts` migrates as-is; layout normalises per the no-prefix and feature-folder rules.
### Parity Checklist
| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
| `SeedBuilder` fluent class + `defineSeed` | ✅ | ✅ | Class & function preserved; subpath `./builder/builder` (renamed from `./core/seed-builder/seed-builder`) |
| `SeedRegistry` with env-filter, dup-detect | ✅ | ⬆️ Enhanced | `throw new Error("already registered")` → `SeedError({ code: SEED.DUPLICATE_NAME, seedName })` |
| `SeedRunner` with topo-order, filtering, batching, upsert, dry-run, continue-on-error, progress callback | ✅ | ✅ | Behaviour preserved bit-for-bit; constructor gains optional `LoggerLike`, config gains optional `signal` |
| `topoSort` with priority tiebreak | ✅ | ⬆️ Enhanced | Upstream has dead-code first pass (`+ 0` no-op) and a second cleanup pass — rewritten cleanly as one Kahn's-algorithm pass; `throw new Error(...)` replaced with `SeedError` |
| `detectCycles` DFS reporter | ✅ | ✅ | Unchanged logic; relocated to `utils/topo-sort/topo-sort.ts` |
| `FixtureFactory<T>` + `createFixtureFactory` | ✅ | ✅ | Generic preserved; `counter` getter preserved |
| `InMemorySeedStore` + `createInMemoryStore` | ✅ | ✅ | Full `SeedStore` surface + `getRecords` / `clear` test affordances preserved |
| All public types from `types/common.ts` | ✅ | ✅ | Re-exported from root; subpath `./types/common` added |
| 4 constants (`DEFAULT_ENVIRONMENTS`, `MAX_DEPENDENCY_DEPTH`, `DEFAULT_BATCH_SIZE`, `SEED_STATUS`) | ✅ | ✅ | Preserved; subpath `./constants` added |
| Subpath exports per public module | ✅ | ⬆️ Enhanced | Upstream exposes 4 subpaths under `./core/seed-*` and `./fixtures/fixtures`; genesis exposes 7 subpaths with the redundant `core/` and `seed-` prefix dropped |
| Plain `throw new Error(...)` at boundaries | ✅ | ⬆️ Replaced | All raised errors now extend `SuaveplanError` with stable `SEED.*` codes (rule §63 of CLAUDE.md) |
| `LoggerLike` injection | 🚫 upstream | ⬆️ Added | Genesis convention; no-op default, zero runtime cost when unwired |
| `AbortSignal` plumbing | 🚫 upstream | ⬆️ Added | `SeedRunConfig.signal` checked at every seed boundary; consistent with `storage-http`/`drive` precedent |
| Plugin wiring (`definePlugin`, lifecycle) | ✅ enterprise audit asks for it | 🚫 deferred | Scoped to `add-api-plugin-seed` follow-up; the core stays universal-tier |
| SurrealDB schema migrations / `./surql` | ✅ enterprise audit asks for it | 🚫 deferred | Belongs to a future store adapter package; the core is store-agnostic |
| JWT tenant / RBAC / rate-limit / audit / webhooks / idempotency | ✅ enterprise audit asks for it | 🚫 deferred | API-tier concerns; deferred to `add-api-plugin-seed` |
| OTel spans / Prometheus metrics | ✅ enterprise audit asks for it | 🚫 deferred | Wire when the `api-plugin-seed` package lands and there's a real call site; the core emits structured logs via `LoggerLike` only |
| Benchmarks (`src/__benchmarks__`) + CI gate | ✅ enterprise audit asks for it | 🚫 deferred | Requires real store; defer to enterprise wave |
| `@suaveplan/api-schemas` Zod boundary | ✅ enterprise audit asks for it | 🚫 deferred | Not applicable to in-process developer API; revisit if seed config is ever serialised over a wire |
### Reference docs consulted
- ✅ `../suaveplan/packages/server/seed/src/` — all 17 implementation files read
- ✅ `../suaveplan/enterprise/seed.md` — enterprise audit, in full
- ✅ `../suaveplan/packages/server/seed/package.json` — dep graph, public exports
- 🚫 `../suaveplan/openspec/specs/seed/spec.md` — upstream had no formal spec for this package; the parity contract is the source itself
- 🚫 `../suaveplan/openspec/changes/*/proposal.md` touching seed — none found
## Impact
### Affected dependents
No dependents today — `@suaveplan/seed` is being added net-new. Future consumers (`api-plugin-seed`, any package shipping a `SeedStore` adapter, integration test fixtures) will land in subsequent changes against the public contract this proposal locks in.
### Risk
- **Low.** Pure orchestration; no IO of its own. The buggy `topoSort` first-pass rewrite is the only non-mechanical change to upstream behaviour — covered by adding explicit unit tests against the upstream test suite to prove output-equivalent ordering before the rewrite.
- **Naming change to subpath exports** (`./core/seed-runner/seed-runner` → `./runner/runner`) is technically a public-API rename, but the package is at `0.1.0` on genesis with zero consumers, so there is no migration path to worry about.
### Migration notes
None. Net-new package on genesis.