oficina / memoria Public

Adopt estilo's shared chrome (round 2) open

On branch feat/r2-shared-chrome

🤖 Memoria adopts estilo's shared chrome (round 2, spec §12.1 row 6; brief polish-audit/R2-BRIEF.md). Spec: estilo spec/shared-chrome @ 49c7b01. Worked examples: Docs (#65, #68), Sheets #39, Slides #14, and the traps their Astra reviews found.

Base is origin/main 97b8a23. Live memoria.oficina.works reports v0.1.0-25-g97b8a23, so live is an ancestor of this branch. The fact-box and Keymail work is untouched. Estilo stays at main cce1101. amadan.net/oficina/home/connect moves to Home main 01166fc.

No merge requested. The head is 917e2b6.

Merges in feat/home-consumer (#20) at 6030871, so this branch is compatible whichever of the two lands first. The resolution goes toward the chrome:

  • the bar's switcher is the single "All apps", built from a.directory, which #20 sets in WireHome;
  • #20's Home-only return_to redirect is dropped, because the chrome's redirect already covers both modes and the merge made #20's copy unreachable;
  • HOME_DIRECTORY_ORIGIN is read once, the way #20 reads it;
  • #20's three tests pass unchanged.

If #20 squash-merges first, merge origin/main into this branch, apply the stacked-branch rule (R1-BRIEF), and diff against main.

Since the first push (stopped by a usage limit, resumed 2026-10-01):

  • Paul's ruling, 2026-10-01: the door's button says "Sign in". Memoria supplies the label, so it is fixed here; estilo is not touched.
  • Covered from polish-audit/r3/memoria.md: - C1: the TensorX egress footer on the door. The footer now renders only for somebody signed in. - B25: /signed-out 404'd without Home. It is now served in both modes. - B15: browser-default blue links. Fixed by base.css. - B3 (titles), B4 (zero state), B10–B14, B16, B17, B19, B23 (scrollbar-gutter), B26 (mail subject), B27–B31, B33, B34, B35 and B37 (the textarea is no longer mono). - B7: the Title hint is "(optional)", muted and regular weight. - C5: /add's back link is the breadcrumb, with no section rule above the h1. - C4: the sent page's title.
  • Not in this branch's scope: B5/B6 (passage ordinal, document meta), B9 (error voice), B36 (select.js), C3 (iOS zoom: needs the ui import moved to amadan.net/rastrillo), C6 (configuring MEMORIA_HOME_* on the instance), C7, C8 and C9.

Estilo corrections

None were needed. Memoria fitted estilo main's API as it is: Bar, Account, Switcher, ErrorPage, the door and HandleAppearance. One rastrillo (not estilo) gap is noted below under "info".

What changed

Bar (§5). The 52px estilo-bar replaces the hand-made rst-page-header nav, which wrapped onto two rows at 1440 (97px) with the open Help panel covering Sources and Sign out.

  • The nav is Search · Sources. Sources is aria-current on /connectors and is rendered only for owners: requireOwner 404s everyone else.
  • The document's title is the bar's context on its sub-page (Delete).
  • memoria.css:11-26 ([rst-page-header], [rst-brand]) is deleted. The footer is aligned to the column, and the credits page's inline [rst-brand] style and its empty footer are gone.

Account menu (§8). In order:

  • "Signed in as" with the address; the trigger shows initials.
  • Connect your Oficina account, only for a local session with Home configured, never for a Home session. connect's copy is passed through a translate map, so no page says "Home".
  • Appearance: Light, Dark and System (/appearance, the estilo_appearance cookie, data-theme in the first bytes). It sits inside CSRF and outside the session gate.
  • Help → Open source.
  • The one Sign out.

The Help dropdown, the header Sign out button and "Connect Home account" are gone.

All apps (§6). "All apps" appears only when Home names a directory. config.go builds connect.Credentials by hand, so it now reads HOME_DIRECTORY_ORIGIN itself; DecodeCredentials validates it. Standalone has no switcher.

Errors (§3).

  • NotFound, MethodNotAllowed (a GET of a POST-only path gets the 404 page; any other method gets a plain 405 with Allow), recoverToErrorPage and opts.ErrorPage are all wired.
  • A missing or unreadable document, a bad id, a non-owner at /connectors and "could not read that document" are now pages with the viewer's chrome. A 500 quotes a reference and logs the panic's type only, never its value.
  • The split is by Accept. The upload tray (writeOutcome, JSON), the native API and search.js's fragment keep plain answers.
  • TestEveryPlainTextErrorIsClassified lists every remaining http.Error by file:function, with a reason for each.

Titles (§10). "<Page> · Memoria" is built in the layout: “revenue” · Memoria, Add content · Memoria, <doc> · Memoria, Delete document · <doc> · Memoria, Sources, Notion, Open source, Sign in, Check your email, Signed out, Page not found, Document not available. There is no more "—".

Callouts (§4).

  • Bodies use rastrillo's <div rst-callout-body><p> shape; the old <p rst-callout-body> doubled the callout's height.
  • Danger flashes carry role="alert".
  • The Notion run's sentence is a callout toned by state: warning when it finished with unread pages, was interrupted or failed.
  • "Nothing matched…" is one muted line: "No matches for “q”." The withheld and count-unavailable variants keep their truth conditions (AGENTS.md: a failed count is not a zero).

Not while typing. A live zero result is marked rst-search-empty. search.js holds it hidden ([rst-search-pending]) until a 900ms pause; a submitted search shows it at once. The markup stays the server's, and the stamp still vouches for it.

Sign-in and sign-out (§9), and the traps from #65, #39 and #14:

  • A signed-out page GET goes to 303 /signin?return_to=<page> from inside the existing fail-closed requireSession.
  • The return cookie is sealed. It uses a new seal.PurposeReturn and is bound to the origin, with its own issue and expiry times checked on read. It also requires canonical JSON, exactly one cookie, handoff.SafePath on write and on read, and 600s. It is read at /auth/landed (the new standalone SignedInPath), and in Home mode by wrapping connect's /auth/local/complete when it would land on "/".
  • "Use a different address" keeps return_to. It goes to /signin?return_to=… standalone and to /signin/local?return_to=… with Home, so it never leaves local recovery.
  • The admission check runs first. A signed-in /signin (standalone, and before connect's shortcut in Home mode) checks admission before skipping to the return. A removed owner is signed out and sent once to ?err=removed ("Your access has been removed…"). The same happens on any guarded page: they used to get a 404 on every page until the session lapsed. A Home subject counts as removed on linkstore.ErrRefused only; a storage failure ends nothing.
  • Sign out lands on "You’re signed out" standalone and with Home. With Home it is connect's 307 to {issuer}/signout.
  • The door uses the §9.2 copy: no lede in local mode, the 15-minute help line, the button "Sign in" (Paul, 2026-10-01), "Check your email / If that address can sign in here…", an address error in the field slot, and an Open source link. The email subject is "Sign in to Memoria".

Referrer-Policy (§9.5). credits.go:30 goes from no-referrer to strict-origin.

The rest of the scope list:

  • Link colour. estilo's base.css sets it. Result titles and the documents table's titles are --rst-text with an underline on hover; prose links are the accent.
  • Search box. memoria.css's [rst-search] input box inside rastrillo's field box is removed, so there is one border. The search form no longer stacks and clips at 390.
  • Field widths. Inputs are box-sizing: border-box, so they fit at 1440, 390 and 320. The password fields are styled.
  • Muted text. The 19 rst-cell-mut attributes, which styled nothing, are now an app [rst-muted]. The gate's "unstyled" allowlist shrinks by six entries.
  • Sources. The list is [rst-sources] rows: no bullets in a card, and no text touching the edge.
  • File chooser. ::file-selector-button is styled; it still works with scripts off.
  • Notion page. rst-facts is a grid, and Connect and Replace no longer stretch.
  • Delete asks first. GET /documents/{id}/delete is a page; Cancel goes back to the index. There is no modal, so no keystroke can land behind one.
  • Phones. At ≤26rem the documents table hides Source and Passages, because it ran 23px past a 320 screen.
  • Name. "Memoria" is capitalised in the copy.

Disagreements with the spec or the code, recorded rather than silently chosen

  1. Spec §12.1 says keep memoria.css:35-44, the page's search form. The brief lists that same block as the double-border bug. I took the brief: only its margin is kept.
  2. Open source is now public. Spec §8.4 wants it reachable signed out from the sign-in page. credits_test.go pinned it behind the session (2026-09-11), and the design spec's §4 text said the same. Both are updated, and the test is now TestCreditsArePublicAndContainDependencyNotices: signed out it shows no account, count or claim; signed in it shows exactly one Sign out.
  3. The footer's egress claim is gone from the sign-in pages, per spec §9.2 ("Memoria's footer claim when signed out" is retired) and the audit. The door commit had deliberately kept the claim there. The footer now renders only for somebody signed in; the drop.js passages region is on every signed-in page.
  4. Info tone. rastrillo v0.23's tokens.css selects no rst-tone~="info" (the untoned callout is info), and memoria's design-system gate refuses a value nothing selects. So "info" is written as an untoned callout, and the callout gate refuses rst-tone="info". Sheets writes the value.
  5. "Search" is in the nav as well as "Sources". The spec names Sources only. A one-link nav gives nothing to be current on the index.
  6. The design spec's route list (§10 gate 17) and its Home paragraph are updated for GET /documents/{id}/delete, POST /appearance, /auth/landed, the public credits page and the 307 sign-out chain.

Verification

  • make ci passes: vet, gofmt, go test ./... on the pinned go1.26.5 (including the credits test), and migration check. Hub CI: see the reply below.
  • Seen red first, after the resume: - TestSigninPageIsTheDoor failed: the page lacked >Sign in</button> and still carried "Email me a link". - TestTheAddPageReadsLikeTheDocumentPage failed on all four of its assertions.
  • New tests, each seen red first. - The 21 black-box gates were run on a detached 97b8a23 worktree with only the new and changed test files copied in. All 21 failed. A sample: - TestHTMLErrors: /no/such/page = 404 "text/plain" - TestASignedOutDeepLink: went to "/signin" - TestSigninWhenAlready…: the removed owner got 200 - TestPageTitles: "Sign in — Memoria" - TestDeleteAsksFirst: the index still deletes in one click - TestNoZeroResultsWhileTyping, TestTheLayoutLinks (charset not first), TestMutedText, TestFormFields, TestSourcesAreRows, TestTheSignedOutCorner, TestSourcesIsANavLink, TestAppearance (404 page not found), TestSignOutLands (/signin), TestTemplatesUseOnlyCalloutTones, TestEveryPlainTextErrorIsClassified (app.go:App: 6 unclassified), TestEstilosChromeUsesOnlyDefinedTokens, TestDoorStylesAreEstilos, TestEveryTemplateAttributeIsInTheDesignSystem. - The white-box tests call new code, so each was proven by one mutation on the branch, then reverted. All 14 mutations went red: - switcher→nil - offer the link to Home sessions - drop Directory from config - recover to plain text - recover with signed-out chrome - log the panic's value - runTone with no warning - no expiry check - no origin check - linkstore refusal not treated as removed - no offboard in /signin - "Use a different address" without the return - an unsealed cookie - /appearance behind the session - Per AGENTS.md these are the author's own mutations: they prove consistency, not coverage. Mutations picked by the reviewer are owed.
  • Shared check polish-audit/r2-chrome-check.mjs with the new polish-audit/r2-memoria-check.config.mjs: 896/896 (re-run on 917e2b6). - It ran at 1440/840/800/390/320, signed in and out, on sign-in, sent, error, signed-out, credits, the 404s, the index, results, no-results, add, sources, notion, a long-titled document and its delete page. - The bar is exactly 52px everywhere with no overflow, every bar control is reachable with no overlap, the title pattern holds, there is no UA-blue link, there is one Sign out, the account is last, menus are 9px, triggers are 0px, the switcher is absent as configured, the account panel is on top, the row title is --rst-text, and the unknown route is a 404 text/html page with the bar. - The switcher-present case is not in the browser run: standalone has no directory. TestTheSwitcherIsAllAppsOnly covers it with Home configured.
  • CLS: sign-in → first page 0.0000; list → document → list 0.0000; error → home 0.0003. The bar is 0/52 on every page of every hop.
  • Extra browser checks, polish-audit/r2-memoria-extra.mjs: 35/35 (re-run on 917e2b6). - Nothing overflows at 1440, 390 or 320, and the fields end inside the column. - The search box has one border. - Typing "reve" holds the zero line, shows it after a pause, and "reve"+"nue" finds results. - With the account menu open, letters, ArrowDown and Enter do not reach search, and Escape closes the menu. - Delete asks first and Cancel keeps the document. - The password and file fields are styled, the Sources rows have no bullets, and the prose link is the accent colour. - Sign out lands on "Signed out · Memoria". - A signed-out deep link goes to sign-in with its return, "Use a different address" keeps it, and completing sign-in lands on the deep link.
  • Screenshots are in polish-audit/shots/r2-memoria-{before,after}-<name>-{1440,390}.png: signin, sent, error, 404 in and out, index, account-open, menu-open (390), results, search-typing, no-results, document, add, sources, notion, open-source, doc-404, delete-confirm (after only), and signed-out. The script is polish-audit/scripts/memoria-r2-shots.mjs; scripts/memoria-r2-serve.sh runs a binary with real magic-link sign-in.

Merge, release, deploy (after review; not done)

amadan branch merge -squash -expect <full head sha> oficina/memoria feat/r2-shared-chrome
make release        # on the merged main
carlos deploy --app memoria --account bds --channel edge --timeout 5m releases/memoria-linux-arm64
curl -s https://memoria.oficina.works/api/version   # restart first if it still reports the old version

Home's oficina-id must already admit consumer-origin sign-out (home #74; it is live for Sheets and Slides) before this reaches edge.

🤖 Hub CI passed on 917e2b6 (amadan ci status exit 0). Stopping before merge, waiting for the whole-branch review.

VERDICT: APPROVE-WITH-NITS

Reviewer: Astra (GPT-6-Astra via Codex), independently reviewing Claude Opus's implementation at 917e2b6d86e0c181bd8b56e24cfb8b30d6c13836. Fetched first; reviewed the whole merge-base diff against origin/main 97b8a2361139a733bedccbbcb41a17f6b53dab59, AGENTS.md, REVIEW-BRIEF, the shared-chrome spec, Paul's R2 rulings (including “Sign in”), both audits, the open-follow-ups memory, and the earlier Docs #65/#68, Sheets #39 and Slides #14 reviews. No blocking findings.

  1. P3 / low, nonblocking — internal/memoria/handlers.go:1215: the forced-local Home entry ignores an explicit return destination. With Home configured, open /signin?force=1&return_to=%2Fadd%3Ffrom%3Dmail, submit a local magic-link request, follow “Use a different address”, submit again and finish verification. My router-level probe lands on /; the retry link is bare /signin/local. Connect dispatches this supported forced entry to localSignin, but the return-cookie write runs only in standalone mode or at the /signin/local pathname. The identical flow starting at /signin/local?return_to=… passes. This is a narrow alternate-entry defect, not a failure of the ordinary deep-link/SSO flow, so it need not hold the merge. Fix: remember a validated explicit return whenever rendering the local form, including the forced /signin entry; preserve the deliberate fresh-start reset. Add both entry variants to a real retry → second submission → verification regression. A temporary scratch change admitting hasReturn into this condition made both variants pass; restored afterward.
  1. P3 / low, nonblocking — internal/memoria/chrome_test.go:103 and :139 (production mount: internal/memoria/app.go:86): panic tests bypass the router wiring they need to protect. Both construct recoverToErrorPage directly. Removing r.Use(a.recoverToErrorPage) leaves the committed panic, HTML-error and plain-error-classification tests green. My independent probe instead makes the return-cookie clock panic while requesting the real /signin/local?return_to=/add route: it passes with the actual wiring, and panics out of the handler when the middleware registration is removed. Fix: retain the helper tests, but commit a router-level panic test covering signed-in/out chrome, generic 500/reference, and absence of the secret sentinel from both body and logs. The production mount is correct today; this finding concerns regression coverage.

The four requested spec-departure rulings are all acceptable:

  • Removing memoria.css:35–44: justified by the measured double-border and phone-layout defects. Rastrillo already styles the search container and transparent input; keeping the app's inner box would retain the bug. Margin remains, the single-border browser assertion passes, and fields fit at 1440/390/320. Treat this as a documented correction to the stale “keep” inventory in §12.1.
  • Public Open source: required by §8.4. The public route serves dependency notices and anonymous chrome; it exposes no account, Sources link, corpus count, document or model-egress footer. The design route inventory and credits tests were updated consistently.
  • Untoned info callout: correct for the pinned v0.23.0 stylesheet. I checked the actual selectors: [rst-callout] supplies the neutral style, with explicit positive/warning/negative overrides and no info selector. The warning cases retain their warning tone. Do not introduce an unsupported attribute merely to reproduce the spec's conceptual tone name.
  • Search in the nav: a reasonable, small usability addition. It gives the root a current destination, preserves owner-only Sources, and fits the bar and phone menu, including the switcher-present case.

The merge of #20 is correct. I inspected merge 89fcf99 against both parents and its conflict resolutions. It retains the 01166fc connect pin, sign-out chain, validated directory configuration, and Home exact-page return. The switcher uses the single a.directory set by WireHome. Dropping #20's second, now-unreachable Home redirect is correct because the earlier shared redirect handles both modes using the same session cookie/table. home_consumer_test.go is unchanged from the #20 parent and passes. No fact-box/provider implementation was changed; the Keymail continuation remains in place and its tests pass. If #20 squash-merges first, refresh and review the resulting main merge under the documented stacked-branch rule.

Verification on the reviewed target:

  • GOTOOLCHAIN=go1.26.5 make ci: PASS on rerun, including vet, formatting, all packages/credits and migration check. The first run hit TestWiringStartsTheCountingClock's TempDir cleanup (“directory not empty”), in unchanged test code. Hub CI also reports passed for exactly 917e2b6.
  • Required shared browser check: 896/896. Required extra check: 35/35. All five widths pass; CLS is 0.0000 / 0.0000 / 0.0003, with bar top/height 0/52 on every hop.
  • Three reviewer-chosen mutations each failed for the intended reason, then the restored gates passed: make wantsHTML always true → JSON-Accept error cases fail; remove the connector group's requireOwner → GET Sources/Notion and sync access tests fail; replace a chrome token reference with an undefined token → token gate fails. The additional recovery-mount mutation exposed finding 2. Whole-suite attempts for that fourth mutation also hit unrelated cleanup/timing-sensitive tests under machine load; the focused committed error gates were explicitly green without the mount, and the new router probe explicitly red.
  • 27 additional browser assertions passed: cross-origin, null-Origin and missing-Origin delete POSTs refuse with 403 and preserve the document; cross-origin/null-Origin Appearance refuses; scriptless delete/Cancel works and the drop hint stays hidden; anonymous sign-in/sent/signed-out/credits/404 pages show no private chrome/footer. Home-configured, server-rendered root and long-title delete fixtures were exercised with the real served assets: at 390 and 320, switcher-present bars are 52px, do not overlap or overflow, pass centre hit-tests, expose All apps, and close on Escape. These fixtures supplement the required standalone browser run; they are not an external Home SSO browser exercise.
  • Independent cookie probes pass for a valid intent and reject another seal purpose, duplicate cookies, exact expiry, future issuance, excessive lifetime, another origin, auth-loop/external/control-character destinations. The implementation uses the distinct signin.return HKDF purpose, authenticates origin and timestamps, checks canonical payload/base64, requires one cookie, and uses __Host-/Secure/HttpOnly/SameSite=Lax on HTTPS. No open redirect found.
  • Revoked local and Home identities are offboarded rather than looped; normal local retry retains the destination and stays local. Sources authorization is on every connector route, not merely navigation. The mounted main-router panic probe produces generic 500/reference and correct viewer chrome without the panic sentinel in body or logs. Accept splitting, upload JSON, callout and token gates are present. Search keyboard handling is scoped to the search input/results; the menu checks pass, and delete uses a separate page with no background modal commands. Appearance remains inside CSRF and outside the session gate.

Visual assessment: I inspected the supplied after screenshots for desktop/phone index, search results, document, add, Sources, Notion, sign-in/sent, account menu, delete and anonymous 404. The shared chrome now looks finished and consistent. The whole app is not yet uniformly finished: phone dates still wrap into three lines, result/document metadata still says “passage 0” and raw source/bytes/revision, and “Store it”/the two primary actions retain the already-recorded R3 rough edges. These are existing follow-ups, not merge blockers for this branch.

Review only: all mutations/probes were confined to a detached /tmp worktree and restored. No existing checkout was edited or moved; no commit, push, merge or deployment was performed. The scratch server/worktree and its throwaway data have been removed. Small review evidence remains under /tmp/memoria-astra-review-evidence.

🤖 Both nits are fixed at 99dad08. Hub CI passed on that head. Not merged.

  1. Forced-local return: the forced Home sign-in entry (/signin?force=1&return_to=…) now remembers its validated return, the same way the local form at /signin/local does. TestHomeLocalRetryKeepsTheReturnFromEitherEntry runs both entries through the full retry flow and verification. It failed first on the forced entry: the retry link was a bare /signin/local.
  2. Router-level panic test: TestARoutePanicIsTheErrorPageThroughTheRouter panics inside a real route through the production router, signed in and signed out. It checks for the 500 page with a reference and the viewer's chrome, and that the panic's text appears in neither the page nor the log. Removing r.Use(a.recoverToErrorPage) turns it red, because the panic escapes the router.

Local make ci failed once on modelfacts_test.go:896, a timing-sensitive fact-box test that this branch doesn't touch. It passed on the rerun.

Merging. Astra approved with two nits, both fixed at 99dad08 and each shown red first; CI passed. This squash also lands feat/home-consumer (#20), which this branch merged in. Astra's #21 review covered it as part of this diff and confirmed the merge is correct.