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

12 KiB
Raw Blame History

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:

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):
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 { … }):
    /// 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:

        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:

use server::{Config, create_user, run, seed};
  • Step 4: Add the seed one-shot in crates/server/src/lib.rs, next to create_user:
/// 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.
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:
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.