Files
felhom.eu/REPORT.md
T
admin 9167cf53af
gates / gates (push) Successful in 23s
hub v0.118.0: the household's e-mails follow the household's language (R-558 Part A)
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
2026-09-18 16:20:11 +02:00

6.0 KiB
Raw Blame History

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).