Checkpoint · v4 refactor — form & family storage

A word's family — how do we store it?

A word like makan (eat) has a "family": forms it turns into (makanan = food), compound words built from it (rumah makan = restaurant), and words that mean something similar or opposite. Right now that family gets stored two or three different, inconsistent ways — and none of them can yet hold a French translation next to the Indonesian. This decides the one way going forward, and it's the piece the cluster-card screens you already designed are waiting on.

Follow-up to Decision 4, v4-refactor-plan Audit finding D16 123 duplicate words found 121 rows stuck mid-move Feeds your ratified cluster cards
Checkpoints Latest →
The problem, in plain terms

A word's family is stored two different ways at once

Audit finding D16, plus the related fields that share the same gap

Way #1 — its own entry. A form of a word (like makanmakanan) gets its own full database row, with its own meaning, pronunciation, and example. There are 1,221 of these today.

Way #2 — a copied list on the parent. A "related compound" (a different word built from this one, like rumah makan) instead gets stuffed into a list of text sitting inside the parent word's own entry, duplicating some of the same information. There are 368 words carrying one of these lists, holding 2,159 entries in total.

The trouble: these two ways overlap, and the overlap is a mess. 234 words have both kinds at once. In 123 of those cases, the exact same word shows up twice — once as its own entry, once again copied into the list. And 121 of the "own entry" rows are marked hidden, with a note literally saying they were being moved into the list-on-the-parent approach and the move was never finished. That's a live, half-done migration sitting in the data today.

FieldWhat it holdsSame gap?
Word forms (own entry)1,221 rows — full entry per formDuplicates 123 of the list-on-parent entries below
Related compounds (list on parent)368 words, 2,159 entries — text + meaning + pronunciation copied inlineAlso duplicates the audio file for 211 of those entries
Similar words / opposite words560 / 299 words — a list of {word, meaning} pairsSame shape problem: no agreed structure, English-only meaning
Informal variants403 words — casual ways to say the same thingSame shape problem, plus it's designed to duplicate the others on purpose (see note below)

One thing that's not broken: the "informal variants" sparsity looked like a bug at first, but it isn't — 226 of the 227 "missing" ones are recorded on purpose as "this word has no casual variant," and the design intentionally repeats some of what's in the similar-words/forms lists for a simpler on-screen view. That part is fine as-is; it's flagged here only because it's another user of whatever shared structure gets picked below.

Why this blocks French (or any new language): every one of these fields hard-writes the translated meaning into a single "eng" slot. There's no second slot for French, and no code that knows how to fill one. Fixing the storage shape and fixing the language problem are the same piece of work.

Your notes so far

What you told us on the last round

Quoted from your response to Decision 4 — lightly cleaned up from voice dictation, your words

Your note, v4-refactor-plan · Decision 4

"Perhaps we need to have a heart-to-heart talk about related compounds and the other fields that are similar — similar words, opposites, and things like that. If I want to support them in multiple languages, they need to have a structure when they're being stored. Right now... I don't know if it would be better to have a standard JSON, or another table in the database that relates to those, with a key on these related terms so we connect into it.

Maybe we should have every word flat underneath CT2 as its own entity, and attach a flag to it that had all the different information — reversing the informational structure, using tags rather than database structures. It seems like that's the more modern WordWeb kind of approach. But it would have to be a really strong argument to get me to do that, because it would basically be a refactor of everything.

The enrichment needs to be so extensive for the family forms — that's why we created the CT3 level. But for related compounds, the only place I'm really going to use it is the CT3 level, or the data would just be very simple — just a gloss, or maybe a one-word sentence with it. Maybe there'll be three elements rather than 30, like etymology or why-learn-this. So I don't think it needs its own separate entry at CT3, but I do think it needs some kind of standardized structure and the ability to have translations associated with it.

We have the direction from a user-experience point of view — the cluster cards. However, the data doesn't support it at this point, and it needs to be included in the v4 enrichment pipeline."

What this must actually feed

The cluster cards you already designed and reviewed

CLUSTER-CARD-PLAN.md, ratified 2026-07-14 — plus the Bodhisattva Vows mockup you already clicked through

You ratified the cluster-card interaction back on 2026-07-14: a word's family members become a flip-through drill on one card, instead of one flashcard per family member. There are two kinds already scoped — a related-cluster (the related-compounds list) and a form-cluster (the CT3 word-form rows) — and the plan says outright that both pull their rows directly from the two storage shapes this checkpoint is about.

You've since seen this pattern working, live and clickable, on a real deck — the Bodhisattva Vows cluster mockup. It uses a 4-face flip: an identifier, a short gloss, the fuller text, then loops back. Here's what one row's cycle looks like:

📿 Root Vow 8
Face 1 · identifier → Face 2 · short gloss → Face 3 · full text → loop

