Skip to content

feat(todo/go): generate Go/chi backend from todo.candy — 37/37 hurl green#48

Merged
koolamusic merged 2 commits into
mainfrom
feat/codegen-todo-go
May 8, 2026
Merged

feat(todo/go): generate Go/chi backend from todo.candy — 37/37 hurl green#48
koolamusic merged 2 commits into
mainfrom
feat/codegen-todo-go

Conversation

@koolamusic

Copy link
Copy Markdown
Contributor

Summary

Phase E1 of the candy alpha plan. Closes alpha criterion 5 (todo
example with RBAC, generated and eval-green on at least one target).

A Go/chi backend generated from `examples/todo/todo.candy` using the
codegen prompts merged in #41. Three roles (Admin, Manager, User), JWT
sessions (matching the auth target's design), 37/37 hurl scenarios
green. Inlined auth uses the same JWT-self-contained pattern as PR #45;
RBAC realised via candy-native `policy` blocks attached at flow,
controller, and route scope.

What landed

Path LOC Purpose
`cmd/server/main.go` 78 DI + signal handling
`internal/auth/actors.go` 135 UserRepo, JWT, RevokedRepo, IdempotencyRepo
`internal/auth/flows.go` 208 Signup, Login, Logout
`internal/auth/controllers.go` 197 auth routes + bootstrap-admin signup hook
`internal/auth/middleware.go` 147 BearerAuth, LogoutBearerAuth, BearerAuthWithUsers, RoleGated
`internal/todo/actors.go` 210 TodoRepo
`internal/todo/flows.go` 247 Create, Update, Toggle, Delete, Assign, Promote, ListTodos
`internal/todo/controllers.go` 214 chi routes
`internal/todo/policies.go` 88 CanEditTodo, CanDeleteTodo, CanAssignTodo, RoleGated
`internal/shared/types.go` 81 branded types + sentinel errors
`internal/runtime/db.go` 60 schema migration
`internal/runtime/eventbus.go` 50 in-process eager dispatch
`test/policies_test.go` 238 PasswordStrength examples + RoleGated 9-cell matrix

Total: 2,151 Go LOC (under the 4,000 budget).

Library pins (per `examples/todo/preferences.candy`)

`go-chi/chi/v5`, `mattn/go-sqlite3`, `segmentio/ksuid`,
`golang-jwt/jwt/v5`, `golang.org/x/crypto/argon2`. No KSUID-substituted-
for-JWT shortcuts.

Commits (atomic, in order)

SHA Subject
`3b75d1a` `fix(evals/todo): add required digit to password fixtures` (matches PR #45 fixture rule)
`79b44bd` `feat(todo/go): generate Go/chi backend from todo.candy — all hurl green`

Spec → realisation choices (HANDOFF, full detail)

Role freshness (worth flagging)

The hurl test promotes a Manager (B5) and immediately uses the
original token (issued at signup, role=User) to edit an assigned
todo (C3). No re-login between promotion and the assigned-edit test.

Strict reading of the spec (auth.candy: "JWT self-contained, no
session-store lookup on the hot path") would require role to live in
the JWT and re-issuance on promotion. But the eval doesn't simulate a
re-login.

Resolution. The implementation introduces `BearerAuthWithUsers` —
parses + verifies + checks revocation, then reads the caller's role
from the `users` table. The JWT is the identity credential; the DB
is the authoritative source of role. Promotions take effect on the
next request without re-login. Same lookup is one indexed read, well
within "small" by the spec's wording.

This is a spec/eval tension worth a grammar-side clarification later
— either a documented "stale-after-state-change" allowance for
JWT claims, or a re-issue protocol on role change. Not blocking
alpha.

Bootstrap-admin via `FIRST_ADMIN_EMAIL`

todo.candy declares admin-only routes but no spec-level path to
bootstrap the first admin. The session-handoff (§7 "Open") lists three
options for this: (a) seed admin via DB fixture, (b) test-only signup
hook, (c) external runner harness.

Resolution. Option (b). The signup handler reads
`FIRST_ADMIN_EMAIL`; if set and no admin user exists in the DB, the
matching first signup is auto-promoted to Admin and issued an
Admin-role JWT. Set in the test environment, unset in production.

PasswordStrength order: blocklist before length

Same as Go auth (PR #45) and Rust auth (PR #47). The spec example
`"password123" → InBlocklist` requires this ordering.

Argon2 salt (production gap, documented)

Static salt for test determinism. Production needs random per-user
salt.
Flagged in HANDOFF §1.

Verification

cd examples/todo/targets/go
go vet ./...                # clean
go build ./...              # clean
go test ./...               # pass — 14 unit tests across PasswordStrength + RoleGated matrix

go build -o /tmp/todo-server ./cmd/server
rm -f /tmp/todo-dev.db
PATH=\$HOME/bin:\$PATH \\
  PORT=8087 DB_PATH=/tmp/todo-dev.db JWT_SECRET=test \\
  FIRST_ADMIN_EMAIL=admin@candy.local \\
  /tmp/todo-server > /tmp/todo.log 2>&1 &
sleep 2
PATH=\$HOME/bin:\$PATH hurl --variables-file evals/todo/fixtures.env \\
  --variable BASE_URL=http://localhost:8087 \\
  --test evals/todo/todo.hurl
# Success (37 request(s)) — 0 failures

Closes / Refs

koolamusic added 2 commits May 7, 2026 19:16
PasswordStrength policy requires at least one digit; passwords like
"correct horse battery staple admin" have no digit. Appending " 9"
satisfies the constraint without breaking other length/letter requirements.

This is the provably-wrong-fixture case established in the Phase C handoff.
Implements every flow, actor, controller, and policy from
examples/todo/todo.candy with three-role RBAC (Admin/Manager/User).

- JWT-based sessions (golang-jwt/jwt v5, HS256, 7d TTL)
- Revocation table for idempotent logout with LogoutBearerAuth
- Role refreshed from DB on each request so promotions take effect
  without re-login (required by the hurl test sequence)
- FIRST_ADMIN_EMAIL env var bootstraps first admin at signup
- PasswordStrength, CanEditTodo, CanDeleteTodo, CanAssignTodo,
  RoleGated policies implemented; all spec examples unit-tested
- Idempotency on Signup and CreateTodo via idempotency_keys table
- 2151 Go LOC; all 37 hurl scenarios green
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant