Command-line planner

Suggest Review
practise what you are avoiding

A good week on DP turns into three weeks on DP, while linked list, design and slide window quietly go a month untouched — and they are exactly what a coding round is made of. This script measures that drift against the repo's own history, then spends its picks on the topics that are owed them.

python3 script/suggest_review.py
== Balance: attention vs importance ==

  category                       n  importance     attention         ratio  last
  Array                        139  ######...... #...........    0.19 UNDER  today
  Linked list                   24  ##.......... ............    0.00 UNDER  36d
  Design                        45  #........... ............    0.24 UNDER  23d
  Binary Search                 46  ###......... ##..........    0.94        today
  Tree                          62  ####........ ####........    1.12        1d
  Recursion                     29  ##.......... ####........    1.94 over   1d
  Dynamic Programming           94  #####....... ############    2.19 over  today

The table is the finding. The list of problems underneath it is one way to act on it.

No install, no dependencies

Running it

Standard library Python, run from the repo root. It reads files that are already in the repo and never touches the network, so it is as fast offline as on.

python3 script/suggest_review.py                      # the balanced plan
python3 script/suggest_review.py --top 20 --per-category 3
python3 script/suggest_review.py --only must         # MUST rows only
python3 script/suggest_review.py --only top100liked
python3 script/suggest_review.py --section "Binary Search" --top 10
python3 script/suggest_review.py --no-balance        # plain importance rank
python3 script/suggest_review.py --markdown doc/review_suggestions.md
python3 script/suggest_review.py --self-test         # the unit tests

What it reads

Nothing is invented — all three signals are already in the repo.

SourceWhat it contributes
README.md The problem universe and this repo's own judgement of what matters: the MUST marker, the curated-list tags (blind75, neetcode150, neetcode250, top100liked), the company tags, and the status column's OK/AGAIN plus its * run of review passes.
git history When each problem was last worked on — the commit that touched its solution file, or named its LC number in the subject.
data/progress.txt When it was last practised, which is not the same thing: a re-read that produced no commit still counts.

doc/must_lc_list.md is generated from the same README rows, so reading README covers it — and a test asserts this script and extract_must_lc.py still agree on which rows carry MUST.

Three factors, then a round-robin

How a problem gets chosen

A flat “most important, least recently seen” ranking makes bias worse, because the biggest sections hold the most important problems and would fill the whole list. So the score is only half the job; the other half is who gets to spend it.

score = importance

MUST +5.0 · top100liked +2.5 · blind75 +2.5 (the NeetCode lists nest, so only the narrowest scores: 150 → +1.5, 250 → +0.7) · google +1.5 · other company tags +0.2 each, capped at +1.0 · Medium +0.5, Hard +0.3 · and +0.2 per recorded pass on an AGAIN row, capped at +2.4.

× staleness

1 − 0.5 ^ (days / half-life), half-life 21 days. Touched today scores 0; never touched saturates near 1. The clock reads whichever is more recent, the git touch or the practice-log entry.

× category balance

Each README section's share of total importance against its share of recent attention. A section getting none of the practice it is owed nearly doubles its problems' scores; one getting twice its share is cut to 0.4.

Picks are then spent round-robin across categories in deficit order, capped at --per-category. The cap and the score floor are a preference for breadth, not a quota: once every eligible category has had its share, the rest of --top is filled by score.

Why the pass count counts as difficulty, not progress.

The AGAIN marker in this repo never graduates — 854 of 1,191 tracked rows carry it, some after 20+ passes. So a row that has already cost twelve passes is read as an unclosed gap and scores higher, not lower. An OK row's passes are real progress and score nothing.

Read this first

The balance table

ratio = share of recent attention ÷ share of importance. 1.00 is a fair share. Below 0.5 the run marks the row UNDER — that is the bias to fix. Above 1.8 it marks it over — that is where the practice has been pooling.

Array 0.19
Linked list 0.00
Design 0.24
Binary Search 0.94
Tree 1.12
Recursion 1.94
Dynamic Programming 2.19

share of importance share of recent attention underlined ratio = UNDER

Array is the row worth staring at. Its last touched column says today, so nothing looks wrong — but it is holding 11% of the importance on 1.9% of the attention. It is being touched, one problem at a time, not worked.

Real runs, real data

What comes back

Three sections, always in this order: what you have been working on lately, the balance table, then the picks. The picks are listed most-neglected category first, not highest score first — that order is the recommendation.

$ python3 script/suggest_review.py --top 12

== Recent focus (last 30 days) ==

  818 touches on 188 problems across 22 categories.
  (69 bulk commits ignored — a commit touching more than 6 solution
   files is an import, not a session.)
    Dynamic Programming        166  ##############
    Recursion                   75  ######........
    Breadth-First Search        69  ######........
    Depth-First Search          58  #####.........

