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 annotations at 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 in app/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(), never datetime.now().
  • No new dependencies without agreement. The budget is five plus gunicorn.

Templates and CSS#

  • Use the tokens and components in base.css and the macros in partials/_macros.html. See Frontend.
  • Page CSS goes in app/static/css/<module>.css, pulled in with {% block styles %}. Page JS goes in app/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": []
}
  1. key is forever. Student attempts and assessments are attached to the case row, matched on key. Renaming one orphans real data.
  2. setup_moves must validate. Applying setup_moves and 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.
  3. 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

  1. 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#

  1. Create the markdown file under docs/<section>/. Sections are user, admin, install, developer.
  2. Add front matter. order controls the sidebar position, title overrides the first # heading, summary is 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 ```

  1. 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.
  2. 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.
  3. tests/test_docs.py picks 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#

  1. 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__.py is owned by the lead.
  2. Add the template under app/templates/<module>/, extending base.html.
  3. Style with existing tokens; if you need module CSS, put it in app/static/css/<module>.css and pull it in with {% block styles %}.
  4. If it should appear in the primary navigation, ask the lead — services/navigation.py is shared.
  5. Add a test. tests/test_smoke.py should 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.

Source: docs/developer/contributing.md