Skip to content

Roadmap

NesiciCoding edited this page Aug 12, 2026 · 24 revisions

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.


Phase 0 β€” Foundations βœ… (shipped)

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 pretty for the terminal, json for 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.


Phase 1 β€” One text, one verdict βœ… (shipped)

Make the two profilers answer as one, and answer against a target.

  • βœ… Unified difficulty report β€” text_report.py runs both profilers and prints one combined summary (vocabulary band + grammatical range + a blended estimatedLevel), 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|flashcards write the above-target words and structures as a handout, spreadsheet, or RubricMaker-importable flashcard deck; --cloze turns 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-enrich primes 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.py gap-fills the CEFR lists from the open CEFR-J + Octanove profiles and emits levels.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.


Phase 2 β€” A class set, not one text βœ… (shipped)

Scale from one text to the folder of candidate readings a teacher actually has.

  • βœ… Batch mode β€” --file accepts 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 β€” --sort ranks the set by level (or band / word count / name); --min-level/--max-level keep only the texts in the band β€” so "which of these 20 articles suits B1?" is exactly class_profile.py --file essays/ --max-level B1.
  • βœ… Several classes at once β€” --targets A2,B1,B2 shows 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-vocab dumps the distinct words in each CEFR band to CSV (vocab-A1.csv … vocab-C2.csv, plus vocab-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|flashcards writes 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-profile ships 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.


Phase 3 β€” From analysis to adaptation πŸ”¨ (in progress)

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-level and the band-tinted vocabulary view; the class profile ranks the offenders across a whole set.
  • βœ… Simpler-synonym suggestions β€” text_report.py --suggest suggests a lower-band alternative (purchase β†’ buy (A1)) for each above-target word in the bundled curated list (WordLists/synonyms.csv, validated against levels.json by build_wordlists.py --check) β€” inline in the terminal, in JSON (aboveTarget.words[i].suggestion), and as a Simpler alternative column in the --export md handout.
  • βœ… Grammar gap report β€” text_report.py --gap-report lists 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 md handout. Needs the grammar side; the class profile reuses the same per-text handouts.
  • βœ… Vocabulary interleaving β€” class_profile.py --interleave builds a spaced-introduction schedule across a folder of readings: each reading introduces at most --new-words-per-reading new 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|csv plan next to the handouts, so repeated readings introduce new words in a spaced pattern instead of all at once.
  • βœ… Watch mode β€” text_report.py --watch keeps re-profiling the --file input whenever it changes on disk (polls every second; --watch 0.2 for 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.


Phase 4 β€” Standards & framework alignment πŸ“‹

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.pass in JSON, a terminal checklist, and a Curriculum checklist section in the --export md handout β€” threaded through class_profile.py --export md so a folder run shows which texts cover the unit's requirements, and through --export csv as 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 md shows the grid too, and rides in the JSON report as curriculumCoverage (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 like second conditinal get a did you mean hint) β€” warn before any profiling (Use Case 7).
  • βœ… Cambridge English exam mapping β€” text_report.py --cambridge reports 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 md handout.
  • βœ… CEFR Can-Do framing β€” text_report.py --cando expresses 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 md handout. With --target-level, each dimension also reports aboveTarget: 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 through class_profile.py --export md so a folder run shows every text's Can-Do demands (and their gap against the class level) per handout, with --export flashcards writing the demands as a combined Can-Do reference deck in the RubricMaker import shape β€” under --targets each class level gets its own deck, demands measured against that level β€” and --cando-diff aggregating 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: --profile on all three CLIs swaps the bundled CEFR lists for a directory of A1.txt..C2.txt (file = level, replaces the lists) or a single recognition-list file (levels from the bundled levels.json index, --profile-level fallback, words outside it off-list); the active profile rides in the report payload as vocabulary.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.json doesn't know lands in one fallback band (default B1) and words outside the team list are all off-list, so coverage, typical, and the blended estimatedLevel/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 --profile directory with no A1.txt..C2.txt files 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 (a profile note in the verdict or a vocabulary.profileContext flag) 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.


Phase 5 β€” The analysis engine behind RubricMaker πŸ”¨

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), and python3 -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.py re-exports them), and the payload is a versioned contract: every report carries schemaVersion (1.3), the JSON Schema is checked in at analysis.schema.json (byte-equal to analysis.payload_schema(), asserted in tests + CI, and printable via --schema on 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), a used/not used status with pass/fail, and count + up to two examples when 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_comments turns 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 as grammarComments under --comments and rendered as a Rubric comments section in the --export md handouts β€” per text in a folder run too. The vocabulary half of the pass is shipped too: analysis.vocabulary_comments turns the above-target words into one comment each (Above B1: "anticipate" (B2) β€” used 1Γ—. E.g. "…", with the simpler synonym appended under --suggest), carried as vocabComments and rendered as a Vocabulary comments section alongside the grammar half β€” so --comments covers the whole report. Under a targetLevel the 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 curated WordLists/structure-rewrites.csv (validated by build_wordlists.py --check against the construction registry): Rewrite: swap for a past simple or a present modal ("would have passed" β†’ "passed"). The folder surface completes the picture: --export md adds 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 --comments writes a combined rubric-comment deck β€” one per --targets level, so the same construction is a rubric card at its own level and a pre-teach card 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_profile JSON 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 --taxonomy exports the full construction registry (id/name/category/level/cefrjCode, levelled exactly as detection resolves them); GrammarProfile/taxonomy.json is 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.py is the shared layered lookup β€” the cambridgeApi.lookupWord replacement (dictionary.lookup_word), levels always from levels.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 by build_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-enrich reads 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_words flips to offline mode after the first network error, so one unreachable API None-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_word has the milder variant (offline misses persisted as source: "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 persist source == "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 imports text_report at module level while both CLIs import dictionary only inside functions (four+ sites) precisely to dodge a cycle β€” the module graph is dictionary β†’ text_report β†’ analysis β†’ {vocab_profile, grammar_profile}. Impact: the function-level import dictionary sprinkles 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 of text_report.py into the shared layer so it's dependency-free and the fallback chain, cache, and read_word_list live in one place β€” the mock-HTTP test harness in test_text_report.py already covers this surface.
  • πŸ’­ Rate-limit resilience for --pre-enrich β€” lookup_dictionary folds 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).


Phase 6 β€” Beyond the command line πŸ“‹

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 vocabkitchen package β€” stop being five sibling scripts and become a package. Every module imports its siblings by bare name (all must sit on sys.path together), the plugins are symlink farms CI has to dereference at install time, and each plugin bundle carries a full copy of WordLists β€” now including the 10 MB wordnet.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, just python3 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_index re-parses the 3.5 MB levels.json on every call (deck export, pre-enrich, lookup paths β€” several times per run), and the codebase already has the _wordnet_cache pattern and tracks perf baselines (perf-*.json in CI), so this is a 5-line win in the project's own value system. And watch the 10 MB wordnet.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_entry already receives phonetics[].audio URLs 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.


How the phases build on each other

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.


After the phases β€” a GUI front end πŸ’­

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.


See also