phase: 02_level_content_and_validation
Phase `02_level_content_and_validation` — final verification **green**. - **Gate:** `bash .agents/validate.sh` → ruff ✅ · pyright 0 errors ✅ · `undercooked-levels validate` OK (2 levels) ✅ · **668 passed** (incl. 10 Playwright e2e) ✅ · coverage **100%** ✅ · repo hygiene ✅ - **Criteria:** `validate` exit 0, one line per level; `list` = `01_first_kitchen`, `02_double_shift` in campaign order; live `:8000` `/healthz` 200, `/api/levels` (+detail) carries both levels with numeric `target_score`/`time_limit` and no grid/station leakage, `/api/config` correct, unknown id → 404, startup log `content loaded: 2 level(s), 4 recipe(s)`; `validate --dir tests/fixtures/levels/_bad/` → exit 1 with named `RECIPE_STATION_MISSING` / `SERVE_REACHABLE` / `TARGET_IMPLAUSIBLE`; A8 lock test proves a YAML-only level reaches loader+API+CLI. - **Defect fixed (pre-existing path, phase-02-triggered):** `undercooked` with unusable content printed the report then a raw traceback and exited `1`. `src/undercooked/__main__.py` now catches `ContentError` → one closing line, exit `2` (like a bad setting); 2 new tests in `tests/unit/test_main.py` (incl. non-content failures still propagate). - **Docs pass (full, project-wide):** README (content gate + exit `2`, config section), AGENTS.md §3, `docs/LEVELS.md` (startup refusal + exact CLI exit codes), `docs/DEPLOYMENT.md` (crash-loop diagnosis bullet); DESIGN/TESTING re-read, consistent. - **Container re-verified (no phase-01 regression):** `podman build -f Containerfile` succeeds; the image ships `levels/` and serves `/healthz` 200, `/api/levels` (2 levels), `/` 200 as non-root. - **Deviation to note:** the phase overview names `tests/fixtures/levels/bad/`; the actual broken-fixture dir is `tests/fixtures/levels/_bad/` (the `_` prefix is what keeps it out of normal discovery). Overview files are read-only to me — README/LEVELS.md document the real path. - No git index/history commands run; all changes left unstaged for the harness. ntfy notification sent.
This commit is contained in:
@@ -10,21 +10,34 @@ email, no password: you create a room, share the 5-character code, play.
|
||||
|
||||
## Status
|
||||
|
||||
Phase 01 (runnable service scaffold) is **complete**; the next pending phase is
|
||||
`02_level_content_and_validation`. What runs today: the Python toolchain is locked
|
||||
(`uv.lock`), the pytest harness covers unit, integration and Playwright e2e layers,
|
||||
configuration is a single typed, validated `Settings` object (`DEBUGPY` support
|
||||
included), and `uv run undercooked` starts a FastAPI server that serves the client
|
||||
shell at `/`, the browser assets from `app/static/`, and `GET /healthz` — which answers
|
||||
`{"status":"ok","version":"0.1.0","rooms":0}` (`rooms` stays `0` until phase 05 owns a
|
||||
supervisor) — with the security headers (CSP `default-src 'self'`) on every response.
|
||||
The rules are tests and not promises: the no-external-asset rule is
|
||||
`tests/unit/test_no_external_assets.py`, the documentation set is guarded by
|
||||
`tests/unit/test_docs_index.py`, `bash .agents/validate.sh` is the gate the phase
|
||||
pipeline runs after every task, and `docs/` holds the DESIGN / TESTING / DEPLOYMENT
|
||||
documents. Lobby buttons answer "Not implemented until phase 05.": levels and their
|
||||
validator land in phase 02, the simulation in 03-04, rooms and the WebSocket protocol
|
||||
in 05-06, the playable renderer in 07. Execute phases with the `phased-execution` skill
|
||||
Phases **01** (runnable service scaffold) and **02** (level content, schema and
|
||||
validation) are **complete**; the next pending phase is
|
||||
`03_simulation_movement_and_interaction`. What runs today:
|
||||
|
||||
- **The service.** The Python toolchain is locked (`uv.lock`), configuration is a
|
||||
single typed, validated `Settings` object (`DEBUGPY` support included), and
|
||||
`uv run undercooked` starts a FastAPI server that serves the client shell at `/`,
|
||||
the browser assets from `app/static/`, and `GET /healthz` — which answers
|
||||
`{"status":"ok","version":"0.1.0","rooms":0}` (`rooms` stays `0` until phase 05 owns
|
||||
a supervisor) — with the security headers (CSP `default-src 'self'`) on every
|
||||
response.
|
||||
- **The content.** The recipe book and the first two campaign levels live in
|
||||
`levels/`; `sim/level.py` parses them into typed models, `sim/validation.py` runs
|
||||
PLAN §8's named rules, and `create_app` loads + validates that bundle at startup and
|
||||
**refuses to boot** broken content — the named-rule report goes to stderr and
|
||||
`undercooked` exits `2` instead of serving a half-working kitchen. `GET /api/levels`,
|
||||
`GET /api/levels/{id}` and `GET /api/config` answer from it, and
|
||||
`undercooked-levels` gives authors the same gate from the command line
|
||||
(`docs/LEVELS.md` is the schema reference).
|
||||
- **The rules are tests, not promises.** The no-external-asset rule is
|
||||
`tests/unit/test_no_external_assets.py`, "no level or recipe id in Python" is
|
||||
`tests/unit/test_content_not_hardcoded.py`, the documentation set is guarded by
|
||||
`tests/unit/test_docs_index.py`, and `bash .agents/validate.sh` is the gate the
|
||||
phase pipeline runs after every task.
|
||||
|
||||
What does not exist yet: the simulation (phases 03-04), rooms and the WebSocket
|
||||
protocol (05-06 — the lobby buttons answer "Not implemented until phase 05.") and the
|
||||
playable Canvas renderer (07). Execute phases with the `phased-execution` skill
|
||||
(`auto-phase.sh`); it always takes the lowest-numbered directory still in
|
||||
`.agents/phases/todo/`.
|
||||
|
||||
@@ -56,14 +69,16 @@ uv run undercooked # http://127.0.0.1:8000 (shell page + /health
|
||||
uv run undercooked --host 0.0.0.0 --port 8000 # e.g. to reach it from a phone
|
||||
uv run undercooked --reload # dev reload
|
||||
uv run undercooked --levels-dir levels # point at another content dir
|
||||
curl -fsS http://127.0.0.1:8000/api/levels # the campaign cards the lobby will show
|
||||
curl -fsS http://127.0.0.1:8000/api/config # client-safe settings + protocol version
|
||||
```
|
||||
|
||||
`python -m undercooked` is the same command (the container uses it). A flag
|
||||
always beats the environment, which beats the built-in default. There is no
|
||||
`/docs` or `/openapi.json`: FastAPI renders those pages from a public CDN, which
|
||||
this project forbids. `undercooked-levels validate` (check every level file loads
|
||||
and is solvable) is declared in `pyproject.toml` but becomes runnable in phase 02,
|
||||
when the content loader lands.
|
||||
this project forbids. The validation the server runs at startup (every level
|
||||
file loads, and no rule of PLAN §8 is broken) is a command too —
|
||||
`undercooked-levels`, see **Authoring content** below.
|
||||
|
||||
Debug the server with a real debugger attached (dev only - the listener binds
|
||||
`0.0.0.0`, so never enable it on a public host):
|
||||
@@ -72,6 +87,27 @@ Debug the server with a real debugger attached (dev only - the listener binds
|
||||
DEBUGPY=1 uv run undercooked # opens a debugpy listener on :5678, then serves as usual
|
||||
```
|
||||
|
||||
Authoring content:
|
||||
|
||||
```bash
|
||||
uv run undercooked-levels list # the campaign, in campaign order
|
||||
uv run undercooked-levels show 01_first_kitchen --grid # resolved level + reachability overlay
|
||||
uv run undercooked-levels validate # the content gate; --json for scripts
|
||||
uv run undercooked-levels new 03_riverboat # scaffold a level from the recipe book
|
||||
uv run undercooked-levels axes # escalation-axis table (phase 11's curve)
|
||||
```
|
||||
|
||||
`undercooked-levels` is a thin shell over the loader and the PLAN §8 rules
|
||||
`create_app` runs at startup, so a green `validate` is a server that boots. It
|
||||
finds content through the settings (`--dir` > `UNDERCOOKED_LEVELS_DIR` >
|
||||
`levels`, plus `--recipes` > `UNDERCOOKED_RECIPES_PATH` > `<dir>/recipes.yaml`)
|
||||
and never from the current directory, so it works from any path. Exit codes:
|
||||
`0` ok, `1` a level breaks a rule, `2` nothing usable to work with (load
|
||||
failure, unknown id, refused overwrite, usage error). The schema, every field,
|
||||
the overlay trick and a "which rule did I just hit" table live in
|
||||
[docs/LEVELS.md](docs/LEVELS.md); `tests/unit/test_content_not_hardcoded.py`
|
||||
is what keeps levels and recipes out of the Python.
|
||||
|
||||
Run the checks:
|
||||
|
||||
```bash
|
||||
@@ -91,8 +127,9 @@ running first, and no test ever listens on a routable interface.
|
||||
|
||||
`bash .agents/validate.sh` is the gate a phase task must pass before it moves to
|
||||
`.agents/phases/complete/`. It stops at the first failure, in this order:
|
||||
`ruff check .`, `pyright`, `undercooked-levels validate` (skipped with a notice until
|
||||
phase 02 creates the loader), the **full** `pytest` suite with the 90 % coverage floor,
|
||||
`ruff check .`, `pyright`, `undercooked-levels validate` (unconditional: the same
|
||||
loader and PLAN §8 rules `create_app` runs at startup, so a campaign that cannot
|
||||
validate cannot commit), the **full** `pytest` suite with the 90 % coverage floor,
|
||||
then repo-hygiene checks (nothing tracked that `.gitignore` excludes, no log drops or
|
||||
key material committed). Run it from the repo root or from `.agents/`.
|
||||
|
||||
@@ -113,7 +150,10 @@ nothing else in the codebase touches `os.environ`. Precedence: `undercooked` CLI
|
||||
flags, then exported environment variables, then a local `.env` (found from the
|
||||
current directory upwards; never commit it), then the built-in defaults. A blank value counts as
|
||||
unset. Invalid values (`UNDERCOOKED_TICK_HZ=99`) raise `ConfigError` at startup
|
||||
instead of half-configuring the server. `.env.example` and the table below list
|
||||
instead of half-configuring the server, and unusable content is refused the same
|
||||
way — an unreadable `UNDERCOOKED_LEVELS_DIR` or a level that breaks a PLAN §8
|
||||
rule prints the named-rule report and exits `2` (under `--reload` the reload
|
||||
child reports and restarts instead; docs/LEVELS.md). `.env.example` and the table below list
|
||||
every variable with its default; tests keep both in sync with `config.py`.
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
@@ -125,6 +165,7 @@ every variable with its default; tests keep both in sync with `config.py`.
|
||||
| `UNDERCOOKED_MAX_PLAYERS` | `4` | Players per room (1-4) |
|
||||
| `UNDERCOOKED_MAX_ROOMS` | `200` | Concurrent rooms before new rooms are refused |
|
||||
| `UNDERCOOKED_LEVELS_DIR` | `levels` | Directory of level YAML files (`~` is expanded) |
|
||||
| `UNDERCOOKED_RECIPES_PATH` | *(derived)* | Recipe book file; empty = `recipes.yaml` inside `UNDERCOOKED_LEVELS_DIR` (here: `levels/recipes.yaml`) |
|
||||
| `UNDERCOOKED_STATIC_DIR` | *(derived)* | Directory served at `/static`; empty = `app/static/` beside the source (containers set it) |
|
||||
| `UNDERCOOKED_LOBBY_IDLE_TIMEOUT_S` | `900` | Close empty/idle lobbies |
|
||||
| `UNDERCOOKED_GUEST_IDLE_TIMEOUT_S` | `45` | Drop a disconnected player's seat after this |
|
||||
@@ -142,17 +183,34 @@ src/undercooked/ server: FastAPI app, rooms, WebSocket protocol, simulati
|
||||
src/undercooked/__main__.py the `undercooked` command (CLI > env > default)
|
||||
src/undercooked/config.py typed env settings (the only `os.environ` reader)
|
||||
src/undercooked/debug.py DEBUGPY-gated debugger bootstrap
|
||||
src/undercooked/server/app.py app factory, `/healthz`, `/`, `/static`
|
||||
src/undercooked/sim/ pure simulation kernel: entities (`model`), grid geometry
|
||||
(`grid`), content schema + loader (`recipe`, `level`,
|
||||
`content` — the YAML contract is docs/LEVELS.md), the
|
||||
level validator (`validation` — PLAN §8 named rules;
|
||||
rule table in docs/LEVELS.md), and the
|
||||
A5 purity guard (`purity`, test-enforced). There is no
|
||||
`step()` yet: the tick arrives with phase 03
|
||||
src/undercooked/tools/levels.py the `undercooked-levels` author CLI — validate, list,
|
||||
show [--grid], new, axes; same loader, same rules, no
|
||||
hard-coded content list (docs/LEVELS.md "Authoring workflow")
|
||||
src/undercooked/server/app.py app factory, `/healthz`, `/`, `/static`, mounts the content API
|
||||
src/undercooked/server/routes_content.py `/api/levels`, `/api/levels/{id}`, `/api/config`,
|
||||
plus the startup gate: broken content refuses startup
|
||||
src/undercooked/server/headers.py the security-header middleware
|
||||
.env.example every configuration variable, with its default
|
||||
levels/ data-driven level definitions (YAML) + recipes (empty until 02)
|
||||
levels/ data-driven content (YAML): the recipe book (A8) and the
|
||||
first two campaign levels (01_first_kitchen, 02_double_shift)
|
||||
app/static/ browser client: the ES-module shell today; Canvas 2D renderer
|
||||
and this repo's SVG art arrive with phase 07
|
||||
tests/ unit/, integration/, sim/, e2e/ (one Playwright suite per story);
|
||||
fixtures/levels/ (test-isolated content dir); conftest.py
|
||||
docs/ DESIGN.md, TESTING.md (how this project is verified),
|
||||
DEPLOYMENT.md; PROTOCOL / LEVELS / SIMULATION / CLIENT land with
|
||||
their own phases (the index is PLAN.md §13)
|
||||
tests/ unit/, integration/, e2e/ (one Playwright suite per story) and
|
||||
sim/ (the pure kernel: grid, entities, purity guard, validation);
|
||||
fixtures/levels/ is the content dir tests see (a recipe book and no
|
||||
levels; `_bad/` holds the deliberately-broken campaign the `_`
|
||||
discovery rule ignores); conftest.py
|
||||
docs/ DESIGN.md, LEVELS.md (the level & recipe YAML contract),
|
||||
TESTING.md (how this project is verified), DEPLOYMENT.md;
|
||||
PROTOCOL / SIMULATION / CLIENT land with their own phases
|
||||
(the index is PLAN.md §13)
|
||||
.agents/ PLAN.md, user_stories/, phases/, validate.sh — the phased-execution plan
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user