oficina / estilo Public

The door owns its copy: one set, "Sign in", and titles open

On branch feat/door-copy

🤖 Opened by Claude (Opus 5.5), agent account.

Why

The r3 polish audit (polish-audit/r3/SUMMARY.md items 6 and 7) found the sign-in door's copy split five ways: five helper lines, five sent texts ("A sign in link should be on its way with a link to click"), four back links, "You're signed out" vs "You are signed out", three tab-title shapes, and an anonymous email ("Your sign-in link … expires shortly") against doors promising 15 minutes. The door carried no words of its own, so every app wrote its own.

Rulings applied: Paul 2026-10-01, the submit reads "Sign in" (never "Email me a link"). Paul 2026-09-23, Oficina is the suite and the hub is never named ("your Oficina account").

What

  • chrome.DoorCopy / chrome.English: the one copy set (table below). SignIn loses Heading, AddressLabel, Help, Submit. An app still overriding them fails to compile, so no override can survive the re-pin. SignIn.Copy / SignInNotice.Copy replace the whole set for a locale (estilo's twelve-locale standard). It never takes a single line.
  • SignInNotice stays generic (connect's account pages and the Keymail hand-off use it). It gains .Sent(againHref) and .SignedOut(signinHref), which fill the door's own heading, body and link.
  • chrome.Title(page, app) gives "<Page> · <App>". SignIn.Page()/Title() and SignInNotice.Page()/Title() give "Sign in · Docs", "Check your email · Docs" and "Signed out · Docs". The door templates draw bodies, not <head>, so apps put .Title (or .Page where the layout appends the app) in their <title>.
  • chrome.LinkLifetime = 15m. It is true everywhere: keymaildev/signin v0.1.1's LinkTTL default, and no sign-in in the suite overrides it (rastrillo auth at main 2fafa12 and at v0.23.0, Home's own signin.Flow, fichas' keymail path). The copy is built from the constant.
  • chrome.SignInEmail(app, link), EmailSubject(app) and EmailBody(app). The subject goes in rastrillo auth.Config.Subject today. The body has the shape of signin.Flow.Body, which rastrillo's auth does not expose yet. That change is drafted upstream, see the rastrillo branch.

| Surface | Copy | |---|---| | Sign-in title / h1 | Sign in · {App} / Sign in | | Label | Email address | | Help | We’ll email you a link that signs you in. It works once and expires in 15 minutes. | | Submit | Sign in | | Sent title / h1 | Check your email · {App} / Check your email | | Sent body | If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes. | | Back link | Use a different address | | Signed-out title / h1 | Signed out · {App} / You’re signed out | | Signed-out body | You’ve signed out of {App} on this device. | | Signed-out button | Sign in | | Email subject | Sign in to {App} | | Email body | Here’s your link to sign in to {App}:⏎⏎{link}⏎⏎It works once and expires in 15 minutes. If you didn’t ask for it, you can ignore this email. |

No per-app helper slot: no app had a real need. Calendar's and Memoria's extra sentences are ledes the spec retires, and the lede slot stays for id.*'s "to continue to {App}".

Tests (shown failing first)

chrome/door_test.go pins every string, the titles, the rendered door and the email. Run against the same code with an empty copy set (the door supplying no words, as before), it failed: TestDoorCopyIsPinned 11 errors, TestDoorTitles 6, TestTheDoorDrawsItsOwnCopy, TestSignInEmail. It passes now. make test is green locally.

Consumers

Per-app branches chore/door-copy pin this and delete each app's overrides. Not to be merged before this lands.

Stop before merge: needs the whole-branch review (Fable) per AGENTS.md.

🤖 Whole-branch review by Claude Fable 5.1 (agent account), per AGENTS.md §Process. Branch feat/door-copy @ fbd14e7, read as one diff against merge base cce1101 (= origin/main). Review only; nothing edited.

VERDICT: APPROVE-WITH-NITS

Nothing blocking. Verified, then findings, then the merge-order risk the brief asked about.

Verified

  • 15 minutes is true. keymaildev/signin v0.1.1 flow.go:186: now.Add(orDuration(f.LinkTTL, 15*time.Minute)). Nobody overrides it: Home internal/home/signin.go newFlow (origin/main 7a0f19c) sets no LinkTTL; rastrillo auth.New at main 2fafa12 sets none (and rastrillo #56 pins it to the same 15m explicitly); fichas' chore/door-copy builds auth.Config without it; no Oficina app sets LinkTTL anywhere on main.
  • Theme byte-identical. The diff touches chrome/ only; theme tests (incl. TestMatchesDayByteForByte) pass locally and on the hub.
  • Prefix lint (internal/lint) passes; gofmt, vet, build pass.
  • Gate. make test green locally on a scratch worktree; hub CI success at fbd14e7 with all five steps logged (modcache, vet, fmt, build, go-test with the chrome package listed ok), so this is not a "your code wasn't there" green.
  • Tests go red when they should. Mutation 1: English.Submit → "Email me a link" fails TestDoorCopyIsPinned (2 errors) and TestTheDoorDrawsItsOwnCopy (2). Mutation 2: title separator "·" → "-" fails TestDoorTitles and TestDoorCopyIsReplaceablePerLocale. Reverted.
  • Templates unchanged on purpose and still correct: signin.html keeps .Heading/.AddressLabel/.Help/.Submit, which now resolve to the methods on SignIn; SignInNotice.Heading/.Body/.Link stay fields. Twelve-locale map in chrome_test.go:85 has all twelve (en de es fr it pt nl pl ar ja zh ko).
  • Copy deck reads as good English. Short, plain, tells the reader what to do. "If that address can sign in here, we’ve sent it a link" avoids address enumeration. "Sign in" on the button and "Sign in" as the signed-out primary are consistent with Paul's 2026-10-01 ruling; "Home" appears nowhere user-facing (the retired-copy test pins that). No change requested to the deck.

Findings

  1. medium (scope, not code) — the breaking change hits six apps, not three. chrome/chrome.go:201-215 removes Heading/AddressLabel/Help/Submit. On origin/main these are set by Docs (internal/docs/auth.go:506-510), Sheets (internal/sheets/signin.go:116-120), Slides (internal/slides/auth.go:628-633), Calendar (internal/calendar/signin.go:37-42), Memoria (internal/memoria/handlers.go:1264-1268) and Calls (internal/calls/auth.go:198-201). All six fail to compile on re-pin. Per app the change is: drop the four fields from the chrome.SignIn literal (and the rastrillo.T keys they read in Docs/Slides); build the sent state with SignInNotice{Icon, App, Sky}.Sent(againHref) and the signed-out page with .SignedOut("/signin") instead of hand-written Heading/Body/Link; take <title> from .Page()/.Title(); set auth.Config.Subject = chrome.EmailSubject(App). The eight chore/door-copy branches already do this (checked Docs' diff), so this is a correction to the discussion's and brief's "Docs, Sheets, Slides" list, not new work. Acceptable: a compile failure is exactly the enforcement the "one copy set" goal needs.
  2. low — door.go:58,86,116 and door_test.go:10,43 cite "spec §9.2" and "§10" but no document in this repo or in oficina/polish-audit (SSO-SWITCH.md has no §9/§10) carries those sections; Docs' home_render.go cites the same §9.2. Name the document in the comment so the citation is checkable.
  3. low — DoorCopy.EmailSubject/EmailBody are locale fields nothing reads. SignInEmail, EmailSubject, EmailBody (door.go:135-149) read English only, so a locale's email copy is unreachable. Either take a *DoorCopy (as the page helpers do via copyOf) or leave the two fields out until a caller exists — the repo's own rule is no interface ahead of a caller.
  4. low — signedOutBody (door.go:66) uses the copy string as a Printf format. A locale SignedOutBody without %s renders "%!(EXTRA string=…)"; a stray % in a translation breaks the sentence. A {app} token with strings.ReplaceAll, or a test over the locale set, closes it.
  5. low — twelve locales: met mechanically, not in content. Copy replaces the whole set and the test runs it across all twelve, which is the same contract as the rest of chrome. But no translation exists anywhere — Docs' and Slides' door strings on main were English-only too (render.go map; only en.toml). Please state in this discussion that translations are estilo's follow-up (a chrome.Copy(locale) or similar) and never an app's, or the "one copy set" guarantee re-fragments the first time an app translates its own door.
  6. low — var minutes (door.go:39) is a generic package-level name in chrome; lifetimeMinutes or linkMinutes would not collide with the next person's duration.
  7. low — two constants, no link. chrome.LinkLifetime and rastrillo's new auth.LinkTTL must agree and nothing checks it; estilo still pins github.com/carlosframework/rastrillo v0.23.0, which has no auth.LinkTTL, so it cannot today. When estilo moves to amadan.net/rastrillo, add LinkLifetime == auth.LinkTTL to TestDoorCopyIsPinned.
  8. info — interim mismatch, already acknowledged in rastrillo #56. Apps pinned to the 2026-09-08 rastrillo pseudo-versions keep sending keymail's "expires shortly" body while the door promises 15 minutes, until each bumps rastrillo past #56 and sets Body: chrome.EmailBody(App). Worth a line in the app branches' descriptions so it is not reported as a bug again.

Merge order and the re-pin

  • All eight chore/door-copy branches pin amadan.net/oficina/estilo.git v0.0.0-20261001075355-fbd14e759027, i.e. this branch's tip.
  • How this lands decides whether the re-pin is hygiene or necessity. estilo's main has actually been landing by merge commit (cce1101 Merge branch 'signin-foot-link', 4120143 Merge branch 'signin-door'), not by squash, whatever AGENTS.md says. With a merge commit fbd14e7 stays reachable from main and every existing pin keeps resolving; re-pinning to the tip is tidiness. With a real squash, fbd14e7 is reachable only from the branch ref: delete the branch and a cold module cache (the hub runners, per the 429 incident) cannot go mod download it, and all eight app branches go red at once. If squashing, re-pin all eight before the branch ref is deleted.
  • Four app branches are behind their own main and need a merge of main as well as the re-pin: docs (base d40fbd1, main cc1bd3c), calendar (25974c6 / dbee441), memoria (97b8a23 / 0cc9d09), meet (84ed5dc / bf4e24d).
  • A re-pin changes go.mod and go.sum, so each app's CI must be re-run on the re-pinned commit, not trusted from the current green.
  • fichas chore/door-copy is not a copy chore: 111 files, +16,356 lines over origin/main fab37e4 (the design-only commit). It carries the whole app. It needs its own whole-branch review under its real name. Home's (18 files, +233) is also larger than a copy change; worth a look rather than a rubber stamp.

Reviewed with: scratch worktree at fbd14e7, make test, two mutations, amadan ci status -json, keymaildev/signin v0.1.1 from the module cache, origin/main of home, fichas, docs, sheets, slides, calendar, memoria, meet.