Files
biggus-dickus/docs/superpowers/plans/2026-06-06-spectrum-seed-wiring.md
2026-06-06 00:12:15 +02:00

238 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.
# Wire the Spectrum Seed into Runtime Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Expose the existing idempotent `db::seed::seed_spectrum_cataloguing` as a `server seed` CLI subcommand (plus a `just seed` recipe and README note), so an operator can seed an instance's baseline cataloguing fields.
**Architecture:** Mirror the existing `create-user` one-shot exactly — add a `Seed` variant to the clap `Command` enum, dispatch it to a new `server::seed(database_url)` that connects with a tiny pool, applies migrations (idempotent, so it works on a fresh DB), runs the seed inside a transaction, commits, and exits. The seed content and its idempotency are already tested at the db layer; the new code is thin glue.
**Tech Stack:** Rust (clap derive, sqlx/Postgres, anyhow, tokio). Backend-only + docs.
**Conventions:** `cargo +nightly fmt`; `cargo clippy --workspace --all-targets -- -D warnings`; tests via `cargo nextest run`; never write the codename ("biggus"/"dickus"). Test infra: compose Postgres on host **5442**, Meili **7700**; `#[sqlx::test(migrations = "../db/migrations")]` provisions its own temp DB. Env for manual runs comes from `.env` via the justfile's `set dotenv-load`.
**Spec:** `docs/superpowers/specs/2026-06-05-spectrum-seed-wiring-design.md`
---
## File Structure
- `crates/server/src/main.rs` — add a `Seed` variant to the `Command` enum + a dispatch arm.
- `crates/server/src/lib.rs` — add `pub async fn seed(database_url: &str) -> anyhow::Result<()>` (modeled on `create_user`, but with a `db.migrate()` step).
- `crates/server/tests/seed.rs` (new) — a server-crate building-block regression test mirroring `crates/server/tests/create_user.rs` (seed twice via the test pool; assert a known seeded vocabulary + field).
- `justfile` — add a `seed` recipe.
- `README.md` — add a seed step to the "Running locally" setup sequence.
The seed *content* + idempotency stay covered by the existing `crates/db/tests/seed.rs` (unchanged).
---
## Task 1: `server seed` subcommand
**Files:**
- Modify: `crates/server/src/main.rs`
- Modify: `crates/server/src/lib.rs`
- Create: `crates/server/tests/seed.rs`
**Reference (the template to mirror) — `server::create_user` in `crates/server/src/lib.rs`:**
```rust
pub async fn create_user(database_url: &str, email: &str, role: Role) -> anyhow::Result<()> {
// ...email parse + password hash...
let db = Db::connect(database_url, 2).await.context("connecting to the database")?;
let mut tx = db.pool().begin().await?;
let id = db::users::create_user(&mut tx, AuditActor::System, &NewUser { /* ... */ }).await
.context("creating the user (is the email already taken?)")?;
tx.commit().await?;
println!("created user {id} ({role:?})");
Ok(())
}
```
`Db::connect(url, n)`, `db.migrate()`, `db.pool()` all already exist (`run` calls `db.migrate()` at `lib.rs:22`). The seed fn `db::seed::seed_spectrum_cataloguing(conn: &mut sqlx::PgConnection)` is idempotent and uses `AuditActor::System` internally — no actor plumbing needed.
- [ ] **Step 1: Write the server-crate building-block test.** Create `crates/server/tests/seed.rs`. Mirror the harness comment + pool approach from `crates/server/tests/create_user.rs` (the temp-DB URL isn't exposed, so we exercise the building block the command composes — `db::seed::seed_spectrum_cataloguing` — against the test pool, including a second run to prove idempotency):
```rust
use db::{Db, fields, seed, vocab};
use sqlx::PgPool;
// Note: `server::seed` opens its own DB connection by URL, but `#[sqlx::test]`
// provisions a temporary database whose URL is not directly exposed. This test
// exercises the building block the command composes — `db::seed::seed_spectrum_cataloguing`
// — against the test pool, run twice to prove the idempotency the command relies on.
#[sqlx::test(migrations = "../db/migrations")]
async fn seed_is_idempotent_via_building_block(pool: PgPool) {
let db = Db::from_pool(pool);
for _ in 0..2 {
let mut tx = db.pool().begin().await.unwrap();
seed::seed_spectrum_cataloguing(&mut tx).await.unwrap();
tx.commit().await.unwrap();
}
// A representative seeded vocabulary and field definition are present after two runs.
assert!(
vocab::vocabulary_by_key(db.pool(), "material").await.unwrap().is_some(),
"vocabulary 'material' should be seeded"
);
assert!(
fields::field_definition_by_key(db.pool(), "title").await.unwrap().is_some(),
"field definition 'title' should be seeded"
);
}
```
(Confirm the seeded keys by reading `crates/db/src/seed.rs` — it seeds vocabularies `material`/`object_name`/`technique` and a field def `title`; adjust the asserted keys if they differ.)
- [ ] **Step 2: Run the test — it should PASS immediately.**
```
DATABASE_URL=postgres://postgres:postgres@localhost:5442/cms_dev cargo nextest run -p server -E 'test(seed_is_idempotent_via_building_block)'
```
Expected: PASS. (Unlike classic TDD, this guards an already-working building block the new command depends on — there is no failing-first state because `db::seed` already exists. The genuinely new code is the glue in Steps 34, verified by build + the manual smoke in Step 6.)
- [ ] **Step 3: Add the `Seed` command variant + dispatch** in `crates/server/src/main.rs`. Add to the `Command` enum (after `CreateUser { … }`):
```rust
/// Seed the baseline Spectrum cataloguing vocabularies + field definitions (idempotent).
Seed,
```
And add a match arm in `main` (the `match cli.command { … }`), after the `CreateUser` arm:
```rust
Some(Command::Seed) => seed(&cli.config.database_url).await,
```
Update the import at the top of `main.rs` from `use server::{Config, create_user, run};` to:
```rust
use server::{Config, create_user, run, seed};
```
- [ ] **Step 4: Add the `seed` one-shot** in `crates/server/src/lib.rs`, next to `create_user`:
```rust
/// One-shot: apply migrations (idempotent), then seed the baseline Spectrum cataloguing
/// vocabularies + field definitions. Safe to re-run (the seed is idempotent).
pub async fn seed(database_url: &str) -> anyhow::Result<()> {
// CLI one-shot: a tiny pool is plenty.
let db = Db::connect(database_url, 2)
.await
.context("connecting to the database")?;
// Apply migrations first so `server seed` works on a fresh DB without first
// starting the server. Migrations are idempotent.
db.migrate()
.await
.context("running database migrations")?;
let mut tx = db.pool().begin().await?;
db::seed::seed_spectrum_cataloguing(&mut tx)
.await
.context("seeding Spectrum cataloguing baseline")?;
tx.commit().await?;
println!("seeded Spectrum cataloguing baseline (idempotent)");
Ok(())
}
```
(`Db`, `anyhow::Context`/`context` are already imported in `lib.rs` — verify the `use` lines; `create_user` already uses `.context(...)` and `Db::connect`, so the imports exist.)
- [ ] **Step 5: Build, fmt, clippy, and run the server tests.**
```
cargo +nightly fmt
cargo clippy --workspace --all-targets -- -D warnings
DATABASE_URL=postgres://postgres:postgres@localhost:5442/cms_dev cargo nextest run -p server
```
Expected: builds clean, clippy clean, all server tests pass (including the existing `create_user` + `config` + `serve` + `embed` tests and the new seed test). Also confirm the subcommand is wired:
```
cargo run -p server -- --help
```
Expected: the help output lists a `seed` subcommand alongside `create-user`.
- [ ] **Step 6: Manual smoke — verify the real command (connect + migrate + commit glue).** With compose up (`docker compose up -d`):
```
DATABASE_URL=postgres://postgres:postgres@localhost:5442/cms_dev cargo run -p server -- seed
DATABASE_URL=postgres://postgres:postgres@localhost:5442/cms_dev cargo run -p server -- seed
```
Expected: both print `seeded Spectrum cataloguing baseline (idempotent)` and exit 0 (the second run is a no-op). (This exercises the URL-connect + migrate + commit path that `#[sqlx::test]` can't.)
- [ ] **Step 7: Commit.**
```bash
git add crates/server
git commit -m "feat(server): 'seed' subcommand wiring the Spectrum cataloguing seed (#14)"
```
---
## Task 2: `just seed` recipe + README note
**Files:**
- Modify: `justfile`
- Modify: `README.md`
- [ ] **Step 1: Add the `seed` recipe** to `justfile`. Insert after the `run` recipe (keeping the existing comment style), before `test`:
```
# Seed the baseline Spectrum cataloguing vocabularies + field definitions (idempotent)
seed:
cargo run -p server -- seed
```
- [ ] **Step 2: Verify just parses it.**
```
just --list
```
Expected: `seed` appears in the recipe list with its description.
- [ ] **Step 3: Add a seed step to the README "Running locally" setup sequence.** Open `README.md`, find the "Running locally" section and the step that creates the admin user (the `create-user` instruction). Immediately after it, add a step:
```markdown
4. Seed the baseline cataloguing fields (idempotent):
```bash
just seed # or: cargo run -p server -- seed
```
```
(Match the surrounding numbering/formatting of the existing steps — renumber subsequent steps if the section is numbered. Read the section first and adapt the wording to its style; the content is: run `just seed` once after creating the admin user to populate the baseline Spectrum vocabularies + field definitions.)
- [ ] **Step 4: Commit.**
```bash
git add justfile README.md
git commit -m "docs: 'just seed' recipe + README seed step (#14)"
```
---
## Task 3: Final verification
**Files:** none (verification only).
- [ ] **Step 1: Full suite + lints.**
```
cargo +nightly fmt --check
cargo clippy --workspace --all-targets -- -D warnings
DATABASE_URL=postgres://postgres:postgres@localhost:5442/cms_dev MEILI_URL=http://localhost:7700 MEILI_MASTER_KEY=masterKey cargo nextest run --workspace
```
Expected: all green.
- [ ] **Step 2: Codename scan + tree hygiene.**
```
git grep -in 'biggus\|dickus' -- crates README.md justfile || echo "CLEAN"
git status --short
```
Expected: `CLEAN`; working tree clean after the task commits.
---
## Self-Review (completed)
**1. Spec coverage:**
- `server seed` subcommand → Task 1 (main.rs variant + dispatch). ✓
- `server::seed` one-shot mirroring create_user, migrate-first → Task 1 Step 4. ✓
- Idempotent / safe to re-run → asserted in Task 1 Step 1 test + Step 6 smoke. ✓
- `just seed` recipe + README note → Task 2. ✓
- Testing: existing db-layer seed tests unchanged + new server-crate building-block test + manual glue smoke → Task 1. ✓
- Acceptance: nextest green / fmt / clippy / no codename → Task 3. ✓
- Out of scope (no `--seed` flag, no auto-boot, no provisioning, no term seeding, create_user unchanged) → respected; only the four files above change. ✓
**2. Placeholder scan:** No TBD/“handle errors”/“similar to”. The two “confirm the seeded keys / read the section first” notes are verification steps against real files, not deferred implementation; concrete code is given for every code step.
**3. Type consistency:** `seed(database_url: &str) -> anyhow::Result<()>` is defined in Task 1 Step 4 and imported/dispatched in Step 3 (`use server::{… seed}`, `Some(Command::Seed) => seed(&cli.config.database_url).await`). The test uses `db::seed::seed_spectrum_cataloguing(&mut tx)` + `vocab::vocabulary_by_key` + `fields::field_definition_by_key`, all existing signatures (mirrored from `crates/db/tests/seed.rs` and `create_user.rs`).
## Notes
- No new dependencies → no `Cargo.lock` churn expected.
- `Command::Seed` has no clap args; it reuses the flattened `Config.database_url`, exactly like `CreateUser` does.