9167cf53af
gates / gates (push) Successful in 23s
The hub has written every customer e-mail in Hungarian whatever the box was set to. The box has published its language since controller v0.247.0; nothing read it. Now it does. Nothing an operator reads changes. The Hungarian mails are byte-identical, and that is a diff rather than a reading: 56 goldens per language captured from v0.117.0 BEFORE any string moved, and all 56 Hungarian ones pass unchanged after every sentence was routed through the new bundle. - internal/i18n: flat bundle, 79 keys, hu authoritative + hu fallback, ceiling 0. - customerMessages/severityLabels are DERIVED from the bundle, so a sentence is written in one place and all 40+ tests that read those maps still work. - Language order: last reported -> created-with -> hu. reports.language defaults to EMPTY, never hu: "never told us" is not "chose Hungarian". - message_customer on POST /api/v1/event, additive and optional forever, for the sentences the box composes and the hub cannot translate. - The bind page is per-language, and its `expired` state stays Hungarian: it is the state an unknown token lands in, so rendering a real English customer's token in English would make the LANGUAGE answer what the TEXT refuses to. Two defects found inside the release: - R-581: the newest report was picked by received_at, which has SECOND granularity, so same-second reports tied and the winner was arbitrary. Ordered by the autoincrement id now. GetCustomers() still has the shape - row open. - R-582: the English copy-guard stems, ported word for word from Hungarian, convicted 141 honest sentences. The English claim is a phrase with a modal. R-555 closed: the language allowlist entry is out of wire_contract_gate.py. hub_copy_gate.py follows the sentences into the bundle - without that it would have scanned four files that no longer hold any customer text and reported success. Three new decoys incl. an innocent control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
100 lines
6.0 KiB
Markdown
100 lines
6.0 KiB
Markdown
# REPORT — localisation slice 3 Part A: the hub's e-mails follow the household's language
|
||
|
||
**hub v0.118.0** · felhom.eu base `20aafc3dec20` · 2026-09-18 · R-558 (Part A), R-555 closed
|
||
|
||
## Claims in the task that turned out wrong, named first
|
||
|
||
1. **"`customerMessages` 40 entries (L70)"** — **39 entries, at L69.**
|
||
2. **"`customers.language`, `appliances.reported_language`"** — **neither table exists.** There is no
|
||
`customers` table and no `appliances` table. The real ones are **`customer_configs`** (the customer
|
||
record) and **`reports`** (the box's heartbeat, one row per report). The columns added are
|
||
`customer_configs.language` and `reports.language`.
|
||
3. **"no `ALTER TABLE customers ADD` found by grep — the migration pattern is something else; if it
|
||
is not obvious, stop and report."** The pattern is completely obvious and there are ~20 instances:
|
||
`s.db.Exec("ALTER TABLE <t> ADD COLUMN <c> <type> NOT NULL DEFAULT <v>")`, error deliberately
|
||
ignored so it is idempotent. The grep failed only because it named a table that does not exist.
|
||
4. **"`renderBackupRunFailures` gains a `lang` parameter"** — it is **operator-only and already
|
||
English**. Its single caller is `FormatOperatorEmail`. Untouched.
|
||
5. **"`message_customer`'s length is capped like `message`"** — **`message` has no length cap.** Both
|
||
are bounded only by the 1 MB `LimitReader` on the request body. I wrote a cap, then removed it: a
|
||
byte-count truncation would also cut a UTF-8 sequence in half.
|
||
6. **"`python3 scripts/hub_gates.py` (or the runner the hub uses — name it)"** — there is no
|
||
`hub_gates.py`. The hub's gates run from **`scripts/repo_gates.py`**, this repo's single runner.
|
||
7. **Line numbers** were mostly off by one or two (`customerMessages` 69 not 70, `severityLabels` 158
|
||
not 159, `FormatCustomerEmail` 166 not 167).
|
||
|
||
Claims that were **right**: no mail goldens existed; the hub read no `language` anywhere outside a
|
||
comment; `05-hub-architecture.md` had no customer-mail section; the box has published the field since
|
||
v0.247.0. I briefly believed `Report.Language` was never assigned and was wrong — `cmd/` is
|
||
gitignored, so `rg` skips `main.go`, where all four assignments live.
|
||
|
||
## What shipped
|
||
|
||
The hub reads the language the box reports and writes the household's e-mail in it. **Nothing an
|
||
operator reads changed.** Hungarian is byte-identical, measured.
|
||
|
||
- **`internal/i18n`** — flat bundle, 79 keys, hu authoritative + hu fallback, missing ceiling 0.
|
||
- **56 mail goldens per language**, captured from v0.117.0 **before** any string moved. All 56
|
||
Hungarian ones pass unchanged after the rewrite. A `nowFn` seam makes them byte-stable.
|
||
- **`customerMessages`/`severityLabels` are derived from the bundle** — one place per sentence, and
|
||
all 40+ existing tests and comments that read them still work.
|
||
- **Order: last reported → created-with → `hu`.** `reports.language` defaults to **empty**, never
|
||
`hu`: "never told us" is not "chose Hungarian".
|
||
- **`message_customer`** accepted on `POST /api/v1/event` (additive, optional forever).
|
||
- **Per-language bind page**, built the controller's way (substitute markers, then parse).
|
||
- Customer form `language` select; `customer.language` in `controller.yaml` (diff: one line).
|
||
|
||
## Live evidence, read off the running hub
|
||
|
||
The wire already worked and I measured it before writing anything (read-only copy of `hub.db` + WAL):
|
||
|
||
```
|
||
demo-felhom 2026-09-18 13:42:56 language='hu' controller 0.255.0
|
||
demo-hp 2026-09-18 13:45:24 language='en' controller 0.255.0
|
||
drill-r50 / peti-felhom / tester-1 language=<ABSENT> (0.213.0 / 0.115.0 / 0.245.0)
|
||
```
|
||
|
||
That is the positive and the negative control in one read: boxes ≥ 0.247.0 report the field, older
|
||
ones send nothing at all — which is exactly the case the empty default exists for.
|
||
|
||
## Two defects found inside this release
|
||
|
||
- **R-581 (P2, partly still open).** `CustomerLanguage` picked the newest report by `received_at`,
|
||
which has **second** granularity — same-second reports tie and the winner is arbitrary. A household
|
||
that had just switched would get the old language back at random. Fixed by ordering on the
|
||
autoincrement `id`; caught by a test that failed on the first version. **Still open:
|
||
`GetCustomers()` has the same shape** and feeds the whole operator dashboard.
|
||
- **R-582 (closed, kept for the lesson).** The English copy-guard stems, ported word-for-word from the
|
||
Hungarian, convicted **141 honest sentences**. In Hungarian the stem *is* the claim
|
||
(`visszaállíthat` = *can restore*); English splits the modal from the verb, so the claim is a
|
||
phrase. Then the decoy suite caught the fix being too narrow — "can **still** be restored" walked
|
||
through a pattern written for "can be restored".
|
||
|
||
## Gate work
|
||
|
||
`wire_contract_gate.py`: the R-555 `language` allowlist entry **deleted** — the field is genuinely
|
||
decoded now and the gate checks it (202 tags, was 201). `hub_copy_gate.py`: **the sentences moved into
|
||
the bundle**, so `CUSTOMER_SURFACES` had to move with them — without that the four declared Go files
|
||
would all still exist, the gate would still report success, and it would be scanning nothing. It
|
||
learned English, and found one real occurrence (operator-tier, registered with its reason). Three new
|
||
decoys including an **innocent control**; the `hub-copy` exemption is removed from
|
||
`decoy_coverage_gate.py`.
|
||
|
||
## The mails
|
||
|
||
| Group | Count | Golden | English | Live-tested |
|
||
|---|---|---|---|---|
|
||
| Event types (`mail.event.*`) | 39 | ✅ hu + en | ✅ | pending (§13.1) |
|
||
| Severity labels | 4 | ✅ | ✅ | — |
|
||
| Customer wrapper (subject, body, 2 lines, sign-off) | 5 | ✅ | ✅ | — |
|
||
| Claim arc (claim / reset / reenroll / claimed) | 4×2 | ✅ | ✅ | pending (§13.2) |
|
||
| Self-bind mail | 2 | ✅ | ✅ | pending |
|
||
| Bind page | 21 | — (render tests) | ✅ | pending |
|
||
| Operator mails | 3 | ✅ (unchanged) | n/a — never localised | — |
|
||
|
||
## Green
|
||
|
||
`go build` / `go vet` / `go test ./...` clean. All 14 felhom.eu gates OK. 15/15 decoys behave.
|
||
|
||
**Not yet done:** deploy + live proof (§13), and Part B (controller v0.256.0, the box's own sentence).
|