Part 0 shipped as controller v0.258.0 (its own commit). Part A is the guide's English twin. Part B is the walk: a box installed from scratch that day, used by an English speaker following only the English guide and the screens. VERDICT: not yet ready for an English-speaking tester, because the claim page — the one screen between them and their box — is English chrome with Hungarian messages (R-596, P1). Everything else held: the download page, the bilingual console, all three customer mails, the bind page and its refusal, the dashboard's first language from customer.language alone, both app pages, the whole catalog, the language switch both ways — every one of them with zero Hungarian lines. One intervention (I1 = R-494, filed 2026-09-14); the stop rule was not reached. The walk exercised what 2026-09-14 could not: the graphical installer, the auto-reboot, and the mailed link and self-bind page end to end — that walk's H1 is closed, because this session had a mailbox. R-214 CLOSED as a side effect and seen rather than reasoned about: the console's last paint is now the bilingual "the box is linked" banner. R-516 does NOT close, and the item-by-item note says why: more than half its twelve items are about what a HUNGARIAN household reads, and an English walk cannot see them. It now waits on a Hungarian walk with a second drive. Rows opened: R-596 (P1, the claim page), R-597 (the setup code is three Hungarian words), R-598 (the Backup page's protection warnings), R-599 (a drill's teardown is blocked 30 minutes by report staleness and the 409 does not say so). Golden 0.258.0 baked, published, vouched, with its record. The waiver was NOT retired and the record says why in one line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
55 KiB
10 — The product in more than one language
LIVING DOCUMENT. Every localisation slice updates this file in the same session. Opened 2026-09-17 by the localisation starter, AFTER its spike ran (controller v0.247.0). Before it, no architecture document mentioned localisation at all (a grep of
architecture/*.mdfori18n|locali|languagefound one incidental line in 07).Marks, as in 07 and 02: [DESIGN] — a decision taken, not derived from code. [FACT] — observed, with a citation. Unmarked means not yet classified.
This file carries the REASONING. The register (backlog/OPEN-ITEMS.md, R-553..R-562) carries the
work. The numbers are in audits/I18N-INVENTORY-2026-09-17.md. The source is the truth.
1. The rule that makes this safe
[DESIGN] The Hungarian product must render byte-for-byte the same before and after every slice — measured page by page, never by reading. A household that never switches must not be able to tell a localisation release happened. English goes on top of an unchanged Hungarian product.
[FACT] It is enforceable, and enforced for the three converted pages: TestI18nParity
(felhom-controller/controller/internal/web/i18n_parity_test.go) renders 14 fixture states of the
launcher, /backups and /apps/<slug> in Hungarian and compares them with HTML captured from a clean
worktree of origin/main 89dd3e94b1de before any template carried a marker. One changed byte in
hu.json fails it naming the page and the line (red-proof,
felhom-controller/REPORT.md). Fixtures are captured from unconverted templates and never
regenerated to make a conversion pass — that is the release gate for every slice.
The one normalisation: relative ages („3 napja") become „# napja", because they come from the wall clock.
[FACT] And live, on demo-hp guest 9201 (2026-09-17, endpoint-level): the launcher, /backups and
/apps/privatebin fetched in Hungarian on 0.246.0 and again on 0.247.0 are identical apart from the
version string and the CSRF token, with equal raw byte counts (44 462 / 46 250 / 42 081), and again
after a round trip through English (audits/i18n-2026-09-17/live/).
2. The mechanism, as measured
2.1 What was chosen, and why
[DESIGN] A flat message bundle per language, expanded into the template BEFORE html/template
parses it. Decided by CC in the spike (the starter delegated the choice, §4 1.1 of the task).
| option | cost | verdict |
|---|---|---|
(a) golang.org/x/text/message catalogs + a T func |
a new external dependency; its value is gender/plural/ordinal selection | not needed: the inventory found no gender agreement and only count plurals; Hungarian does not inflect after a numeral |
(b1) flat bundle, runtime T template func |
every string passes through the contextual escaper: +, ', " change bytes; inside <script> a string becomes a quoted JS literal — 618 of the 1 867 template strings are in <script> |
rejected: breaks byte parity by construction |
| (b2) flat bundle, expanded at template LOAD | one parsed template set per language (startup parses every template once per language — not measured); a translation is raw source text, so its context safety must be tested | chosen: the Hungarian set is parsed from the same bytes, in the same escaping contexts — parity holds by construction and is then measured |
2.2 How it works
- [FACT] Bundles:
controller/internal/i18n/locales/hu.json(authoritative — every key) anden.json, embedded (internal/i18n/i18n.go). Flatkey → text. A value may carry template actions ({{.RecoveryAbandonDate}}) and inline markup (<strong>,<a>): those are the message's parameters, kept inside one message so a translator can move them. English plurals arekey.one/key.other(Bundle.Plural), and a template value may also branch ({{if eq $n 1}}file{{else}}files{{end}}). - [FACT] A converted template carries
{{T "key"}}.Server.loadTemplatesparses one set per supported language throughparseTemplateSet=ParseFS+i18n.Expandper file, naming templates exactly asParseFSdoes.s.tmplstays the Hungarian set, so every renderer that predates i18n is unchanged. - [FACT] An undefined key is left in place, so the set fails to load („function T not
defined") — loud at startup and in every render test (
TestExpandLeavesUndefinedMarker). - [FACT]
executeTemplaterenders dashboard pages in the request's language. Since slice 1 (v0.249.0 recovery, v0.250.0 the rest) the pages outside the dashboard chrome — login, claim, recovery, both guest share pages, the catch-all — render throughexecuteTemplateLang: the language set andLang, and deliberately NOT the session CSRF fields or the escrow reminder (R-543) thatexecuteTemplateadds. Pinned byTestDirectRenderHandlersFollowLanguage(the real routes) andTestI18nDirectRenderPagesHaveNoAdminChrome(reminder genuinely due, absent from the page data of all six and from the guest page in both languages). No renderer usess.tmpldirectly any more. - [FACT] Go-side copy on a converted page: the handler names its title key (
data["TitleKey"];TestHandlerTitleKeysMatchHungarianTitlepins each handler's Hungarian literal equal to its hu.json value); the non-Hungarian sets override the copy-producing funcsstateLabel,timeAgo,timeAgoStr,nextRunLabel,statusText,infraMetafrom the bundle (web/i18n_web.golocaleFuncs). The Hungarian funcs are not touched;TestLocaleFuncsHungarianBundleMatchesFuncMappins thathu.jsoncarries the same words they return.
2.3 What a translation must not do (pinned by tests)
- Carry different parameters. Same
{{.Field}}set and same printf verbs as the Hungarian (TestBundleParametersMatchAcrossLanguages). - Break its context. Expansion is textual; inside a JS string a bare
'/"/\or newline ends it, inside a double-quoted attribute a"ends it — andhtml/templatecannot see this, because it reads the expansion as the author's own source. A value may carry a quote only where the Hungarian carries the same one (TestI18nJSContextValuesAreSafe, red-proofed withCopied'). English therefore avoids apostrophes in JS contexts („do not", not „don't"); elsewhere it uses ’. - Render blank or raw. No raw key, no marker, no element empty in English that is not empty in
Hungarian (
TestI18nEnglishPages).
[FACT] What stays Hungarian on the English pages, live (v0.250.0, all 31 slice-1 templates converted):
Go view-model text (flash lines, errors, the backup-target banner and offer, the whole-guest tier
labels, the update badge „Naprakész", the catch-all's status sentence), three page titles built in Go
around an app name (logs, deploy, tier-2 settings — R-566), and catalog copy (tagline, use cases, first
steps) — slices 2 and 5. Deliberately left in a template: „Fut…" on backups_remote, which the page's
own JS reads (R-563). Evidence: audits/i18n-slice1-2026-09-17/{A,B,C}/live/.
[FACT] Measured limits of the English page test. (1) It subtracts fixture DATA strings before looking for Hungarian; since slice 1 release A only data strings of ≥ 2 words or ≥ 12 characters are subtracted, so a one-word template title is no longer masked. (2) It sees accented Hungarian only. An ASCII-only Hungarian word left in a template („mp", „ db", „jelenlegi:", „FIGYELEM:", „, majd a(z)") passes it on the English page; release C found six such fragments by eye, none by a test (R-565).
3. Who picks the language, and how it flows
[DESIGN] Operator decision 2 (2026-09-17, agreed): the operator sets it per customer on the hub at creation (default Hungarian); the household can switch it on their dashboard; the box reports it so the hub's e-mails follow.
- [FACT] Built in v0.247.0:
settings.jsonlanguage(hu|en; empty readshu,Settings.GetLanguage);POST /settings/language(session CSRF like every form;lang,back; redirect drops the query);?lang=hu|enper-request override, never persisted; the hub report's"language"field, always present, set at all four report build sites incmd/controller/main.go. - [FACT] Live 2026-09-17 on demo-hp:
POST /settings/language lang=en→ 302,settings.json"language": "en", the three pages English; the hub's stored reports read no field (0.246.0),hu,enat 12:54:43Z,huat 12:55:22Z after switching back; without_csrf→ 403 and nothing changed (audits/i18n-2026-09-17/live/README.md). - [FACT] Slice 3 Part A, hub v0.118.0 (2026-09-18): the hub READS the reported language and writes
the household's e-mails in it.
hub/internal/i18n(79 keys, hu authoritative, hu fallback, missing ceiling 0); 56 mail goldens captured from v0.117.0 BEFORE any string moved, and all 56 Hungarian ones pass unchanged.customerMessages/severityLabelsare derived from the bundle. Order: last reported →customer_configs.language→hu(Store.CustomerLanguage).message_customeris accepted onPOST /api/v1/eventfor the box's own sentences. The bind page is per-language, withexpiredpinned to Hungarian so the language cannot become the oracle the text refuses to be. Full design:05-hub-architecture.md§15. R-555 closed — thelanguageallowlist entry is out ofwire_contract_gate.pyand the gate now checks the field for real. - [DESIGN] Slice 3 Part A as planned — now built; the box half (Part B) is the remaining piece: the hub stores a per-customer language, renders it into
controller.yamlnext tocustomer.id/name/domain/email(hub/internal/configgen/configgen.go), and the box uses it only while the household has never chosen (settings.jsonempty). The household's own choice always wins; the hub's e-mails follow the language the box reports, which is therefore the household's choice. The hub decodes the report with independentjson.Unmarshalcalls and noDisallowUnknownFields(hub/internal/api/handler.gohandleReport), so the field was additive.
[DESIGN] Decided by CC in the spike — operator may reverse: the switch is SHOWN only on a page that
is not Hungarian, or on a request carrying ?lang=. One answerable sentence: should a Hungarian
household see an English switch while only three pages are English? Options: (1) show it to everyone
now — cost: one click from a half-English dashboard, and every page but three keeps a Hungarian body;
(2) hide it until slice 1 converts the remaining pages — cost: a household cannot discover English
yet, which no household has asked for. Chosen (2): it follows rule §1 (nothing visible changes)
and is reversed by deleting one condition in addLanguageData. The way back from English is always on
screen.
[FACT] SUPERSEDED 2026-09-17 by slice 1 release C (controller v0.250.0): every template is converted,
so the condition is deleted and the switch is on every dashboard page, in both languages. The 89
layout parity fixtures were re-captured and each equals its predecessor plus exactly one switch form
(audits/i18n-slice1-2026-09-17/C/switch-fixture-diff.txt); TestLanguageSwitch_EndToEnd pins that a
Hungarian household with no ?lang= sees it. Live on demo-hp: every Hungarian dashboard page equals
its 0.249.0 fetch once the switch form is removed (live numbers aside); login, claim and the catch-all
are unchanged. A Hungarian household sees it only when the fleet floor reaches 0.250.0 — the
operator's cadence (R-242/R-468); no floor was raised.
4. Fallback
[DESIGN] Operator decision 3 (2026-09-17, agreed): a missing English line shows the Hungarian one and is counted by a gate; a missing line never shows a key or an empty box.
- [FACT]
Bundle.Textfalls back to Hungarian and flags it; the loader logsi18n: en template set shows Hungarian for N markers;Bundle.Msgof a key absent everywhere returns the key (visible, never blank) andTestBundleKeysUsedExistInHungarianrefuses to ship one. - [FACT]
controller/scripts/i18n_missing_gate.py(incontroller_gates.py, pre-push): keys exist, no orphans, the English gap is a ratchet —EN_MISSING_CEILING(0 today) convicts above AND below, so it can only be lowered deliberately. Raising it is a decision recorded here.
5. The gates, per language
[FACT] Found in the spike: moving copy out of the templates blinded four gates and staled one.
The retrieval-promise gate went red on STALE allowlist entries the moment the first page converted;
the emoji, native-confirm and secret-in-markup gates silently stopped seeing the moved copy. The HEAD
versions of the emoji and retrieval gates PASSED a planted emoji and a planted retrieval promise in the
bundles. They now judge the page as rendered in each language (controller/scripts/i18n_bundle.py
read_template(path, lang)); mojibake scans the bundles; decoys for each are in
controller/scripts/test_gate_decoys.py.
[DESIGN] Voice, per language:
- hu — the product speaks in „te". The converted copy carries formal („ön") forms (R-516). They are
not fixed by a localisation release (§1), so the gate counts them against
HU_FORMAL_CEILINGas a ratchet: a new one convicts, fixing one lowers the ceiling. The ceiling followed the conversion: 6 (spike) → 10 (release A) → 12 (release B) → 16 (release C). [FACT] Measured limit: the stem list is short — on the release C pages it counts 4 formal forms, while a wider list („írja be", „adja meg", „válassza", „biztosan eltávolítja", „engedélyezze", …) finds at least 22 keys (R-516). - en — second person, plain: no „please", no „kindly".
[FACT] The retrieval-promise gate scans English too since slice 1 release A (EN_PATTERNS,
ALLOWLIST_EN, decoys). Translating exposed that its Hungarian stems miss split verbs („állíthatók
vissza") — R-564. The hub copy gate and the catalog have no language concept
(scripts/hub_copy_gate.py stems end in a Hungarian character class; catalog_gates.py checks no copy).
6. Deliberately out
- [DESIGN] The hub's operator pages — the operator reads them; they stay English (decision 1).
- [DESIGN] The first-boot wizard (
controller/internal/setup/, 8 pages, 95 strings) — out of scope and obsolete (decision 4, 2026-09-17;02-controller-module-map.mdL56). A household still reaches it when bootstrap ingestion leavescustomer.idempty (inventory §2.7). Its deletion is R-554. - [FACT] App containers' own UIs (Uptime Kuma, PrivateBin…) — not Felhom's copy (R-516 items 5-6).
7. The catalog's copy model
[FACT] MEASURED 2026-09-20 (controller v0.257.0 + catalog pilot). The proposal below was tried and it holds; what changed under measurement is written out, because the numbers it was specced against were wrong in two places.
A sibling block per language inside .felhom.yml, not a sibling file:
description: Titkosított jegyzetek…
app_info:
first_steps: [...]
i18n:
en:
description: Encrypted notes…
app_info:
first_steps: [...]
Why: one app's copy stays in one file a reviewer sees whole; the controller's yaml.Unmarshal ignores
unknown keys (internal/stacks/metadata.go L316 — verified: this repo constructs no yaml.Decoder
at all, so there is no KnownFields anywhere to turn strict), so older controllers are unaffected;
a missing English field falls back field by field.
[FACT] The read path is Metadata.For(lang) in controller/internal/stacks/metadata_i18n.go,
reached only through LocalizeStacks / LocalizeStackPtr / Stack.MetaFor.
For("hu")is the parsed struct withI18ncleared and nothing else, deep-compared against all 53 real catalog files (TestMetaForHuIsIdentity, fixtures ininternal/stacks/testdata/catalog/). §1 therefore holds by construction for catalog copy, andLocalizeStacksreturns the INPUT SLICE for Hungarian rather than a copy, so nothing on the Hungarian path is even touched.- Fallback is field by field, and blank counts as absent (ruling 3). A half-translated app is a legal state — that is what makes the batches pushable one at a time.
- The three prose lists replace WHOLE (
use_cases,first_steps,prerequisites): merging item 3 of one language with item 4 of another produces a list nobody wrote. Every other list is matched by its own key —deploy_fieldsbyenv_var, options byvalue,optional_configgroups bymatch_group(the Hungariangroupvalue they translate; a group has no other identity), its fields byenv_var, integrations bytarget, data paths bypath. Position matching mistranslates silently the first time a Hungarian field is inserted above another, and the page still looks right. Fornever writes through. The metadata lives in the stack manager's cache and is shared by concurrent requests; an in-place merge would put one household's language on another's page.- Two copy producers have no request and therefore no language — the integration rows
(
internal/integrations) and the initial-credentials note (internal/stacks/initialcreds.go). Both are re-taken from the localised metadata in the handler.
[FACT] The numbers, measured on 94bc5febaca2 — the inventory's were close on one count and wrong
on another, and both matter to the gate:
| inventory / task | measured | |
|---|---|---|
| copy strings | 835 | 1 032 (incl. 13 placeholders the table omitted, and every ASCII label) |
| with a Hungarian letter | 832 | 832 ✔ |
| ASCII-ONLY Hungarian | „Igen", „Nem", „Nincs" — three | ~120. Those three do not occur; the three ASCII-only OPTION labels are „Magyar", „Angol", „Magyar + Angol" — language names, not yes/no words |
The ASCII-only count is the one that decides an instrument: „Aldomain" (53×), „A szerver domain neve"
(53×), „Jelentkezz be: …" (5×), „Magyar", „Angol", „Titkos kulcs", „Oszd meg a linket". An
accent-only scan passes every one of them inside an English block — the same blind spot §2.3 records
for the English page test (R-565), arriving again in a different repo. So
app-catalog-felhom.eu/scripts/check-copy-i18n.py folds and matches word-bounded stems, with a
positive and two negative controls printed on every run.
[FACT] The gate, fifth row of catalog_gates.py, static, in the pre-push hook:
- FREEZE — every Hungarian copy string equals
scripts/copy_freeze/hu.json, captured in its own commit before any translation. Runs on all 53 apps whatever scope is named. A new app must be admitted with--add-app NAME --reason "…". - STRUCTURE — the
enblock may carry copy fields and nothing else; every key-matched entry must have a Hungarian twin, or it is INERT on the box and the translator never learns. - LANGUAGE — no accented letter, no ASCII-only Hungarian, no „please"/„kindly", no English retrieval promise the Hungarian does not make, the app name and „Felhom" preserved.
- CREDENTIALS — the login tokens inside
default_credsand the initial-credentials note survive translation verbatim, matched on WORD boundaries. - RATCHET —
EN_MISSING_CEILING, convicting above and below, counted over the WHOLE catalog whatever scope is named.
33 decoy cases (R-421). Three defects in the gate were found by its own decoys and not by reading it — R-592.
[FACT] What a gate cannot judge, and the pilot report lists per app: whether an app's „first steps" name the buttons that app's ENGLISH interface actually shows. The Hungarian was written against whatever interface the author had; the English UI label may differ. Those lines are marked unverified and belong to the slice-6 walk (R-561).
8. Dates, numbers, plurals — as found
- [FACT] Plurals: 42 Go format strings with
%dand 7 template runs with a numeric parameter (inventory §2.2/2.1). Hungarian needs one form; English two. - [FACT] Dates: 10 layout literals in
internal/webGo, 2 in templates that disagree with each other (2006. 01. 02. 15:04vs2006-01-02 15:04), 25 more in the controller; 9 copy-producing helpers (timeAgo„%d perce",nextRunLabel„ma"/„holnap",pruneLabel„vasárnap"…). - [FACT] Sizes print a decimal POINT (
%.1f GB) — Hungarian convention is a comma. The Hungarian pages are already un-Hungarian there; §1 keeps it that way until someone decides otherwise (R-562). - [FACT] Word order: 237 Go format strings and 111 concatenations; in templates, JS sentences split
around
+ name +(„Az alkalmazás (" + name + ") törölve lett.") — translatable but fragile. - [FACT] Flash messages travel inside the redirect URL (
?flash=<text>) in the language of the handler that redirected — slice 2 moves them to keys.
9. Compared, not shown — latent bugs a translation would trigger
[FACT] There were FIVE, and they are fixed (controller v0.251.0, R-553 + R-563, 2026-09-17). Four in Go and one in a page script. Each producer now attaches a machine-readable signal and each decision reads that signal; every Hungarian sentence is byte-identical (pinned per producer), and the hub report is unchanged.
| decision | read before | reads now |
|---|---|---|
deploy HTTP status — api.deployStatusFor |
„kötelező", „memória", "does not exist", "already deployed" | stacks.ErrRequiredField, ErrPathMissing, ErrNotEnoughMemory (400); ErrAlreadyDeployed (409) |
off-site failure class — ClassifyOffsiteFailure |
„tárhelykeretet" | backup.ErrOffsiteQuota |
alert placement — web/alerts.go |
„meghajtó" / „adattároló" in the warning | monitor.WarnKindStorageNotSeparate, carried in HealthReport.WarningKinds (internal; NOT on the wire) |
the stale off-site note — offboxWarningDisplay |
„nincs mentésre jelölt alkalmazás" in the PERSISTED text | settings.OffboxTarget.LastWarningKind = backup.OffboxWarnNoAppsSelected |
the remote-backup poll — backups_remote.html |
the displayed word „Fut" | data-status="running" on the status element (R-563) |
[DESIGN] The rule this establishes: a text signature may remain ONLY where the text is not ours.
The classifier's restic and ssh signatures stay, because that output is neither written nor translated
here; every sentence this product writes is display, and a decision reads a signal beside it.
internal/util.KindErrorf exists for exactly that: the same message bytes fmt.Errorf produced, plus
a sentinel for errors.Is — never fmt.Errorf("%w: …"), which would prepend the sentinel's own text
to the customer's sentence.
[FACT] One exception, with an end date: a box upgraded to 0.251.0 carries the OLD persisted note
with no kind until its next off-site run, so offboxWarningDisplay keeps the substring test for
kind == "" only. Slice 2 (R-557) must not translate that producer until R-570 closes.
[FACT] Measured after the fix (audits/r553-2026-09-17/): the inventory's comparison detector finds
no template compare (was one) and no Go compare of ours — the two remaining hits are the known
false positive (an [INFO] log line) and the deliberate legacy fallback above; planted decoys prove the
detector still convicts. What it does NOT cover: four API handlers pick their status by matching
ENGLISH internal words — the same shape, one language over, filed as R-569 rather than folded in here.
10. The plan
ALL SIX SLICES ARE DONE — slice 1 (2026-09-17, v0.250.0), slice 2 (2026-09-18, v0.254.0), slice 3 (2026-09-18, hub v0.118.x), slice 4 (2026-09-18, ISO 1.29.0), slice 5 (2026-09-20, v0.257.0 + the whole catalog), slice 6 (2026-09-20, v0.258.0 + the guide + the walk). What remains is written in §10.6b's residue table, and every line of it has a row.
Costs are CC-hours, estimated from the spike (mechanism + three pages + layout + gates ≈ one working session). Each slice ends with the parity gate green for every page it touched.
| slice | row | what | proves | cost |
|---|---|---|---|---|
| 0 (done) | — | mechanism, 3 pages + layout, setting, report field, gates | Hungarian unchanged by measurement; English reachable | spent |
| 1 (done, v0.248.0–v0.250.0) | R-556 | the other 31 dashboard templates (~1 500 strings, 600 of them JS), parity fixtures per page; switch shown to everyone; English retrieval stems; the extractor's ASCII word list reviewed per page | the whole dashboard in English with Hungarian byte-identical | 12–16 h, three releases |
| 2 (done, v0.252.0–v0.254.0) | R-557 | Go-side customer strings; flash-in-URL → keys; country names; alert texts; the 179 error messages; the saved notes; the globe | a page's server messages follow the language | CLOSED 2026-09-18 |
| 3 | R-558 | hub: per-customer language at creation; configgen renders it; customerMessages (39), the 5 lifecycle mails and the self-bind page in English; dispatcher reads the reported language |
a household's e-mails arrive in its language | 6–8 h, one hub + one controller release |
| 4 | R-559 | console banner (34 lines, console-font limits) and the download page | an English household meets English from the first boot screen (in scope — ruling 1b) | 4–6 h + an ISO train |
| 5 | R-560 | catalog: §7 format, controller reads it, 835 strings / ~5 000 words, catalog copy gates per language | an app card, its settings and its first steps in English | 12–16 h |
| 6 | R-561 | the volunteer guide in English (~1 600 words); then a stranger's first hour in English, the 2026-09-14 walk; closes R-516 against the inventory | a newcomer can set up and use a box in English | 6–8 h |
10.1 Slice 2, as measured (release A, controller v0.252.0, 2026-09-18)
[FACT] The counts the plan carried were the inventory's, and they moved. At slice 2's base commit
736f54b49610 there were 1 120 Hungarian Go literals, not 1 141; handlers.go 120 (not 121),
offbox_handlers.go 70 (not 72), storage_handlers.go 45 (not 48), api/router.go 18 (not 19).
Release A converted 226 of them and added 237 country keys.
[DESIGN] A flash is a KEY, because it is read by a different request than the one that wrote it.
?flash=<key>&fa=<param>…, built by flashQuery, resolved by s.flashFrom. A value the bundle does
not know is shown verbatim — that is what keeps a link already in a customer's tab, a bookmark or
a back-forward cache reading correctly, and it is the same rule that makes a hand-typed ?flash=
harmless (TestFlashKeyRoundTrip). The same shape one layer in: an alert banner is built by a
background health cycle and read minutes later, so Alert carries MessageKey + MessageArgs and
GetAlerts(lang) renders on the way out.
[DESIGN] Word order is Go's explicit argument index, not a second placeholder syntax. The plan
proposed a named-parameter ({{.Name}}) form for multi-parameter Go messages. English reorders with
%[2]s, which fmt already understands, so the Hungarian value stays the format string the code
always had, byte for byte — and that is precisely what the parity gate compares. A second syntax
would have needed an exception in the measurement, which is the thing this slice cannot afford.
TestBundleParametersMatchAcrossLanguages counts an indexed verb as one verb.
[FACT] Parity for Go copy is measured, not read. controller/scripts/i18n_go_parity.py freezes
every Go string literal at the base commit (7 467, scripts/i18n_go_base.json) and refuses a key
whose Hungarian is not that text byte for byte — reworded, re-punctuated, split or joined.
scripts/i18n_go_keys.json records what each key replaced (a string for one literal, an ordered list
for a join, {"inside": …} for the three sites where the Hungarian was already URL-escaped inside a
redirect target). In controller_gates.py; three decoys, each seen to convict.
[FACT] The gate found a live defect in its own first version — the R-565 class again. The 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 gate refused the keys citing them. The filter is gone. The index answers one question — did the base commit contain this text? — and an unfiltered index answers it for every string. The lesson generalises: a word list is a list, and nobody knows which words are missing.
[FACT] Two claims in the plan that live source disproved, both about the wire.
cloudflare/countries.gois not on the wire.report/builder.gocarries country codes; no name leaves the box, andCountryName()has no caller. The 237 names were therefore free to translate, and are — at DISPLAY, inapi/geo.go, re-sorted per language. The table stays: it validates codes.- The hub does not compose every customer mail from the event kind.
FormatCustomerEmail(hub/internal/notify/templates.goL167-210) usescustomerMessages[eventType]when it has one and falls back to the controller's message when it has none, appending it as „- Üzenet: %s" whenever the two differ; several event types carry no entry precisely so the controller's sentence IS the mail. Sonotify/notifier.go's 31 messages are customer-facing text on the wire. The verdict (do not translate them here) was right; the reason was not, and the reason is what slice 3 has to act on.
[DESIGN] On the wire = not translated, and now measurable. internal/monitor and
internal/notify carry wire goldens capturing those producers' exact bytes at the base commit. A
later slice that translates one fails a test that prints both strings. R-558 is the row that unfreezes
them, by teaching the hub which language to compose in.
[FACT] Still Hungarian after release A: 894 literals — 176 of them fmt.Errorf/errors.New
(release B), the text a background run persists (release C), the R-570 producer, handler_debug.go
(R-574), two copy-producing funcmap helpers (R-572) and the two channel-health banners (R-573).
[DESIGN] Persisted text (release C) — the plan's §16 option 1, its own stated default. A note is written in the box's language at the time it is written, and a household that switches sees last night's note in the old language until the next run rewrites it. The alternative (store a code, render live) costs a dozen new persisted fields and a legacy path for each — the R-570 shape, a dozen times over. Recorded here when release C ships.
10.2 Slice 2 release B — an error carries the key of the sentence it is (v0.253.0)
[DESIGN] An error is made where there is no language and printed where there is no key, so it
carries the key across. util.MsgError(key, args…) / MsgErrorf(kind, key, args…). All 179
Hungarian error literals are converted; zero remain (an ASCII-fragment search with a positive and a
negative control, case-insensitively, plus an accented-letter search).
Four properties, each one a different failure if it is missing:
| property | the failure without it | pinned by |
|---|---|---|
Error() is the Hungarian, byte for byte |
179 producers could not be converted until every printer was, in one commit | TestMsgErrorKeepsKindAndHuText |
errors.Is answers for the kind and a wrapped cause |
KindErrorf returned the kind alone; a caller testing for the cause silently stops matching |
TestMsgErrorUnwrapsTheCauseToo |
| an error ARGUMENT renders recursively | „formázás sikertelen: %w" is a sentence wrapping a sentence; half of it would stay Hungarian | TestErrTextRendersAWrappedMessageErrorToo |
| a FOREIGN error prints verbatim | restic, docker, ssh and the stdlib are not ours (§9's rule) | TestErrTextFallsBackVerbatim |
[DESIGN] Plurals are a BUNDLE rule, not a call-site flag. A key that carries .one/.other
forms in a language is a plural key, and its FIRST parameter is the count (i18n.Bundle.form).
Hungarian never carries them, so a Hungarian render is unchanged at every count. One answerable
sentence: where does the fact that English needs two forms live? Options: (1) a flag at every
producer of every count message, in three packages — cost: the one somebody forgets reads „3 app is
not running", with nothing to catch it; (2) the bundle. Chosen (2): the bundle is where a
translator works, and it needs no change at any call site. .one/.other become RESERVED suffixes —
TestNoOrdinaryKeyEndsInAPluralSuffix, which caught a real collision (alert.deadapp.one) the day
the rule landed.
[FACT] The parity gate has a blind spot, measured rather than reasoned (R-576). The bulk
converter silently dropped the continuation of a multi-line concatenation, damaging 7 producers —
and i18n_go_parity.py stayed GREEN, because every surviving fragment WAS a byte-equal base-commit
literal. Its question is "is this text real?"; it cannot ask "did the call keep all of it?". Two
BEHAVIOUR tests caught it, because they assert the sentence a customer reads. A structural gate over
the TEXT cannot see a defect in the CALL — the general form, and the reason a rendered test is not
made redundant by a gate.
[FACT] A second instrument defect, same session (recorded because the shape recurs). The script counting what was left was case-sensitive, so it reported "0 error literals remain" while five did („occ parancs sikertelen", „hub hiba", „OnlyOffice aldomain nem ismert" ×2). That is R-565's shape inside the measurement. Every "no Hungarian left" claim in this slice is made case-insensitively and with both controls.
[FACT] One gap release B could not close: R-575. memoryVerdict's soft overcommit WARNING is a
string with no error to carry a key and no language where it is built, so it renders Hungarian on an
English page. The copy is in the bundle; only the render is fixed. Named in the code, filed as a row.
10.3 Slice 2 release C — the saved notes, and a globe (v0.254.0). SLICE 2 CLOSED.
[DESIGN] A note a background run SAVES is written in the BOX's language at write time (operator
ruling, §16 option 1). ~70 producers. The consequence, recorded because it is the cost of the
choice: a household that switches language sees the previous run's note in the old language until
the next run rewrites it — usually the next night. The alternative (store a code, render live) needs a
dozen new persisted fields and a legacy path for each: the R-570 shape a dozen times over.
EndRestoreOp now receives no Hungarian literal from anywhere.
[FACT] A deadlock, introduced and caught by the suite hanging (R-578). UpdateOffboxStatus holds
the settings WRITE lock while running its callback; boxLang() reads the language through the READ
lock; sync.RWMutex is not reentrant. A note rendered inside that callback deadlocks holding the
settings lock, which wedges everything else on the box that touches settings.json. The only
symptom was go test going from 8 minutes to a 25-minute timeout. The general rule this establishes:
a helper that takes a lock must never be called from inside a callback that holds one — and a hang
is the worst symptom to diagnose, which is why R-578 asks for a gate rather than one test in one package.
[DESIGN] Decision 6, superseded a second time: the switch is a GLOBE, and a visitor's language is
their own. One answerable sentence: how does someone who cannot read Hungarian find the way out of
Hungarian? Options: (1) keep two text links („Magyar"/„English") in the sidebar footer — cost: they
wrap at the sidebar's width, and finding them means recognising two words as links; (2) one globe, the
symbol every web user already reads as "language". Chosen (2), <details>/<summary> so the menu
needs no script and a screen reader announces it. Language names inside are shown in their own
language and are never translated. Drawn inline rather than in the icon sprite, because the sprite
lives only in layout.html and the pages outside the dashboard chrome have their own shell.
[DESIGN] Who the page is FOR decides where its globe posts, and getting that wrong makes the button do nothing.
| page | reader | globe posts to | their choice lives in |
|---|---|---|---|
| every dashboard page | the household, signed in | /settings/language (session CSRF) |
settings.json |
/recovery — an AUTHENTICATED route |
the household, signed in | /settings/language |
settings.json |
/login, /claim |
a visitor, no session | /lang (no CSRF) |
the felhom_lang cookie, their browser |
| the two guest share pages, the catch-all | a stranger / nobody | no globe | — (R-577, the operator's) |
langFor's order is fixed: ?lang= → the household's setting when a session exists → the cookie
when there is none → the setting → hu. A signed-in household never reads the cookie, so they
cannot inherit a language a previous visitor picked in the same browser. The recovery row above was
measured live before it was right: an anonymous form there sets a cookie that langFor then ignores,
and the button appears to do nothing.
[FACT] Where the globe SITS, and a stale-stylesheet trap that only a screenshot could show
(v0.255.0, R-579). On the pages outside the dashboard chrome the globe is inside the card, centred
under the footer, with the menu opening upward — the shared .lang-globe-menu rule, so the two
surfaces cannot drift apart. It was first placed at the corner of the VIEWPORT, which read as a stray
browser control rather than part of the page. And five of those shells requested style.css with no
?v=, so a browser holding a copy from before the globe existed kept serving CSS with no
.lang-globe rules and it rendered as a bare, unstyled <details>. Every test passed, because they
all read the markup and the fault was in which CSS file the browser fetched. Version is now set in
executeTemplateLang, once, for every shell.
[DESIGN] POST /lang is CSRF-exempt, for a reason narrow enough to check. The only achievable
effect of a forged request is to change the language of the page the victim's own browser shows them.
It writes one display-only cookie, reads nothing, touches no setting, and safeBackPath refuses a
protocol-relative //host as well as an absolute URL — "starts with /" alone is not the test,
because a browser reads //evil.example as another origin. If that handler ever gains a second
effect it needs CSRF that day. That is the anonymous surface 04-control-plane-authorization.md
governs: changing what a visitor reads is within it, changing anything the household owns is not.
[DESIGN] §16, the operator's default, taken: a successful CLAIM carries the visitor's language into the household's setting. Someone who switched the claim page to English and then claimed the box chose English. Only on success, and only there — the one moment an anonymous visitor becomes the household.
[FACT] The parity exceptions, measured rather than asserted. 106 fixtures, a real (LCS) diff
against the fixtures as they stood at v0.253.0: 3 change shapes (the footer; the recovery globe;
the login/claim globe) and 5 byte-identical, which are exactly the pages that must not change.
audits/i18n-slice2-2026-09-18/C/parity-exception-diff.txt. A second measurement error worth
keeping: the first attempt compared LINE BY INDEX, and an insertion shifts every line below it — it
reported 60 520 changed lines and measured nothing. A line-index compare is not a diff.
10.4 Slice 3 — the hub's e-mails follow the household (hub v0.118.x, controller v0.256.x). SLICE 3 CLOSED.
SHIPPED 2026-09-18. Full design: 05-hub-architecture.md §15.
The hub half (v0.118.0, v0.118.1). hub/internal/i18n, 79 keys, hu authoritative, hu fallback,
missing-key ceiling 0. Every customer mail and the public bind page render from it.
customerMessages/severityLabels are DERIVED from the bundle, so a sentence is written once and
"a new event type enters allowedEventTypes and customerMessages together" now means a line in
hu.json plus its English twin. 56 mail goldens per language, captured from v0.117.0 before any
string moved; all 56 Hungarian ones pass unchanged. Language order: last reported →
created-with → hu; reports.language defaults to EMPTY (never told us ≠ chose Hungarian) and
the newest report is found by the autoincrement id, because received_at has second granularity.
The bind page's expired state always renders Hungarian — it is the state an unknown token lands
in, and the language must not answer what the text refuses to.
The box half (v0.256.0, v0.256.1). message_customer on POST /api/v1/event: the same sentence
in the household's language, omitempty, beside the unchanged Hungarian message. A Hungarian
household sends no second copy at all, so the fleet's payload is byte-for-byte what it is today and
the hub's fallback path stays the one production exercises. 19 producers render both from ONE bundle
key; the Go parity gate checks all 19 against the base-commit literals. customer.language
bootstraps a new box (stored choice → config → hu) and is never persisted into settings.json.
v0.256.1 added the one observable the feature was missing: the push log says [hu-only] or
[+household(en)].
Proven live, twice. Part A: a real English e-mail read in the operator's inbox and the Hungarian
one 74 seconds later, same button, same box. Part B: the same event pushed 57 seconds apart showing
[hu-only] then [+household(en)], with an identical Hungarian sentence both times.
What is still Hungarian for an English household: six producers whose sentence arrives already
finished from another package (R-585) — of which offbox_enlarge_blocked matters most, because it
has no hub entry so its raw sentence IS the mail. Plus the R-570 sentence. The 15 operator-tier types
are Hungarian by design.
Gaps filed: R-581 (same-second report ties, and GetCustomers() still has the shape), R-582 (an
English copy-guard ported word-for-word convicted 141 honest sentences), R-583 (the test mail was the
one mail that did not follow the language — and the one an operator would use to check), R-584
(credential-bearing probes left in a live guest's /tmp), R-585. R-555 closed.
2026-09-17 (on the starter)
- Scope: what the household sees — the controller, its e-mails, the guide, the app catalog. Not the hub's operator pages.
- Who picks: the operator per customer at creation (default Hungarian); the household switches on the dashboard; the box reports it so the hub's e-mails follow.
- Fallback: a missing English line shows the Hungarian one and is counted; never a key or a blank.
- The first-boot wizard: out of scope, obsolete — a separate row to delete it (R-554).
Decided by CC in the spike — operator may reverse
-
Mechanism (b2) — §2.1.
-
Switch hidden while only three pages are English — §3.Superseded 2026-09-17 by slice 1 release C (v0.250.0): every template converted, switch shown to every household (§3). Superseded again 2026-09-18 by slice 2 release C (v0.254.0): the switch is a GLOBE, it is on the sign-in and claim pages too, and a visitor's choice lives in their own browser (§10.3). -
A successful claim carries the visitor's language into the household's setting (§16 default, taken 2026-09-18). Only on success; every other anonymous request leaves
settings.jsonalone.
2026-09-17 (evening) — the two open questions, ruled
1b. The console banner and the download page ARE in scope (operator: „yes"). Slice 4 (R-559) is unblocked; it stays late in the plan and rides an ISO release train. 7. Interface nouns translate (operator: „translate"). „Indítópult" → Launcher, „Vezérlőpult" → Dashboard, „Biztonsági mentés" → Backup, as the spike built. App names and „Felhom" stay as they are. Every slice follows this.
10.5 Slice 4 — the console banner and the download page (ISO 1.29.0 source, R-559)
SOURCE SHIPPED 2026-09-18. The image is built but NOT published — that is an operator step; see
documentation/audits/i18n-slice4-2026-09-18/README.md.
The box's screen is BILINGUAL, and that is a decision rather than a stage. At the moment the
pairing code is shown, nobody has told the box who owns it — there is no language to follow. So the
three console texts carry the Hungarian block byte-for-byte as before, then one blank line, then an
English block, inside the same frame: print_pairing_banner, print_bound_banner and
install_felhom_issue (with the postinst's byte-coupled copy of the issue text). The GRUB entries
gain an English half. §16 default taken: bilingual for good — it needs no hub change, no protocol
field, and it has no way to show the wrong language.
The Hungarian is a golden, not a grep (scripts/iso/test/golden/*.hu.txt, captured before any
English existed). The harness also pins: no Hungarian letter in the English block (with the Hungarian
block as the positive control), every line ≤ 80 columns, and the whole paint ≤ 25 rows. The
pairing banner is 24 rows on a 25-row console — one row of margin, which is why the height is
pinned: two more lines and the Hungarian pairing code, at row 5, scrolls off the top.
The download page has an English twin at felhom.eu/en/download, linked both ways with
hreflang. The rest of the marketing site stays Hungarian (ruling 1b). A site gate refuses the two
pages naming different installer files or checksums.
The release gate had to be amended. G16 required every Felhom-authored string to be Hungarian and would have stopped this publication; ruling 1b supersedes that scope, so G16 was rewritten rather than waived — Hungarian FIRST, pinned by the golden.
Gaps filed: R-586 (the ISO harness had been red for two days and is in no gate), R-587 (root-password files in the publish source directory), R-588 (release records live in two places).
10.6 Slice 5 — the app catalog's own words (controller v0.257.0 + the catalog pilot, R-560)
PART A + PART B SHIPPED 2026-09-20. PART C — the other fifty apps — is deliberately not started: the §16 default was to stop after the pilot for the operator's read, and nobody has read it yet.
The format is no longer a proposal — §7 is now measured. The controller reads the block; three apps carry one; the fleet is unaffected until the floor moves.
What was proven, live, rather than reasoned about:
- The English pages show the English. On demo-hp (0.257.0) the app pages for privatebin, paperless-ngx and romm, the Apps list and two deploy pages render the catalog's English text.
- The Hungarian did not move. The same seven pages fetched in Hungarian before and after the catalog push are byte-identical apart from the per-session CSRF token — equal raw byte counts (43 253 / 41 532 / 41 547 / 147 121 / 74 326 / 70 290) and equal hashes once the token is normalised.
- An old controller ignores the block. demo-felhom runs 0.255.0. With the pilot synced onto that box — confirmed positively: the sync named the three apps and the block is in both the cache and the stack copy — its pages hash identically before and after, and its log carries no parse warning in a 93-line window that includes the sync lines, so the absence is a fact and not a dead log.
What the English pages still carry in Hungarian, measured with an ASCII-folded scan plus an accent scan, both controlled: exactly three things, and one of them is correct.
- The language picker's own „Magyar" button — a language picker names each language in its own tongue. Not a defect.
- The update badge „Naprakész" and its tooltip — R-589. Slice 1 listed it (§2.3); slice 2 was to take it and closed without it.
- The data-folder card's consequence sentence — R-590, and this one makes a promise about the customer's files while the label above it is already English.
Everything else Hungarian on an English Apps LIST is simply the fifty apps nobody has translated yet.
Gaps filed: R-589, R-590, R-591 (Stack.Copy() has one shallow field and it is the new one),
R-592 (three defects inside the new gate, each found by its own decoy).
Evidence: documentation/audits/i18n-slice5-2026-09-20/.
PART C SHIPPED THE SAME DAY. The operator read the pilot's English and said go, so the other fifty followed its voice in three pushes — 17, 17 and 16 apps; 319, 317 and 306 strings. 1 031 of the catalog's 1 032 customer-facing strings now carry an English twin.
[DESIGN, decided by CC] The blocks are GENERATED from a flat {path: english} map, not
hand-written. Fifty nested blocks whose keys must match the Hungarian side exactly is fifty
chances to mistype an env_var — and a mistyped key is INERT on the box, not an error, so the
translator never learns. The generator builds from the same flat paths the freeze uses, which are
derived from the Hungarian file itself, so a key that does not exist on the Hungarian side cannot be
written at all. The review then happens on the map, where one line is one string, instead of on
YAML indentation.
[FACT] The one string left untranslated, on purpose. papra's
deploy_fields[AUTH_SECRET].description reads „Az alkalmazás aldomainje" — the sentence that belongs
on SUBDOMAIN, sitting on a session-signing key (R-593). §1 forbids changing the Hungarian in a
localisation release, and translating a wrong sentence faithfully would ship the error in a second
language. So it falls back. That is why EN_MISSING_CEILING's floor is 1 rather than 0, and it is
written into the ceiling's own comment so a later session does not "reach zero" by editing
Hungarian.
[FACT] Measured on the English Apps list, all 53 apps: ZERO Hungarian app descriptions. The only Hungarian left on that page is the „Naprakész" badge (R-589, ten occurrences) and the language picker naming itself. The Hungarian Apps list is identical to the pre-slice capture once the per-session CSRF token and Docker's own „Up N hours" string are normalised.
[FACT] The gate convicted two of the translator's own sentences and was HALF right. Vaultwarden's
invite step ended „…can open an account", and the English retrieval-promise pattern reads can … open as the claim that sealed backups can be opened. Opening an ACCOUNT is not that claim, so the
conviction was a false positive — and the wording was also the weaker wording, so it became „can
sign up". But the gate has no way to REGISTER a true occurrence, which the shared vocabulary's
own design calls for; R-594.
[FACT] The fleet floor was raised to 0.257.0 the same session (operator asked), min_agent
0.131.0 declared — above the vouched golden 0.246.0, so the declaration is what carries it (R-472).
Hub: managed floor SERVED for demo-felhom: floor 0.257.0, agent requirement "0.131.0" from declared (golden 0.246.0). demo-felhom went 0.255.0 → 0.257.0 by itself in under 12 seconds, healthy,
its own log reading settle-gate: GO — at/above floor 0.257.0, and it then rendered the English
tagline — the floor delivered function, not just a version string. Peti's box and tester-1 are DOWN
and take it unattended when they return; untested on this version.
10.6b Slice 6 — the guide in English, and a stranger's first hour (controller v0.258.0, R-561)
SHIPPED 2026-09-20. All six slices are now done. The slice had three parts and all three landed: the four leftovers slice 5 found live (v0.258.0), the guide's English twin, and the walk.
Part 0 — the four leftovers. R-589 (the update badge) and R-590 (the data-folder card's backup
promise) went through localeFuncs and s.msgLang; R-573 (the agent-channel and endpoint-drift
banners) is now keyed by the checker's own CLASSIFICATION, with the composed sentence kept as a
fail-open fallback. R-572 was not what its row said — measured, no template and no Go file called
pruneLabel/nextPruneLabel; they were dead func-map entries returning Hungarian, so they were
deleted, and the deletion is fail-loud (a template naming a removed func panics loadTemplates
at startup, proven).
Part A — the guide. runbooks/VOLUNTEER-first-hour.en.md, a twin: 16 sections in the same order,
identical step counts, table rows and warning blocks per section (0 sections differing in structure).
Word counts are NOT a twin — English runs 19 % longer overall and up to 42 % on the short
sections, because Hungarian is agglutinative; the ±15 % criterion the task set does not survive
contact with this language pair, and structure was measured instead. The walk then corrected the
guide in five places — it had been written from the bundle, and the bundle is not the screen.
Part B — the walk. audits/DRILL-first-hour-en-0258-2026-09-20.md. The verdict:
not yet ready for an English-speaking tester, because the claim page answers in Hungarian (R-596) —
everything else held.
[FACT] What stays Hungarian for an English household — built ONLY from what the stranger saw:
| what | why | row |
|---|---|---|
| the claim page's messages („Hibás vagy lejárt kód", „A jelszónak legalább 12 karakter…", the lockout) | composed sentences passed into page data, the R-573 shape one screen earlier | R-596 (P1) |
| the setup code itself (and the recovery code and owner passphrase) | one Hungarian wordlist, not chosen per language | R-597 |
| the Backup page's two protection warnings and its two target names | composed sentences in backup_handlers.go / backup_target_offer.go |
R-598 |
| the menu word "Debug" | it is already English; the complaint was the Hungarian household's | R-516 item 1 |
| the operator's copy of every event | operator-tier is Hungarian by design (ruling 1) | — |
| the 18 formal „ön" forms | a localisation release may not change Hungarian bytes (§1); counted, ratcheted | R-516 |
| the apps' own English UIs | not Felhom's copy — and for this reader an advantage | — |
That is the honest residue. Everything not in this table — the download page, the console, all three customer mails, the bind page and its refusal, the dashboard, Apps, Storage, Monitoring, the Launcher, both app pages, the whole catalog, the language switch — was English with zero Hungarian lines on a box installed from scratch that day.
R-516 does NOT close, and audits/i18n-slice6-2026-09-20/R-516-item-by-item.md says why item by
item: more than half its twelve items are about what a Hungarian household reads, and an English
walk cannot see them.
R-214 closed as a side effect — the console's last paint is now the bilingual "the box is linked" banner. The 2026-09-14 walk recorded it as still reproducing.
11. Operator decisions
These are rulings, not proposals. Anything specced against a different assumption is wrong.