gen¶
Generates chords and alts for chords.csv in-place.
chordgen gen --dry-run # preview without writing CSV, config, or progress
chordgen gen --preserve-learned # keep learned mappings and their family slots
chordgen gen --yes # explicitly accept changes to learned mappings
Generation lists changed chord/alt cells before writing. If a currently
learned mapping changes or disappears, it asks for confirmation; an accepted
change retires only the affected learning card. --preserve-learned reserves
learned mappings for this run without clearing their source frequency or
changing the manual-pinning convention. It also preserves their existing alt
slots even when alt overwrite is enabled. Ignoring a protected learned word
is a conflict and stops generation before any writes.
Progress now identifies the physical chord, actual keyboard layout, and alt slot. Legacy cards have no original mapping identity: they are bound once to the current dictionary with a warning, retaining their existing FSRS state. A dry run performs that binding only in memory. Reordering the letters of a chord or editing unrelated dictionary rows does not invalidate mastery.
The pipeline runs in three phases:
- Score — for each word, enumerate every chord that keeps the first letter and preserves left-to-right order, then score each candidate using the configured keyboard layout (effort per key, same-row / same-column / scissor / directional penalties).
- Generate alts — based on the word's
category(verb, noun, adjective, adverb, pronoun), fillalt1–alt3with inflected forms (e.g.look → looks, looked, looking;I → me, my, myself). Alt slots already filled by hand are kept by default. - Assign — solve a sparse minimum-cost bipartite matching so
each word gets a unique chord and the total
score × family_weightis globally minimised. Frequent words attract short / low-effort chords. Words for which every viable chord is already cheaper for another word are reported in the diagnostics.
Words shorter than gen.min_word_length are skipped, except rows
with category: contraction ('s, 'm, …) which are always
eligible — they're short by construction.
By default the matcher runs a single global pass that minimises total
score × family_weight. If you find rare words bumping common ones onto
longer chords, set gen.assignment.priority_tiers in config.yaml
(e.g. [500, 1000]) to solve in tiers — top 500 most-frequent words
first, then the next 500, then the rest, with each tier's chords
reserved out of the next. This protects common words like the /
and / have from being out-bid by rare words competing for the
same key.
Tradeoffs of tiered assignment¶
Priority tiers give high-frequency words first pick, but they carry observable costs:
-
Words below the tier cut can become permanently untypeable. Once a key is taken by an earlier tier it is reserved forever — later tiers can only shift chords among themselves. On a real 2,000-word chords.csv with
[600, 1000]andkey_replacementset, a global solve assigned chords to 94 more words (recoveringdie,fall,learn,add,star,tree,ice,hurt,dream,wear, and others) at ~7% higher weighted cost — the cost being a slightly longer chord for ~190 top-600 words. Only 7 genuinely blocked words remained. -
Orphaned alts need remaining free keys. A form counts as covered only if its base actually receives a chord. If that base is blocked, a recovery pass attempts to assign the form its own primary chord using remaining keys. Recovery handles coverage chains and cycles, but does not displace successful earlier assignments or guarantee a globally optimal family allocation. Unassigned rows retain their alt definitions so rerunning generation cannot lose those relationships.
-
Family value is potential coverage, not guaranteed coverage. Base weights include their unique represented forms. A form shared by multiple bases contributes to the first viable owner in CSV order, and an already assigned primary/family takes precedence. If a prospective owner loses its chord, recovery can still assign the form, but earlier assignments are not re-optimized. This remains a heuristic around the exact matcher.
If you use tiers, watch the unmatched list in gen diagnostics for
moderately common words that vanished. Removing tiers (setting
priority_tiers: []) and running chordgen gen will show how many
you're really losing.
frequency_exponent¶
Controls how sharply frequency amplifies cost differences between competing words. The cost formula is:
form_weight = max(frequency, min_frequency_weight)^frequency_exponent
family_weight = base_weight + sum(unique covered forms' weights)
cost = chord_score × family_weight
Each covered form contributes at most once. Repeated slots, competing owners, and independently assigned primaries do not double-count it. Forms absent from the CSV contribute no invented frequency. The exponent is applied before adding weights; the source-defined CSV frequency is never rewritten or assumed to be Zipf. These are utility weights, not probabilities or measured time savings. The exponent must be greater than zero.
- 1.0 — linear weighting. A word with frequency 6.0 is weighted 6× as much as a word with frequency 1.0.
- 3.0 (default) — cubic weighting. A word with frequency 6.0 is weighted 216× as much as a word with frequency 1.0. This strongly favours short chords going to high-frequency words.
Raise this if you still see rare words getting short chords at the expense of common ones. Lower positive exponents soften that preference.
key_replacement¶
If your keyboard is missing certain keys (e.g. no q or z), you can
map them to alternate letters when generating chord candidates. The
replacement affects only the chord string — the typed word stays the
same.
gen:
key_replacement:
q: k # "quick" → chord uses "k" instead of "q"
z: s # "zebra" → chord uses "s" instead of "z"
"'": x # "o'clock" → chord can use "x" wherever the apostrophe sits
Each key must be a single lowercase character — letters or punctuation
(useful for remapping ' since few layouts put it on a comfortable
chord position). Each value must be a single lowercase letter that
is present on your keyboard. Chords containing a replaced character
will be scored against the replacement letter's key position and
effort on your keyboard.