Checkpoint 1 · Related-words data shape, v3

Confirm the data shape v3 — related words

A third pass at the same design, after your own pushback — confirm before any build runs.

supersedes v2 4 decisions 1 comment
Checkpoints Latest →
The context

Why v3

Your own words, then what they mean for the design below.
Quoted — your answer to UN, 2026-09-18

Frank's answer to UN (2026-09-18, UN-e) named seven concerns with the v2 design: it breaks the enrichment process; lite enrichment is an expensive low-value step; other languages cannot reuse the cluster learning structure; learners will want lite converted to full, so the work is done twice; the database is so complicated that changes become hard; lite content is hard to translate to other L1 languages; data redundancy. Read together they reject two parts of v2: the lite card (with its enrichment_level state, its umbrella container and its “relations-lite” pass) and the four per-language side tables. v3 keeps the one part they do not reject, the linking table, and drops everything else.

Concernv3
1. Breaks the enrichment processNo new state and no new pass. The only pipeline change is that S4 writes link rows instead of arrays, after a parity check, and the judge runs over four kinds instead of two.
2. Lite enrichment is expensive and low valueThere is no lite enrichment. A target is enriched fully, once, or not at all.
3. Other languages cannot reuse the structureThe link table is keyed by target language today. Kinds become a lookup table. Nothing in the shape is Indonesian-specific.
4. Lite to full means double workNo lite, so no second pass over the same target.
5. The database is too complicated to changeOne existing table gains seven columns and a lookup table. Four inline arrays retire. No side tables.
6. Lite content is hard to translate to other L1sThe only new text is the meaning and note per mention, stored the way every other text in the estate is stored today.
7. RedundancyContent lives once, on the full card. The link row repeats a short text string, not content. The arrays, which today repeat both, retire.
The design

The shape