That's the concrete shape the storage decision below has to support, per family member: an identifier (the word/compound text), a short gloss (what shows on the quick flip), and a fuller text (the full definition or sentence) — each one needing an Indonesian version and, per the language-swap goal, a version in whatever language the learner reads in. The plan's form-cluster variant adds one more flip for the grammar rule that applies to that form, pulled from form_rule_keys. This is exactly the "some kind of standardized structure, with the ability to have translations" your note above is asking for — it's not a hypothetical, it's what's already sitting live on wordsense-reviews.pages.dev.

Decision 1 of 3

Where does a word's family live?

The core storage call — audit D16
🌱
What this is: Pick one home for every family member — same-word forms (tenses, plurals) and distinct compound words alike — instead of the two overlapping ways in use today.
A — Always its own card. Every family member — form or compound — gets its own full entry. Nothing ever gets copied onto the parent.
B — Always a list on the parent. Drop the separate entries entirely; every family member lives as a list attached to the main word.
C — Split by rule (closest to what's happening today, made consistent). Forms of the exact same word (makan / makanan / dimakan) stay their own entries at CT3, since — as your note says — they need the deep enrichment. Distinct compound words (rumah makan) live as a small list on the parent, since you only need a gloss for those.
D — The parent's list becomes a pointer, not a copy. Instead of copying a family member's text and gloss onto the parent, the parent just links to the real entry by id. This is the option the audit itself calls cleanest — it also fixes the duplicated audio-file problem (D10) for free, since the audio lives in one place. Important nuance: "its own entry" here doesn't have to mean the full 30-field treatment your note is worried about — the pointed-to entry only needs the fields Decision 2 below settles on (a gloss, maybe one line of text), not a full CT1/CT2/CT3 build-out.
Recommended: D, with the nuance above — it's the audit's own pick for "cleanest," and it can stay as lightweight as your note wants once Decision 2 settles the field list. But this is explicitly a "let's talk about it" item — C is the closer match to what's live today and a fair fallback if D's migration cost feels too big right now.
Decision 2 of 3

One standard shape for related-compounds, similar words, and opposites

Needs to be small, and needs to hold a translation
🧩
What this is: Related compounds, similar words, opposite words, and informal variants each hold roughly the same kind of information today — a word plus a meaning — but none of them agree on a shape, and all of them hard-write the meaning in English only.
A — A standard shape per term: a small object with the word text, a short gloss, and a couple of optional fields (maybe a fuller line of text), with every text field keyed by language (ind, eng, and later fra) instead of one hard-wired English slot. This is the "three elements, not thirty" shape your note describes.
B — A separate related-terms table, keyed to the word it belongs to, instead of a JSON list sitting inside the word's own row. Same idea as A, but queryable as its own table rather than JSON — closer to what you floated as "another table in the database that relates to those."
C — The flat tagged-entity ("WordWeb") approach: every word becomes its own entity, and relationships (similar-to, opposite-of, compound-of) become tags rather than structured fields.
Recommended: A — smallest change, matches the "three elements, translatable" size your note already lands on, and it's what the cluster-card rows in Decision 3 actually need.
Your own caveat on C, carried forward verbatim: "it would have to be a really strong argument to get me to do that, because it would basically be a refactor of everything." Flagging it here so it stays visible — this isn't off the table, but it needs that strong argument made explicitly before it's chosen.
Decision 3 of 3

Confirm: this is building toward the cluster cards

Mostly a confirm-and-note, not a multiple choice
What this is: Confirming that the family/related-terms storage in Decisions 1 & 2 is being built specifically to feed the ratified cluster-card experience (identifier → short gloss → full text, plus a rule-flip for forms) — not some other future use. If there's anything specific those cards need that isn't in the shape above, note it here.
What the cluster cards need, derived from the mockup:
Per row: an identifier (the word/compound text), a short gloss (the quick-flip face), and a fuller text (the deep-flip face) — each translatable.
For form-clusters specifically: a link to the grammar rule that applies (already exists as form_rule_keys) for the 5th flip face.
Audio, where it exists — the mockup's L2 faces are audio-enabled; today that audio is duplicated three places (D10), which Decision 1's pointer option would also clean up.
Worth your eye

Where to spend your attention

🔀
Whatever Decision 1 lands on, the 123 exact-duplicate words and the 121 stuck-mid-migration rows need a real cleanup pass, not just a rule for new data going forward.
🌐
This checkpoint and the language-dial decision you already answered (Decision 2, v4-refactor-plan) are the same underlying fix from two angles — a language-keyed shape here is what makes the family fields swappable there.
🧵
The informal-variants duplication-by-design (the note above) doesn't need a decision here — it's intentional and already documented — but it will read from whatever shape Decision 2 picks, so it's not entirely separate either.