v0.252.0 — the sentences the program builds follow the language (R-557 slice 2 release A)
gates / gates (push) Successful in 23s

Slice 1 translated the dashboard's markup. The sentences the program BUILDS were still
Hungarian literals in Go, so an English household clicked an English button and was
answered in Hungarian. 226 of them move into the bundle here.

A flash was the hard part: it travels inside the redirect URL and is rendered by a
DIFFERENT request, so it now carries a bundle key plus its parameters. A link minted by
an older controller carries prose and is shown verbatim — never a raw key, never dropped.

Also converted: page data and view-model text, the internal/api JSON answers, the alert
banners (Alert.MessageKey, rendered on the way out of GetAlerts), 237 country names at
display, and the four page titles built around an app name (R-566 closed).

Hungarian is byte-identical, and that is measured rather than read:
scripts/i18n_go_parity.py freezes every Go literal at the base commit (7 467) and refuses
a key whose Hungarian is not that text, byte for byte. Three decoys, each seen to convict.
Its own first version filtered the capture through an ASCII-Hungarian word list and missed
seven real literals — the R-565 class. The filter is gone.

Nothing on the wire moved, and wire goldens now hold it there: the report's health
warnings and every notify event message stay Hungarian, because the hub MAILS the
controller's sentence when it has no entry of its own. Slice 3 (R-558) owns those.