The diagram below and its labels are the actual subject of this checkpoint, so they stay technical — read the four-line legend under it first if any term is unfamiliar.
erDiagram
    CT1_LEMMA ||--o{ CT2_SENSE : "has meanings"
    CT2_SENSE ||--o{ CT3_FORM : "has forms (unchanged, one parent)"
    CT2_SENSE ||--o{ RELATION_LINK : "mentions (t073, extended)"
    RELATION_LINK }o--o| CT2_TARGET : "a full card in the catalog, or nothing yet"
    RELATION_LINK }o--|| RELATION_KIND : "kind, from a lookup table"
    RELATION_LINK {
        bigint id PK "existing"
        text l2_language "existing; ind today; lets another target language reuse the table"
        uuid source_card_id FK "existing; the owning CT2 sense"
        text relation_field "existing; similar_expressions | opposite_expressions | informal_variants | related_compounds"
        text target_text_norm "existing; the item's text as authored, normalized"
        uuid target_card_id FK "existing; nullable; the full CT2 card when one exists"
        text decision_code "existing; NA_NO_SENSE_MATCH | NA_NOT_IN_ESTATE when there is no card"
        int ordinal "NEW; order within its section"
        text register "NEW; formal | informal | slang | neutral, of the pairing, authored"
        bool is_idiom "NEW"
        text tag_raw "NEW; jenis as authored"
        jsonb gloss "NEW; the meaning next to this sense, {eng, ind} (LB-a)"
        jsonb note "NEW; the context note, {eng, ind} (LB-a)"
        jsonb provenance "NEW; opaque, stored whole"
    }
    CT2_TARGET {
        uuid card_id PK "an existing full sense or phrase card; nothing else"
    }
    RELATION_KIND {
        text kind PK "the four kinds today"
        jsonb section_label "per learner language"
        int display_order
    }
    

1. CT1 is a lemma — the root word a card's meanings belong to.

2. CT2 is the learnable card — a specific meaning a learner can actually study.

3. CT3 is a form of one specific sense — unchanged by this design.

4. A link row is not a card — it just points at a full card, when one exists, or at nothing yet.

The evidence

Today's link table, measured

Four real queries against the live database, so the numbers below aren't a guess.

1. Rows by relation kind

How many mentions exist today for each of the four kinds, and how many of those already point at a real card.
select relation_field, count(*) as rows,
  count(*) filter (where target_card_id is not null) as linked,
  count(*) filter (where target_card_id is null) as absent,
  count(distinct target_text_norm) as distinct_texts,
  count(distinct source_card_id) as distinct_source_senses
from t073_relation_sense_links group by relation_field order by relation_field;
KindRowsLinkedAbsentDistinct textsDistinct source senses
similar_expressions30,64910,49320,15618,3779,395
opposite_expressions8,9543,5265,4284,3684,023
informal_variants00000
related_compounds00000
informal_variants and related_compounds sit at zero because those two kinds haven't been judged yet — see the worked example below for what that looks like on a real card.

2. Absent rows by reason

When a mention doesn't have a card yet, why not — no matching sense in the estate at all, or a sense exists but the meanings don't line up.
select relation_field, decision_code, count(*) from t073_relation_sense_links
where target_card_id is null group by relation_field, decision_code order by relation_field, decision_code;
KindReasonCount
similar_expressionsNA_NO_SENSE_MATCH271
similar_expressionsNA_NOT_IN_ESTATE19,885
opposite_expressionsNA_NO_SENSE_MATCH579
opposite_expressionsNA_NOT_IN_ESTATE4,849

3. Overall totals

The size of the whole table right now, added up.
select count(distinct target_text_norm) as total_distinct_texts,
  count(distinct source_card_id) as total_distinct_senses, count(*) as total_rows
from t073_relation_sense_links;
20,816 distinct texts · 9,407 distinct senses · 39,603 total rows.

4. The four arrays' mention count from the views, measured live today

The same count, done the old way — straight from today's four arrays — as a cross-check against the new table.
select
  sum(case when jsonb_typeof(similar_expressions)='array' then jsonb_array_length(similar_expressions) else 0 end) as similar_n,
  sum(case when jsonb_typeof(opposite_expressions)='array' then jsonb_array_length(opposite_expressions) else 0 end) as opposite_n,
  sum(case when jsonb_typeof(informal_variants)='array' then jsonb_array_length(informal_variants) else 0 end) as informal_n,
  sum(case when jsonb_typeof(related_compounds)='array' then jsonb_array_length(related_compounds) else 0 end) as related_n
from v_senses where l2_language='ind' and card_tier=2 and card_archetype='vocabulary';
similar 31,144 + opposite 9,087 + informal 1,118 + related 7,832 = 49,181 total mentions measured live today.
The spec expected about 48,844 mentions (measured 2026-09-18). Today's live count is 49,181 — 337 more (+0.7%). This is normal corpus growth (more senses have been authored since the last measurement), not a data error — but the brief said to flag any difference loudly rather than rounding toward the expected number, so here it is stated plainly rather than quietly matched to the spec's figure.
Grounded in real data

Worked example: makan

makan's “to eat” and “to work” senses, mapped onto the v3 shape from what the judge has actually decided so far. Most items don't have a full card yet — under v3 that's completely fine, they just show their text and meaning until one exists.

Sense 1 — “to eat”

KindTextTargetRegisterIs idiomEng meaningInd paraphrase
informalmamNone yet — not yet judgedto be authoredto be authoredbaby talk for 'eat' used with small children at hometo be authored
informalmamamNone yet — not yet judgedto be authoredto be authoredcute child language for 'eat', common when coaxing toddlersto be authored
informalmknNone yet — not yet judgedto be authoredto be authoredtexting shorthand for 'makan' in chatsto be authored
oppositeberpuasaNone yet — not in the estateto be authoredNoto fast (abstain from eating)to be authored
oppositemenahan laparNone yet — not in the estateto be authoredNoto hold back hunger (not eat)to be authored
related compoundkasih makanNone yet — not yet judgedto be authoredto be authoredto give food to a person or animalmemberi makan pada orang atau hewan
related compoundkereta makanNone yet — not yet judgedto be authoredto be authoreddining cargerbong kereta api yang dipakai sebagai tempat makan; restorasi
related compoundmakan malamNone yet — not yet judgedto be authoredto be authoredto have dinnermakan pada malam hari
related compoundmakan pagiNone yet — not yet judgedto be authoredto be authoredto have breakfastmakan pada waktu pagi; menyarap
related compoundmakan siangNone yet — not yet judgedto be authoredto be authoredto have lunchmakan pada waktu siang hari
related compoundmakan sirihNone yet — not yet judgedto be authoredto be authoredthe habit of chewing betel leafkebiasaan mengunyah daun sirih
related compoundmencari makanNone yet — not yet judgedto be authoredto be authoredto earn a livingmencari nafkah
related compoundnafsu makanNone yet — not yet judgedto be authoredto be authoredthe feeling of wanting to eatrasa ingin makan
related compoundrumah makanNone yet — not yet judgedto be authoredto be authoredrestaurantkedai tempat makan (menjual makanan)
related compoundsudah makan?None yet — not yet judgedto be authoredto be authoreda friendly greeting asking if you have eatensapaan akrab untuk tanya sudah makan
similarbersantapNone yet — not in the estateto be authoredNoto dineto be authored
similarmelahapNone yet — not in the estateto be authoredNoto devourto be authored
similarmenikmati makananNone yet — not in the estateto be authoredNoto enjoy a mealto be authored
similarmenyantapNone yet — not in the estateto be authoredNoto partake (eat formally)to be authored

Sense 3 — “to work”

KindTextTargetRegisterIs idiomEng meaningInd paraphrase
informalgak makanNone yet — not yet judgedto be authoredto be authoredcolloquial for 'rem tidak makan' in chat and speechto be authored
oppositemacetExisting full cardto be authoredNoto jam, to get stuckto be authored
oppositerem blongNone yet — not in the estateto be authoredNobrakes failto be authored
oppositetidak berfungsiNone yet — not in the estateto be authoredNoto not functionto be authored
similarbekerja sebagaimana mestinyaNone yet — not in the estateto be authoredNoto work as it shouldto be authored
similarberfungsiNone yet — not in the estateto be authoredNoto functionto be authored
similarberjalanNone yet — no matching senseto be authoredNoto run properlyto be authored
similarbisaNone yet — no matching senseto be authoredNoto work, can functionto be authored

Distinct targets across these two senses

TextTarget
bekerja sebagaimana mestinyaNone yet — not in the estate
berfungsiNone yet — not in the estate
berjalanNone yet — no matching sense
berpuasaNone yet — not in the estate
bersantapNone yet — not in the estate
bisaNone yet — no matching sense
gak makanNone yet — not yet judged
kasih makanNone yet — not yet judged
kereta makanNone yet — not yet judged
macetExisting full card
makan malamNone yet — not yet judged
makan pagiNone yet — not yet judged
makan siangNone yet — not yet judged
makan sirihNone yet — not yet judged
mamNone yet — not yet judged
mamamNone yet — not yet judged
melahapNone yet — not in the estate
menahan laparNone yet — not in the estate
mencari makanNone yet — not yet judged
menikmati makananNone yet — not in the estate
menyantapNone yet — not in the estate
mknNone yet — not yet judged
nafsu makanNone yet — not yet judged
rem blongNone yet — not in the estate
rumah makanNone yet — not yet judged
sudah makan?None yet — not yet judged
tidak berfungsiNone yet — not in the estate

Link-row table: 23 of 81 cells exist today — the rest are “to be authored”. (The distinct-target table has no missing cells to count: under v3 a target is either an existing full card or nothing yet — there is no partial/lite state to measure.)

What changes for the learner

What the learner sees

Two states, quoted from the design spec's section 5.
Quoted — spec §5

An item with a linked full card: kind label, the text, the meaning next to this sense, phonetics, sound, examples, Learn, and the plus (add to deck) — the same as any item renders today.

An item with no card yet: kind label, the text, the meaning next to this sense. The plus queues full enrichment through the existing path; the chip shows pending until the card lands.

Validation

Before it runs

The proof, and the order the build happens in.
1
The proof: Two clusters, makan and dalam, end to end: migrate their mentions into link rows, check they match exactly, render both cluster cards from those rows at phone width showing both item states, tap the plus on an uncarded item and confirm it queues, then carry one target all the way through to a full card and confirm the item upgrades on its own.
2
The build order: Migration first (new columns, the kind lookup table) → migrate the two proof clusters and check they match → the judge runs on all four kinds, not just two → the app reads link rows instead of arrays → new authoring writes rows directly → then, sized by the FL answer, the rest of the catalog runs through the ordinary pipeline.
Decide

Your decisions

Four picks, plus space for anything else.
1
SH — confirm the v3 shape
Why it matters: this is the actual design being confirmed — full card or nothing, one existing table extended, no in-between state.
2
LB — how the two new pieces of text (the meaning and the note) are stored
Why it matters: a near-term storage choice with a known future migration attached.
3
MZ — measure the real cost of a full card, and how many targets are shared by two or more senses, before deciding how many to build up front
Why it matters: this number directly drives the next decision (FL).
4
FL — how many targets get a full card built up front, before anyone taps anything
Why it matters: sets the size and cost of the very first batch.
5
CM — anything else about this design
Why it matters: free-text only — use this for anything not covered above.