Code style¶
Python¶
backend/ruff.toml exists and is a real, checked-in configuration:
indent-width = 4
[format]
indent-style = "space"
line-ending = "lf"
quote-style = "double"
[lint]
select = ["E", "F", "I"]
ignore = ["E501"]
That selects pycodestyle errors (E), pyflakes (F), and import sorting (I), and explicitly
ignores line-length (E501), so this project has decided not to enforce a line-length limit, not
that it forgot to.
ruff is the only Python style tool here, and it is a real dev dependency, so uv sync installs
it and these commands work on a fresh clone:
cd backend
uv run ruff check . # lint: E, F, I as ruff.toml selects them
uv run ruff check --fix . # and fix what is safely fixable
uv run ruff format . # format the tree
uv run ruff format --check . # or just report, which is what CI runs
There is no black and no isort. ruff format is a reimplementation of black and produces
the same output, and ruff's I rules do isort's job, so all three would be doing one job with two
of them redundant, and black and ruff format can disagree at the edges. Older docs and habits
name uv run black . && uv run isort .; neither is installed and neither should be added.
CI runs both, and both must pass. .github/workflows/tests.yml runs uv run ruff check . and
uv run ruff format --check . as hard steps, not warnings. uv run rather than uvx on purpose:
ruff is pinned in uv.lock, so your machine and CI check against one version and a ruff release
cannot turn the build red overnight.
The tree conforms. It was formatted in full on 2026-08-07 (205 files) with imports sorted in
112 more, so ruff format . on a clean checkout changes nothing and your diff stays your diff.
That is the point of formatting everything at once: before it, running the formatter across the
tree turned any focused PR into an unreviewable one, which is why the old advice here was to scope
uvx to the files you touched. That advice no longer applies. Run the commands above on the whole
tree and commit only what you meant to change.
One thing worth knowing before you run --fix over code you did not write: a # noqa binds to the
line ruff reports the diagnostic on, so a suppression that has drifted onto the next line, or that
names the wrong rule code, silently suppresses nothing, and the autofix will then delete what it
was protecting. common/models.py's SecurityAuditLog import is the live example: it exists only
so Django discovers the model for migrations, and its # noqa: F401,E402 has to sit on the same
line as the imported name.
Frontend¶
frontend/package.json defines the real commands:
pnpm run lint # prettier --check . && eslint .
pnpm run format # prettier --write .
pnpm run check # svelte-kit sync && svelte-check --tsconfig ./jsconfig.json
These tools are installed (prettier, eslint, svelte-check, and their plugins are all in
frontend/package.json's devDependencies) and CI runs all three, but not symmetrically: .github/workflows/tests.yml's frontend-checks job runs the Prettier check with a
|| echo "::warning::..." fallback, so a formatting difference shows up as a CI warning, not a
failed build. The ESLint step (pnpm eslint .) has no such fallback. A lint error fails the job.
pnpm check (svelte-check) and pnpm build run unconditionally after that and also fail the job on
error. So in practice: fix ESLint errors and type errors before opening a PR, but don't be surprised
if Prettier differences are flagged as a warning rather than a hard failure. Run pnpm run format
yourself rather than relying on CI to catch it.
Flutter¶
mobile/analysis_options.yaml includes package:flutter_lints/flutter.yaml with no
project-specific rules added or removed. Neither of this repository's two GitHub Actions workflows
(tests.yml, codeql-analysis.yml) mentions Flutter, Dart, or mobile/ at all, so nothing in CI
enforces Dart formatting or lint cleanliness. Unlike the backend, which now gates on ruff, running
these commands yourself before opening a PR is the only check that happens. See
Build and configure for the full command set
including flutter test.