test(dialects): guard the catalogue against a proposal's two limits

DialectCatalogueSizeTest, modelled on ChapterProposalSizeTest and measured the
way the manager measures it -- the real encoding rather than an estimate.
Neither limit is checked anywhere a reviewer would see it: proposeSigningBatch
enforces both with a require, so the first sign of the catalogue outgrowing a
batch would be a wave that split in two, or one that threw. Somebody adding a
language has no reason to be thinking about FROST.

The third assertion changed shape while being written. "A wave is small rather
than just legal", as a fraction of MAX_PROPOSAL_BYTES, failed: the catalogue
encodes to 16,832 bytes and a quarter of the budget is 16,384. The quarter was a
guess. What actually wants guarding is the cost of one dialect -- 301 bytes,
bounded here at twice that -- because that is the number that would drift if a
dialect event grew something large, and unlike a fraction it does not move every
time a language is added.

The size test's failure message says not to trim the catalogue. The view model
already chunks by MAX_BATCH_SIZE, so a larger catalogue is two sessions rather
than a bug; what would be lost is a group adopting the whole list answering once,
and that is a decision rather than a repair.

Also records the plan as built, with a table of the seven places the build chose
differently, and indexes it in docs/README.md.

One of those entries is a finding rather than a choice: m3-touch-targets.py reads
comments as code, so a KDoc explaining that a row used a clickable Card "rather
than a Modifier.clickable" broke a zero budget and with it ./gradlew check. The
comment was reworded, which is the wrong fix. m3-audit.sh already solves this for
the colour check and says why, at NOT_A_COMMENT.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso NgakoandClaude Opus 5 committed 2026-10-02 23:03:08 +02:00
1 parent 226653d282
commit dd405e90e5
3 files changed
+186 -1

No files matched your search

+36 -1
View File
@@ -5,7 +5,14 @@ spoken in Africa instead of typing each one into a three-field form, and how
admins top that list up later — proposing only what is still missing, in waves,
without two of them proposing the same wave twice.
**Not built.** This is the plan. Seven phases, one commit each.
**Built.** All seven phases are in, one commit each, and the phases below are
kept as written -- they are the reasoning, and the code is easier to read against
the argument it came from than against a summary of itself. Where the
implementation chose differently the table at the end says so.
`AfricanLanguages.ALL` is the catalogue, `DialectScaffold` answers what a room is
missing, and `AddDialectsFromCatalogueScreen` is the picker, reached from the
group's detail screen beside "Add dialect".
Two facts shape every phase and are stated before them, because both are easy to
design around and expensive to discover afterwards.
@@ -480,3 +487,31 @@ is, not a convenience method.
**No automatic wave.** Nothing proposes the catalogue on a room's behalf when it
is created. A dialect is a statement the group signs about what it will
translate into, and fifty of them arriving unasked is a statement nobody made.
---
## Where the build chose differently
| the plan said | the build does | why |
|---|---|---|
| "about fifty entries" | 56 — 53 African, 3 European | the fan-out across countries landed there. 8 under `MAX_BATCH_SIZE`, which is thinner than it reads; phase 7's test is the tripwire |
| one `african` list | two, `african` and `european`, with `ALL` their sum | the ordering test needs a list that is actually grouped by country, and appending the three European entries breaks that grouping. It also makes "drop them" one edit, which is what the plan promised |
| `missingFrom(existing, pending, catalogue)` | that, plus a public `DialectScaffold.Key` and three `keyOf` overloads | the identity rule has four callers — the scaffold, the row classifier, the selection and the view model's unpick — and spelling `(code, country)` four times is four chances to forget `trim()` |
| phase 4 ships the picker with its FAB | phase 4 shows and picks, phase 5 proposes | a commit whose FAB calls a method written in the next commit does not compile. Split so each commit does something whole |
| `proposeSelectedDialects(localChatRoom, selected, …)` | the selection is read internally | the view model owns `selected`; passing it back in lets a caller pass something else, and nothing needs to |
| `CatalogueRow.rowsOf` computes the three standings | it derives them from `missingFrom` | two functions deciding what is proposable is one too many. MISSING is whatever that function says, and held-versus-in-flight is a label on what it already excluded |
| a wave "is small rather than just legal" — under a quarter of `MAX_PROPOSAL_BYTES` | at most 602 bytes *per entry* | the catalogue encodes to 16,832 bytes, which is 25.7% of the budget — the quarter was a guess and it failed. The per-entry cost is the thing that would actually drift, and it does not move when a language is added |
### One thing found on the way
`docs/scripts/m3-touch-targets.py` reads comments as code. A KDoc line on the
picker's row composable explaining that it used a clickable `Card` "rather than a
`Modifier.clickable`" was counted as a bare clickable chain, which breaks a zero
budget and so breaks `./gradlew check`. The comment was reworded to get around
it, which is the wrong fix -- the sentence was right.
`m3-audit.sh` already solves this for the colour check and says why, at
`NOT_A_COMMENT`: "a note *about* a hardcoded colour counts as one -- which
happened the first time a call site was fixed and the commit explained what it
had replaced." The touch-target checker wants the same treatment, and
`m3-title-case.py` should be looked at too, since it scans whole files.