Guidance for AI assistants working in this repository.
- Use Conventional Commit messages (
feat:,fix:,chore:,docs:,test:…). - Keep commits small and reviewable — one concern each.
- Write everything in English.
- Do NOT add any Claude/AI attribution to commits. Specifically:
- No
Claude-Session:trailer or any Claude Code session URL. - No
Co-Authored-By: Claudeor similar co-authorship / "in collaboration with Claude" lines. - Commits should read as authored solely by the repository owner.
- No
- Run the linters before every commit / PR — without being asked. CI runs
uv run ruff format --check .anduv run ruff check .and fails on any drift, so before committing (and before opening a PR) runuv run ruff format . && uv run ruff check .and fix what they report. For frontend changes also run the JS checks (npm run lint,npm run build) as noted under Web UI.
- Keep comments minimal. Comment only what the code cannot say itself — the why behind a non-obvious choice, not the what. Do not restate what the line plainly does.
- No header/banner comments describing a file's purpose (a module docstring is enough),
and no step-numbering narration (
# 1),# 2)). Match the surrounding file's density. - Tests are an exception: verbose, explanatory comments (expected values, intent of each case) are welcome there. This rule targets production code.
- Public API docs are an exception: every FastAPI route handler should have a concise
docstring — FastAPI surfaces it as the endpoint's description in the OpenAPI/Swagger docs.
Keep it short: what it does plus the access rule (shared / owner / admin). Likewise use
Field(description=...)/summary=where it improves the generated docs. Do not strip these to be terse.
- NEVER read the
.envfile, under any circumstances. Do not open it,catit, grep it, or print its contents — it holds real production secrets. Use.env.examplewhen you need to know which variables exist. - Never write secret values into the repository, commits, logs, or chat output.
- All code, comments, and identifiers in English.
- Respect the layering rule:
routes → services → repositories → db, withdomain/as pure functions.domain/never imports ORM/FastAPI/session; routes contain no SQL; repositories are the only DB-access layer. - Typed SQLAlchemy 2.x (
Mapped[...]), Pydantic v2, FastAPI dependency injection. - Every API route is served under
/api/v1(settings.API_V1_STR)./healthstays at the root — container healthchecks probe it. - Services stay framework-agnostic: they raise domain errors from
bloom/services/errors.py(NotFoundError→404,ForbiddenError→403,ConflictError→409,UnprocessableError→422), mapped to HTTP inbloom/main.py. Don't raiseHTTPExceptionfrom a service for these. - A schema change needs an Alembic migration (
alembic/versions/), and validate it up and down. The test DB is built from the ORM models (create_all), so a missing migration won't fail tests — but it breaks a real deploy, which runsalembic upgrade headon startup. - See
docs/ARCHITECTURE.mdfor the architecture, data model, and design decisions (source of truth).README.mdcovers how to run and use the project. - Keep
docs/ARCHITECTURE.mdin sync with the code — without being asked. Whenever a change touches something it documents — the data model (a new table, column, or constraint), the layering rule, an ownership/access rule, anON DELETEpolicy, or anything that contradicts an existing numbered decision — update the relevant section (or add a new numbered decision) as part of the same change. Pure UI or behaviour tweaks that don't alter a documented decision need no update.
React + TypeScript SPA: Vite, Tailwind v4, shadcn/ui, TanStack Router + Query,
react-hook-form + zod. See frontend/README.md for the stack and how to add a page.
frontend/src/client/is generated — never edit it by hand. It is committed, so it looks editable, but any change is lost on the next run. After touching a route or a schema:uv run python scripts/dump_openapi.py && (cd frontend && npm run generate-client).- Ownership: rows are readable by everyone, editable only by their creator or an admin.
Drive the UI from
canEdit(row, user)(src/lib/auth.ts) — never re-derive the rule. - PATCH never sends
null: the API rejects an explicitnullfor fields backed by NOT NULL columns (bloom/schemas/common.py:reject_null). Build payloads withstripEmpty()(src/lib/format.ts), which omits empty keys. - Numeric columns arrive as strings (
"18.50") — format viasrc/lib/format.ts. - The UI is served by the API from
bloom/static, built into the same image; there is no separate frontend container. It therefore calls the API with relative URLs — do not reintroduce an absolute base URL, or the image stops working behind a reverse proxy. - Checks:
npm run lint,npm run build(both run in CI, and the build gates the release image). Node comes fromnvm.
The version lives in five places and they must agree: pyproject.toml, uv.lock, the image
tag in docker/docker-compose.yml, openapi.json, and frontend/package.json. Bump
pyproject.toml, then run uv lock and uv run python scripts/dump_openapi.py to refresh
the derived ones — a stale uv.lock fails the Docker build, which runs uv sync --frozen.
CI regenerates openapi.json and the frontend client and fails if either drifts — this also
catches a half-done version bump, since openapi.json embeds the version.