From c95a64e78ec9551706edfcaf35bd59ce4e3bbde9 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Thu, 14 May 2026 17:50:48 +0200 Subject: [PATCH] docs(01): replan with cross-AI review feedback (codex) --- .planning/STATE.md | 2 +- .planning/phases/01-foundation/01-01-PLAN.md | 42 +++++++++------ .planning/phases/01-foundation/01-02-PLAN.md | 55 +++++++++++++------- .planning/phases/01-foundation/01-03-PLAN.md | 14 +++-- .planning/phases/01-foundation/01-04-PLAN.md | 9 ++-- .planning/phases/01-foundation/SKELETON.md | 4 +- 6 files changed, 84 insertions(+), 42 deletions(-) diff --git a/.planning/STATE.md b/.planning/STATE.md index 875dc05..a8ba398 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,7 +3,7 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: unknown -last_updated: "2026-05-14T15:18:59.172Z" +last_updated: "2026-05-14T15:41:00.506Z" progress: total_phases: 7 completed_phases: 0 diff --git a/.planning/phases/01-foundation/01-01-PLAN.md b/.planning/phases/01-foundation/01-01-PLAN.md index 68df887..22d21a7 100644 --- a/.planning/phases/01-foundation/01-01-PLAN.md +++ b/.planning/phases/01-foundation/01-01-PLAN.md @@ -35,10 +35,12 @@ tags: must_haves: 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 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" - ".env.example documents DATABASE_URL, PORT, ENV and .env is gitignored" - "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/SKELETON.md (locked versions table) - 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). + 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). - 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 + 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 - `backend/go.mod` declares `module backend` - - All five runtime deps pinned at the versions above (verified by grep, not just presence) - - `go mod verify` exits 0 - - `go.sum` is populated and committed + - All five runtime deps pinned at the exact versions above (verified by grep on the literal `name version` line) + - `go.sum` exists and is non-empty (populated by `go get`) + - `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 - `backend/go.mod` and `backend/go.sum` exist, module is `backend`, all five deps pinned at locked versions, `go mod verify` passes. + `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. @@ -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) - go-backend/compose.yaml (existing healthy reference; strip dev seed mounts as the research note instructs) - 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). + 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). 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 @@ -251,7 +258,7 @@ Output: `backend/` exists with a valid Go module, all directory placeholders, wo Write `backend/justfile` modeled on the RESEARCH Code Examples block. Required at minimum, these recipes (names exact): - `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-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) @@ -261,17 +268,20 @@ Output: `backend/` exists with a valid Go module, all directory placeholders, wo - `test` → runs `just generate` then `go test ./...` - `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` + - `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. 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. - 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}' + 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}' - - `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 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 - `dev` recipe runs `db-up`, `generate`, then `air` - 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 | |-----------|----------|-----------|-------------|-----------------| -| 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-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. | @@ -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 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) -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 diff --git a/.planning/phases/01-foundation/01-02-PLAN.md b/.planning/phases/01-foundation/01-02-PLAN.md index 0ff1604..d71d1b3 100644 --- a/.planning/phases/01-foundation/01-02-PLAN.md +++ b/.planning/phases/01-foundation/01-02-PLAN.md @@ -18,6 +18,7 @@ files_modified: - backend/internal/web/ui/ui_test.go - backend/internal/web/handlers_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 requirements: - FOUND-01 @@ -33,7 +34,8 @@ tags: must_haves: truths: - - "After this plan lands, `go test ./internal/...` runs and the listed handler tests FAIL with 'undefined: ' 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 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" @@ -72,11 +74,11 @@ must_haves: provides: Smoke tests rendering each Phase-1 variant; asserts root CSS class appears in output contains: "TestButton" - 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 - contains: "TestHealthz_OK" + 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: "//go:build red_gate" - path: backend/internal/db/pool_test.go - provides: pgxpool integration test that SKIPS when DATABASE_URL is unset - contains: "t.Skip" + 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: "//go:build red_gate" key_links: - from: backend/internal/web/handlers_test.go 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 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 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 `
...
` wraps the literal child content; the exact templ source syntax is the executor's call within that contract. @@ -270,7 +274,7 @@ templ Badge(props BadgeProps) 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 `