Fable whole reconciliation review APPROVE at a75c7a90834418254c8e242bcd420e878783298d, actual model claude-fable-5-1. Full local make test and exact-head CI are green. Human copy review17 strings and deployed canary remain open; neither is waived. Reviewer specifically asks preservation branch stay verbatim; follow-ups after merge: narrow writer/viewer title sizing, refused-drop screen reader/focus notification, deferred live rename/history behavior. No deployment or main merge.
Verification done. Writing up the verdict now.
Verdict: APPROVE the reconciliation code and design at a75c7a9
Reviewed by Claude Fable 5.1, read-only. Approval covers the merge and its spec only. The human copy review of the 17 carried strings and the deployed-canary gate remain open and are not waived.
Merge claims verified. The merge base of 7212864 and 4aea8f4 is 8ed3926. The merge d1ecaaf differs from deployed in exactly the three doctrine files and from main in exactly the 21 deployed files. a75c7a9 adds only the reconciliation spec. The doctrine delta is documentation only and nothing on this branch contradicts it.
Rename handler is sound (internal/sheets/handlers_rename.go):
- Body capped at 8 KiB, ParseForm error refused with 400, title read from PostForm only, so a query string can never name a sheet. All three cases are pinned in
handlers_rename_test.go:175. - mayWrite runs after the body read and immediately before the UPDATE, closing the grant-revoked-mid-body window. Viewers get 404, not 403.
- Title is sealed under the same purpose as create, updated by explicit column, truncated by runes. No title reaches a log or error string.
- App-wide origin CSRF covers the new POST. The form posts and redirects, so it works scriptless.
Import page and refusal recovery are correct. Refusals and the busy 429 re-render on the import page with the form, static route is registered before the wildcard and pinned by test, and the page is gated by session.
List and drop chrome hold up. The overlay is hidden at rest with the reinstating CSS rule guarded by test, the drop box paint excludes the overlay, Escape and backdrop click dismiss and restore the hint, and the recovery controls carry no negative tabindex. Every token used exists in tokens.css or theme.css, so dark resolves through light-dark() with no undefined-property fallout.
Follow-ups, none blocking this merge
- Narrow-viewport title wrap, sheet page. Moving overflow:hidden off the flex item at
internal/sheets/static/sheets.css:114 removed the implicit min-width of zero. The bar is flex-wrap:wrap, so at roughly 400px the title form, or a viewer's long title, drops onto a second row instead of truncating. Fix is one line, min-inline-size: 0 on [data-doc-title], plus flex: 1 1 0 on the input. The canary narrow check must cover the sheet page top bar with a 200-rune title, writer and viewer, not only the list. - Overlay accessibility after a refused drop.
internal/sheets/templates/sheets.html:135 renders the panel as a div with no dialog role, no focus move, and the hint is not a live region. A screen-reader user who drops two files hears nothing. Add aria-live="polite" on the hint and focus the file input from drop.js:111. The Import link remains the scriptless floor, so this is a should-fix. - Rename does not propagate live or into history. Other open tabs keep the stale title until reload, and the version log records only cell edits. The spec does not require either today. Record it as a deferred item with a trigger.
- Input width caps the visible title at about 20 characters rather than the 22rem the static span allowed. Cosmetic.
inline-size: 100% on the input alongside item 1 resolves it.
Nothing here should be fixed on this branch, since its purpose is verbatim preservation of both parents. Items 1 and 2 belong on the first fix branch cut after this merge lands.