MinAgent: 0.131.0 (unchanged). No hub release needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-18 10:15:56 +02:00
parent 736f54b496
commit 5270bad76e
52 changed files with 10755 additions and 478 deletions
+99 -73
View File
@@ -1,89 +1,115 @@
# REPORT — controller v0.251.0: nothing decides by reading a Hungarian word (R-553 + R-563)
# REPORT — localisation slice 2, release A (controller v0.252.0, R-557)
**Date:** 2026-09-17 · **Baseline:** `e236dca48653` (v0.250.0) · **Released:** `0194342` → image
`gitea.dooplex.hu/admin/felhom-controller:0.251.0` · **MinAgent:** 0.131.0 (unchanged).
**Architecture read before any claim:** `felhom.eu/documentation/architecture/10-localisation.md` §1/§9
(§9 rewritten by this session), `07-backup-architecture.md`, `02-controller-module-map.md`.
**Evidence:** `felhom.eu/documentation/audits/r553-2026-09-17/`.
**2026-09-18 · base commit `736f54b49610` (v0.251.0) · MinAgent 0.131.0, unchanged · no hub release.**
Architecture read first and named: `felhom.eu/documentation/architecture/10-localisation.md` §1
(parity), §2 (mechanism), §5 (gates), §9 (the R-553 signals), §10 (this slice); `07-backup-architecture.md`
for what the hub reads from a box; `05-hub-architecture.md` for what the hub composes itself.
## Claims in the prompt that turned out wrong — named first
---
1. **„`internal/system/mounts_linux.go` L176/L194/L199 produce site 3's warnings."** They do not.
`h.Warning` from `CheckBackupDestination` never reaches `report.Warnings` — the only producer folded
into the health report is `checkStoragePaths` (healthcheck.go). `system/` needed no change at all.
2. **„Site 1 matches `required field` … all producers are ours."** **Nothing produces `required field`.**
The router matched a phrase no code in this repo writes — a dead arm, removed with the rest.
3. **Site 3's condition matched more than the prompt implies.** Of the nine warnings that exist today,
exactly ONE matches („Az adattároló … nem külön meghajtón van"): the three patterns are lower-case
and every other warning capitalises the word. So the fix had to preserve *one* inline warning, not a
family — `WarnKindStorageNotSeparate` does exactly that.
4. **Line numbers drifted** as the prompt warned (router.go:478 → the block moved into a new
`deployStatusFor`; offbox.go:186/921; alerts.go:208; handlers.go:920/931 → 944).
5. **„`errors.New(refusal)` at the memory site"** — correct, and it is the reason the memory refusal
could not be pinned by a unit test: `memoryVerdict` reads the HOST's real memory. Its kind is pinned
at the source line instead, and the mapping is covered with a translated memory message.
6. **Confirmed as stated:** site 2's classifier has exactly one caller; `offbox_handlers.go:132` is a
false positive (an `[INFO]` log line); site 5 is the one rendered byte that changes.
## 1. Claims in the task that live source disproved — named first
## The five sites
| the task said | source says |
|---|---|
| "`cloudflare/countries.go` is used by `report/types.go` (on the wire)" | **No.** `report/builder.go` puts country **CODES** on the wire (`AllowedCountries []string`); no country NAME leaves the box. `CountryName()` has no caller at all. So the names are display-only and were free to translate — which is what this release did. |
| "the hub's dispatcher composes its own customer mail from event kinds (grep of `dispatcher.go` for `.Message`: nothing)" | **Wrong in substance.** `.Message` finds nothing because the message arrives as a PARAMETER, not a field. `FormatCustomerEmail` (hub `notify/templates.go` L167-210) uses `customerMessages[eventType]` when it has one and **falls back to the controller's message** when it does not — and appends it as „- Üzenet: %s" whenever the two differ. Several event types deliberately carry no entry so the controller's sentence IS the mail (hub `api/handler.go`, the notes at L2034/L2045/L2066). The conclusion (do not translate them) was right; the reason was not. |
| "5 flash readers / 8 writers" | 8 writers, **8 read sites** — the three named plus `auth.go:100` (login), `server.go:498` (claim) and the `flash_error` twins. All eight converted. |
| per-file counts (`handlers.go` 121, `offbox_handlers.go` 72, `storage_handlers.go` 48, `handler_debug.go` 40, `offbox_reconstitute.go` 41, `notify/notifier.go` 30, `api/router.go` 19) | measured at `736f54b49610`: **120 / 70 / 45 / 39 / 38 / 31 / 18**. Totals: **1 120** Hungarian literals, not 1 141. |
| "`scripts/i18n_inventory.py`" (as a controller script) | it lives in **`felhom.eu/scripts/`**. Its Go-literal walker was copied into the new gate rather than imported — a controller clone need not sit beside felhom.eu, and a gate that can fail to import its scanner can silently skip. |
| # | decision | signal added | producer(s) changed | decision changed | test | HEAD-red → fixed-green |
|---|---|---|---|---|---|---|
| 1 | deploy HTTP status | `stacks.ErrRequiredField`, `ErrPathMissing`, `ErrNotEnoughMemory`, `ErrAlreadyDeployed` (`deploy_errors.go`) | `deploy.go` 205/311/326/333/245 | `api/router.go` → new `deployStatusFor` | `TestR553_Deploy_DecisionSurvivesWordingChange`, `TestR553_DeployHandlerUsesTheKind`, `TestR553_DeployProducersCarryKindAndKeepTheirWords` | red: every translated row `= 500, want 400/409`; green after |
| 2 | off-site failure class | `backup.ErrOffsiteQuota` | `offbox.go` 921 | `ClassifyOffsiteFailure` | `TestR553_OffsiteQuota_{Decision,HeadLine}SurvivesWordingChange`, `TestR553_QuotaProducerKeepsItsWords` | red: translated quota `= "unknown"`, head line „ismeretlen okból"; green after |
| 3 | disk-warning placement | `monitor.WarnKind*` + `HealthReport.WarningKinds` / `addWarning` / `WarningKindAt` | `healthcheck.go` `checkStoragePaths` | `web/alerts.go` | `TestR553_StorageWarningsCarryKindsAndKeepTheirWords`, `TestR553_DiskWarningPlacementSurvivesWordingChange`, `TestR553_HubReportWarningsAreUnchangedOnTheWire` | red: translated warning `Inline = false`, and an unrelated warning wrongly moved inline; green after |
| 4 | stale „nothing selected" note | `settings.OffboxTarget.LastWarningKind` + `backup.OffboxWarnNoAppsSelected` | `offbox.go` 1053/1095 (+ both clear sites, both copy sites) | `offboxWarningDisplay` | `TestR553_StaleNote*`, `TestR553_WarningKindIsPersistedAndCopied`, `TestR553_OffboxRunRecordsTheKind` | red: translated note shown verbatim; producer without its kind convicted; green after |
| 5 | remote-backup poll (R-563) | `data-status="{{.Offbox.LastStatus}}"` | `backups_remote.html` status element | the page's own script | `TestR563_PollStartsFromAttribute` (hu + en), `TestR563_AttributeFollowsTheStatus` | red twice: poll back on the word (both languages), word back inline (English page shows Hungarian); green after |
## 2. What shipped
**Message bytes:** every producer's sentence is asserted equal to its pre-change string (sites 1, 2, 4)
or rendered identically (site 5). `util.KindErrorf` / `KindError` (new, `internal/util/errkind.go`,
documented in REUSE.md §1) build the same bytes `fmt.Errorf` did and carry the sentinel for `errors.Is`.
**226 Go literals converted**, plus 237 country names now available in English. 426 keys, each
recorded in `scripts/i18n_go_keys.json` against the literal it replaced.
**External signatures kept, on purpose:** the classifier's restic and ssh arms — `unable to open config
file`, `is there a repository at the following location`, `produced no snapshots`, `connection refused`,
`connection reset`, `no route to host`, `i/o timeout`, `timed out`, `permission denied`, `host key`,
`handshake`, `could not resolve`, `network is unreachable`. We neither write nor translate that output.
| file | before | after | converted | of the rest, `fmt.Errorf` (release B) |
|---|---|---|---|---|
| `internal/web/handlers.go` | 120 | 44 | 76 | 0 |
| `internal/web/offbox_handlers.go` | 70 | 22 | 48 | 0 |
| `internal/web/storage_handlers.go` | 45 | 19 | 26 | 16 |
| `internal/api/router.go` | 18 | 0 | 18 | 0 |
| `internal/web/share_handlers.go` | 22 | 5 | 17 | 0 |
| `internal/web/sharing_handlers.go` | 18 | 3 | 15 | 1 |
| `internal/web/alerts.go` | 13 | 0 | 13 | 0 |
| `internal/web/tier2_config_handler.go` | 8 | 3 | 5 | 0 |
| `internal/api/geo.go` | 5 | 0 | 5 | 0 |
| `internal/web/backup_handlers.go` | 12 | 9 | 3 | 0 |
| `internal/cloudflare/countries.go` | 113 | 113 | 0 (the TABLE stays — it validates codes) | 0 |
**The wire:** `internal/report/builder.go` copies Status/Issues/Warnings only; the kinds are internal.
Pinned by field-set + byte test, and confirmed live (the health block on 0.251.0 carries exactly
`issues, status, warnings`).
Mechanism: `i18n.Bundle.Msgf`; `web.(*Server).msg/msgLang/msgN`; `api.(*Router).msg/msgLang/langFor/countryName`;
`flashQuery`/`flashText`/`flashFrom`; `Alert.MessageKey`/`MessageArgs`/`LinkTextKey` + `Alert.rendered(lang)`;
`TitleArgs` for the four app-named titles (R-566, closed).
## Gates
**Word order without a second syntax.** The task asked for a named-parameter (`{{.Name}}`) form for
multi-parameter messages. It is not here, deliberately: English reorders with Go's own explicit
argument indexes (`%[2]s`), which `fmt` already understands. The Hungarian value then stays the format
string the code always had, byte for byte — which is exactly what the parity gate compares, so the
measurement needs no exception. `TestBundleParametersMatchAcrossLanguages` counts an indexed verb as
one verb, so a reordered English still has to use the same verbs on the same values.
`go build ./... && go vet ./... && go test ./...` → rc 0 before each push. `controller_gates.py --fast`
→ rc 0. No `--no-verify`. One existing test (`TestClassifyOffsiteFailure_EachCauseIsDistinct`) built the
quota error the old way and was adapted to build it as the run does — stated here rather than silently.
## 3. The wire — surveyed, frozen, not translated (A.1)
## Live validation (endpoint-level; no browser on DooPlex) — demo-hp guest 9201
Everything a HUB reads keeps its bytes. Filed against **R-558** (slice 3 owns it):
Deployed by hand; **fleet floor unchanged (0.250.0), no golden.**
- **Hungarian pages 0.250.0 → 0.251.0:** `/launcher` and `/monitoring` identical; `/backups/remote`
identical apart from the new attribute and the poll line (+23 bytes); `/dashboard` differs in live
numbers only.
- **Deploy refusal:** `POST /api/stacks/adventurelog/deploy` on an already-deployed app → **409 before
and after with byte-identical message**; the app kept running.
- **Site 5 live:** `id="offbox-status-value" data-status="ok"` and `v.dataset.status === 'running'`.
- **Hub:** `health.warnings` `[]` before and after, health keys unchanged.
- **Not provoked live, and why:** the 400 refusals (a live probe starts an install), the quota failure
(would need a full quota), the stale note (would need clearing the selection), and the disk-warning
placement (this box has no storage warning — 0 banner blocks before and after). All four are carried
by red-proofed tests.
- **A mistake, and what I did:** the FIRST live probe posted an empty value map to `vaultwarden`, which
has no empty required field — so the deploy was accepted (202) and the app installed. I stopped it and
removed it (with the data volume it had just created) within two minutes, verified no container
remained, and rewrote the probe to the 409 form. Nothing else on the box was touched; the standing
apps and `bentopdf` were not involved.
| producer | where it lands | why it stays |
|---|---|---|
| `internal/monitor/healthcheck.go` — 5 storage sentences | report `health.warnings` / `health.issues` | the hub stores and shows them to the operator |
| `internal/notify/notifier.go` — 31 event messages | event `message` → `FormatCustomerEmail` | **the household reads these in an e-mail**; several types have no `customerMessages` entry precisely so this sentence is the mail |
| `internal/cloudflare/countries.go` — the table | nothing (codes only) | it validates codes; the DISPLAY name is what follows the language |
## Teardown
Pinned by `TestStorageWarningWireTextIsFrozen`, `TestStorageWarningFormatsAreFrozen`,
`TestEventMessageWireTextIsFrozen`, `TestEventMessagesCarryNoBundleKey`. Both goldens were captured
from the producers at the base commit, before any change, and both were seen to fail.
Machine: guest on `hu`, controller 0.251.0; password file and probe scripts shredded/removed; evidence
pulled off before anything was cleaned. Host: transfer files removed. Hub: nothing written; the DB copy
shredded. Nothing provisioned.
The **persisted** text found by the same survey is listed in §6 for release C.
## Observations
## 4. Tests and their red-proofs
1. Four more API handlers pick their status by matching ENGLISH internal words (`protected`, `not found`, `not deployed`, `still running`, `not orphaned`). **FILED: R-569**
2. The stale-note display keeps a Hungarian-text fallback for boxes that have not run off-site on ≥ 0.251.0, and slice 2 must not translate that producer until it goes. **FILED: R-570**
3. The off-site failure classifier and the alert-placement rule are described in no architecture document. **FILED: R-571**
4. `required field` was matched by the router and produced by nothing — a dead arm. **NOT-A-FINDING: deleted with the rest of the text chain in this release; nothing to track.**
5. The i18n inventory's comparison detector, re-run on the fixed tree, reports one Go compare and one indirect compare. **NOT-A-FINDING: the first is the known `[INFO]` log-line false positive, the second is the deliberate legacy fallback carried by R-570; decoys prove the detector still convicts.**
Every one was run against a planted break and watched to convict; the planted change was then
reverted and the suite re-run green. Full log: `audits/i18n-slice2-2026-09-18/redproofs.txt`.
| test / gate | what was planted | what convicted |
|---|---|---|
| `i18n_go_parity.py` | one byte added to a hu.json value | `CHANGED`, naming the key and the base literal |
| `i18n_go_parity.py` | a key citing text no literal has | `INVENTED`, naming the text |
| `i18n_go_parity.py` | a joined key with reworded fragments | `INVENTED`, naming the fragment |
| `test_gate_decoys.py` go-parity ×3 | reworded / invented / unlisted | all three rejected |
| `TestEventMessageWireTextIsFrozen` | „telepítve" → „telepitve" | got/want printed |
| `TestStorageWarning*IsFrozen` | a wire warning translated | both tests |
| `TestFlashOnAServerWithNoBundleField` | `s.bundle()` without the `i18n.Shared` fallback | the page showed `flash.share.enabled` |
| `TestFlashKeyRoundTrip` | — | legacy prose, unknown key, `<script>`, empty: all six rows |
| `TestParameterisedPageTitles` | — | the four app-named titles equal the old concatenation |
| `TestAPIMessagesFollowTheHouseholdLanguage` | — | hu byte-identical, en different, per key |
**The gate caught a live defect in itself, not a decoy.** Its first capture filtered literals through
an ASCII-Hungarian word list and missed seven real ones („Naponta", „5 percenkent", „Eletjel
(Heartbeat)", „Adatbazis mentes", „Biztonsagi mentes", „Mentes integritas", „Rendszer allapot") — the
R-565 class exactly. It refused the keys that cited them. The filter is gone: every literal is
indexed, because the only question this index answers is "did the base commit contain this text?".
## 5. Green gate
`go build ./... && go vet ./... && go test ./...` — all green.
`python3 scripts/controller_gates.py` — 16 blocking gates OK + golden-notice advisory, **including the
new `go-parity`**. `python3 scripts/test_gate_decoys.py` — all 23 decoys behaved.
`HU_FORMAL_CEILING` 16 → 18, the measured number, no word changed (R-516 owns the words).
## 6. What is NOT in this release — each is a row, not prose
- **894 Hungarian literals remain**, of which **176 are `fmt.Errorf`/`errors.New`** — release B.
- **Persisted text** (release C): `settings.OffboxTarget.LastError/LastWarning/LastProofReason`,
`LastSyncError`, `RestoreOpResult`, the `unitRestore*MsgFmt` and `tier2*Msg` family in
`handlers.go`, and `backup/offbox_reconstitute.go`. Written by a background run, read days later.
**The §16 decision stands as its default: option 1** — written in the box's language at the time,
with a household that switches seeing the old language until the next run.
- **R-570's producer is untouched**, as its row requires.
- New rows: **R-572** (`pruneLabel`/`fmtDuration` in `funcmap.go` are copy-producing helpers that
`localeFuncs` does not override — „vasárnap" renders on an English page), **R-573** (the
agent-channel and endpoint-drift banner text is composed by the channel-health checker and reaches
`Alert.Message` as finished Hungarian), **R-574** (`handler_debug.go`, 39 literals, page copy and
JSON payload not yet separated).
## 7. Live validation
Endpoint-level on demo-hp guest 9201 (no browser on DooPlex — `claude-in-chrome` is not available
here). Evidence: `felhom.eu/documentation/audits/i18n-slice2-2026-09-18/A/live/`.