diff --git a/docs/superpowers/plans/2026-06-06-spectrum-seed-wiring.md b/docs/superpowers/plans/2026-06-06-spectrum-seed-wiring.md new file mode 100644 index 0000000..f84062e --- /dev/null +++ b/docs/superpowers/plans/2026-06-06-spectrum-seed-wiring.md @@ -0,0 +1,237 @@ +# 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 3–4, 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.