October 13, 2026
Phase 1 · Domain model
Every later step (compliance checks, capacity checks, agents, approvals, and the dashboard) reads and writes the same core records: owners, pets, vaccine rules, kennels, reservations, and booking requests. A booking request also has to move through a strict lifecycle. A single approved flag can't tell "not checked yet" apart from "rejected" or "crashed halfway," and it can't stop untrusted input from jumping straight to approval. The domain model and lifecycle have to exist, and be enforced by the database and plain code, before any LLM step is added.
What changed, 25 requirements, 19 of 19 tasks
- Add a PostgreSQL schema, managed by versioned migrations, for: owners, pets, facilities, vaccine rules, vaccine records, kennels, reservations, booking requests, booking-request transitions, approvals, traces, and spans.
- Derive a pet's size class from its species and weight using fixed thresholds.
- Make the database itself reject overlapping reservations for the same kennel.
- Add a booking-request lifecycle with an explicit table of allowed transitions, including a
failedstate that records which step failed and why. Retry returns the request to that step. - Restrict entry into
approvedto two paths only: the system, fromready(all deterministic checks clear), or staff, fromneeds_review. - Record every transition in an append-only transition log.
- Detect concurrent updates to the same booking request and reject the stale one.
- Add a seed command that loads a synthetic dataset into a development database, and that can safely be run more than once.
- Add a test convention that links each test to the spec requirement it verifies.
Requirements
- addedPets belong to an owner and have a speciesProven
- addedSize class is derived from species and weightProven
- addedFacilities have a time zoneProven
- addedVaccine rules are defined per facility, service, and speciesProven
- addedVaccine records store dates, not flagsProven
- addedKennels have a size classProven
- addedReservations never overlap on the same kennelProven
- addedA booking request creates at most one reservationProven
- addedBooking requests have exactly one lifecycle stateProven
- addedOnly listed transitions are allowedProven
- addedThe system drives processing stepsProven
- addedDecisions lead to approved or rejectedProven
- addedDecided requests are sent by the systemProven
- addedApproval requires deterministic clearance or a personProven
- addedFailures record the failed step and the errorProven
- addedEvery transition is loggedProven
- addedConcurrent updates do not overwrite each otherProven
- addedStaff decisions are recorded as approvalsProven
- addedA trace groups the spans of one runProven
- addedSpans form a tree within their traceProven
- addedSpans have a kind and a statusProven
- addedSpans store payloads, timing, and usageProven
- addedSeeding loads a known synthetic datasetProven
- addedSeeding is repeatableProven
- addedSeeding refuses non-development databasesProven
Decisions it relied on
Tasks
- ✓Add `sqlalchemy[asyncio]`, `alembic`, and `pytest-asyncio` to the backend. Verify with `uv sync` and `uv run python -c "import sqlalchemy, alembic"`.
- ✓Add `app/db.py`: an async engine and session factory built from `DATABASE_URL`, rewriting the scheme to `postgresql+psycopg://`. Verify with `tests/test_db.py::test_engine_executes_select_1`.
- ✓Write [decision record 0003](../../../../docs/decisions/0003-async-database-access.md) (async database access). Verify the file exists and that design D1 links to it.
- ✓Add the `pawdesk_test` database fixture. It creates the database if missing, runs `alembic downgrade base` then `alembic upgrade head`, and gives each test a session inside a transaction that is rolled back afterward. Verify with `tests/test_isolation.py`: a row committed in one test is absent in the next.
- ✓Register the `spec(capability, requirement)` pytest marker and add a `conftest.py` hook that writes it into JUnit XML properties. Verify that `uv run pytest --junitxml=…` output contains the marker properties for a marked test.
- ✓Write [decision record 0004](../../../../docs/decisions/0004-spec-test-traceability.md) (spec-to-test traceability). Verify the file exists and that design D13 links to it.
- ✓Add `app/enums.py` (Species, Service, SizeClass, plus later enums) and ORM models for facilities, owners, pets, vaccine rules, vaccine records, kennels, and reservations. Verify with `uv run ruff check` and that the models import.
- ✓Initialize Alembic with the async template and write the first migration: the `btree_gist` extension, all booking-domain tables, the generated `size_class` column, the reservation exclusion constraint, and every CHECK and unique constraint. Verify that a fresh test session runs the downgrade/upgrade round trip.
- ✓Add the facility time-zone validator. Verify with `tests/test_booking_domain.py`, covering every booking-domain scenario (owner/species/weight rejection, size-class boundaries and recompute, invalid time zone, per-service rules, duplicate rule, vaccine record dates, kennel size class, overlap/back-to-back/different-kennel/zero-night reservations, one reservation per request).
- ✓Write `tests/test_lifecycle_guard.py`. It enumerates the allowed set from the spec and parametrizes over **every** (from, to, actor) combination, asserting that allowed ones pass and all others raise `IllegalTransition` naming both states. It also covers retry to the recorded failed step versus a different step, `sent` being terminal, and both paths into `approved`. Verify that it collects and fails, since the guard doesn't exist yet.
- ✓Implement `app/lifecycle.py`: the `State` and `Actor` enums, the allowed-transition table, and the guard function, with no I/O. Verify that `tests/test_lifecycle_guard.py` passes in full.
- ✓Add models and a migration for booking requests (status, `failed_step`, `last_error`, `version`), the transition log, and approvals, including the failed-state CHECK and the reject-requires-reason CHECK. Verify the round trip and `tests/test_booking_lifecycle.py::test_new_request_starts_received` / `test_unknown_state_rejected`.
- ✓Implement `app/transitions.py`: `transition()` applies the guard, updates the state, sets or clears the failure fields, appends the log row, and flushes, converting `StaleDataError` into `TransitionConflict`. Verify with `tests/test_booking_lifecycle.py`, covering the logged transition, no log entry on rejection, failure recorded, retry clearing the failure, staff rejecting a failed request, the stale update rejected with exactly one log row, and reject-without-reason rejected.
- ✓Add models and a migration for traces and spans: kind and status CHECKs, error-requires-message, non-negative tokens and cost, `ended_at >= started_at`, `NUMERIC(14,10)` cost, `JSONB` payloads, and the composite parent foreign key. Verify with `tests/test_run_tracing.py`, covering every run-tracing scenario, including the 8-decimal cost round trip and the cross-trace parent rejection.
- ✓Implement `scripts/seed.py`: the host allowlist, a single `TRUNCATE … CASCADE`, and the fixed fictional dataset with dates relative to today, all in one transaction. Verify with `tests/test_seed.py`, covering every table populated, all five kennel size classes, the three vaccine edge cases, identical counts after two runs, and a non-local host refused with no changes.
- ✓Run the seed against the development database. Verify with `uv run python -m scripts.seed`, which prints per-table counts.
- ✓Make the backend container run `alembic upgrade head` before uvicorn. Verify that `docker compose up --build` reports all services healthy and `/health/db` returns ok.
- ✓Update `README.md` with the migrate, seed, and test commands. Verify that each documented command runs as written.
- ✓Run the full suite and lint. Verify that `uv run pytest` and `uv run ruff check . && uv run ruff format --check .` pass, and that every spec requirement in this change has at least one test carrying a matching `spec` marker.