Maths practice site

From prototype to maths.malcolm.nz · written 2026-10-05 · status Step 0 Complete Step 1 Not Started · text version: mvp_plan.md

Goal: a 14-year-old in the Cook Islands passes GCSE Foundation (grade 4). Short daily practice on the topics with the most marks, with stars, worlds, reviews that keep topics fresh, and real rewards the parent sets.

Where we are: two prototypes prove the loop and the questions. Nothing is playable by him yet: no answer box, no checking, nothing saved, nothing deployed.

Plan: grow the prototype into a static site, World 1 first, live at maths.malcolm.nz. Progress stays in the browser until parent rewards need accounts (Step 4).

57subtopics in topics.yaml: 31 core ★, 20 extend ◆, 6 later ○
40question generators: all 34 world subtopics + 6 extend
0answer checkers. Generators return the answer as text only
10worlds on the learning path; Shape & data is "11+"

1 · The core loop

Decided in zdocs/summary.txt and topics.md, and built (with simulated days) in prototype/core-loop.html.

Path / Today next on path, ≤5 due Run of 10 generated per level Check answer new work: checkers ≥ 8/10 pass level n → n★, box +1 < 8/10 stars kept, box −1 due date = today + box interval: 1 → 3 → 7 → 14 → 30 → 60 days; overdue = stale (stars stay, stop counting as fresh)
Stars come from passing harder levels (easy, standard, exam), so 3★ means exam-ready. Stars are never taken away; only "fresh stars" and the exam-ready meter drop when a topic goes stale.

2 · One learning path, and when each part goes live

1 Warm-up 3 topics · Step 1 2 Fractions 4 topics 3 Percentages 2 topics 4 Algebra basics 5 topics 5 Ratio 4 topics 6 Line graphs 5 topics 7 Sequences 2 topics 8 Quadratics 3 topics 9 Rounding, primes 3 topics 10 Powers 3 topics 11+ Shape & data · Step 5 Step 1 Step 2 each arrow = 10-question checkpoint, 8/10
Worlds 1–10 hold 34 subtopics, every one with a generator already. It is one path: the next subtopic opens when the one before has 1★. This order already respects every needs entry. Full tables: zdocs/topics.md.

3 · Static site first, server later

Static site, browser storage proposed

  • nginx Dockerfile like ../duane, deployed with malcolm-apps
  • Prototype JS reused as is; live in days, not weeks
  • Rules and checkers are DOM-free modules, tested with node --test, portable to a server
  • Cost: progress is on one device until Step 4, then imported once

Server app from Step 1 not now

  • Stack (SvelteKit or Flask) still undecided
  • Logins, database and backups before he answers one real question
  • Only needed for parent rewards across devices

AI-generated questions no

  • Cost per question
  • Wrong answers would teach wrong maths
  • Code generators already cover every world

One path, not a graph

Linear path proposed

  • Subtopics in world order; the next opens at 1★
  • World order already respects every needs (checked 2026-10-05, all 34)
  • needs kept in the yaml, used only by a test that guards the order
  • Worlds stay as sections with checkpoints and badges
  • Cost: no choice of branch; a hard topic blocks the rest. Low risk: 1★ is the easy level, and reviews mix older topics in

Prerequisite graph no

  • needs drives unlocks; branches open side by side
  • Graph logic, a harder map and more tests
  • Choice he doesn't need to make

What the app looks like

zdocs/topics.yaml ──embed_topics.py──▶ app/topics-data.js
app/
  generators.js   gen(rng, level, ctx) → { q, a, svg? }      (from prototype)
  checkers.js     check(answerType, given, expected) → { ok, feedback }
  progress.js     pure rules: path position, stars, boxes, due dates, queue, meters
  store.js        load/save progress (localStorage now, HTTP API in Step 4)
  index.html      path → run (10 questions) → result
tests/            node --test: generators, checkers, progress
Dockerfile        nginx:alpine, copies app/

Checkers: the real new work

Built in the order the worlds need them. Every generator gets a test: 100 seeded questions per level, and its own answer must pass its checker.

Answer typeFirst neededHow it checks
numberWorld 1 (Step 1)Accept − or -, commas, spaces; exact compare; rounding answers must match the dp/sf asked
fractionWorld 2Parse a/b, a b/c; equal value; feedback if not simplest
choiceWorld 2Buttons, no typing
expressionWorld 4Evaluate both at random values (as topics.md suggests), so term order doesn't matter; plus a form rule per subtopic: expand → no brackets, factorise → brackets, simplify → no like terms left
ratio, number_pair, coordinateWorlds 5–6Parse a : b, (x, y); compare parts
tableWorld 6One box per cell, each a number check
standard_formWorld 10Parse a × 10^n or aEn; require 1 ≤ a < 10

4 · Decisions

Already made

Still needed

QuestionRecommendationBy
Stars from levels or from score?Levels, as prototyped: 3★ means exam-readyStep 1
Laptop, tablet or phone?Ask him. Touch needs an on-screen keypad for −, ÷, fractions, x²Step 1
Fractions in simplest form / mixed?Accept any equivalent; show simplest as feedbackStep 2
Calculator flag per level?Yes: "no calculator" on L1–L2 number skillsStep 2
Server: TypeScript or Python?Decide at Step 4; the JS modules work with eitherStep 4
SQLite on a storage mount or Dokku Postgres?SQLite: two users, one appStep 4
Parent view: rewards only, or weak spots too?Both; weak spots come from the attempts tableStep 4

5 · Development steps

