# Foundation 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:** Replace the placeholder single-package scaffold with a role-named Cargo workspace running a tested, single-tenant axum server that exposes health probes and a config-driven OpenAPI document. **Architecture:** A Cargo virtual workspace with role-named crates (`domain`, `db`, `api`, `server`). `domain` holds pure types (no I/O); `db` owns all SQL behind a `Db` handle; `api` builds the axum router, handlers, and OpenAPI doc; `server` is the binary that loads config (clap derive, env+args), connects the database, and serves. The product name is never hardcoded — it comes from config and flows into the OpenAPI title. **Tech Stack:** Rust 2024, tokio, axum 0.8, sqlx 0.8 (PostgreSQL), clap 4 (derive), utoipa 5 (code-first OpenAPI), tracing. Tests use `#[sqlx::test]` + `tower::ServiceExt::oneshot` and a TCP smoke test with reqwest. --- ## Prerequisites (read once before starting) - **Rust** stable ≥ 1.85 (edition 2024) and the **nightly** toolchain for formatting (`cargo +nightly fmt`). - **Docker** (for a local PostgreSQL during tests) — or any reachable PostgreSQL where the connecting role may `CREATE DATABASE` (required by `#[sqlx::test]`, which provisions a temp DB per test). - **`just`** (optional, for the task runner added in Task 7). - **Verify crate versions before pinning.** Versions below are expected-current for 2026; confirm the latest compatible release with the `cratesio-mcp` tools and adjust if needed. Add/update dependencies with `cargo-mcp` rather than hand-editing where practical. - Set `DATABASE_URL` before running DB-backed tests, e.g. `export DATABASE_URL=postgres://postgres:postgres@localhost:5432/cms_dev` (Task 7 ships a `docker-compose.yml` and `.env.example`). ## File Structure ``` Cargo.toml virtual workspace (no [package]) crates/ domain/ Cargo.toml src/lib.rs re-exports src/id.rs newtype IDs (OrgId) db/ Cargo.toml src/lib.rs Db handle: connect/from_pool/pool/ping tests/ping.rs #[sqlx::test] ping api/ Cargo.toml src/lib.rs AppState, build_app src/health.rs /health/live, /health/ready src/openapi.rs ApiDoc + /api-docs/openapi.json tests/health.rs oneshot tests server/ Cargo.toml src/lib.rs Config re-export, run(), serve() src/config.rs clap-derive Config src/main.rs binary entrypoint tests/config.rs arg/env parsing tests/serve.rs TCP smoke test docker-compose.yml local Postgres .env.example justfile ``` Removed: the root `[package]` and `src/main.rs` (the `biggus-dickus` hello-world scaffold). --- ## Task 1: Workspace skeleton (dissolve the placeholder package) **Files:** - Modify: `Cargo.toml` (replace entire contents) - Delete: `src/main.rs` (and the now-empty `src/`) - Create: `crates/domain/Cargo.toml`, `crates/domain/src/lib.rs` - Create: `crates/db/Cargo.toml`, `crates/db/src/lib.rs` - Create: `crates/api/Cargo.toml`, `crates/api/src/lib.rs` - Create: `crates/server/Cargo.toml`, `crates/server/src/main.rs` - [ ] **Step 1: Replace the root `Cargo.toml` with a virtual workspace** Overwrite `Cargo.toml` with exactly: ```toml [workspace] resolver = "3" members = ["crates/domain", "crates/db", "crates/api", "crates/server"] [workspace.package] edition = "2024" rust-version = "1.85" [workspace.dependencies] tokio = { version = "1", features = ["full"] } axum = "0.8" sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "macros"] } uuid = { version = "1", features = ["v4", "serde"] } serde = { version = "1", features = ["derive"] } serde_json = "1" clap = { version = "4", features = ["derive", "env"] } utoipa = { version = "5", features = ["uuid"] } anyhow = "1" thiserror = "2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tower = { version = "0.5", features = ["util"] } http-body-util = "0.1" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } ``` - [ ] **Step 2: Remove the placeholder binary** Run: `git rm src/main.rs` Expected: `rm 'src/main.rs'`. The `src/` directory is now empty (removed). - [ ] **Step 3: Create the `domain` crate (empty for now)** `crates/domain/Cargo.toml`: ```toml [package] name = "domain" version = "0.0.0" edition.workspace = true rust-version.workspace = true [dependencies] uuid.workspace = true serde.workspace = true ``` `crates/domain/src/lib.rs`: ```rust //! Core domain types and invariants. No I/O dependencies. ``` - [ ] **Step 4: Create the `db` crate (empty for now)** `crates/db/Cargo.toml`: ```toml [package] name = "db" version = "0.0.0" edition.workspace = true rust-version.workspace = true [dependencies] sqlx.workspace = true thiserror.workspace = true [dev-dependencies] tokio.workspace = true ``` `crates/db/src/lib.rs`: ```rust //! Database access. All SQL lives in this crate. ``` - [ ] **Step 5: Create the `api` crate (empty for now)** `crates/api/Cargo.toml`: ```toml [package] name = "api" version = "0.0.0" edition.workspace = true rust-version.workspace = true [dependencies] axum.workspace = true serde.workspace = true utoipa.workspace = true db = { path = "../db" } [dev-dependencies] tokio.workspace = true tower.workspace = true http-body-util.workspace = true serde_json.workspace = true sqlx.workspace = true ``` `crates/api/src/lib.rs`: ```rust //! HTTP API: router, handlers, and OpenAPI document. ``` - [ ] **Step 6: Create the `server` crate with a placeholder main** `crates/server/Cargo.toml`: ```toml [package] name = "server" version = "0.0.0" edition.workspace = true rust-version.workspace = true [lib] path = "src/lib.rs" [[bin]] name = "server" path = "src/main.rs" [dependencies] tokio.workspace = true axum.workspace = true clap.workspace = true anyhow.workspace = true tracing.workspace = true tracing-subscriber.workspace = true api = { path = "../api" } db = { path = "../db" } [dev-dependencies] reqwest.workspace = true serde_json.workspace = true api = { path = "../api" } db = { path = "../db" } sqlx.workspace = true ``` `crates/server/src/lib.rs`: ```rust //! Server wiring: configuration and startup. ``` `crates/server/src/main.rs`: ```rust fn main() { println!("placeholder"); } ``` - [ ] **Step 7: Verify the workspace builds** Run: `cargo build --workspace` Expected: compiles successfully; four crates (`domain`, `db`, `api`, `server`) build with warnings only about unused placeholders being acceptable. - [ ] **Step 8: Commit** ```bash git add -A git commit -m "chore: replace placeholder package with role-named workspace" ``` --- ## Task 2: `domain` — newtype IDs **Files:** - Create: `crates/domain/src/id.rs` - Modify: `crates/domain/src/lib.rs` - [ ] **Step 1: Write the failing test** Create `crates/domain/src/id.rs`: ```rust use std::fmt; use std::str::FromStr; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// Identifier for an organization (tenant). /// /// A newtype over [`Uuid`] so it can never be confused with another entity's id. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct OrgId(Uuid); #[cfg(test)] mod tests { use super::*; #[test] fn parses_and_displays_round_trip() { let text = "550e8400-e29b-41d4-a716-446655440000"; let id: OrgId = text.parse().expect("valid uuid should parse"); assert_eq!(id.to_string(), text); } #[test] fn rejects_invalid_uuid() { assert!("not-a-uuid".parse::().is_err()); } } ``` Wire it into `crates/domain/src/lib.rs`: ```rust //! Core domain types and invariants. No I/O dependencies. mod id; pub use id::OrgId; ``` - [ ] **Step 2: Run the test to verify it fails** Run: `cargo test -p domain` Expected: FAIL — compile error, `FromStr`/`Display` not implemented for `OrgId` (and `parse` unavailable). - [ ] **Step 3: Implement the newtype API** Append to `crates/domain/src/id.rs`, immediately after the `pub struct OrgId(Uuid);` line (before the `#[cfg(test)]` module): ```rust impl OrgId { /// Generate a fresh random id. pub fn new() -> Self { Self(Uuid::new_v4()) } /// Wrap an existing [`Uuid`]. pub fn from_uuid(uuid: Uuid) -> Self { Self(uuid) } /// The underlying [`Uuid`]. pub fn as_uuid(&self) -> Uuid { self.0 } } impl Default for OrgId { fn default() -> Self { Self::new() } } impl fmt::Display for OrgId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } impl FromStr for OrgId { type Err = uuid::Error; fn from_str(s: &str) -> Result { Ok(Self(Uuid::parse_str(s)?)) } } ``` - [ ] **Step 4: Run the test to verify it passes** Run: `cargo test -p domain` Expected: PASS — 2 tests (`parses_and_displays_round_trip`, `rejects_invalid_uuid`). - [ ] **Step 5: Commit** ```bash git add crates/domain git commit -m "feat(domain): add OrgId newtype" ``` --- ## Task 3: `server` — configuration via clap derive **Files:** - Create: `crates/server/src/config.rs` - Modify: `crates/server/src/lib.rs` - Test: `crates/server/tests/config.rs` - [ ] **Step 1: Write the failing test** Create `crates/server/tests/config.rs`: ```rust use clap::Parser; use server::Config; #[test] fn parses_from_args_with_defaults() { let cfg = Config::try_parse_from([ "server", "--database-url", "postgres://localhost/test", ]) .expect("should parse"); assert_eq!(cfg.database_url, "postgres://localhost/test"); assert_eq!(cfg.bind_addr, "0.0.0.0:8080"); assert_eq!(cfg.app_name, "Collection Management System"); } #[test] fn database_url_is_required() { assert!(Config::try_parse_from(["server"]).is_err()); } ``` - [ ] **Step 2: Run the test to verify it fails** Run: `cargo test -p server --test config` Expected: FAIL — `server::Config` does not exist. - [ ] **Step 3: Implement the config and export it** Create `crates/server/src/config.rs`: ```rust use clap::Parser; /// Runtime configuration, sourced from CLI arguments and environment variables. #[derive(Debug, Clone, Parser)] #[command(version, about = "Collection management system server")] pub struct Config { /// PostgreSQL connection string. #[arg(long, env = "DATABASE_URL")] pub database_url: String, /// Address to bind the HTTP server to. #[arg(long, env = "BIND_ADDR", default_value = "0.0.0.0:8080")] pub bind_addr: String, /// User-facing application name (OpenAPI title, page title, …). /// /// Defaults to a neutral name; set this to the real product name at deploy /// time. The product name must never be hardcoded in source. #[arg(long, env = "APP_NAME", default_value = "Collection Management System")] pub app_name: String, } ``` Replace `crates/server/src/lib.rs` with: ```rust //! Server wiring: configuration and startup. mod config; pub use config::Config; ``` - [ ] **Step 4: Run the test to verify it passes** Run: `cargo test -p server --test config` Expected: PASS — 2 tests. - [ ] **Step 5: Commit** ```bash git add crates/server git commit -m "feat(server): add clap-derive Config (args + env)" ``` --- ## Task 4: `db` — connection pool and readiness ping **Files:** - Modify: `crates/db/src/lib.rs` - Test: `crates/db/tests/ping.rs` - [ ] **Step 1: Write the failing test** Create `crates/db/tests/ping.rs`: ```rust use db::Db; use sqlx::PgPool; #[sqlx::test] async fn ping_succeeds(pool: PgPool) { let db = Db::from_pool(pool); db.ping() .await .expect("ping should succeed against a live database"); } ``` - [ ] **Step 2: Run the test to verify it fails** Run: `cargo test -p db` Expected: FAIL — `Db`, `Db::from_pool`, and `Db::ping` do not exist (compile error). - [ ] **Step 3: Implement the `Db` handle** Replace `crates/db/src/lib.rs` with: ```rust //! Database access. All SQL lives in this crate. use sqlx::postgres::{PgPool, PgPoolOptions}; /// A handle to the organization's PostgreSQL database. #[derive(Clone)] pub struct Db { pool: PgPool, } impl Db { /// Connect to the database at `database_url`, opening a connection pool. pub async fn connect(database_url: &str) -> Result { let pool = PgPoolOptions::new() .max_connections(5) .connect(database_url) .await?; Ok(Self { pool }) } /// Build a handle from an existing pool (used in tests). pub fn from_pool(pool: PgPool) -> Self { Self { pool } } /// Borrow the underlying pool for repository modules. pub fn pool(&self) -> &PgPool { &self.pool } /// Readiness check: run a trivial query to confirm the database answers. pub async fn ping(&self) -> Result<(), sqlx::Error> { sqlx::query_scalar::<_, i32>("SELECT 1") .fetch_one(&self.pool) .await?; Ok(()) } } ``` - [ ] **Step 4: Run the test to verify it passes** Ensure PostgreSQL is running and `DATABASE_URL` is set (see Prerequisites / Task 7). Run: `cargo test -p db` Expected: PASS — 1 test (`ping_succeeds`). `#[sqlx::test]` provisions and drops a temporary database automatically. - [ ] **Step 5: Commit** ```bash git add crates/db git commit -m "feat(db): add Db handle with pool connect and readiness ping" ``` --- ## Task 5: `api` — health probes, OpenAPI doc, and router **Files:** - Create: `crates/api/src/health.rs` - Create: `crates/api/src/openapi.rs` - Modify: `crates/api/src/lib.rs` - Test: `crates/api/tests/health.rs` - [ ] **Step 1: Write the failing test** Create `crates/api/tests/health.rs`: ```rust use api::{AppState, build_app}; use axum::body::Body; use axum::http::{Request, StatusCode}; use http_body_util::BodyExt; use sqlx::PgPool; use tower::ServiceExt; // for `oneshot` fn state(pool: PgPool, app_name: &str) -> AppState { AppState { db: db::Db::from_pool(pool), app_name: app_name.to_string(), } } #[sqlx::test] async fn live_returns_ok(pool: PgPool) { let app = build_app(state(pool, "Test")); let resp = app .oneshot( Request::builder() .uri("/health/live") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let bytes = resp.into_body().collect().await.unwrap().to_bytes(); let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); assert_eq!(json["status"], "ok"); } #[sqlx::test] async fn ready_reports_database_true(pool: PgPool) { let app = build_app(state(pool, "Test")); let resp = app .oneshot( Request::builder() .uri("/health/ready") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let bytes = resp.into_body().collect().await.unwrap().to_bytes(); let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); assert_eq!(json["database"], true); } #[sqlx::test] async fn openapi_doc_uses_configured_title(pool: PgPool) { let app = build_app(state(pool, "My Museum CMS")); let resp = app .oneshot( Request::builder() .uri("/api-docs/openapi.json") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), StatusCode::OK); let bytes = resp.into_body().collect().await.unwrap().to_bytes(); let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); assert_eq!(json["info"]["title"], "My Museum CMS"); } ``` - [ ] **Step 2: Run the test to verify it fails** Run: `cargo test -p api --test health` Expected: FAIL — `AppState`, `build_app`, and the routes do not exist (compile error). - [ ] **Step 3: Implement health handlers** Create `crates/api/src/health.rs`: ```rust use axum::{ Json, Router, extract::State, http::StatusCode, response::IntoResponse, routing::get, }; use serde::Serialize; use utoipa::ToSchema; use crate::AppState; /// Liveness payload: the process is running. #[derive(Serialize, ToSchema)] pub(crate) struct Live { /// Always `"ok"` when the process serves requests. pub status: &'static str, } /// Readiness payload: dependencies were checked. #[derive(Serialize, ToSchema)] pub(crate) struct Ready { /// `"ok"` when ready, `"degraded"` otherwise. pub status: &'static str, /// Whether the database responded to a ping. pub database: bool, } /// Liveness probe — no dependencies checked. #[utoipa::path(get, path = "/health/live", responses((status = 200, body = Live)))] pub(crate) async fn live() -> Json { Json(Live { status: "ok" }) } /// Readiness probe — confirms the database answers. #[utoipa::path( get, path = "/health/ready", responses( (status = 200, body = Ready, description = "Ready"), (status = 503, body = Ready, description = "A dependency is unavailable") ) )] pub(crate) async fn ready(State(state): State) -> impl IntoResponse { match state.db.ping().await { Ok(()) => ( StatusCode::OK, Json(Ready { status: "ok", database: true, }), ), Err(_) => ( StatusCode::SERVICE_UNAVAILABLE, Json(Ready { status: "degraded", database: false, }), ), } } /// Health routes, parameterized over [`AppState`]. pub(crate) fn routes() -> Router { Router::new() .route("/health/live", get(live)) .route("/health/ready", get(ready)) } ``` - [ ] **Step 4: Implement the OpenAPI document and route** Create `crates/api/src/openapi.rs`: ```rust use axum::{Json, Router, extract::State, routing::get}; use utoipa::OpenApi; use crate::{AppState, health}; #[derive(OpenApi)] #[openapi( paths(health::live, health::ready), components(schemas(health::Live, health::Ready)), info(title = "Collection Management System", version = "0.0.0") )] struct ApiDoc; /// Serve the OpenAPI document, overriding the title from runtime config so the /// product name is never hardcoded. async fn openapi_json(State(state): State) -> Json { let mut doc = ApiDoc::openapi(); doc.info.title = state.app_name.clone(); Json(doc) } /// OpenAPI routes, parameterized over [`AppState`]. pub(crate) fn routes() -> Router { Router::new().route("/api-docs/openapi.json", get(openapi_json)) } ``` - [ ] **Step 5: Implement `AppState` and `build_app`** Replace `crates/api/src/lib.rs` with: ```rust //! HTTP API: router, handlers, and OpenAPI document. mod health; mod openapi; use axum::Router; use db::Db; /// Shared application state passed to handlers. #[derive(Clone)] pub struct AppState { /// Database handle for this organization. pub db: Db, /// User-facing product name (from config). Never hardcoded. pub app_name: String, } /// Build the application router from shared state. pub fn build_app(state: AppState) -> Router { Router::new() .merge(health::routes()) .merge(openapi::routes()) .with_state(state) } ``` - [ ] **Step 6: Run the tests to verify they pass** Run: `cargo test -p api --test health` Expected: PASS — 3 tests (`live_returns_ok`, `ready_reports_database_true`, `openapi_doc_uses_configured_title`). - [ ] **Step 7: Commit** ```bash git add crates/api git commit -m "feat(api): add health probes, OpenAPI doc, and router" ``` --- ## Task 6: `server` — wire dependencies and serve **Files:** - Modify: `crates/server/src/lib.rs` - Modify: `crates/server/src/main.rs` - Test: `crates/server/tests/serve.rs` - [ ] **Step 1: Write the failing test** Create `crates/server/tests/serve.rs`: ```rust use std::net::SocketAddr; use api::AppState; use db::Db; use server::serve; use tokio::net::TcpListener; #[tokio::test] async fn serves_health_live_over_tcp() { let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set for this test"); let db = Db::connect(&database_url).await.expect("connect to database"); let state = AppState { db, app_name: "Test".to_string(), }; let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr: SocketAddr = listener.local_addr().unwrap(); let handle = tokio::spawn(async move { serve(listener, state).await.unwrap(); }); let url = format!("http://{addr}/health/live"); let body: serde_json::Value = reqwest::get(&url) .await .expect("request succeeds") .json() .await .expect("json body"); assert_eq!(body["status"], "ok"); handle.abort(); } ``` - [ ] **Step 2: Run the test to verify it fails** Run: `cargo test -p server --test serve` Expected: FAIL — `server::serve` does not exist (compile error). - [ ] **Step 3: Implement `run` and `serve`** Replace `crates/server/src/lib.rs` with: ```rust //! Server wiring: configuration and startup. mod config; pub use config::Config; use anyhow::Context; use api::{AppState, build_app}; use db::Db; use tokio::net::TcpListener; /// Connect dependencies from `config` and serve until shutdown. pub async fn run(config: Config) -> anyhow::Result<()> { let db = Db::connect(&config.database_url) .await .context("connecting to the database")?; let state = AppState { db, app_name: config.app_name.clone(), }; let listener = TcpListener::bind(&config.bind_addr) .await .with_context(|| format!("binding to {}", config.bind_addr))?; tracing::info!(addr = %config.bind_addr, "server listening"); serve(listener, state).await } /// Serve the API on an already-bound listener (used by `run` and tests). pub async fn serve(listener: TcpListener, state: AppState) -> anyhow::Result<()> { let app = build_app(state); axum::serve(listener, app) .await .context("running the HTTP server")?; Ok(()) } ``` - [ ] **Step 4: Implement the binary entrypoint** Replace `crates/server/src/main.rs` with: ```rust use clap::Parser; use server::{Config, run}; #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .init(); let config = Config::parse(); run(config).await } ``` - [ ] **Step 5: Run the test to verify it passes** Ensure PostgreSQL is running and `DATABASE_URL` is set. Run: `cargo test -p server --test serve` Expected: PASS — 1 test (`serves_health_live_over_tcp`). - [ ] **Step 6: Run the whole workspace test suite** Run: `cargo test --workspace` Expected: PASS — all tests across `domain`, `db`, `api`, `server`. - [ ] **Step 7: Commit** ```bash git add crates/server git commit -m "feat(server): wire config, database, and HTTP serving" ``` --- ## Task 7: Developer ergonomics (local Postgres, task runner, env) **Files:** - Create: `docker-compose.yml` - Create: `.env.example` - Create: `justfile` - Modify: `.gitignore` - [ ] **Step 1: Add a local PostgreSQL service** Create `docker-compose.yml`: ```yaml services: postgres: image: postgres:17 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: cms_dev ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data volumes: pgdata: ``` - [ ] **Step 2: Add an example environment file** Create `.env.example`: ```dotenv # Connection string for local development and tests. # The role must be allowed to CREATE DATABASE (sqlx::test provisions temp DBs). DATABASE_URL=postgres://postgres:postgres@localhost:5432/cms_dev BIND_ADDR=0.0.0.0:8080 APP_NAME=Collection Management System ``` - [ ] **Step 3: Add a task runner** Create `justfile`: ```just set dotenv-load # Run the server (reads .env) run: cargo run -p server # Run the full test suite test: cargo test --workspace # Format with the nightly toolchain fmt: cargo +nightly fmt # Lint, treating warnings as errors lint: cargo clippy --workspace --all-targets -- -D warnings # Format, lint, and test check: fmt lint test ``` - [ ] **Step 4: Ignore the local env file** Append to `.gitignore` (a newline then): ```gitignore .env ``` - [ ] **Step 5: Verify the local stack end to end** Run: ```bash docker compose up -d cp .env.example .env cargo +nightly fmt cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace ``` Expected: containers start; formatting clean; clippy reports no warnings; all tests pass. - [ ] **Step 6: Commit** ```bash git add docker-compose.yml .env.example justfile .gitignore git commit -m "chore: add local Postgres, justfile, and env example" ``` --- ## Self-Review (completed) **Spec coverage (foundation slice of `docs/specs/2026-06-02-mvp-architecture.md`):** - §5 role-named workspace, dependency direction inward → Tasks 1–6 (domain has no I/O deps; api depends on db; server depends on api+db). ✓ - §13 product name never hardcoded; from config → Task 3 (`Config::app_name`) + Task 5 (OpenAPI title override). ✓ - §7 OpenAPI code-first via utoipa → Task 5. ✓ - §8 sqlx + SQL confined to `db` crate → Task 4 (only crate with SQL). ✓ - §12 self-host single binary, sensible defaults → Tasks 3, 6 (defaults for bind/app_name; one binary). ✓ - §19 testing: repositories integration-tested against Postgres; deterministic handler tests → Tasks 4–6. ✓ - §21 first scaffolding task = dissolve `biggus-dickus` package → Task 1. ✓ - Out of scope here (later plans): isolation/credentials (deployment), auth, the data model, search, audit, export. Not foundation gaps. **Placeholder scan:** no `TODO`/`TBD`/"add error handling"/"similar to" — every step has concrete code and commands. ✓ **Type consistency:** `AppState { db: Db, app_name: String }` is constructed identically in Task 5 tests, Task 6 lib, and Task 6 test; `build_app`/`serve`/`Db::from_pool`/`Db::connect`/`Db::ping` signatures match across tasks; `Config` field names (`database_url`, `bind_addr`, `app_name`) are consistent between Task 3 and Task 6. ✓ --- ## Notes for follow-on plans - Add `storage`, `search`, `auth` crates to `members` when their plans begin. - CI (gitea actions running `just check`) is intentionally deferred — add once a second plan lands. - Licensing for the workspace is undecided; `[workspace.package]` deliberately omits `license` until chosen.