docs(01): replan with cross-AI review feedback (codex)

This commit is contained in:
Arthur Belleville 2026-05-14 17:50:48 +02:00
parent a54a10a7fd
commit c95a64e78e
No known key found for this signature in database
6 changed files with 84 additions and 42 deletions

View file

@ -3,7 +3,7 @@ gsd_state_version: 1.0
milestone: v1.0 milestone: v1.0
milestone_name: milestone milestone_name: milestone
status: unknown status: unknown
last_updated: "2026-05-14T15:18:59.172Z" last_updated: "2026-05-14T15:41:00.506Z"
progress: progress:
total_phases: 7 total_phases: 7
completed_phases: 0 completed_phases: 0

View file

@ -35,10 +35,12 @@ tags:
must_haves: must_haves:
truths: truths:
- "Running `just bootstrap` from a fresh clone installs goose, templ, sqlc, air CLIs and downloads the Tailwind standalone binary + htmx.min.js into backend/bin and backend/static" - "Running `just bootstrap` from a fresh clone installs goose, templ, sqlc, air CLIs and bootstrap-downloads the Tailwind standalone binary + htmx.min.js into backend/bin and backend/static (these assets are NOT committed; they are gitignored and reproduced by `just bootstrap`)"
- "Running `just db-up` (or `podman compose up -d postgres`) starts a healthy Postgres 16 container reachable at localhost:5432" - "Running `just db-up` (or `podman compose up -d postgres`) starts a healthy Postgres 16 container reachable at localhost:5432"
- "Running `just migrate up` against the running Postgres applies migration 0001_init.sql cleanly and `just migrate status` shows it applied" - "Running `just migrate up` against the running Postgres applies migration 0001_init.sql cleanly and `just migrate status` shows it applied"
- "Running `just --list` shows recipes for at least: bootstrap, dev, db-up, db-down, migrate, generate, styles-watch, test, lint, build" - "Running `just --list` shows recipes for at least: bootstrap, dev, db-up, db-down, migrate, generate, styles-watch, test, lint, build, clean"
- "Phase-1 caveat: the `generate`, `test`, `build`, and `dev` recipes are scaffolded in Plan 01-01 but only become runnable after Plans 01-02 and 01-03 land their consumers (templ files, ui/*.css, cmd/web/main.go, cmd/worker/main.go). The justfile parses and `just --list` enumerates them in Plan 01-01; their end-to-end execution is covered by Plan 01-03's checkpoint."
- "CDN policy (clarified): no runtime CDN references in served HTML/CSS/JS or in any committed source file consumed at request time; bootstrap-time download URLs (Tailwind GitHub release, HTMX from unpkg) appear ONLY inside `backend/justfile` and are the single authoritative source for the pinned asset versions."
- "Tailwind input CSS declares @source globs that include backend/templates/**/*.templ and backend/internal/web/**/*.{templ,go} so JIT does not purge classes used only in templ files" - "Tailwind input CSS declares @source globs that include backend/templates/**/*.templ and backend/internal/web/**/*.{templ,go} so JIT does not purge classes used only in templ files"
- ".env.example documents DATABASE_URL, PORT, ENV and .env is gitignored" - ".env.example documents DATABASE_URL, PORT, ENV and .env is gitignored"
- "D-01: Two-binary cmd/web + cmd/worker layout with shared internal/ packages" - "D-01: Two-binary cmd/web + cmd/worker layout with shared internal/ packages"
@ -164,17 +166,22 @@ Output: `backend/` exists with a valid Go module, all directory placeholders, wo
- .planning/phases/01-foundation/01-RESEARCH.md (Standard Stack section — pinned versions are authoritative) - .planning/phases/01-foundation/01-RESEARCH.md (Standard Stack section — pinned versions are authoritative)
- .planning/phases/01-foundation/SKELETON.md (locked versions table) - .planning/phases/01-foundation/SKELETON.md (locked versions table)
</read_first> </read_first>
<action>From repo root, create `backend/`, then `cd backend && go mod init backend`. Pin the following runtime deps with `go get` at the exact versions from RESEARCH.md Standard Stack: `github.com/go-chi/chi/v5@v5.2.5`, `github.com/a-h/templ@v0.3.1020`, `github.com/jackc/pgx/v5@v5.9.2`, `github.com/pressly/goose/v3@v3.27.1`, `github.com/google/uuid@v1.6.0`. Do NOT use any version other than those listed. Run `go mod tidy` so `go.sum` is populated. The Go directive should match the developer's installed Go (1.22 minimum; existing `go-backend/go.mod` uses 1.26 — use the same `go` line if available, otherwise `1.22`). Do not import or fetch `air`, `sqlc`, `templ` CLI, `goose` CLI as Go module deps in `go.mod` — those are installed via `go install` from the `just bootstrap` recipe (Task 6).</action> <action>From repo root, create `backend/`, then `cd backend && go mod init backend`. Pin the following runtime deps with `go get` at the exact versions from RESEARCH.md Standard Stack: `github.com/go-chi/chi/v5@v5.2.5`, `github.com/a-h/templ@v0.3.1020`, `github.com/jackc/pgx/v5@v5.9.2`, `github.com/pressly/goose/v3@v3.27.1`, `github.com/google/uuid@v1.6.0`. Do NOT use any version other than those listed.
**Do NOT run `go mod tidy` in this task.** Because no Go source file imports these packages yet in Plan 01-01, `go mod tidy` would aggressively strip the `require` lines we just pinned (Codex review concern #1). `go get` writes the pins into `go.mod` and `go.sum` directly; that is sufficient for Plan 01-01's purposes. `go mod tidy` becomes safe to run only after Plan 01-03 lands the consumer code in `internal/db/pool.go`, `internal/web/{router,handlers,middleware,slog}.go`, and `cmd/{web,worker}/main.go` — Plan 01-03's `just generate` / `just build` workflow will execute it implicitly, and at that point all five deps have real importers so `tidy` is a no-op on the require list.
The Go directive should match the developer's installed Go (1.22 minimum; existing `go-backend/go.mod` uses 1.26 — use the same `go` line if available, otherwise `1.22`). Do not import or fetch `air`, `sqlc`, `templ` CLI, `goose` CLI as Go module deps in `go.mod` — those are installed via `go install` from the `just bootstrap` recipe (Task 5).</action>
<verify> <verify>
<automated>cd backend && go mod verify && grep -q 'github.com/go-chi/chi/v5 v5.2.5' go.mod && grep -q 'github.com/a-h/templ v0.3.1020' go.mod && grep -q 'github.com/jackc/pgx/v5 v5.9.2' go.mod && grep -q 'github.com/pressly/goose/v3 v3.27.1' go.mod && grep -q 'github.com/google/uuid v1.6.0' go.mod</automated> <automated>cd backend && test -f go.mod && test -f go.sum && grep -q '^module backend$' go.mod && grep -q 'github.com/go-chi/chi/v5 v5.2.5' go.mod && grep -q 'github.com/a-h/templ v0.3.1020' go.mod && grep -q 'github.com/jackc/pgx/v5 v5.9.2' go.mod && grep -q 'github.com/pressly/goose/v3 v3.27.1' go.mod && grep -q 'github.com/google/uuid v1.6.0' go.mod</automated>
</verify> </verify>
<acceptance_criteria> <acceptance_criteria>
- `backend/go.mod` declares `module backend` - `backend/go.mod` declares `module backend`
- All five runtime deps pinned at the versions above (verified by grep, not just presence) - All five runtime deps pinned at the exact versions above (verified by grep on the literal `name version` line)
- `go mod verify` exits 0 - `go.sum` exists and is non-empty (populated by `go get`)
- `go.sum` is populated and committed - `go mod tidy` is NOT run in this task (Codex concern #1: would strip the require lines because no Go source imports them yet)
- `go mod verify` is NOT asserted here because it can interact badly with un-imported requires on some Go versions; Plan 01-03 reasserts it after real importers land
</acceptance_criteria> </acceptance_criteria>
<done>`backend/go.mod` and `backend/go.sum` exist, module is `backend`, all five deps pinned at locked versions, `go mod verify` passes.</done> <done>`backend/go.mod` and `backend/go.sum` exist, module is `backend`, all five deps pinned at locked versions; tidying deferred to Plan 01-03 where real imports exist.</done>
</task> </task>
<task type="auto"> <task type="auto">
@ -204,7 +211,7 @@ Output: `backend/` exists with a valid Go module, all directory placeholders, wo
- .planning/phases/01-foundation/01-RESEARCH.md (Code Examples section — compose.yaml and .env.example are verbatim references; D-15, D-20) - .planning/phases/01-foundation/01-RESEARCH.md (Code Examples section — compose.yaml and .env.example are verbatim references; D-15, D-20)
- go-backend/compose.yaml (existing healthy reference; strip dev seed mounts as the research note instructs) - go-backend/compose.yaml (existing healthy reference; strip dev seed mounts as the research note instructs)
</read_first> </read_first>
<action>Write `backend/compose.yaml` based on the verbatim block in RESEARCH.md Code Examples. Required: service name `postgres`, image `postgres:16-alpine`, container name `xtablo-backend-postgres`, env `POSTGRES_DB=xtablo`, `POSTGRES_USER=xtablo`, `POSTGRES_PASSWORD=xtablo`, port mapping `5432:5432`, named volume `postgres_data`, `pg_isready -U xtablo -d xtablo` healthcheck (interval 5s, timeout 5s, retries 10), `restart: unless-stopped`. Do NOT include the seed-mount volume from `go-backend/compose.yaml` — Phase 1 has nothing to seed. Write `backend/.env.example` with exactly the three keys locked in D-15: `DATABASE_URL=postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable`, `PORT=8080`, `ENV=development`. Add a brief comment line above each key. Write `backend/.gitignore` covering: `bin/` (Tailwind binary + go-installed CLIs may live here), `tmp/` (air rebuild output), `.env`, `.env.local`, `static/tailwind.css` (generated), `static/htmx.min.js` (vendored on bootstrap, not committed), `*_templ.go` (templ-generated; per RESEARCH Pitfall 1 these are never committed), `internal/db/sqlc/*.go` (generated, except keep `.gitkeep`). Write `backend/migrations/0001_init.sql` exactly per RESEARCH.md Code Examples — goose annotations `-- +goose Up` and `-- +goose Down`, each section containing `SELECT 1;`. The file MUST start with `-- +goose Up` on the first non-comment line (goose parser requirement).</action> <action>Write `backend/compose.yaml` based on the verbatim block in RESEARCH.md Code Examples. Required: service name `postgres`, image `postgres:16-alpine`, container name `xtablo-backend-postgres`, env `POSTGRES_DB=xtablo`, `POSTGRES_USER=xtablo`, `POSTGRES_PASSWORD=xtablo`, port mapping `5432:5432`, named volume `postgres_data`, `pg_isready -U xtablo -d xtablo` healthcheck (interval 5s, timeout 5s, retries 10), `restart: unless-stopped`. Do NOT include the seed-mount volume from `go-backend/compose.yaml` — Phase 1 has nothing to seed. Write `backend/.env.example` with exactly the three keys locked in D-15: `DATABASE_URL=postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable`, `PORT=8080`, `ENV=development`. Add a brief comment line above each key. Write `backend/.gitignore` covering: `bin/` (Tailwind binary + go-installed CLIs may live here), `tmp/` (air rebuild output), `.env`, `.env.local`, `static/tailwind.css` (generated), `static/htmx.min.js` (bootstrap-downloaded by `just bootstrap`, never committed), `*_templ.go` (templ-generated; per RESEARCH Pitfall 1 these are never committed), `internal/db/sqlc/*.go` (generated, except keep `.gitkeep`). Write `backend/migrations/0001_init.sql` exactly per RESEARCH.md Code Examples — goose annotations `-- +goose Up` and `-- +goose Down`, each section containing `SELECT 1;`. The file MUST start with `-- +goose Up` on the first non-comment line (goose parser requirement).</action>
<verify> <verify>
<automated>cd backend && grep -q 'postgres:16-alpine' compose.yaml && grep -q 'pg_isready' compose.yaml && grep -q '^DATABASE_URL=' .env.example && grep -q '^PORT=' .env.example && grep -q '^ENV=' .env.example && grep -q '^\.env$' .gitignore && grep -q 'tailwind.css' .gitignore && grep -q 'htmx.min.js' .gitignore && grep -q '_templ\.go' .gitignore && grep -q '\-\- +goose Up' migrations/0001_init.sql && grep -q '\-\- +goose Down' migrations/0001_init.sql</automated> <automated>cd backend && grep -q 'postgres:16-alpine' compose.yaml && grep -q 'pg_isready' compose.yaml && grep -q '^DATABASE_URL=' .env.example && grep -q '^PORT=' .env.example && grep -q '^ENV=' .env.example && grep -q '^\.env$' .gitignore && grep -q 'tailwind.css' .gitignore && grep -q 'htmx.min.js' .gitignore && grep -q '_templ\.go' .gitignore && grep -q '\-\- +goose Up' migrations/0001_init.sql && grep -q '\-\- +goose Down' migrations/0001_init.sql</automated>
</verify> </verify>
@ -251,7 +258,7 @@ Output: `backend/` exists with a valid Go module, all directory placeholders, wo
<action>Write `backend/justfile` modeled on the RESEARCH Code Examples block. Required at minimum, these recipes (names exact): <action>Write `backend/justfile` modeled on the RESEARCH Code Examples block. Required at minimum, these recipes (names exact):
- `default``@just --list` - `default``@just --list`
- `bootstrap` → mkdir bin, then `go install` the four CLI tools at the exact RESEARCH-pinned versions: `github.com/pressly/goose/v3/cmd/goose@v3.27.1`, `github.com/a-h/templ/cmd/templ@v0.3.1020`, `github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1`, `github.com/air-verse/air@v1.65.1`. Then `curl -sSL` the Tailwind v4 standalone binary into `./bin/tailwindcss` (use the platform-detection pattern `tailwindcss-$(uname -s | tr A-Z a-z)-$(uname -m)`) and `chmod +x`. Then `curl -sSL` the HTMX v2.x `htmx.min.js` from `unpkg.com/htmx.org@2/dist/htmx.min.js` into `./static/htmx.min.js` (vendored once at bootstrap; subsequently committed-via-gitignore semantics — per CONTEXT D-10 we vendor, never CDN). Pin the Tailwind version in a recipe-local variable at the top of the justfile (e.g., `tailwind_version := "v4.0.0"` — planner: use the latest 4.x stable available at bootstrap time; document the version in a comment). - `bootstrap` → mkdir bin, then `go install` the four CLI tools at the exact RESEARCH-pinned versions: `github.com/pressly/goose/v3/cmd/goose@v3.27.1`, `github.com/a-h/templ/cmd/templ@v0.3.1020`, `github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1`, `github.com/air-verse/air@v1.65.1`. Then bootstrap-download the Tailwind v4 standalone binary into `./bin/tailwindcss` using an **explicit OS/arch mapping** (Codex concern #2 — the `uname -s`/`uname -m` outputs do NOT match GitHub release asset naming; `darwin` vs `macos`, `x86_64` vs `x64`, `aarch64`/`arm64` vs `arm64`). The recipe must compute the asset name with a shell case on `uname -s` and `uname -m` resolving to one of: `tailwindcss-macos-x64`, `tailwindcss-macos-arm64`, `tailwindcss-linux-x64`, `tailwindcss-linux-arm64` (verify these against `https://github.com/tailwindlabs/tailwindcss/releases/latest` at implementation time — the four-name set above is current as of Tailwind v4.0). Use a recipe-local `tailwind_version` variable (e.g., `tailwind_version := "v4.0.0"` — use the latest 4.x stable available at bootstrap time and document in a comment). After download, `chmod +x bin/tailwindcss`. Then bootstrap-download the HTMX v2.x `htmx.min.js` from `unpkg.com/htmx.org@2/dist/htmx.min.js` into `./static/htmx.min.js` — this asset is bootstrap-downloaded (not committed; `.gitignore` excludes it), and the justfile is the single authoritative source for the HTMX version. Per CONTEXT D-10 the runtime page MUST NOT reference any CDN; the bootstrap-time `unpkg.com` URL inside the justfile is the explicit allowed exception.
- `db-up``podman compose up -d postgres` - `db-up``podman compose up -d postgres`
- `db-down``podman compose down` - `db-down``podman compose down`
- `migrate cmd="status"` → invokes `goose` with `GOOSE_DRIVER=postgres`, `GOOSE_DBSTRING` set to the dev DSN, `GOOSE_MIGRATION_DIR=migrations`, passing `{{cmd}}` (so `just migrate up`, `just migrate down`, `just migrate status` all work) - `migrate cmd="status"` → invokes `goose` with `GOOSE_DRIVER=postgres`, `GOOSE_DBSTRING` set to the dev DSN, `GOOSE_MIGRATION_DIR=migrations`, passing `{{cmd}}` (so `just migrate up`, `just migrate down`, `just migrate status` all work)
@ -261,17 +268,20 @@ Output: `backend/` exists with a valid Go module, all directory placeholders, wo
- `test` → runs `just generate` then `go test ./...` - `test` → runs `just generate` then `go test ./...`
- `lint` → runs `go vet ./...` and `gofmt -l .` (failing if any file would be reformatted) - `lint` → runs `go vet ./...` and `gofmt -l .` (failing if any file would be reformatted)
- `build` → runs `just generate`, then `go build -o bin/web ./cmd/web`, then `go build -o bin/worker ./cmd/worker` - `build` → runs `just generate`, then `go build -o bin/web ./cmd/web`, then `go build -o bin/worker ./cmd/worker`
- `clean` → removes bootstrap-downloaded and generated artifacts: `rm -rf bin/ tmp/ static/htmx.min.js static/tailwind.css` and `find . -name '*_templ.go' -delete`. Does NOT remove the Postgres volume — call `just db-down` first if a full reset is needed. (Codex concern #10 — README Method B references `just clean`; provide a real recipe.)
Document at top of justfile in a comment block: project name, podman/docker portability note (CONTEXT D-11 — `compose.yaml` works under either; README will spell out the alternative `docker compose` invocation), and the two-terminal `dev` + `styles-watch` workflow. Do NOT add a recipe to download `goose`/`templ`/`sqlc`/`air` outside of `bootstrap` — keep one install path. Document at top of justfile in a comment block: project name, podman/docker portability note (CONTEXT D-11 — `compose.yaml` works under either; README will spell out the alternative `docker compose` invocation), and the two-terminal `dev` + `styles-watch` workflow. Do NOT add a recipe to download `goose`/`templ`/`sqlc`/`air` outside of `bootstrap` — keep one install path.
Note: `build` will fail until Plan 03 ships `cmd/web/main.go` and `cmd/worker/main.go`. That is expected and acceptable for Plan 01's verification — we only assert the justfile parses and `just --list` enumerates the required recipes.</action> Note: `build` will fail until Plan 03 ships `cmd/web/main.go` and `cmd/worker/main.go`. That is expected and acceptable for Plan 01's verification — we only assert the justfile parses and `just --list` enumerates the required recipes.</action>
<verify> <verify>
<automated>cd backend && just --list 2>/dev/null | grep -E '^\s+(bootstrap|db-up|db-down|migrate|generate|styles-watch|dev|test|lint|build)\b' | wc -l | awk '$1>=9{exit 0} {exit 1}'</automated> <automated>cd backend && just --list 2>/dev/null | grep -E '^\s+(bootstrap|db-up|db-down|migrate|generate|styles-watch|dev|test|lint|build|clean)\b' | wc -l | awk '$1>=10{exit 0} {exit 1}'</automated>
</verify> </verify>
<acceptance_criteria> <acceptance_criteria>
- `just --list` enumerates at least: bootstrap, db-up, db-down, migrate, generate, styles-watch, dev, test, lint, build - `just --list` enumerates at least: bootstrap, db-up, db-down, migrate, generate, styles-watch, dev, test, lint, build, clean
- `bootstrap` recipe installs goose v3.27.1, templ v0.3.1020, sqlc v1.31.1, air v1.65.1 at exact pinned versions (verifiable by grep on the justfile) - `bootstrap` recipe installs goose v3.27.1, templ v0.3.1020, sqlc v1.31.1, air v1.65.1 at exact pinned versions (verifiable by grep on the justfile)
- `bootstrap` recipe downloads htmx.min.js into static/ from unpkg.com (NOT a CDN reference at runtime — D-10) - `bootstrap` recipe resolves the Tailwind asset name via an explicit OS/arch case — `darwin→macos`, `linux→linux`, `x86_64→x64`, `arm64`/`aarch64→arm64` — and downloads one of the four documented asset names (Codex concern #2). Grep test: `grep -E 'tailwindcss-(macos|linux)-(x64|arm64)' backend/justfile` returns ≥ 1 match.
- `bootstrap` recipe bootstrap-downloads htmx.min.js into static/ from unpkg.com (this is a bootstrap-time exception to D-10's no-CDN rule; D-10 forbids runtime CDN references — the served HTML/CSS/JS must reference only `/static/htmx.min.js`)
- `clean` recipe exists and removes `bin/`, `tmp/`, `static/htmx.min.js`, `static/tailwind.css`, and `*_templ.go` files
- `migrate` recipe sets GOOSE_DRIVER, GOOSE_DBSTRING, GOOSE_MIGRATION_DIR - `migrate` recipe sets GOOSE_DRIVER, GOOSE_DBSTRING, GOOSE_MIGRATION_DIR
- `dev` recipe runs `db-up`, `generate`, then `air` - `dev` recipe runs `db-up`, `generate`, then `air`
- No pnpm / npm / node references anywhere in the justfile (D-12) - No pnpm / npm / node references anywhere in the justfile (D-12)
@ -316,7 +326,7 @@ Output: `backend/` exists with a valid Go module, all directory placeholders, wo
| Threat ID | Category | Component | Disposition | Mitigation Plan | | Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------| |-----------|----------|-----------|-------------|-----------------|
| T-01-01 | T (Tampering) | `static/htmx.min.js` vendored via curl from unpkg | accept | Phase 1 scope: vendoring from a public CDN is an explicit decision (D-10 forbids runtime CDN, not bootstrap-time fetch). Subresource integrity hashes deferred to Phase 7 along with deploy hardening. Rationale: low-value local-dev attack surface; CSS/HTMX assets do not handle secrets in Phase 1. | | T-01-01 | T (Tampering) | `static/htmx.min.js` bootstrap-downloaded via curl from unpkg | accept | Phase 1 scope: vendoring from a public CDN is an explicit decision (D-10 forbids runtime CDN, not bootstrap-time fetch). Subresource integrity hashes deferred to Phase 7 along with deploy hardening. Rationale: low-value local-dev attack surface; CSS/HTMX assets do not handle secrets in Phase 1. |
| T-01-02 | T (Tampering) | `bin/tailwindcss` binary downloaded via curl | accept | Same rationale as T-01-01. Tailwind is local-dev tooling only; not shipped in any production image in Phase 1. Hash pinning revisited at Phase 7 (deploy) where reproducible CI bootstrap matters. | | T-01-02 | T (Tampering) | `bin/tailwindcss` binary downloaded via curl | accept | Same rationale as T-01-01. Tailwind is local-dev tooling only; not shipped in any production image in Phase 1. Hash pinning revisited at Phase 7 (deploy) where reproducible CI bootstrap matters. |
| T-01-03 | I (Information disclosure) | `.env` containing dev DSN | mitigate | `.gitignore` excludes `.env`; only `.env.example` (with placeholder/dev-only credentials) is committed. Dev DSN uses the literal `xtablo:xtablo` credential set, which is harmless for local-only bound containers. | | T-01-03 | I (Information disclosure) | `.env` containing dev DSN | mitigate | `.gitignore` excludes `.env`; only `.env.example` (with placeholder/dev-only credentials) is committed. Dev DSN uses the literal `xtablo:xtablo` credential set, which is harmless for local-only bound containers. |
| T-01-04 | I (Information disclosure) | `goose_db_version` table | accept | Standard goose metadata table; contains only migration version numbers and timestamps. No user data. | | T-01-04 | I (Information disclosure) | `goose_db_version` table | accept | Standard goose metadata table; contains only migration version numbers and timestamps. No user data. |
@ -338,7 +348,9 @@ Output: `backend/` exists with a valid Go module, all directory placeholders, wo
4. `just db-up` starts a healthy postgres:16-alpine container reachable at localhost:5432 4. `just db-up` starts a healthy postgres:16-alpine container reachable at localhost:5432
5. `just migrate up` applies the no-op bootstrap migration; `just migrate status` shows it as Applied 5. `just migrate up` applies the no-op bootstrap migration; `just migrate status` shows it as Applied
6. No Node/npm/pnpm artifacts anywhere in `backend/` (D-12) 6. No Node/npm/pnpm artifacts anywhere in `backend/` (D-12)
7. No CDN references in any committed file (D-10) 7. No **runtime** CDN references in any served HTML/CSS/JS (D-10 clarified): bootstrap-time download URLs inside `backend/justfile` (Tailwind GitHub release URL, HTMX unpkg URL) are the explicit allowed exception; templates and committed CSS reference only `/static/*` paths
8. Tailwind asset name is resolved via explicit OS/arch mapping (`darwin→macos`, `x86_64→x64`, `arm64`/`aarch64→arm64`) — never via raw `uname` interpolation (Codex concern #2)
9. `go mod tidy` is NOT executed in Plan 01-01 (Codex concern #1) — pinning is via `go get` only; tidy lands implicitly in Plan 01-03 when consumers import the deps
</success_criteria> </success_criteria>
<output> <output>

View file

@ -18,6 +18,7 @@ files_modified:
- backend/internal/web/ui/ui_test.go - backend/internal/web/ui/ui_test.go
- backend/internal/web/handlers_test.go - backend/internal/web/handlers_test.go
- backend/internal/db/pool_test.go - backend/internal/db/pool_test.go
# Tests in handlers_test.go and pool_test.go carry the `//go:build red_gate` build tag during Plan 01-02 so a stray `go test ./...` does NOT fail with `undefined: web.NewRouter` / `db.NewPool` errors before Plan 01-03 lands the implementations (Codex concern #3). Plan 01-03 removes the build tag from both files as part of the GREEN step.
autonomous: true autonomous: true
requirements: requirements:
- FOUND-01 - FOUND-01
@ -33,7 +34,8 @@ tags:
must_haves: must_haves:
truths: truths:
- "After this plan lands, `go test ./internal/...` runs and the listed handler tests FAIL with 'undefined: <handler>' or equivalent — proving the test scaffold targets the right functions/signatures before Plan 03 implements them (RED step of MVP)" - "After this plan lands, `go test -tags=red_gate ./internal/web/... ./internal/db/...` FAILS with `undefined: web.NewRouter` / `db.NewPool` (or equivalent) — proving the RED test scaffold targets the exact symbols Plan 01-03 will implement. Without the `red_gate` tag, `go test ./...` SUCCEEDS (the red-gated files are excluded from default compilation), so the intentional RED state does not leak into normal `go test ./...` invocations (Codex concern #3)."
- "Verification in Plan 01-02 is package-scoped (`go test ./internal/web/ui/...` for the GREEN ui smoke tests; `go test -tags=red_gate ./internal/web/...` for the RED gate). `go test ./...` without `-tags=red_gate` MUST pass at the end of Plan 01-02 because the red-gated test files are excluded from default compilation."
- "The `internal/web/ui` design-system package compiles standalone and its `ui_test.go` smoke tests PASS — each shipped variant of Button, Card, Badge renders without panicking and emits the expected root CSS class" - "The `internal/web/ui` design-system package compiles standalone and its `ui_test.go` smoke tests PASS — each shipped variant of Button, Card, Badge renders without panicking and emits the expected root CSS class"
- "The UI component package mirrors the enum surface of `go-backend/internal/web/ui/` for Size, ButtonVariant, ButtonTone, BadgeVariant — future phases extend these enums' CSS without redefining them" - "The UI component package mirrors the enum surface of `go-backend/internal/web/ui/` for Size, ButtonVariant, ButtonTone, BadgeVariant — future phases extend these enums' CSS without redefining them"
- "Test files declare the exact handler function signatures and templ component names that Plan 03 will implement, so Plan 03 is a fill-in not an exploration" - "Test files declare the exact handler function signatures and templ component names that Plan 03 will implement, so Plan 03 is a fill-in not an exploration"
@ -72,11 +74,11 @@ must_haves:
provides: Smoke tests rendering each Phase-1 variant; asserts root CSS class appears in output provides: Smoke tests rendering each Phase-1 variant; asserts root CSS class appears in output
contains: "TestButton" contains: "TestButton"
- path: backend/internal/web/handlers_test.go - path: backend/internal/web/handlers_test.go
provides: Failing tests for healthz OK + down, index hx-get presence, demo/time fragment, RequestID header, slog handler switch provides: Failing tests for healthz OK + down, index hx-get presence, demo/time fragment, RequestID header, slog handler switch — gated behind `//go:build red_gate` until Plan 01-03 lands the handlers
contains: "TestHealthz_OK" contains: "//go:build red_gate"
- path: backend/internal/db/pool_test.go - path: backend/internal/db/pool_test.go
provides: pgxpool integration test that SKIPS when DATABASE_URL is unset provides: pgxpool integration test that SKIPS when DATABASE_URL is unset; gated behind `//go:build red_gate` until Plan 01-03 lands `db.NewPool`
contains: "t.Skip" contains: "//go:build red_gate"
key_links: key_links:
- from: backend/internal/web/handlers_test.go - from: backend/internal/web/handlers_test.go
to: backend/internal/web/{router.go,handlers.go,middleware.go} (Plan 03) to: backend/internal/web/{router.go,handlers.go,middleware.go} (Plan 03)
@ -201,9 +203,11 @@ type BadgeProps struct {
// Templ components — match these names exactly // Templ components — match these names exactly
templ Button(props ButtonProps) templ Button(props ButtonProps)
templ Card(attrs templ.Attributes) // children via templ children templ Card(attrs templ.Attributes) // accepts children via templ's child-content syntax — verify exact syntax against the pinned templ v0.3.1020 docs before locking the rendered HTML test assertions (Codex concern #8)
templ Badge(props BadgeProps) templ Badge(props BadgeProps)
``` ```
**templ syntax verification (Codex concern #8):** Before authoring `card.templ` and its smoke test in Task 2 / Task 3, the executor MUST run `templ version` to confirm v0.3.1020 is installed and consult `https://templ.guide/syntax-and-usage/template-composition` (or the equivalent docs version) to confirm the exact child-content syntax — early templ versions used `{ children... }` inside the templ body, later versions evolved the spelling. The Task 2 acceptance below asserts ONLY that the rendered `<section class="ui-card">...</section>` wraps the literal child content; the exact templ source syntax is the executor's call within that contract.
</interfaces> </interfaces>
</context> </context>
@ -270,7 +274,7 @@ templ Badge(props BadgeProps)
<action> <action>
Write `button.templ`: declare `templ Button(props ButtonProps)`. Inside, compute `class` from `ui.ButtonClass(NormalizedButtonVariant(props.Variant), NormalizedButtonTone(props.Tone), NormalizedSize(props.Size))`. Render a `<button>` element with `type={props.Type or "button"}`, `class={class}`, and the pass-through `{props.Attrs...}` spread (templ attribute spread). The button body is `{props.Label}`. Do NOT inline Tailwind utility classes inside the `<button>` markup — the CSS rules live in `button.css`. Write `button.templ`: declare `templ Button(props ButtonProps)`. Inside, compute `class` from `ui.ButtonClass(NormalizedButtonVariant(props.Variant), NormalizedButtonTone(props.Tone), NormalizedSize(props.Size))`. Render a `<button>` element with `type={props.Type or "button"}`, `class={class}`, and the pass-through `{props.Attrs...}` spread (templ attribute spread). The button body is `{props.Label}`. Do NOT inline Tailwind utility classes inside the `<button>` markup — the CSS rules live in `button.css`.
Write `button.css`: declare `.ui-button` as a base rule (display, font, focus-ring per UI-SPEC), and `.ui-button-solid-default-md` with the EXACT Tailwind-equivalent values from UI-SPEC §HTMX Interaction Pattern: `display: inline-flex; align-items: center; border-radius: 0.375rem; background-color: #2563eb; padding: 0.5rem 1rem; font-size: 1rem; font-weight: 600; color: #ffffff;` and `&:hover { background-color: #1d4ed8; }` and the focus-ring spec from UI-SPEC. Add the `htmx-request:opacity-60 htmx-request:pointer-events-none` behavior either via Tailwind variants (kept as utility classes on the `<button>`) or via plain CSS `.htmx-request { opacity: 0.6; pointer-events: none; }` — UI-SPEC's markup example uses Tailwind variants on the element, but since this contract centralizes classes in `button.css`, prefer the plain-CSS `.htmx-request` rule scoped under `.ui-button`. Document the choice in a one-line CSS comment. Write `button.css`: declare `.ui-button` as a base rule (display, font, focus-ring per UI-SPEC), and `.ui-button-solid-default-md` with the EXACT Tailwind-equivalent values from UI-SPEC §HTMX Interaction Pattern: `display: inline-flex; align-items: center; border-radius: 0.375rem; background-color: #2563eb; padding: 0.5rem 1rem; font-size: 1rem; font-weight: 600; color: #ffffff;`. Hover state MUST be expressed as a **non-nested selector** `.ui-button-solid-default-md:hover { background-color: #1d4ed8; }` — do NOT use CSS nesting (`&:hover`) (Codex concern #7 — Tailwind v4 standalone's CSS pipeline does not reliably support nested selectors in plain-CSS component layer files, and explicit selectors work under every Tailwind version). Same rule applies to focus-ring: write `.ui-button-solid-default-md:focus-visible { ... }` as a top-level rule. Add the `htmx-request` loading behavior as a plain-CSS rule `.ui-button.htmx-request { opacity: 0.6; pointer-events: none; }` (also non-nested). Document the no-nesting choice in a one-line CSS comment at the top of `button.css`.
Write `card.templ`: declare `templ Card(attrs templ.Attributes)` accepting children. Render `<section class="ui-card" {attrs...}>{ children... }</section>`. Write `card.templ`: declare `templ Card(attrs templ.Attributes)` accepting children. Render `<section class="ui-card" {attrs...}>{ children... }</section>`.
@ -362,21 +366,34 @@ templ Badge(props BadgeProps)
<action> <action>
Write both test files. Stub types live in the same file as the tests that use them (no separate helpers file in Phase 1). Write both test files. Stub types live in the same file as the tests that use them (no separate helpers file in Phase 1).
**Red-gate isolation (Codex concern #3).** Both files MUST start with the build tag line:
```
//go:build red_gate
```
followed by a blank line, then the `package web` / `package db` clause. This tag ensures:
- Default `go test ./...` and `go build ./...` invocations EXCLUDE these files — so `undefined: web.NewRouter` / `undefined: db.NewPool` errors do NOT leak into normal CI or developer workflows during the Plan 01-02 → 01-03 transition.
- Explicit `go test -tags=red_gate ./internal/web/... ./internal/db/...` INCLUDES the files and fails with the expected `undefined:` errors — that is the proof that Plan 01-02's RED gate is wired against the right symbols.
- Plan 01-03's GREEN slice removes both `//go:build red_gate` lines as part of its work (the implementations exist; the gate is no longer needed). Plan 01-03's verify runs `go test ./...` without the tag and expects every test green.
Required imports (handlers_test.go): `bytes`, `context`, `encoding/json`, `errors`, `net/http`, `net/http/httptest`, `regexp`, `strings`, `testing`. Use Go's stdlib `regexp` for the UUID + timestamp patterns. Required imports (handlers_test.go): `bytes`, `context`, `encoding/json`, `errors`, `net/http`, `net/http/httptest`, `regexp`, `strings`, `testing`. Use Go's stdlib `regexp` for the UUID + timestamp patterns.
These tests reference `web.NewRouter`, `web.HealthzHandler`, `web.NewSlogHandler`, `web.Pinger` interface, all of which Plan 03 introduces. The test file's `package web` declaration ensures it compiles as part of the package — if those symbols don't exist, `go test ./internal/web/` will fail with `undefined: NewRouter` etc. That is the desired RED state at end of Plan 02. These tests reference `web.NewRouter`, `web.HealthzHandler`, `web.NewSlogHandler`, `web.Pinger` interface, all of which Plan 03 introduces. With the build tag in place, the symbols don't need to exist yet under default compilation. Under `-tags=red_gate` they will fail with `undefined: NewRouter` — that is the desired RED state.
Do NOT add `// +build` constraints. Do NOT skip the failing tests with `t.Skip` — they must remain RED until Plan 03. Do NOT use `// +build` (legacy syntax) — use only the new `//go:build red_gate` form (Go 1.17+). Do NOT skip the failing tests with `t.Skip` — they must remain RED under the `red_gate` tag until Plan 01-03 removes the tag.
</action> </action>
<verify> <verify>
<automated>cd backend && test -f internal/web/handlers_test.go && test -f internal/db/pool_test.go && grep -c '^func Test' internal/web/handlers_test.go | awk '$1>=6{exit 0} {exit 1}' && grep -q 'TestPool_Connects' internal/db/pool_test.go && grep -q 't.Skip' internal/db/pool_test.go && (cd internal/web && ! go test -count=1 -run 'TestHealthz_OK' . 2>&1 | grep -q '^ok')</automated> <automated>cd backend && test -f internal/web/handlers_test.go && test -f internal/db/pool_test.go && grep -q '^//go:build red_gate' internal/web/handlers_test.go && grep -q '^//go:build red_gate' internal/db/pool_test.go && grep -c '^func Test' internal/web/handlers_test.go | awk '$1>=6{exit 0} {exit 1}' && grep -q 'TestPool_Connects' internal/db/pool_test.go && grep -q 't.Skip' internal/db/pool_test.go && go test ./internal/web/ui/... -count=1 && (! go test -tags=red_gate ./internal/web/... 2>&1 | grep -q '^ok\s')</automated>
</verify> </verify>
<acceptance_criteria> <acceptance_criteria>
- `handlers_test.go` contains ≥6 `Test*` functions covering the behaviors listed - `handlers_test.go` contains ≥6 `Test*` functions covering the behaviors listed
- `pool_test.go` contains `TestPool_Connects` with a `t.Skip` branch for missing `DATABASE_URL` - `pool_test.go` contains `TestPool_Connects` with a `t.Skip` branch for missing `DATABASE_URL`
- Running `go test ./internal/web/` exits NON-ZERO at end of Plan 02 (handlers don't exist yet — this is the RED gate) - Both test files start with `//go:build red_gate` on line 1 (Codex concern #3)
- Running `go test ./internal/db/` exits 0 (skips cleanly when `DATABASE_URL` unset) — the db package compiles because Plan 03 will add `pool.go`; until then this test file references `db.NewPool` which won't exist either. The verify command above only requires the FILE to exist with `t.Skip` content — Plan 03's verify proves the test runs green. - `go test ./...` (no tag) at the end of Plan 01-02 exits 0 — the red-gated files are excluded from default compilation
- Failure messages are informative (`undefined: web.NewRouter`) — this exact error is what Plan 03's executor will see and fix - `go test -tags=red_gate ./internal/web/...` at the end of Plan 01-02 exits NON-ZERO with `undefined: web.NewRouter` (or similar) — proves the RED gate targets the right symbols
- `go test ./internal/web/ui/...` exits 0 — the ui smoke tests are GREEN
- Failure messages under `-tags=red_gate` are informative (`undefined: web.NewRouter`) — this exact error is what Plan 01-03's executor will see and fix
</acceptance_criteria> </acceptance_criteria>
<done>Failing tests are committed. Plan 03 inherits a concrete, behavior-specified target list — no exploration needed.</done> <done>Failing tests are committed. Plan 03 inherits a concrete, behavior-specified target list — no exploration needed.</done>
</task> </task>
@ -401,12 +418,13 @@ templ Badge(props BadgeProps)
<verification> <verification>
- `cd backend && go build ./internal/web/ui/` succeeds - `cd backend && go build ./internal/web/ui/` succeeds
- `cd backend && templ generate && go test ./internal/web/ui/ -count=1` exits 0 (all ui smoke tests GREEN) - `cd backend && templ generate && go test ./internal/web/ui/... -count=1` exits 0 (all ui smoke tests GREEN — package-scoped)
- `cd backend && go test ./internal/web/` exits NON-ZERO with `undefined: NewRouter` (or similar) — confirms RED gate is set for Plan 03 - `cd backend && go test ./... -count=1` exits 0 — the red-gated test files are excluded from default compilation (Codex concern #3); no leak into `go test ./...`
- `cd backend && go test ./internal/db/` exits 0 (TestPool_Connects skips cleanly) OR exits non-zero with `undefined: db.NewPool` — either is acceptable here since Plan 03 lands `pool.go` - `cd backend && go test -tags=red_gate ./internal/web/... ./internal/db/... -count=1` exits NON-ZERO with `undefined: web.NewRouter` / `undefined: db.NewPool` — confirms the RED gate is set for Plan 01-03
- All ten files under `internal/web/ui/` exist - All ten files under `internal/web/ui/` exist
- All three test files exist and contain the required `Test*` functions - Both red-gated test files exist, start with `//go:build red_gate`, and contain the required `Test*` functions
- `internal/web/ui/ui_test.go` covers Button (default+attrs), Card, Badge (info+success+zero-value) - `internal/web/ui/ui_test.go` covers Button (default+attrs), Card, Badge (info+success+zero-value)
- `button.css` declares non-nested `.ui-button-solid-default-md:hover` and `:focus-visible` selectors (Codex concern #7)
</verification> </verification>
<success_criteria> <success_criteria>
@ -415,7 +433,8 @@ templ Badge(props BadgeProps)
3. UI package CSS files are pickable by Tailwind input CSS imports (already wired in Plan 01) 3. UI package CSS files are pickable by Tailwind input CSS imports (already wired in Plan 01)
4. `internal/web/handlers_test.go` declares the exact test names from VALIDATION.md and targets the exact handler signatures Plan 03 will implement 4. `internal/web/handlers_test.go` declares the exact test names from VALIDATION.md and targets the exact handler signatures Plan 03 will implement
5. `internal/db/pool_test.go` declares `TestPool_Connects` with a graceful skip when `DATABASE_URL` is unset 5. `internal/db/pool_test.go` declares `TestPool_Connects` with a graceful skip when `DATABASE_URL` is unset
6. RED gate established: `go test ./internal/web/` fails because production handlers don't exist yet — this is intentional and is the target Plan 03 fills in 6. RED gate established via `//go:build red_gate` (Codex concern #3): `go test -tags=red_gate ./internal/web/... ./internal/db/...` fails with `undefined: web.NewRouter` / `db.NewPool` — this is intentional and is the target Plan 01-03 fills in. Default `go test ./...` succeeds because the gated files are excluded.
7. Component CSS uses non-nested selectors only (Codex concern #7): `.ui-button-solid-default-md:hover` and `:focus-visible` are top-level rules, not `&:hover` nested inside the base selector
</success_criteria> </success_criteria>
<output> <output>

View file

@ -32,7 +32,8 @@ tags:
must_haves: must_haves:
truths: truths:
- "After this plan lands, every test in `backend/internal/web/handlers_test.go` and `backend/internal/db/pool_test.go` is GREEN (the RED gate from Plan 02 is now turned over)" - "After this plan lands, every test in `backend/internal/web/handlers_test.go` and `backend/internal/db/pool_test.go` is GREEN under default `go test ./...` — the `//go:build red_gate` tags placed in Plan 01-02 are REMOVED in this plan's Task 1 (Codex concern #3). After removal, `go test ./...` (no tag) runs the full suite green."
- "`go mod tidy` runs cleanly at the start of Task 1 (Codex concern #1 — deferred from Plan 01-01) and does NOT strip any of the five pinned runtime deps because real importers now exist in `internal/db/pool.go`, `internal/web/{router,handlers,middleware}.go`, and `cmd/{web,worker}/main.go`."
- "`go build ./cmd/web ./cmd/worker` succeeds — both binaries compile" - "`go build ./cmd/web ./cmd/worker` succeeds — both binaries compile"
- "Running `./tmp/web` (or `go run ./cmd/web`) with `DATABASE_URL` pointing at the compose-managed Postgres serves `/` (200, HTML), `/healthz` (200, JSON with db:ok), `/demo/time` (200, HTML fragment), and `/static/*` (file server)" - "Running `./tmp/web` (or `go run ./cmd/web`) with `DATABASE_URL` pointing at the compose-managed Postgres serves `/` (200, HTML), `/healthz` (200, JSON with db:ok), `/demo/time` (200, HTML fragment), and `/static/*` (file server)"
- "The root page is rendered via `internal/web/ui` components — handlers do NOT inline Tailwind classes — and contains the canonical `hx-get` button declared in UI-SPEC" - "The root page is rendered via `internal/web/ui` components — handlers do NOT inline Tailwind classes — and contains the canonical `hx-get` button declared in UI-SPEC"
@ -172,6 +173,11 @@ func NewPool(ctx context.Context, dsn string) (*pgxpool.Pool, error)
- `web.SlogLoggerMiddleware(logger)` is a factory returning a `func(http.Handler) http.Handler` middleware that logs one structured line per request (method, path, status, duration, request_id) — uses `chi/v5/middleware.WrapResponseWriter` to capture status. - `web.SlogLoggerMiddleware(logger)` is a factory returning a `func(http.Handler) http.Handler` middleware that logs one structured line per request (method, path, status, duration, request_id) — uses `chi/v5/middleware.WrapResponseWriter` to capture status.
</behavior> </behavior>
<action> <action>
**Pre-step (Codex concerns #1 and #3).** Before writing any new code, perform two cleanup steps:
1. **Remove the `//go:build red_gate` line** (and the blank line immediately after it) from BOTH `backend/internal/web/handlers_test.go` and `backend/internal/db/pool_test.go`. Plan 01-02 placed these tags so the failing tests would not leak into default `go test ./...` runs; this plan turns them GREEN, so the gate is no longer needed.
2. **Now that consumer code is about to exist**, the runtime deps pinned in Plan 01-01 will have real importers in this plan. After Task 1's files are written, run `cd backend && go mod tidy` — it should be a no-op on the require list (Codex concern #1: tidy was deferred to here precisely so this is safe).
Write `internal/db/pool.go` per RESEARCH Pattern 1 exactly. Use `github.com/jackc/pgx/v5/pgxpool`. The function MUST NOT call `pool.Ping` (Pitfall 2 — lazy is correct). Write `internal/db/pool.go` per RESEARCH Pattern 1 exactly. Use `github.com/jackc/pgx/v5/pgxpool`. The function MUST NOT call `pool.Ping` (Pitfall 2 — lazy is correct).
Write `internal/web/slog.go`: declare `NewSlogHandler(env string, w io.Writer) slog.Handler`. Use `slog.HandlerOptions{Level: slog.LevelInfo}`. Switch by `env == "production"` per RESEARCH Pattern 3. Do NOT call `slog.SetDefault` here — that's `cmd/web`'s job. The function returns the handler, callers decide what to do with it. Write `internal/web/slog.go`: declare `NewSlogHandler(env string, w io.Writer) slog.Handler`. Use `slog.HandlerOptions{Level: slog.LevelInfo}`. Switch by `env == "production"` per RESEARCH Pattern 3. Do NOT call `slog.SetDefault` here — that's `cmd/web`'s job. The function returns the handler, callers decide what to do with it.
@ -185,9 +191,11 @@ func NewPool(ctx context.Context, dsn string) (*pgxpool.Pool, error)
All three files: package clause matches directory (`package db` for pool.go; `package web` for the other two). All three files: package clause matches directory (`package db` for pool.go; `package web` for the other two).
</action> </action>
<verify> <verify>
<automated>cd backend && templ generate && go build ./internal/db/ ./internal/web/ && go test ./internal/web/ -run 'TestRequestID_HeaderSet|TestSlog_HandlerSwitch' -count=1</automated> <automated>cd backend && ! grep -q '^//go:build red_gate' internal/web/handlers_test.go && ! grep -q '^//go:build red_gate' internal/db/pool_test.go && go mod tidy && templ generate && go build ./internal/db/ ./internal/web/ && go test ./internal/web/ -run 'TestRequestID_HeaderSet|TestSlog_HandlerSwitch' -count=1</automated>
</verify> </verify>
<acceptance_criteria> <acceptance_criteria>
- `//go:build red_gate` line is removed from both `internal/web/handlers_test.go` and `internal/db/pool_test.go` (Codex concern #3 — RED gate retired)
- `go mod tidy` runs cleanly and produces no removal of the five pinned runtime deps (Codex concern #1 — deferred tidy from Plan 01-01 is safe here)
- `go build ./internal/db/ ./internal/web/` succeeds (no missing symbols from Plan 02 tests) - `go build ./internal/db/ ./internal/web/` succeeds (no missing symbols from Plan 02 tests)
- `TestRequestID_HeaderSet` passes: response carries a UUIDv4 in `X-Request-ID` - `TestRequestID_HeaderSet` passes: response carries a UUIDv4 in `X-Request-ID`
- `TestSlog_HandlerSwitch` passes: production handler emits JSON, dev handler emits text - `TestSlog_HandlerSwitch` passes: production handler emits JSON, dev handler emits text
@ -387,7 +395,7 @@ func NewPool(ctx context.Context, dsn string) (*pgxpool.Pool, error)
- `cd backend && go build ./cmd/web ./cmd/worker` produces `bin/web` (if `just build`) or compiles cleanly via `go build` - `cd backend && go build ./cmd/web ./cmd/worker` produces `bin/web` (if `just build`) or compiles cleanly via `go build`
- Checkpoint Task 5 walks the full Walking Skeleton: dev loop, browser interaction, graceful shutdown, worker boot - Checkpoint Task 5 walks the full Walking Skeleton: dev loop, browser interaction, graceful shutdown, worker boot
- No `chi/v5/middleware.Logger` import anywhere (Pitfall 6): `grep -r 'middleware.Logger' backend/` returns empty - No `chi/v5/middleware.Logger` import anywhere (Pitfall 6): `grep -r 'middleware.Logger' backend/` returns empty
- No CDN reference anywhere: `grep -r 'unpkg.com\|cdn\.' backend/internal backend/templates backend/cmd` returns empty (justfile's bootstrap-time curl is the only allowed exception, lives in Plan 01) - No runtime CDN reference anywhere in served code (Codex concerns #4 and #5): `grep -r 'unpkg.com\|cdn\.' backend/internal backend/templates backend/cmd backend/static/htmx.min.js` returns empty in the `internal/`, `templates/`, and `cmd/` paths. The justfile's bootstrap-time download URLs (Tailwind GitHub release URL, HTMX unpkg URL) are the explicit allowed exceptions and live ONLY in `backend/justfile`.
- No raw `<button class="bg-blue-`anywhere in templates/: `grep -r 'class="bg-blue-' backend/templates/` returns empty (pages consume `ui.Button`, never inline) - No raw `<button class="bg-blue-`anywhere in templates/: `grep -r 'class="bg-blue-' backend/templates/` returns empty (pages consume `ui.Button`, never inline)
</verification> </verification>

View file

@ -108,7 +108,7 @@ Output: One file (`backend/README.md`) and a successful clean-clone walkthrough.
6. **Environment variables.** Table of the three keys from `.env.example`: `DATABASE_URL`, `PORT`, `ENV` — with one-line descriptions and default values. Note `.env` is gitignored; `.env.example` is committed. 6. **Environment variables.** Table of the three keys from `.env.example`: `DATABASE_URL`, `PORT`, `ENV` — with one-line descriptions and default values. Note `.env` is gitignored; `.env.example` is committed.
7. **Common commands.** Table mapping recipe → what it does → when to use it: 7. **Common commands.** Table mapping recipe → what it does → when to use it:
- `just bootstrap` — install dev tools + vendored assets (one-time per clone) - `just bootstrap` — install dev tools + bootstrap-downloaded assets (one-time per clone)
- `just db-up` / `just db-down` — start/stop local Postgres - `just db-up` / `just db-down` — start/stop local Postgres
- `just migrate up` / `migrate down` / `migrate status` — apply / revert / inspect migrations - `just migrate up` / `migrate down` / `migrate status` — apply / revert / inspect migrations
- `just generate` — runs `templ generate`, `sqlc generate`, Tailwind compile (one-shot) - `just generate` — runs `templ generate`, `sqlc generate`, Tailwind compile (one-shot)
@ -131,7 +131,10 @@ Output: One file (`backend/README.md`) and a successful clean-clone walkthrough.
Hard constraints: Hard constraints:
- Keep the file under ~300 lines. README is a quickstart, not an architecture doc. - Keep the file under ~300 lines. README is a quickstart, not an architecture doc.
- Every command shown must match a recipe that actually exists in `backend/justfile` — no aspirational commands. - Every command shown must match a recipe that actually exists in `backend/justfile` — no aspirational commands (Codex concern #6`generate`, `test`, `build`, `dev` are scaffolded in Plan 01-01 but become end-to-end runnable only after Plan 01-03; the README quickstart sequence is the order in which they first become useful and is correct as written).
- Use "bootstrap-downloaded" wording (NOT "vendored") for `bin/tailwindcss` and `static/htmx.min.js` — both are reproduced by `just bootstrap` from pinned URLs in the justfile and are gitignored, not committed (Codex concern #4).
- When discussing HTMX hosting, clarify: "HTMX is served from `/static/htmx.min.js` at runtime — no CDN. The justfile's bootstrap-time `unpkg.com` URL is the single authoritative version pin." (Codex concern #5).
- `just clean` IS a real recipe (added in Plan 01-01 per Codex concern #10); Method B in the onboarding checkpoint references it.
- No emoji. No marketing voice (UI-SPEC tone rules — "friendly, declarative, no marketing"). - No emoji. No marketing voice (UI-SPEC tone rules — "friendly, declarative, no marketing").
- No screenshots in Phase 1. Plain text + code blocks only. - No screenshots in Phase 1. Plain text + code blocks only.
</action> </action>
@ -172,7 +175,7 @@ Output: One file (`backend/README.md`) and a successful clean-clone walkthrough.
8. Stop here. Do not commit to the sibling clone. `cd ..` and `rm -rf xtablo-source-onboarding-test`. 8. Stop here. Do not commit to the sibling clone. `cd ..` and `rm -rf xtablo-source-onboarding-test`.
**Method B — In-place dry run (acceptable fallback):** **Method B — In-place dry run (acceptable fallback):**
1. From a clean working tree in `backend/`, run `just clean` (or manually delete `bin/`, `tmp/`, `static/tailwind.css`, `static/htmx.min.js`, `*_templ.go`). 1. From a clean working tree in `backend/`, run `just clean` (the recipe was added to the justfile in Plan 01-01 per Codex concern #10; it removes `bin/`, `tmp/`, `static/tailwind.css`, `static/htmx.min.js`, and `*_templ.go` files — leaves the Postgres volume intact, run `just db-down` separately if a DB reset is needed).
2. Open `README.md` and follow the Quickstart literally. 2. Open `README.md` and follow the Quickstart literally.
3. Same time + ambiguity checks as Method A. 3. Same time + ambiguity checks as Method A.

View file

@ -14,7 +14,7 @@ A developer can clone the repo, run `just bootstrap`, `podman compose up -d`, `j
| Language / runtime | Go ≥ 1.22 (developer machine has 1.26) | No JS framework / no BaaS thesis; chi v5 requires 1.22 | | Language / runtime | Go ≥ 1.22 (developer machine has 1.26) | No JS framework / no BaaS thesis; chi v5 requires 1.22 |
| HTTP router | `github.com/go-chi/chi/v5` v5.2.5 | Idiomatic, standard middleware set (CONTEXT D-08) | | HTTP router | `github.com/go-chi/chi/v5` v5.2.5 | Idiomatic, standard middleware set (CONTEXT D-08) |
| HTML templating | `github.com/a-h/templ` v0.3.1020 | Compile-time type checking; first-class HTMX fragment story (D-07) | | HTML templating | `github.com/a-h/templ` v0.3.1020 | Compile-time type checking; first-class HTMX fragment story (D-07) |
| Client interactivity | HTMX v2.x (vendored at `static/htmx.min.js`) | "No JS framework" — HTMX swaps server-rendered fragments (D-10) | | Client interactivity | HTMX v2.x (bootstrap-downloaded to `static/htmx.min.js` by `just bootstrap`; gitignored, never committed) | "No JS framework" — HTMX swaps server-rendered fragments (D-10) |
| CSS | Tailwind v4 standalone CLI binary in `./bin/tailwindcss` | Avoids any Node toolchain in `backend/` (D-12) | | CSS | Tailwind v4 standalone CLI binary in `./bin/tailwindcss` | Avoids any Node toolchain in `backend/` (D-12) |
| DB driver | `github.com/jackc/pgx/v5` + `pgxpool` v5.9.2 | sqlc-recommended, richer pg types (D-16) | | DB driver | `github.com/jackc/pgx/v5` + `pgxpool` v5.9.2 | sqlc-recommended, richer pg types (D-16) |
| Migrations | `github.com/pressly/goose/v3` v3.27.1 (CLI in Phase 1) | Embeddable library, one `.sql` per migration, sqlc-compatible (D-04, D-05) | | Migrations | `github.com/pressly/goose/v3` v3.27.1 (CLI in Phase 1) | Embeddable library, one `.sql` per migration, sqlc-compatible (D-04, D-05) |
@ -68,7 +68,7 @@ backend/
index.templ index.templ
fragments.templ fragments.templ
static/ static/
htmx.min.js (vendored, not CDN) htmx.min.js (bootstrap-downloaded by `just bootstrap`; gitignored; no runtime CDN)
tailwind.css (generated by Tailwind standalone CLI) tailwind.css (generated by Tailwind standalone CLI)
bin/ (gitignored — tailwindcss, air, etc.) bin/ (gitignored — tailwindcss, air, etc.)
.air.toml .air.toml