Each step deploys on its own and he uses it before the next one starts. Offline tests: node --test tests/. Live check: he plays on his own device and the parent looks at the result.

playableonline all 10worlds reviews onreal dates accounts,rewards shape& data Step 0 · prototypes Step 1 · World 1 live Step 2 · all ten worlds Step 3 · freshness Step 4 · accounts, rewards Step 5 · shape & data
What each step switches on; each keeps everything before it. Step 1 is the first one he can use, so it is kept as small as possible: one world, one checker.

Step 0 · Prototypes (no deploy) Complete

Goal
Check the core loop and question generation feel right before building.
Done looks like
A clickable loop with simulated days, and generators for every world.
Result
  • prototype/core-loop.html: map, worlds, levels → stars, Leitner boxes, freshness halo, review queue, checkpoints, exam-ready meter. Built on topics.yaml via embed_topics.py → topics-data.js
  • prototype/question-generator.html + generators.js: 40 generators, 3 levels each, local context, SVG for grids and distance-time graphs
  • Latest generator work (generators.js, topics-data.js) is uncommitted

Step 1 · World 1 playable at maths.malcolm.nz Not Started

Goal
He earns a real star on his own device.
Changes
  • app/ from the prototype: generators.js, topics-data.js, progress.js (rules lifted from core-loop.html), store.js (localStorage, versioned JSON)
  • checkers.js: number only
  • index.html: the path's first 3 nodes, World 1 (negative_numbers, order_of_operations, rounding_decimal_places) → 10 questions one at a time, answer box, right/wrong with the correct answer → result with stars
  • On-screen keypad only if he plays on a phone or tablet
  • Dockerfile (nginx:alpine, like ../duane); deploy with malcolm-apps
Done looks like
https://maths.malcolm.nz loads; he passes L1 of a World 1 topic and sees 1★; a reload keeps it.
How we verify
  • Offline: number checker accepts -4, −4, 1,200, 3.5 ; rejects 3.50 when 1 dp is asked
  • Offline: each World 1 generator × level × 100 seeds gives an answer its checker accepts
  • Offline: 8/10 passes a level, 7/10 doesn't; passing L(n) sets stars to n and never lowers them
  • Live: he plays one session; the parent sees the stars on his device

Step 2 · All ten worlds playable Not Started

Goal
The whole learning path is open to him, world by world.
Changes
  • Path through all 10 worlds; next node opens at 1★; checkpoints (10 mixed questions, 8/10 opens the next world, badge)
  • Checkers: fraction, choice, expression, ratio, number_pair, coordinate, table, standard_form. SVG diagrams shown with the question
  • "No calculator" label per level, if decided
Done looks like
Every subtopic in Worlds 1–10 can be played and passed; passing World 1's checkpoint opens World 2.
How we verify
  • Offline: every world generator × level × 100 seeds checks as correct against its own answer
  • Offline: equivalent answers accepted (2/4 for 1/2, x + 3 for 3 + x); unexpanded 2(x + 3) rejected where the level asks to expand
  • Offline: a node without 1★ before it can't be started; a checkpoint draws only from its world; path order respects every needs in topics.yaml
  • Live: he finishes World 1's checkpoint; keep a list of "my answer was right but it said wrong" for the first week, aim for none

Step 3 · Freshness on real dates Not Started

Goal
Topics he has passed come back for review at the right time.
Changes
  • Leitner boxes on calendar dates (local time); due/stale halo on the path
  • "Today" panel with up to 5 due reviews, most overdue first; a review is one run at his current star level
  • Fresh stars and the exam-ready meter (share of core subtopics at fresh 2★+: 26 in Worlds 1–10, all 31 after Step 5) on the home screen
Done looks like
A topic passed today shows as due on the right date; after a holiday the queue still shows at most 5.
How we verify
  • Offline: pass → box up, due = today + interval; fail → box down (min 1); stars never drop
  • Offline: queue cap 5; meter counts only fresh 2★+ core subtopics
  • Live: over a week, reviews appear on the expected days

Step 4 · Accounts and parent rewards Not Started

Goal
The parent sets real rewards on their device; he sees them on his.
Changes
  • Stack decided. Small server app + SQLite on a Dokku storage mount; two accounts created upfront, no signup page
  • Tables: users, topic_state(user, topic, stars, box, due_at), attempts(user, topic, level, question, given, correct, ms), rewards
  • store.js talks to the API; one-time import of the localStorage progress
  • Parent page: create a reward (fresh stars ≥ N, branch at 2★, topic at 3★, streak of N days, checkpoint passed, exam-ready ≥ N%); mark earned rewards as given; weak spots from attempts
  • He sees each reward with a progress bar ("14 / 20 fresh stars"); earned is never taken back
Done looks like
A reward set on the parent's phone shows on his device with the right progress; earning it shows on both.
How we verify
  • Offline: each condition type against fixtures; earned → given only by the parent; importing a Step 3 save loses nothing
  • Live: one real reward set and earned

Step 5 · Shape & data Not Started

Goal
The ~30% of Foundation marks in Shape & data.
Changes
  • Generators and worlds 11+, appended to the path, for the 11 non-canvas Shape & data subtopics (angles, area_perimeter, pythagoras, trigonometry, …), with SVG diagrams
  • The 3 missing algebra/graph subtopics: inequalities, simultaneous_equations, perpendicular_lines
  • transformations, constructions, plotting_draw stay parked until a drawing canvas is worth building
Done looks like
The new worlds are playable with their checkpoints.
How we verify
  • Same generator/checker tests as Step 2

6 · Not in scope