== Suggested review (12 problems, 12 categories) ==

  Ordered by how neglected the category is, not by score — that order
  is the recommendation.

  #   LC    title                          diff    category         last  score  why
  1   48    Rotate Image                   Medium  Array            220d  27.85  MUST · top100liked · blind75 · google · AGAIN x13
  2   5     Longest Palindromic Substring  Medium  String            33d  15.45  MUST · top100liked · blind75 · google · AGAIN x19
  3   24    Swap Nodes in Pairs            Medium  Linked list       47d  20.31  MUST · top100liked · google · AGAIN x16
  4   739   Daily Temperatures             Medium  Stack             47d  16.94  MUST · top100liked · neetcode150 · google · AGAIN x19
  5   146   LRU Cache                      Medium  Design            90d  15.00  top100liked · neetcode150 · google · AGAIN x10
  6   347   Top K Frequent Elements        Medium  Sort              99d  15.16  top100liked · blind75 · google · AGAIN x8
  7   19    Remove Nth Node From End       Medium  Two Pointers      29d  14.28  MUST · top100liked · blind75 · google · AGAIN x10
  8   3     Longest Substring w/o Repeat   Medium  Hash Table        51d  15.61  MUST · top100liked · blind75 · google · AGAIN x16
  9   55    Jump Game                      Medium  Greedy            88d  16.64  MUST · top100liked · blind75 · google · AGAIN x11
  10  1658  Min Ops to Reduce X to Zero    Medium  Slide Window      54d  14.63  MUST · google · AGAIN x8
  11  33    Search in Rotated Sorted Array Medium  Binary Search     28d   9.77  MUST · top100liked · blind75 · google · AGAIN x12
  12  1943  Describe the Painting          Medium  Scan Line         31d  10.32  MUST · google · AGAIN x5

Twelve picks, twelve different categories. Notice the scores do not descend — the order is deficit, not score.

Every flag

Options

--top, --per-category, --window, --half-life and --bulk-limit must be greater than zero, and --min-score-frac must be within 0–1; anything else is a usage error rather than a traceback.

Shaping the list

FlagDefaultWhat it does
--top N15 How many problems to suggest.
--per-category N2 Most picks any one category may take before every other eligible category has had a turn. A preference, not a quota — the remainder of --top is filled by score.
--no-balanceoff Rank by score alone. Useful for seeing what the balancing is actually changing.
--min-score-frac F0.30 Skip a category whose best candidate scores below this fraction of the top pick. Stops a five-row section from taking a slot with a problem nothing recommends.

Narrowing the pool

FlagDefaultWhat it does
--only MARKER One of must, blind75, neetcode150, neetcode250, top100liked, google, again.
--section NAME A README section such as "Binary Search". Repeatable and case-insensitive.
--difficulty D Easy, Medium or Hard. Repeatable.
--all-sectionsoff Put SQL, Shell Script, Concurrency and the unverified kamyu104 drafts back into the pool and the balance maths.

Tuning the measurement

FlagDefaultWhat it does
--window DAYS30 How much history counts as “recent attention”. Shorter reacts faster and is noisier.
--half-life DAYS21 How fast a problem goes stale. Roughly the point where a solved-once problem stops being recallable without re-deriving it.
--bulk-limit N6 A commit touching more solution files than this is an import, not a session, and its file paths are ignored.

Output and plumbing

FlagDefaultWhat it does
--markdown PATH Also write a markdown report, with the balance table and the picks as linked tables.
--json PATH Also write the full result — every category's shares and every pick's factors — as JSON.
--self-test Run the unit suite and exit.
--readme / --progress / --lists / --reporepo root Point the four inputs somewhere else. Mostly for tests.

Two deliberate blind spots

What it refuses to count

Both of these change the answer materially, and both are the difference between measuring practice and measuring commits.

Bulk commits are not study sessions

This repo has commits that add 393 generated Java files at once. Counted naively they made every one of those problems look practised on the same day, and put 563 problems inside a 30-day window that actually saw 188. LC 48 read as “61 days ago” when the real gap was 220.

So a commit touching more than --bulk-limit solution files has its file paths dropped. An LC number written into the subject by hand — update 131 py — still counts, because that is somebody recording work.

Some sections are not DSA practice

SQL, Shell Script, Concurrency and the Newly Added (kamyu104 gap) drafts — which have never been run against LeetCode's judge — are left out of the balance maths so they cannot distort a category's share. --all-sections puts them back.

Why there is a test suite for a helper script

The parsers are the risky part

Three of the four inputs are hand-written files whose shape nobody controls. The failure mode is not a crash — it is a parser that quietly reads fewer rows than there are and hands back a plausible but shrunken plan.

python3 script/test_suggest_review.py           # stdlib unittest, no deps
python3 script/test_suggest_review.py -v
python3 script/test_suggest_review.py ParseProgress
python3 script/suggest_review.py --self-test    # the same suite

Every fixture is a line that is really in those files: a MUST in the status cell versus the word “must” in prose, a duplicate README row for one LC, an annotation containing a comma, a line wrapped mid-annotation, a DP: label, a stray period between entries. A LiveFiles class then holds the real README.md and data/progress.txt, so a format drift fails a test instead of silently shrinking the plan.

One of several

Where this sits

The review plan answers when — a spaced-repetition schedule off the same practice log. The roadmap answers in what order, for a topic you have not learned yet. The random picker answers anything, just start.

This script answers a different question: given everything you have already done, which topic is the practice quietly skipping? Its output pairs naturally with LC Coach — that one grades the attempt, this one chooses it.

Full written reference: doc/utility-scripts.md · source: script/suggest_review.py