-
Notifications
You must be signed in to change notification settings - Fork 0
Roadmap
A phase-by-phase plan for growing the profilers from two standalone CLIs into a complete text-leveling toolkit for the EFL classroom β and, ultimately, the shared analysis engine behind the RubricMaker grading platform.
The through-line is one teacher question: "is this text right for this class, and if not, what has to change?" Each phase moves closer to answering it in one command, and closer to feature parity with what RubricMaker already does inside a full app (see RubricMaker Alignment).
Status legend β β shipped Β· π¨ in progress Β· π planned Β· π idea
This is a direction of travel, not a dated commitment. Phases are ordered by dependency: each builds on the one before.
Everything that exists today.
- β Vocabulary profiler β CEFR (A1βC2), AWL, and NAWL banding, deterministic word-list matching, dependency-free Python 3.
- β Grammar profiler β ~70 constructions across every major family, mapped to CEFR via the CEFR-J Grammar Profile, rule-based over a spaCy parse (no AI).
- β
File formats β
.txt,.md,.docx,.pdf, and stdin. - β
Two output modes β colour
prettyfor the terminal,jsonfor scripts, auto-selected by whether output is a TTY. - β Claude Code plugins & skills β ask for a level in a Cowork session.
- β Rebuilt, provenance-documented word lists and a citable grammar profile.
Outcome: you can profile any single text two ways, by hand or in chat.
Make the two profilers answer as one, and answer against a target.
- β
Unified difficulty report β
text_report.pyruns both profilers and prints one combined summary (vocabulary band + grammatical range + a blendedestimatedLevel), instead of two separate invocations. - β
--target-level B1β pass the class's level and the report flags what exceeds it: words above B1, constructions above B1, and a one-line verdict ("on level" / "reaches B2 β pre-teach 6 words, 2 structures"). - β Coverage figure β "a B1 learner will already know ~92% of the running words," the single number teachers ask for most.
- β Readability line β FleschβKincaid / Flesch Reading Ease, reported alongside β never instead of β the CEFR bands.
- β
Pre-teaching exports β
--export md|csv|flashcardswrite the above-target words and structures as a handout, spreadsheet, or RubricMaker-importable flashcard deck;--clozeturns the examples into fill-the-gap sentences that paste straight into a RubricMaker fill-the-gap question. - β
Free dictionary enrichment β decks ship with plain definitions,
phonetics and part-of-speech from the Free Dictionary API (no key),
backed by a persistent JSON lookup cache;
--pre-enrichprimes the cache for a whole class's word list in one polite, rate-limited pass. Offline or on a miss, the deck falls back to in-context sentences. - β
OLP-EN-CEFRJ wordlist merge β
build_wordlists.pygap-fills the CEFR lists from the open CEFR-J + Octanove profiles and emitslevels.json, so CEFR levels need no dictionary API.
RubricMaker parallel: its document analysis already reports vocabulary and grammar on one uploaded essay β Phase 1 gives the CLI the same combined view, and its deck import now reads the CLI's enriched output directly.
Scale from one text to the folder of candidate readings a teacher actually has.
- β
Batch mode β
--fileaccepts a directory, a glob (**for recursion), or a single file; every supported text (.txt/.md/.docx/.pdf) is profiled in one run, with unreadable files skipped β never fatal. - β CSV / summary report β one row per text (filename, typical & reached vocabulary band, grammar range, % above target, fits verdict), ready for a spreadsheet.
- β
Rank & filter β
--sortranks the set by level (or band / word count / name);--min-level/--max-levelkeep only the texts in the band β so "which of these 20 articles suits B1?" is exactlyclass_profile.py --file essays/ --max-level B1. - β
Several classes at once β
--targets A2,B1,B2shows each text's fits / %-above verdict for every level side by side in one run. - β Aggregate distribution β the pooled CEFR distribution over the whole set (typical band, 90%-coverage band, off-list share) β the dashboard's headline chart, from the command line.
- β
Vocabulary-list export by band β
--export-vocabdumps the distinct words in each CEFR band to CSV (vocab-A1.csvβ¦vocab-C2.csv, plusvocab-off-list.csv), each row a word with its running occurrences and the texts it appears in β ready-made pre-teaching lists and glossaries. - β
Pre-teaching exports for the whole set β
--export md|csv|flashcardswrites one handout / worksheet / RubricMaker deck per text (reusing the Phase 1 writers), plus a combined class-wide deck with a level-keyed markdown index of every above-target word and the texts it came from (per level under--targets), and a set-level summary handout. - β
--pre-enrichβ prime the dictionary cache from the whole folder's distinct vocabulary in one polite, rate-limited pass before exporting decks. - β
Plugin & skill β
class-profileships as a Claude Code plugin and skill like the other three tools, so "which of these suits B1?" works in a session. - β
Sample folder + CI golden checks β the repo bundles
sample-readings/(an A1βC2 gradient) and CI asserts the full CSV / JSON / export / band contracts against it end to end.
Why now: Use Cases 5 and 10 are one-command operations now β what used to be shell loops is first-class.
RubricMaker parallel: directly mirrors RubricMaker's Vocabulary Profile dashboard, which aggregates a class's texts into a CEFR distribution and exports vocabulary lists by CEFR band to CSV β Phase 2 produces the same artifact from the command line, plus ranking/filtering the dashboard doesn't have.
Stop only describing difficulty and start helping change it β the heart of the original VocabKitchen ("adjust a text to a target level").
- β
Above-target highlighting β the report lists every word and structure
above the target level, coloured in pretty mode, so a rewriter sees the
offenders at a glance (Use Case 6).
Already delivered by
text_report.py --target-leveland the band-tinted vocabulary view; the class profile ranks the offenders across a whole set. - β
Simpler-synonym suggestions β
text_report.py --suggestsuggests a lower-band alternative (purchase β buy (A1)) for each above-target word in the bundled curated list (WordLists/synonyms.csv, validated againstlevels.jsonbybuild_wordlists.py --check) β inline in the terminal, in JSON (aboveTarget.words[i].suggestion), and as a Simpler alternative column in the--export mdhandout. - β
Grammar gap report β
text_report.py --gap-reportlists the target-level constructions the text does not use yet β the "introduce these structures" checklist for graded-reader authors who need to add, not just remove β grouped by category in the terminal, in JSON (grammarGap.missing), and as a Constructions to introduce section in the--export mdhandout. Needs the grammar side; the class profile reuses the same per-text handouts. - β
Vocabulary interleaving β
class_profile.py --interleavebuilds a spaced-introduction schedule across a folder of readings: each reading introduces at most--new-words-per-readingnew above-target words (overflow is deferred to the next reading with room), words that recur later are flagged for spaced review, and words absent for two or more readings are marked due β written as a<set>-interleave-<LEVEL>.md|csvplan next to the handouts, so repeated readings introduce new words in a spaced pattern instead of all at once. - β
Watch mode β
text_report.py --watchkeeps re-profiling the--fileinput whenever it changes on disk (polls every second;--watch 0.2for faster) until Ctrl-C β the tight edit β re-check loop: save the graded reader and the report updates on the spot.
Why now: Phase 1 knows the target and Phase 2 handles many texts, so adaptation aids are the natural next step and the highest-value one for materials writers β all five Phase 3 items are now shipped.
RubricMaker parallel: RubricMaker's grammar linker checks target grammar per-criterion; the CLI's gap report is the authoring-side twin β what's missing versus what's present.
Speak the frameworks teachers are held accountable to, not just raw CEFR.
- β
Curriculum checklist β profile a text (or unit) against a supplied
checklist file (sections
[vocabulary]and[grammar]) and get a pass/fail coverage report: each required word present or missing (with its CEFR band when recognised), each required construction used or not,curriculum.passin JSON, a terminal checklist, and a Curriculum checklist section in the--export mdhandout β threaded throughclass_profile.py --export mdso a folder run shows which texts cover the unit's requirements, and through--export csvas a folder-level coverage grid (one row per text, one column per required item) β the same grid is mirrored as a Curriculum coverage section in the set-level summary handout so--export mdshows the grid too, and rides in the JSON report ascurriculumCoverage(items Γ rows Γ cells) so scripts can consume the pass/fail matrix without CSV parsing. The checklist file itself is schema-validated before any profiling: unknown section headers (e.g.[grammer]) fail fast with a did you mean hint, malformed headers error, and empty required sections β plus grammar items that don't resolve against the construction list (typos likesecond conditinalget a did you mean hint) β warn before any profiling (Use Case 7). - β
Cambridge English exam mapping β
text_report.py --cambridgereports the matching Cambridge Qualification (A2 Key, B1 Preliminary, B2 First, C1 Advanced, C2 Proficiency) next to each band the report actually shows β vocabulary typical/reaches, grammar typical/reaches, estimated level β as an opt-in, in JSON (cambridge), the terminal, and the--export mdhandout. - β
CEFR Can-Do framing β
text_report.py --candoexpresses a text's demands as Can-Do descriptors, the language rubrics and self-assessment already use: one descriptor per band the report actually shows (reading/listening, interaction, spoken/written production) β opt-in, in JSON (cando), the terminal, and the--export mdhandout. With--target-level, each dimension also reportsaboveTarget: the descriptors the text demands beyond what the class is expected to do yet (every level strictly above the target up to the text's own band), shown as an Above the {target} target block β the same diff threads throughclass_profile.py --export mdso a folder run shows every text's Can-Do demands (and their gap against the class level) per handout, with--export flashcardswriting the demands as a combined Can-Do reference deck in the RubricMaker import shape β under--targetseach class level gets its own deck, demands measured against that level β and--cando-diffaggregating the set's shared above-target demands into the summary handout β which descriptors the texts demand together, most-common first (or, with--cando-diff-sort band, ordered by the CEFR ladder ascending to see which demand levels to tackle in order). - β
Pluggable vocabulary profiles β first-class support for swapping in Oxford
3000/5000 or the Octanove C1/C2 profile via the existing one-word-per-line
format, for teams standardised on a licensed list. Shipped:
--profileon all three CLIs swaps the bundled CEFR lists for a directory ofA1.txt..C2.txt(file = level, replaces the lists) or a single recognition-list file (levels from the bundledlevels.jsonindex,--profile-levelfallback, words outside it off-list); the active profile rides in the report payload asvocabulary.profile(contract 1.4). - π Profile semantics hardening β a profile run changes what "level" means,
and the payload doesn't say so: with a single-file recognition profile, every
word the bundled
levels.jsondoesn't know lands in one fallback band (default B1) and words outside the team list are all off-list, socoverage,typical, and the blendedestimatedLevel/verdict mean "relative to the team's list", not CEFR β a teacher could misread a profile run's "suits B1?" verdict as a standard CEFR verdict. Separately, a--profiledirectory with noA1.txt..C2.txtfiles is silently accepted (all-empty bands β 100% off-list, exit 0). Fix (small, medium value): reject a profile directory with no level files; when a profile is active, tag the payload (aprofilenote in the verdict or avocabulary.profileContextflag) so consumers know the semantics changed.
RubricMaker parallel: RubricMaker offers the exact same Cambridge English exam mapping and links criteria to standards (CCSS/NGSS, Dutch kerndoelen, CEFR Can-Do). Phase 4 makes the CLI produce output in those same frameworks, so a text profiled on the command line and a criterion graded in the app describe proficiency identically.
Close the loop: make these profilers the authoritative text-analysis backend RubricMaker calls, so both tools share one CEFR taxonomy and one implementation.
- β
Stable library / JSON contract β a documented, versioned output schema and
an importable entry point (not just a CLI), so RubricMaker's document analysis
(vocabulary & grammar checking on uploaded essays) can call the profilers
directly instead of maintaining a parallel implementation. Shipped: the
shared analysis engine (
analysis.py) β word lists + grammar engine loaded once, one payload builder β is imported by both CLIs, so a single text and a whole folder now produce byte-identical report shapes (a folder handout is the same artifact as the single-text report), andpython3 -c "import analysis"is the entry point the app can build on. The engine is fully self-contained (readability, target flagging, Cambridge/Can-Do mapping and the curriculum checklist live in it;text_report.pyre-exports them), and the payload is a versioned contract: every report carriesschemaVersion(1.3), the JSON Schema is checked in atanalysis.schema.json(byte-equal toanalysis.payload_schema(), asserted in tests + CI, and printable via--schemaon either CLI). - β
Per-criterion grammar pass/fail β emit results in the shape RubricMaker's
grammar linker consumes for its per-criterion, apply-as-comment breakdown.
Shipped: every grammar-enabled payload carries
grammarCriteriaβ one entry per registered construction (id/name/category/level), aused/not usedstatus with pass/fail, andcount+ up to twoexampleswhen used, ladder-ordered β so a comment can be attached per criterion without re-deriving anything. The apply-as-comment reference implementation is shipped too:analysis.grammar_commentsturns the criteria into one rubric comment per construction (Uses the β¦ β E.g. "β¦"with the detected span as evidence /Doesn't use the β¦ yet), carried in the payload asgrammarCommentsunder--commentsand rendered as a Rubric comments section in the--export mdhandouts β per text in a folder run too. The vocabulary half of the pass is shipped too:analysis.vocabulary_commentsturns the above-target words into one comment each (Above B1: "anticipate" (B2) β used 1Γ. E.g. "β¦", with the simpler synonym appended under--suggest), carried asvocabCommentsand rendered as a Vocabulary comments section alongside the grammar half β so--commentscovers the whole report. Under atargetLevelthe grammar half is filtered to the class level: at/below constructions keep their pass/fail comments (kind"rubric"), used above-target constructions become"pre-teach"notes (Uses the β¦ β above the B1 target: pre-teach or rewrite.), and unused above-target ones drop β a B1 class isn't expected to produce C2 structures. Every pre-teach note carries a rewrite suggestion from the curatedWordLists/structure-rewrites.csv(validated bybuild_wordlists.py --checkagainst the construction registry):Rewrite: swap for a past simple or a present modal ("would have passed" β "passed"). The folder surface completes the picture:--export mdadds a Demand scan table to the set summary (per-text above-target word + pre-teach structure counts, the whole-class demand at a glance), and--export flashcards --commentswrites a combined rubric-comment deck β one per--targetslevel, so the same construction is arubriccard at its own level and apre-teachcard above it. - β
Feed the Vocabulary Profile dashboard β batch-profile a class's essays and
hand back the per-student CEFR vocabulary distribution the dashboard renders.
Shipped: every
class_profileJSON row now carries the per-student CEFR distribution (per-level counts + percentages, total, typical, coverage), so a class's essays become per-student band spreads without re-deriving from the pooled aggregate. - β
Single source of truth for grammar β the CEFR-J Grammar Profile bundled
here becomes the shared taxonomy both tools cite, so a construction is levelled
the same way whether it's detected in a reading (CLI) or an essay (app).
Shipped:
grammar_profile --taxonomyexports the full construction registry (id/name/category/level/cefrjCode, levelled exactly as detection resolves them);GrammarProfile/taxonomy.jsonis the checked-in document both tools cite, guarded byte-equal by tests and CI. - β
Free lookup swap in RubricMaker β replace
cambridgeApi.lookupWord(the paid Cambridge Dictionary API) with the free stack the CLI now uses: CEFR levels from the bundled CEFR-J /levels.json, definitions + phonetics from the Free Dictionary API, sharing the same cache format so teacher lookups and deck exports use one warmed cache. Shipped:dictionary.pyis the shared layered lookup β thecambridgeApi.lookupWordreplacement (dictionary.lookup_word), levels always fromlevels.json(never the API), definitions/phonetics/POS from the Free Dictionary API with the bundled WordNet as the offline fallback, all through one JSON cache with results tagged by source. - β
Offline dictionary fallback β bundle Open English WordNet (CC BY 4.0) so
definitions ship even with no network, writing results into the same lookup
cache β the final layer that makes enrichment fully offline-capable.
Shipped:
WordLists/dictionary/wordnet.json(built bybuild_dictionary.py, CC BY 4.0) answers when the Free Dictionary API is unreachable or misses, so definitions ship with no network; deck exports fall back to WordNet glosses before the in-text sentence, on both CLIs. - β
Import RubricMaker vocabulary lists into
--pre-enrichβ read a class's vocabulary-list export (CSV/JSON) directly, so the cache is primed from the app's own data without reformatting. Shipped:--pre-enrichreads a class's RubricMaker CSV/JSON vocabulary-list export directly (word column / words array), on both CLIs. - π Harden the shared dictionary cache β transient offline failures must
never be persisted as definitive misses. The Phase 5 rewrite routes every
lookup through one write, so when the API is unreachable and WordNet
doesn't know the word, the entry lands in the cache as
Noneβ byte-identical to a definitive 404 miss.pre_enrich_wordsflips to offline mode after the first network error, so one unreachable APINone-caches every remaining word in a class list, and later online runs answer "cache-miss" without ever touching the API. The "network errors are never cached" docstring is now false;dictionary.lookup_wordhas the milder variant (offline misses persisted assource: "offline"dicts, never retried). Impact: silent, permanent degradation of the one shared cache the whole lookup stack is built on β a single offline pre-enrich poisons teacher lookups and deck exports alike. Fix (tiny): never persistsource == "offline"entries; prefer the API over cached WordNet/offline entries when back online (source-aware reads) so WordNet answers upgrade to real definitions + phonetics; add the regression test β offline pre-enrich, then an online export, must still hit the API. - π Invert the dictionary dependency β
dictionary.py(the shared stack) must not import a CLI: today it importstext_reportat module level while both CLIs importdictionaryonly inside functions (four+ sites) precisely to dodge a cycle β the module graph isdictionary β text_report β analysis β {vocab_profile, grammar_profile}. Impact: the function-levelimport dictionarysprinkles are a smell, any future top-level import from a CLI creates a cycle, and the "single importable entry point for RubricMaker" story is muddier than the docs claim. Fix (medium, low risk): move the fetcher + cache primitives (lookup_dictionary,parse_dictionary_entry,_DictNetworkError, cache load/save,default_dictionary_cache_path) out oftext_report.pyinto the shared layer so it's dependency-free and the fallback chain, cache, andread_word_listlive in one place β the mock-HTTP test harness intest_text_report.pyalready covers this surface. - π Rate-limit resilience for
--pre-enrichβlookup_dictionaryfolds 429 and 5xx into_DictNetworkError, which pre-enrich treats as "offline" β so one rate-limit response mid-way through a 200-word class list stops the entire pass. Fix (smallβmedium): retry with backoff, or soft-fail only after N consecutive errors, so a single 429 doesn't abort the marquee Phase 5 flow against a hobby-host API.
Why last: it depends on the combined report (Phase 1), the framework alignment (Phase 4), and a proven detection surface β but it's the destination that makes the whole roadmap coherent: one leveling engine, two front ends (a CLI for materials prep, a web app for grading).
The engine installable anywhere: package the shared analysis core so any front end β RubricMaker, a future standalone GUI, a notebook β imports one contract, with the performance and data hygiene that implies.
- π Package the suite as a
vocabkitchenpackage β stop being five sibling scripts and become a package. Every module imports its siblings by bare name (all must sit onsys.pathtogether), the plugins are symlink farms CI has to dereference at install time, and each plugin bundle carries a full copy ofWordListsβ now including the 10 MBwordnet.json, three times over. A real package (pyproject.toml, console entry points) behind thin one-line script shims (from vocabkitchen.cli import main; main()) would: kill the symlink/dereference fragility and the deferred-import hackery, give RubricMaker one real contract (pip install vocabkitchen), let the payload schema ride as package metadata (the byte-equality guard stays β it's great), and make the plugins thin wrappers around installed commands. Tradeoff: the README's beloved "no install, justpython3 X.py" story β survivable via the shims. Effort: large β the Phase 6 change that compounds every other one here. - π Performance & data hygiene β memoize
load_level_index:vocab_profile.load_level_indexre-parses the 3.5 MBlevels.jsonon every call (deck export, pre-enrich, lookup paths β several times per run), and the codebase already has the_wordnet_cachepattern and tracks perf baselines (perf-*.jsonin CI), so this is a 5-line win in the project's own value system. And watch the 10 MBwordnet.json: it's parsed eagerly into a big Python dict on the first fallback miss and copied into every plugin install β a gzip + lazy per-initial-letter load (or JSONL) would cut memory and repo weight, but it's a real format change, worth doing only as part of the packaging work. - π Audio-backed flashcards β a new direction nobody's considered:
parse_dictionary_entryalready receivesphonetics[].audioURLs from the Free Dictionary API and drops them. For a flashcard tool aimed at learners, audio on the card back is the kind of feature that changes perceived quality. It needs a RubricMaker importer column (or an audio-manifest sidecar export) and has no offline path β so it's a "when the app catches up" idea, but the freshest branch this diff opens.
Why now: Phase 5 made the engine one importable thing; Phase 6 makes it one installable thing β the enabler for the roadmap's "one leveling engine, two front ends" and the basis for the standalone GUI Frontend research that sits after the phases.
Phase 0 two profilers, one text β
shipped
β
Phase 1 one combined verdict + target β
shipped
β
Phase 2 many texts, CSV, by-band export β
shipped β answers "which of these?"
β
Phase 3 highlight + suggest + gaps β
π¨ β answers "what do I change?"
β
Phase 4 Cambridge / curriculum / Can-Do β answers "in my framework"
β
Phase 5 shared engine for RubricMaker β one taxonomy, two front ends
β
Phase 6 installable engine, beyond the CLI π β answers "pip install it"
Already-shipped groundwork that makes this realistic: a data-driven design (word lists and the grammar profile are swappable files, not hard-coded), a stable JSON output the tests freeze, and a no-AI, rule-based core that is deterministic and auditable β the same principles RubricMaker's own analysis is built on.
Everything above stops at one importable, installable engine. Sitting after the phases β not as one of them β is the standalone GUI: a separate front end over the same engine for teachers who don't want a terminal at all. The viability research (engine-fit evidence, toolkit options, packaging reality, effort estimates, recommended sequencing) lives on GUI Frontend, deliberately placed at the very tail end of this plan: it builds on Phase 5's importable engine and Phase 6's packaging, and it changes none of what the phases deliver.
- Use Cases β the scenarios each phase improves.
- RubricMaker Alignment β the feature-by-feature map these phases converge on.
Vocabkitchen CLI β vocabulary & grammar profilers for the EFL classroom. CEFR-anchored, rule-based, no AI. Β· Companion to RubricMaker.