Contributing#
Definition of done#
A change is finished when all of these are true. Not most of them.
- [x] Feature complete against the requirement — no TODOs, no stubs, no
<!-- placeholder -->, no buttons that do nothing. - [x] Works at 375px wide and at 1920px.
- [x] Keyboard reachable, sensible
aria-attributes, visible focus. - [x] Tests pass in Docker, including
tests/test_smoke.py. - [x] Empty states and error states are designed, not blank.
- [x] No console errors in the browser.
File ownership#
The project is built by several people working in parallel, so ownership is explicit. Stay inside your list. If you need a change in a file someone else owns, ask for it — do not edit it.
| Area | Owner |
|---|---|
docker-compose.yml, Dockerfile, requirements.txt, .env.example |
lead |
app/__init__.py, config.py, extensions.py, models.py |
lead |
app/blueprints/__init__.py |
lead |
app/templates/base.html, app/static/css/base.css, js/app.js |
lead |
app/services/profiles.py, navigation.py, bootstrap.py |
lead |
app/cube/** |
cube engine |
app/static/js/cube/**, app/static/vendor/** |
cube JS |
app/services/scoring.py |
scoring |
app/data/** |
library data |
scripts/extract_algorithms.py |
PDF extraction |
app/blueprints/<module>.py + templates/<module>/** |
that module |
docs/**, app/blueprints/docs.py, templates/docs/** |
docs |
tests/test_<yourmodule>_*.py |
you |
The shared files exist to be used, not extended. base.css and
_macros.html are how you avoid needing to touch anything you do not own: if
the component you need is missing from the macro library, that is a request to
the lead, not a local reimplementation.
Conventions#
Python#
from __future__ import annotationsat the top of every module.- Type hints on every public function.
Mapped[...]on every model column. - Module docstrings say why the module exists, not what its name already says. Function docstrings on anything non-obvious — and on every API view, because the first line becomes the summary in the API console.
- Blueprints stay thin: parse the request, call a service, render. Logic lives
in
app/services/; anything cube-shaped lives inapp/cube/and imports no Flask. - Times are integer milliseconds in the database and in the API. Seconds are for humans and for config.
- Timestamps use
models.utcnow(), neverdatetime.now(). - No new dependencies without agreement. The budget is five plus gunicorn.
Templates and CSS#
- Use the tokens and components in
base.cssand the macros inpartials/_macros.html. See Frontend. - Page CSS goes in
app/static/css/<module>.css, pulled in with{% block styles %}. Page JS goes inapp/static/js/<module>.js. - Do not restyle the shared shell.
JavaScript#
- No build step, no package manager, no CDN. Vendored libraries go in
app/static/vendor/. - Behaviour hooks are
data-attributes; classes are for CSS only. - Guard for missing elements — shared scripts run on pages without your widget.
Commits and pull requests#
- Small, one concern each. A commit that both renames a column and adds a feature is two commits.
- Say why in the message; the diff already says what.
- If you touched the schema, say so loudly — there is no migration framework (see Data model).
- Run both suites before you push. See Testing.
Recipe: add or correct a case#
Cases live in app/data/algorithms.json, generated from the PDFs in pdf/.
The schema is in the build contract; the essentials:
{
"key": "oll-21",
"phase": "oll",
"name": "OCLL7 — Double Cross",
"number": 21,
"group": "all-edges-oriented",
"sort_order": 21,
"probability": "1/108",
"setup_moves": "R U2 R' U' R U R' U' R U' R'",
"mask": {"kind": "oll", "u": [1,0,1,0,1,0,1,0,1], "sides": {"F": [0,1,0]}},
"algorithms": [
{"moves": "R U2 R' U' R U R' U' R U' R'", "is_primary": true,
"move_count": 11, "notes": null, "source": "Zemdegs/Klise OLL sheet"}
],
"media": []
}
keyis forever. Student attempts and assessments are attached to the case row, matched onkey. Renaming one orphans real data.setup_movesmust validate. Applyingsetup_movesand then the primary algorithm to a solved cube must return the cube to the phase's target state — fully solved for OLL and PLL, the target slot solved with the last layer untouched for F2L. A case that fails is a bug, not an acceptable gap.- Re-seed and check:
bash
docker compose run --rm web flask seed
docker compose run --rm -e RT_DATABASE_URL=sqlite:////tmp/t.db web pytest tests/test_library.py
- The counts are fixed: 41 F2L, 57 OLL, 21 PLL. The four slot angles of an F2L case are variations of one case, not four cases.
Recipe: add an algorithm to an existing case#
Append to the case's algorithms list. Set is_primary: false — the primary
is what drills and the viewer assume, and changing it changes what every
student is shown. Give it a source, and a notes line saying why it is worth
having ("left-hand friendly", "sets up a better PLL").
The new algorithm must also solve the case from setup_moves; the validator
checks every algorithm, not only the primary one.
Recipe: add a built-in training plan#
Built-in plans are seeded by app/services/bootstrap.py. A plan is a name, a
level, a description and an ordered block list, documented in
Training plans.
Write the plan against skill, not against a fixed list of cases: a
weakest block means something different for a sub-30 and a sub-15 solver,
which is what makes a plan reusable. Set is_builtin: true so it cannot be
edited in place — students duplicate it instead.
Recipe: add a documentation page#
- Create the markdown file under
docs/<section>/. Sections areuser,admin,install,developer. - Add front matter.
ordercontrols the sidebar position,titleoverrides the first#heading,summaryis what the search index and the landing page show:
```markdown
title: Lookahead training order: 45 summary: One sentence saying what this page is for.
# Lookahead training ```
- Write it and reload
/docs. The tree is re-read whenever a file's mtime changes — no restart, and no navigation to update by hand. - Link to other pages with relative markdown links (
./drills.md,../admin/index.md). They are rewritten to/docs/...URLs at render time, so the same file reads correctly on disk, on GitHub and in the browser. tests/test_docs.pypicks the page up automatically and will fail if any of your links point at something that does not exist.
Available markdown extensions: fenced code with Pygments highlighting, tables,
toc, attr_list, def_list, admonition, sane_lists and footnotes.
Diagrams are inline SVG
There is no Mermaid renderer — vendoring one for a handful of pictures is
not a trade worth making when the app must work offline. Write diagrams as
hand-authored inline SVG inside <figure class="doc-figure">, or as an
ASCII diagram in a fenced block. Both print, both scale, neither needs
JavaScript.
Recipe: add a page to the app#
- Add the view to the blueprint that owns the URL prefix. Do not create a new
blueprint without agreement — the list in
app/blueprints/__init__.pyis owned by the lead. - Add the template under
app/templates/<module>/, extendingbase.html. - Style with existing tokens; if you need module CSS, put it in
app/static/css/<module>.cssand pull it in with{% block styles %}. - If it should appear in the primary navigation, ask the lead —
services/navigation.pyis shared. - Add a test.
tests/test_smoke.pyshould return 200 for it.
Getting help#
Read Architecture first, then the page for the area you are touching. If the documentation is wrong, fixing it is a contribution — the source path of every page is printed at the bottom of it.