Spaced repetition for DSA interview prep. Problems come from LeetCode, you solve them in nvim, a local model tells you how far your answer is from the reference, and an SM-2 scheduler decides when you see each one again.
Everything runs on your machine. The only network calls are seeding the library and, if you want them, model downloads.
Prerequisites: Python 3.11+, nvim, ollama, and clang++ if you want to solve in C++ (macOS ships it with the Xcode command line tools).
pip install -e . # from the repo root
ollama pull qwen2.5-coder:7b # the reviewer model, ~4.7 GB
algorhythm seed # populate the libraryCheck your memory before pulling that model. It needs about 5 GB resident. On a machine that cannot spare it, generation does not merely slow down — it thrashes, and the whole laptop goes with it. See choosing a model.
algorhythm seed calls LeetCode's public GraphQL API, which is against
their terms of service. It's your call whether to run it. If you'd rather
not, python3 scripts/smoke_fixture.py seeds two problems offline and
everything below works the same way.
algorhythm reviewThat's the whole app. What happens, in order:
- Queue screen — today's problems.
j/kto move,lorEnterto open one,fto pick topics,horEscto quit. Arrows work too. - nvim opens with three panes: statement on the left and your solution on the right, equal width, with results along the bottom. Opening the review pane keeps those two balanced.
:wruns the tests and fills the results pane. Save as often as you like.:Reviewasks the local model to compare your solution against the reference. Opens a fourth pane.:qaends the rep.- Grade screen — the model pre-selects a grade.
h/l(or the arrows) to change it,Enterto commit and schedule the next repetition,Escto abandon the rep.
Nothing is written to the database until you grade. Esc on the grade
screen leaves no trace — no attempt, no schedule change.
| Key | Does |
|---|---|
:w |
Run the tests |
:Review |
Ask the model for a review |
:qa |
End the rep, go to grading |
Ctrl-w + hjkl |
Move between panes |
The statement and results panes are read-only.
algorhythm review --limit 10 # up to 10 problems today (default 5)
algorhythm review --new 0 # review only, introduce nothing new (default 2)
algorhythm review --lang cpp # do every rep this session in C++--limit caps the whole queue. --new caps only unseen problems, and
due reviews are filled first — so on a heavy review day nothing new is
introduced. That's deliberate: retention beats coverage, and every new
problem you take on today becomes review load for weeks.
The two are independent, which catches people out: on a library where
everything is still unseen, raising --limit alone changes nothing,
because --new is what's binding. Raise both:
algorhythm review --limit 10 --new 10When that is what shortened your queue, the queue screen says so and names the flag.
Without --lang, each problem uses whatever language you last solved it
in, defaulting to Python.
Press f on the queue screen. You get every topic in the library,
ordered by how many problems carry it:
✓ Array (74)
· Dynamic Programming (35)
· String (27)
· Depth-First Search (24)
space toggles, enter applies, c goes back to everything, esc
cancels. Enter on a highlighted row picks just that topic, so choosing
one takes a single keypress. Picking topics rebuilds the queue rather
than filtering what is on screen — the reason to choose a topic is to
practise something today's selection did not offer.
The active topics are shown above the queue, so a filtered day never looks like a merely quiet one.
Selecting several topics widens the session rather than narrowing it:
Tree and Graph Theory means problems tagged with either. The count
beside each topic is exactly how many you get — topics match on their
whole name, so picking Tree does not quietly pull in Binary Tree.
Filtering applies to due reviews as well as new problems, so a topic session stays on that topic.
algorhythm topics prints the same list from the shell, if you just want
a look without starting a session.
algorhythm list # every problem, its next due date and rep count
algorhythm stats # counts of scheduled problems, reviews, attemptsalgorhythm add two-sum # one problem, by LeetCode slug
algorhythm seed # bulk, from seeds/neetcode150.txt
algorhythm seed --list-path my-problems.txt # bulk, from your own listBoth fetch the statement, import a reference solution, and generate test
cases. seed skips anything already present, so re-running it is safe
and only fetches what's new — that's how you extend the library later.
A slug list is one slug per line; # starts a comment.
~/.local/share/algorhythm/
├── algorhythm.db schedule, reviews, attempts
├── problems/<n>-<slug>/ statement, examples, stubs, reference, tests
└── cache/cpp/ compiled C++ binaries, content-hashed
Problem content is files, not database rows, so it stays greppable and
hand-editable when a fetch comes out wrong. Set ALGORHYTHM_HOME to use
a different root — useful for trying things without touching your real
library:
ALGORHYTHM_HOME=/tmp/scratch algorhythm reviewTest cases come from two places. Example cases carry LeetCode's own stated outputs. Oracle cases are generated by perturbing the example input one parameter at a time and running a reference solution to get the expected output; candidates the reference rejects are dropped, as are candidates where the Python and C++ references disagree — that disagreement is the only signal available that an input fell outside the problem's stated constraints.
The reviewer is given your solution, the reference, and the concrete test results. Grounding it that way turns "is this good?" into "how does this differ from that?", which small models are much better at.
On a repeat rep you also get a "Since last time" line — how many tests passed then against now, how long ago it was, and what you graded it:
Last seen 4 days ago, tests 5/5 → 0/5, fewer passing than last time, you
graded it good.
That is computed from the reviews table, not written by the model. Asked
to compare two attempts, a 3B model got the direction backwards two runs
in three — it called a rewrite from a working hash map to return [0, 0]
an improvement. Five prompt variants did not fix it. A sentence about your
own history is worth having only if it is true, so this one is arithmetic.
The reviewer runs whatever ALGORHYTHM_MODEL names, defaulting to
qwen2.5-coder:7b. Model size is the single thing that decides whether
this is pleasant or unusable, and it depends on your machine rather than
on the code. One identical review, measured on an 8 GB M2:
| model | resident | one review |
|---|---|---|
qwen2.5-coder:7b |
4.9 GB | 468 s |
gemma2:2b |
1.6 GB | 15 s |
Thirty times slower, and that is not the model thinking harder — it is 4.9 GB not fitting beside macOS in 8 GB. Rough guide:
- 16 GB or more →
qwen2.5-coder:7b, the default. - 8 GB → something in the 2–3 GB range.
qwen2.5-coder:3b(1.9 GB) is the closest relative of the default and code-specialised; measured at 6–11 s per review on the machine where the 7B took 468 s.gemma2:2bis lighter still.
Whatever you choose, put it in your shell profile — the default is the 7B,
so a shell without ALGORHYTHM_MODEL set will ask for a model you may not
have and report the reviewer as unavailable.
export ALGORHYTHM_MODEL=qwen2.5-coder:3bThree other knobs, all optional:
| variable | does |
|---|---|
ALGORHYTHM_MODEL |
which model reviews (default qwen2.5-coder:7b) |
ALGORHYTHM_OLLAMA_HOST |
where Ollama lives (default http://localhost:11434) |
ALGORHYTHM_REVIEW_TIMEOUT |
seconds to wait for a review (default 600) |
ALGORHYTHM_OLLAMA_KEEP_ALIVE |
how long the model stays loaded after a review |
Ollama keeps a model resident for five minutes by default. On a tight
machine that is five minutes of swapping after the review has finished —
export ALGORHYTHM_OLLAMA_KEEP_ALIVE=30s frees it promptly, at the cost
of a reload next time.
A review that times out is reported as unavailable and the rep still finishes; you just grade it yourself.
The proposed grade is held to what the tests showed: a solution that
compiled and passed nothing is always proposed as again, whatever the
model says. Models inflate — one proposing hard for a solution failing
every case was measured — and the grade feeds the scheduler, so an
inflated one brings the problem back too late. Partial passes are left to
the model, which is what it is there for.
Some problems accept answers in any order. Those are marked unordered
and compared after sorting at every level, so a correct answer that
groups differently still passes. Problems where order is the answer —
a level-order traversal — are deliberately not marked.
Upstream data is imperfect: some reference solutions don't parse, some
test cases can't be written as JSON (a linked list with a cycle), and
"in any order" appears only in prose. algorhythm/curated/<slug>/ holds
corrections that win over what seeding fetches:
| File | Overrides |
|---|---|
reference.py / reference.cpp |
The fetched reference solution |
tests.json |
The generated test cases, entirely |
problem.json |
Problem fields, e.g. {"comparison": "unordered"} |
Every part is optional. A problem with no directory seeds normally.
seeds/neetcode150.txt holds the full list; 129 of the 150 seed and
work. The rest are reported and skipped:
- Design problems (LRU Cache, Min Stack, Trie) define their own class and are driven by a sequence of operations, so there's no single entry point to test.
- Premium problems have no public statement.
- Four shapes the codecs can't express are commented out of the list, each with its reason — a list of linked lists, random pointers, tree nodes passed as bare values, and problems where any valid topological order is correct.
Nine problems have no C++ reference: upstream ships files holding several
class Solution definitions, and others that are simply wrong. A
reference that can't reproduce LeetCode's own stated outputs is discarded
rather than shown to you as the recommended solution. Those reps still
run in C++; they just have nothing to compare against.
pytest # the full suite
pytest -W error # how CI-quality runs should look; output stays cleanThe editor tests launch real nvim, and the C++ tests invoke the real
compiler; both skip themselves if the tool isn't installed. That
matters — the one bug that made :w silently do nothing was invisible to
every test that didn't drive a real editor.
Design notes and the build plan are in docs/superpowers/.