Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
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 aSeedvariant to theCommandenum + a dispatch arm.crates/server/src/lib.rs— addpub async fn seed(database_url: &str) -> anyhow::Result<()>(modeled oncreate_user, but with adb.migrate()step).crates/server/tests/seed.rs(new) — a server-crate building-block regression test mirroringcrates/server/tests/create_user.rs(seed twice via the test pool; assert a known seeded vocabulary + field).justfile— add aseedrecipe.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 fromcrates/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 3–4, verified by build + the manual smoke in Step 6.)
- Step 3: Add the
Seedcommand variant + dispatch incrates/server/src/main.rs. Add to theCommandenum (afterCreateUser { … }):
/// 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
seedone-shot incrates/server/src/lib.rs, next tocreate_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
seedrecipe tojustfile. Insert after therunrecipe (keeping the existing comment style), beforetest:
# 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 (thecreate-userinstruction). 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 seedsubcommand → Task 1 (main.rs variant + dispatch). ✓server::seedone-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 seedrecipe + 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
--seedflag, 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.lockchurn expected. Command::Seedhas no clap args; it reuses the flattenedConfig.database_url, exactly likeCreateUserdoes.