307 lines
22 KiB
Markdown
307 lines
22 KiB
Markdown
<!-- markdownlint-disable MD024 -->
|
|
|
|
# Capability Spec Delta: seed (seed)
|
|
|
|
**Package**: `@suaveplan/seed`
|
|
**Status**: PROPOSED
|
|
|
|
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
|
|
|
## ADDED Requirements
|
|
|
|
### Requirement: Universal-tier purity
|
|
|
|
The package SHALL ship as a universal-tier package — no imports from `node:*`, `bun:*`, or the bare specifiers `fs`/`path`/`stream`/`net`/`http`/`crypto` in any file under `src/`. The package SHALL run unchanged under Node ≥ 18, Bun, modern browsers, and Web Workers. The `package.json` SHALL NOT declare a `"browser"` field, SHALL NOT pin `engines.node` or `engines.bun`, and SHALL declare `"sideEffects": false`. The single permitted platform-aware call is `performance.now()` (available in every supported runtime).
|
|
|
|
#### Scenario: no platform imports in src/
|
|
|
|
- **WHEN** a CI step runs `grep -rnE "from ['\"](node:|bun:|fs|path|stream|net|http|crypto)['\"]" packages/database/seed/src/`
|
|
- **THEN** the command produces zero matches
|
|
|
|
**Verified by:** `src/runner/runner.test.ts::"runner has no node/bun imports (tier purity)"` AND `src/utils/topo-sort/topo-sort.test.ts::"topo-sort has no node/bun imports (tier purity)"`
|
|
|
|
---
|
|
|
|
### Requirement: Stable SEED_CODES error registry
|
|
|
|
The package SHALL declare an error-code registry via `defineCodes("SEED", [...])` containing exactly the codes `DUPLICATE_NAME`, `UNREGISTERED_DEPENDENCY`, `CIRCULAR_DEPENDENCY`, `DEPENDENCY_DEPTH_EXCEEDED`, `INVALID_CONFIG`, `STORE_ERROR`, `ABORTED`. Every public error path SHALL throw or wrap a `SeedError extends SuaveplanError<SeedErrorCode>` carrying one of those codes. `SeedError` SHALL expose optional `seedName` and `dependency` discriminants for diagnostics. The package SHALL NOT throw raw `Error` instances from any public method.
|
|
|
|
#### Scenario: every code is reachable from a real call site
|
|
|
|
- **WHEN** the test suite exercises duplicate-name registration, missing-dependency topo-sort, cyclic topo-sort, depth-guard overflow, invalid run config, store throwing, and aborted run
|
|
- **THEN** each path raises (or returns a `SeedResult` wrapping) a `SeedError` whose `code` matches the expected `SEED.*` constant
|
|
|
|
#### Scenario: SuaveplanError contract preserved
|
|
|
|
- **WHEN** a `SeedError` is thrown with a `cause` (e.g. wrapping an injected store exception)
|
|
- **THEN** the resulting error is `instanceof SuaveplanError`, exposes the original on `error.cause`, and the wrapped code remains stable across renames
|
|
|
|
**Verified by:** `src/error-codes/error-codes.test.ts::"every SEED.* code is reachable from a call site"` AND `src/error-codes/error-codes.test.ts::"SeedError preserves cause and SuaveplanError contract"`
|
|
|
|
---
|
|
|
|
### Requirement: SeedBuilder fluent API + defineSeed
|
|
|
|
The package SHALL provide a `SeedBuilder` class and a `defineSeed(name: string): SeedBuilder` convenience function. `SeedBuilder` SHALL expose chainable methods `table(name)`, `environments(...envs)`, `dependsOn(...names)`, `data(records)`, `dataFactory(fn)`, `upsert(strategy, keys)`, `description(text)`, `priority(n)`, `truncateFirst(enabled?)`, and a terminal `build()` returning a `SeedDefinition`. The default `table` SHALL equal the seed `name`. `build()` SHALL omit optional fields that were never set (no empty arrays or empty strings serialised into the output). When both `data(...)` and `dataFactory(...)` are called, the most recent call SHALL win. The `ISeedBuilder` interface SHALL match the public method shape exactly so callers can program against the contract.
|
|
|
|
#### Scenario: default table equals name
|
|
|
|
- **WHEN** a caller invokes `defineSeed("users").build()`
|
|
- **THEN** the returned definition has `name: "users"` and `table: "users"`
|
|
|
|
#### Scenario: optional fields omitted when unset
|
|
|
|
- **WHEN** a caller invokes `defineSeed("u").data([{ id: 1 }]).build()`
|
|
- **THEN** the returned definition has no `environments`, no `dependencies`, no `upsertKeys`, no `description`, no `truncate` field on the output
|
|
|
|
#### Scenario: dataFactory wins over data
|
|
|
|
- **WHEN** a caller invokes `defineSeed("u").data([{ a: 1 }]).dataFactory(() => [{ a: 2 }])`
|
|
- **THEN** `build().data` is the factory function
|
|
|
|
**Verified by:** `src/builder/builder.test.ts::"default table equals name"` AND `src/builder/builder.test.ts::"omits optional fields that were never set"` AND `src/builder/builder.test.ts::"dataFactory overrides prior data() call"`
|
|
|
|
---
|
|
|
|
### Requirement: SeedRegistry registration, query, and duplicate detection
|
|
|
|
The package SHALL provide a `SeedRegistry` class with methods `register(seed)`, `registerMany(seeds)`, `get(name)`, `getAll()`, `getForEnvironment(env)`, `has(name)`, `remove(name)`, `clear()`, and accessors `size` and `names`. `register` SHALL throw `SeedError({ code: SEED.DUPLICATE_NAME, seedName })` when the name is already registered. `get` and `getAll` SHALL return defensive copies of stored definitions (mutations of the returned objects MUST NOT affect the registry). `getForEnvironment(env)` SHALL return seeds whose `environments` array is undefined, empty, OR contains `env`. The `ISeedRegistry` interface SHALL match the public method shape exactly.
|
|
|
|
#### Scenario: duplicate registration raises SEED.DUPLICATE_NAME
|
|
|
|
- **WHEN** `registry.register({ name: "x", table: "x", data: [] })` is called twice
|
|
- **THEN** the second call throws `SeedError` with `code === "SEED.DUPLICATE_NAME"` and `seedName === "x"`
|
|
|
|
#### Scenario: defensive copies on read
|
|
|
|
- **WHEN** a caller mutates the array returned from `registry.getAll()`
|
|
- **THEN** subsequent calls to `registry.get` or `registry.getAll` return the original unmodified definitions
|
|
|
|
#### Scenario: environment filter includes unrestricted seeds
|
|
|
|
- **WHEN** the registry contains seed A (no `environments`), seed B (`environments: ["test"]`), seed C (`environments: ["dev"]`)
|
|
- **THEN** `getForEnvironment("test")` returns A and B but not C
|
|
|
|
**Verified by:** `src/registry/registry.test.ts::"register raises SEED.DUPLICATE_NAME on collision"` AND `src/registry/registry.test.ts::"getAll returns defensive copies"` AND `src/registry/registry.test.ts::"getForEnvironment honours unrestricted and matching seeds"`
|
|
|
|
---
|
|
|
|
### Requirement: topoSort produces dependency-respecting order with priority tiebreak
|
|
|
|
The `topoSort(seeds)` utility SHALL return a string array of seed names where every name precedes every seed that lists it in `dependencies`. Among seeds with the same in-degree, ordering SHALL be stable by ascending `priority` (lower priority = earlier execution; default `0`). When a seed's `dependencies` entry names a seed that is not present in the input, `topoSort` SHALL throw `SeedError({ code: SEED.UNREGISTERED_DEPENDENCY, seedName: <dependent>, dependency: <missing> })`. When a cycle is detected (either during traversal or via residual unprocessed nodes), `topoSort` SHALL throw `SeedError({ code: SEED.CIRCULAR_DEPENDENCY, message: <names involved> })`. When the iteration counter exceeds `MAX_DEPENDENCY_DEPTH * seeds.length`, `topoSort` SHALL throw `SeedError({ code: SEED.DEPENDENCY_DEPTH_EXCEEDED })` as a defensive guard.
|
|
|
|
#### Scenario: respects dependency edges
|
|
|
|
- **WHEN** `topoSort([{ name: "a", dependencies: ["b"] }, { name: "b" }])` is called
|
|
- **THEN** the result is `["b", "a"]`
|
|
|
|
#### Scenario: priority breaks ties for independent seeds
|
|
|
|
- **WHEN** `topoSort([{ name: "a", priority: 10 }, { name: "b", priority: 1 }])` is called
|
|
- **THEN** the result is `["b", "a"]`
|
|
|
|
#### Scenario: missing dependency raises UNREGISTERED_DEPENDENCY
|
|
|
|
- **WHEN** `topoSort([{ name: "a", dependencies: ["missing"] }])` is called
|
|
- **THEN** a `SeedError` is thrown with `code === "SEED.UNREGISTERED_DEPENDENCY"`, `seedName === "a"`, `dependency === "missing"`
|
|
|
|
#### Scenario: cycle raises CIRCULAR_DEPENDENCY
|
|
|
|
- **WHEN** `topoSort([{ name: "a", dependencies: ["b"] }, { name: "b", dependencies: ["a"] }])` is called
|
|
- **THEN** a `SeedError` is thrown with `code === "SEED.CIRCULAR_DEPENDENCY"` and the message lists both `a` and `b`
|
|
|
|
#### Scenario: upstream test corpus passes verbatim
|
|
|
|
- **WHEN** every test case from `../suaveplan/packages/server/seed/src/utils/topo-sort.test.ts` is re-run against the genesis implementation
|
|
- **THEN** every assertion that expected a specific order passes, and every assertion that expected an error matches the new `SeedError` instead of the upstream `Error`
|
|
|
|
**Verified by:** `src/utils/topo-sort/topo-sort.test.ts::"respects dependency edges"` AND `src/utils/topo-sort/topo-sort.test.ts::"priority breaks ties for independent seeds"` AND `src/utils/topo-sort/topo-sort.test.ts::"raises UNREGISTERED_DEPENDENCY for missing dep"` AND `src/utils/topo-sort/topo-sort.test.ts::"raises CIRCULAR_DEPENDENCY for cycle"` AND `src/utils/topo-sort/topo-sort.test.ts::"upstream characterisation cases pass"`
|
|
|
|
---
|
|
|
|
### Requirement: detectCycles returns explicit cycle traces
|
|
|
|
The `detectCycles(seeds)` utility SHALL perform DFS-based cycle detection across the dependency graph and return an array of cycle-trace strings of the form `"a -> b -> a"` for every cycle found. The function SHALL NOT throw; an acyclic graph SHALL return an empty array. Nodes that depend on names not in the graph SHALL be skipped (no false-positive cycles).
|
|
|
|
#### Scenario: empty result on DAG
|
|
|
|
- **WHEN** `detectCycles([{ name: "a", dependencies: ["b"] }, { name: "b" }])` is called
|
|
- **THEN** the return value is `[]`
|
|
|
|
#### Scenario: cycle trace identifies participants
|
|
|
|
- **WHEN** `detectCycles([{ name: "a", dependencies: ["b"] }, { name: "b", dependencies: ["a"] }])` is called
|
|
- **THEN** the result contains at least one string matching `/a -> b -> a|b -> a -> b/`
|
|
|
|
**Verified by:** `src/utils/topo-sort/topo-sort.test.ts::"detectCycles returns empty on DAG"` AND `src/utils/topo-sort/topo-sort.test.ts::"detectCycles reports cycle participants"`
|
|
|
|
---
|
|
|
|
### Requirement: SeedRunner dependency-ordered, filtered, batched execution
|
|
|
|
The package SHALL provide a `SeedRunner` class with constructor `(registry: SeedRegistry, store: SeedStore, logger?: LoggerLike)` and an async `run(config: SeedRunConfig): Promise<SeedRunResult>` method. `run` SHALL:
|
|
|
|
1. Query seeds via `registry.getForEnvironment(config.environment)`.
|
|
2. Apply `config.only` and `config.exclude` filters (`only` first, `exclude` second).
|
|
3. Topologically sort the filtered seeds.
|
|
4. Execute seeds in order. For each seed:
|
|
- If any declared `dependency` produced a `failed` result earlier in the run, the seed SHALL be marked `status: "skipped"` with `error: "Dependency failed"`, recorded in `results`, surfaced via `config.onProgress?.(result)`, and the runner SHALL move on.
|
|
- Otherwise, if `seed.truncate` is true, call `store.truncate(seed.table)` before insertion.
|
|
- Resolve `seed.data` (calling the factory if it is a function).
|
|
- If `config.dryRun`, synthesise a `completed` result with `recordsInserted = records.length` and no store IO.
|
|
- Otherwise, batch the records by `config.batchSize ?? DEFAULT_BATCH_SIZE` and apply `seed.upsertStrategy` (default `"skip"`):
|
|
- `"skip"` with non-empty `upsertKeys`: call `store.findExisting(...)`, insert only records whose key tuple is not present, count the rest as `recordsSkipped`.
|
|
- `"update"` with non-empty `upsertKeys`: insert records whose key tuple is absent; call `store.update(...)` for the rest.
|
|
- `"replace"` or empty `upsertKeys`: call `store.insert(...)` for every record.
|
|
5. After each seed, invoke `config.onProgress?.(result)` and append to `results`.
|
|
6. If a seed throws or `store.*` rejects, wrap as `SeedError({ code: SEED.STORE_ERROR, cause })`, record the message on `SeedResult.error`, and either continue (if `continueOnError`) or break the loop.
|
|
7. Return `SeedRunResult` with `environment`, `totalDuration` (ms from `performance.now()`), `results`, `success` (true iff zero `failed`), `seedsRun` (non-skipped), `seedsSkipped`, `seedsFailed`.
|
|
|
|
`SeedRunner` SHALL NOT mutate the registry, the store's internal state beyond the documented operations, or the `config` object.
|
|
|
|
#### Scenario: dependency-ordered execution
|
|
|
|
- **WHEN** the registry holds `users`, `posts` (depends on `users`), and `tags` (no deps), and `run({ environment: "dev" })` is invoked
|
|
- **THEN** the resulting `results` array has `users` before `posts`; `tags` appears in topo-order with priority `0`
|
|
|
|
#### Scenario: only + exclude filters compose
|
|
|
|
- **WHEN** four seeds `a`, `b`, `c`, `d` are registered and the run config sets `only: ["a", "b", "c"]` and `exclude: ["b"]`
|
|
- **THEN** the run executes only `a` and `c`
|
|
|
|
#### Scenario: dependency failure skips dependents
|
|
|
|
- **WHEN** seed `a` fails (store throws) and seed `b` declares `dependsOn("a")`, and `continueOnError: true` is set
|
|
- **THEN** `b`'s result has `status: "skipped"` and `error: "Dependency failed"`; `a`'s result has `status: "failed"` with the underlying store error message
|
|
|
|
#### Scenario: dryRun bypasses store IO
|
|
|
|
- **WHEN** `run({ environment: "dev", dryRun: true })` is invoked with a `SeedStore` whose `insert` throws if called
|
|
- **THEN** the run completes successfully; `insert` is never invoked; every result has `status: "completed"` with `recordsInserted` equal to the resolved record count
|
|
|
|
#### Scenario: batching honours batchSize
|
|
|
|
- **WHEN** a seed has 250 records and `config.batchSize = 100`
|
|
- **THEN** `store.insert` is invoked three times with batch lengths `100, 100, 50`
|
|
|
|
#### Scenario: upsert strategy "skip"
|
|
|
|
- **WHEN** a seed with `upsertStrategy: "skip"` and `upsertKeys: ["id"]` is run, and `store.findExisting` returns one record with `id: 2` from a batch of `[{id:1}, {id:2}, {id:3}]`
|
|
- **THEN** `store.insert` is invoked with `[{id:1}, {id:3}]`, the result has `recordsInserted: 2` and `recordsSkipped: 1`
|
|
|
|
#### Scenario: upsert strategy "update"
|
|
|
|
- **WHEN** a seed with `upsertStrategy: "update"` and `upsertKeys: ["id"]` is run, and `store.findExisting` returns one record with `id: 2` from a batch of `[{id:1}, {id:2}, {id:3}]`
|
|
- **THEN** `store.insert` is invoked with `[{id:1}, {id:3}]` (count = 2), `store.update` is invoked with `[{id:2}]` (count = 1), the result has `recordsInserted: 2` and `recordsUpdated: 1`
|
|
|
|
#### Scenario: store error wrapped with SEED.STORE_ERROR
|
|
|
|
- **WHEN** an injected `store.insert` rejects with `new Error("boom")`
|
|
- **THEN** the corresponding `SeedResult` has `status: "failed"` and `error` contains `"boom"`; the run continues if `continueOnError: true` is set, or halts otherwise
|
|
|
|
**Verified by:** `src/runner/runner.test.ts::"executes seeds in dependency order"` AND `src/runner/runner.test.ts::"only and exclude filters compose"` AND `src/runner/runner.test.ts::"dependent seeds skipped when dep fails"` AND `src/runner/runner.test.ts::"dryRun bypasses store IO"` AND `src/runner/runner.test.ts::"batches by batchSize"` AND `src/runner/runner.test.ts::"skip upsert strategy"` AND `src/runner/runner.test.ts::"update upsert strategy"` AND `src/runner/runner.test.ts::"wraps store errors with SEED.STORE_ERROR"`
|
|
|
|
---
|
|
|
|
### Requirement: AbortSignal plumbing on SeedRunConfig
|
|
|
|
`SeedRunConfig` SHALL accept an optional `signal: AbortSignal`. `SeedRunner.run` SHALL check `signal.aborted` between seeds; when aborted, the runner SHALL stop executing further seeds and mark every remaining seed in the topo-order with `status: "skipped"` and `error: "Aborted"`. The returned `SeedRunResult.success` SHALL be `false`. The signal SHALL be propagated to `SeedStore` method calls via the optional `signal?: AbortSignal` parameter on every `SeedStore` method (default: ignored by `InMemorySeedStore`).
|
|
|
|
#### Scenario: aborted mid-run
|
|
|
|
- **WHEN** a `run` is invoked with three seeds and the signal is aborted after the first seed completes
|
|
- **THEN** the first seed's result is `completed`, the second and third are `skipped` with `error: "Aborted"`, and `success` is `false`
|
|
|
|
**Verified by:** `src/runner/runner.test.ts::"abort signal halts run and skips remaining seeds"`
|
|
|
|
---
|
|
|
|
### Requirement: Structured log events via injected LoggerLike
|
|
|
|
When a `LoggerLike` is injected into `SeedRunner`, the runner SHALL emit structured log records at the following moments with the listed fields. When no logger is injected, the runner SHALL behave identically to the upstream package (silent execution, no IO). Emitted records:
|
|
|
|
- `seed.run.start` — `{ environment, totalSeeds }`
|
|
- `seed.completed` — `{ name, table, recordsInserted, recordsUpdated, recordsSkipped, duration }`
|
|
- `seed.skipped` — `{ name, table, reason }` where `reason` ∈ `"Dependency failed"` | `"Aborted"`
|
|
- `seed.failed` — `{ name, table, error, duration }`
|
|
- `seed.run.complete` — `{ environment, success, seedsRun, seedsSkipped, seedsFailed, totalDuration }`
|
|
|
|
#### Scenario: log events emitted when logger wired
|
|
|
|
- **WHEN** a recording `LoggerLike` (captures every call) is passed to `new SeedRunner(registry, store, logger)` and `run` executes one successful seed
|
|
- **THEN** the recorded calls include `seed.run.start`, `seed.completed`, and `seed.run.complete` records in order, each with the documented fields
|
|
|
|
#### Scenario: silent execution without logger
|
|
|
|
- **WHEN** `new SeedRunner(registry, store)` is constructed with no logger and `run` executes any seed graph
|
|
- **THEN** no log IO is performed and the run completes identically to the equivalent invocation with a no-op logger
|
|
|
|
**Verified by:** `src/runner/runner.test.ts::"emits structured log events when logger is wired"` AND `src/runner/runner.test.ts::"runs silently when no logger is provided"`
|
|
|
|
---
|
|
|
|
### Requirement: FixtureFactory generic with deterministic incrementing index
|
|
|
|
The package SHALL provide a `FixtureFactory<T extends SeedRecord>` class and a `createFixtureFactory<T>(defaults: (index: number) => T): FixtureFactory<T>` factory function. The class SHALL expose `create(overrides?: Partial<T>): T`, `createMany(count: number, overrides?: Partial<T>[]): T[]`, `reset(): void`, and a `counter` getter. The internal counter SHALL start at `0`, increment by `1` for every `create` call (including each iteration of `createMany`), and reset to `0` only via `reset()`.
|
|
|
|
#### Scenario: counter increments deterministically
|
|
|
|
- **WHEN** a factory built from `defaults: (i) => ({ id: i })` is invoked `create()` three times
|
|
- **THEN** the returned records are `{ id: 0 }`, `{ id: 1 }`, `{ id: 2 }`; `counter` is `3`; `reset()` returns counter to `0`
|
|
|
|
#### Scenario: per-record overrides in createMany
|
|
|
|
- **WHEN** `createMany(2, [{ name: "a" }, { name: "b" }])` is called against `defaults: (i) => ({ id: i, name: "default" })`
|
|
- **THEN** the returned records are `[{ id: 0, name: "a" }, { id: 1, name: "b" }]`
|
|
|
|
**Verified by:** `src/fixtures/fixtures.test.ts::"counter increments deterministically and resets"` AND `src/fixtures/fixtures.test.ts::"createMany applies per-record overrides"`
|
|
|
|
---
|
|
|
|
### Requirement: InMemorySeedStore implements SeedStore + test affordances
|
|
|
|
The package SHALL provide an `InMemorySeedStore` class and a `createInMemoryStore(): InMemorySeedStore` factory implementing the full `SeedStore` interface (`insert`/`update`/`findExisting`/`truncate`/`tableExists`) plus the test affordances `getRecords(table): SeedRecord[]` and `clear(): void`. All operations SHALL be in-memory, synchronous in effect but exposed as `Promise`-returning to match the contract. `findExisting` SHALL compare records by the join of `key::value` tuples for the supplied `keyFields`. `truncate(table)` SHALL replace the table's contents with `[]` (the table key remains in the map so `tableExists` returns true).
|
|
|
|
#### Scenario: round-trip insert / findExisting / update
|
|
|
|
- **WHEN** records `[{id:1}, {id:2}]` are inserted, then `findExisting("t", [{id:1}, {id:3}], ["id"])` is queried, then `update("t", [{id:1, v:"x"}], ["id"])` is applied
|
|
- **THEN** `findExisting` returns `[{id:1}]`; after update, `getRecords("t")` reflects the change to `{id:1, v:"x"}`
|
|
|
|
#### Scenario: truncate preserves table existence
|
|
|
|
- **WHEN** a table has records and `truncate(table)` is called
|
|
- **THEN** `getRecords(table)` returns `[]` and `tableExists(table)` remains `true`
|
|
|
|
**Verified by:** `src/fixtures/fixtures.test.ts::"in-memory store round-trips insert/findExisting/update"` AND `src/fixtures/fixtures.test.ts::"truncate empties table but preserves existence"`
|
|
|
|
---
|
|
|
|
### Requirement: Subpath exports tree-shake per feature module
|
|
|
|
The `package.json` SHALL declare subpath exports for every public feature module so consumers can import them without dragging the entire package surface. The required subpaths are exactly: `.` (root barrel), `./constants`, `./error-codes/error-codes`, `./types/common`, `./builder/builder`, `./registry/registry`, `./runner/runner`, `./fixtures/fixtures`, `./utils/topo-sort/topo-sort`. Each subpath SHALL resolve to a built `.js` + `.d.ts` pair under `dist/`. The `sideEffects` field SHALL be `false`.
|
|
|
|
#### Scenario: every public symbol importable via subpath
|
|
|
|
- **WHEN** a consumer writes `import { SeedRunner } from "@suaveplan/seed/runner/runner"` and `import { topoSort } from "@suaveplan/seed/utils/topo-sort/topo-sort"`
|
|
- **THEN** both imports resolve at build time and the resolved file is the dedicated feature module, not the root barrel
|
|
|
|
**Verified by:** `src/runner/runner.test.ts::"subpath export resolves to runner module"` AND `src/utils/topo-sort/topo-sort.test.ts::"subpath export resolves to topo-sort module"`
|
|
|
|
---
|
|
|
|
### Requirement: Banned patterns absent from src/
|
|
|
|
The package SHALL ship with zero matches of `@deprecated`, `TODO`, `FIXME`, `ts-ignore`, `ts-expect-error`, `it.skip`, `test.skip`, `describe.skip`, `.only`, raw `any` (the word as a TypeScript type), or `console.log` in any `.ts` or `.tsx` file under `src/`. The package SHALL ship with zero `// biome-ignore` or other static-analysis suppression comments. All test files SHALL import runner primitives from `@suaveplan/testing/runner` (rule §7).
|
|
|
|
#### Scenario: clean static-analysis grep
|
|
|
|
- **WHEN** the CI step `grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\\.skip\\|\\.only\\|\\bany\\b\\|console\\.log" packages/database/seed/src/ --include="*.ts"` runs
|
|
- **THEN** the command produces zero matches
|
|
|
|
#### Scenario: test runner imports go through wrapper
|
|
|
|
- **WHEN** `grep -rnE "from ['\"](vitest|bun:test)['\"]" packages/database/seed/src/` runs
|
|
- **THEN** the command produces zero matches
|
|
|
|
**Verified by:** `src/error-codes/error-codes.test.ts::"banned patterns absent from src"` AND a CI-level `scripts/lint/check-test-runner-imports.ts` gate
|